@maycombhq/atticus 0.0.0-stage → 2.0.0-beta.2
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-27NKUS3X.js +4330 -0
- package/dist/chunk-2ANOROX3.js +2522 -0
- package/dist/chunk-2BF2EYJV.js +174 -0
- package/dist/chunk-2BFCZ3RK.js +315 -0
- package/dist/chunk-2DIQQJCB.js +378 -0
- package/dist/chunk-2RMYG36Q.js +1227 -0
- package/dist/chunk-2VHF7OXM.js +55 -0
- package/dist/chunk-3ALD2W2A.js +95 -0
- package/dist/chunk-3C7EYRFZ.js +41 -0
- package/dist/chunk-3G4YI4ZU.js +16 -0
- package/dist/chunk-3HI6Z55Y.js +5949 -0
- package/dist/chunk-3USYZGRF.js +324 -0
- package/dist/chunk-42EVSKME.js +37 -0
- package/dist/chunk-4AK7IBVP.js +915 -0
- package/dist/chunk-4BKGMUIK.js +53 -0
- package/dist/chunk-4BN53TG3.js +343 -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-4WBVUHXN.js +69 -0
- package/dist/chunk-5GDRCDQB.js +32 -0
- package/dist/chunk-5GGOLAKC.js +856 -0
- package/dist/chunk-5KLGBXG5.js +48 -0
- package/dist/chunk-5WG5YP4T.js +34 -0
- package/dist/chunk-64EQYWCA.js +180 -0
- package/dist/chunk-652QWTLZ.js +242 -0
- package/dist/chunk-65NYPGRC.js +766 -0
- package/dist/chunk-66YVAGGM.js +46 -0
- package/dist/chunk-6BNC6EQ4.js +41 -0
- package/dist/chunk-6BVONWJ7.js +50 -0
- package/dist/chunk-6D2CQ5II.js +41 -0
- package/dist/chunk-6IU67AEK.js +325 -0
- package/dist/chunk-6J5OKA4Y.js +49 -0
- package/dist/chunk-6JCPI55B.js +92 -0
- package/dist/chunk-6JUBZBMW.js +56 -0
- package/dist/chunk-6M5NK3K3.js +415 -0
- package/dist/chunk-6QSERWXH.js +53 -0
- package/dist/chunk-6TACQN3N.js +41 -0
- package/dist/chunk-76WKMA4J.js +14 -0
- package/dist/chunk-7AVHLZGD.js +45 -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-7FBGJFEE.js +66 -0
- package/dist/chunk-7JUKDRSZ.js +1726 -0
- package/dist/chunk-7SW2QWLU.js +212 -0
- package/dist/chunk-7WD63HU2.js +143 -0
- package/dist/chunk-7ZUIMTOU.js +108 -0
- package/dist/chunk-A3GY6SOA.js +95 -0
- package/dist/chunk-A7UPUQFY.js +2356 -0
- package/dist/chunk-AA6YULYB.js +376 -0
- package/dist/chunk-ABWHYWNI.js +101 -0
- package/dist/chunk-AIOQPLA5.js +540 -0
- package/dist/chunk-AVJDSJ2F.js +18 -0
- package/dist/chunk-B2WLAWNJ.js +374 -0
- package/dist/chunk-BKVWTZ6A.js +211 -0
- package/dist/chunk-BOHJZTEQ.js +1991 -0
- package/dist/chunk-BWZ77RQT.js +25 -0
- package/dist/chunk-C6EKFH7J.js +169 -0
- package/dist/chunk-CAQZUNOH.js +1150 -0
- package/dist/chunk-CEXSU4S6.js +1217 -0
- package/dist/chunk-CKSITVS4.js +16101 -0
- package/dist/chunk-CNUKCS2T.js +2741 -0
- package/dist/chunk-CPA6HPVR.js +3206 -0
- package/dist/chunk-CR3MTH32.js +207 -0
- package/dist/chunk-CSZ4C5N6.js +307 -0
- package/dist/chunk-D3PO4DOZ.js +261 -0
- package/dist/chunk-D46WUV3T.js +33 -0
- package/dist/chunk-DE76JROX.js +80 -0
- package/dist/chunk-DFBCSHVT.js +69 -0
- package/dist/chunk-DHPACDG7.js +104 -0
- package/dist/chunk-DKJA76CJ.js +113 -0
- package/dist/chunk-DPUYDDQG.js +164 -0
- package/dist/chunk-DPZHZQKE.js +285 -0
- package/dist/chunk-E36QOJMD.js +47 -0
- package/dist/chunk-EADP3PEN.js +450 -0
- package/dist/chunk-EBEW3XXQ.js +166 -0
- package/dist/chunk-EDS2L2LD.js +106 -0
- package/dist/chunk-EFHQ3TWP.js +637 -0
- package/dist/chunk-EPTBZ3GP.js +101 -0
- package/dist/chunk-EYWIDJFU.js +130 -0
- package/dist/chunk-F7HYOUFF.js +287 -0
- package/dist/chunk-FHH4I4ZV.js +128 -0
- package/dist/chunk-FITWGOJ2.js +131 -0
- package/dist/chunk-FQXYPF7E.js +147 -0
- package/dist/chunk-G2B4ZQGH.js +240 -0
- package/dist/chunk-GBRFVT63.js +45 -0
- package/dist/chunk-GDLULHTC.js +2919 -0
- package/dist/chunk-GLUX4EEP.js +583 -0
- package/dist/chunk-GQ4N4WKF.js +1142 -0
- package/dist/chunk-GT4I4ZWM.js +38 -0
- package/dist/chunk-HFQCFSJR.js +60 -0
- package/dist/chunk-HITVBIJS.js +1247 -0
- package/dist/chunk-HLJAXMM4.js +233 -0
- package/dist/chunk-HVUPKCGJ.js +162 -0
- package/dist/chunk-I3E7CX4H.js +155 -0
- package/dist/chunk-I4JRCD6M.js +184 -0
- package/dist/chunk-ICY36XCH.js +34 -0
- package/dist/chunk-IHMLFRVV.js +228 -0
- package/dist/chunk-IPKHE5D6.js +1963 -0
- package/dist/chunk-J4K3KVLD.js +131 -0
- package/dist/chunk-JBTK5OP2.js +55 -0
- package/dist/chunk-JCZIKA4C.js +163 -0
- package/dist/chunk-JGZCG5VM.js +189 -0
- package/dist/chunk-JIITQXFB.js +4087 -0
- package/dist/chunk-JMSCMUCA.js +271 -0
- package/dist/chunk-JZU3H6UQ.js +54 -0
- package/dist/chunk-K65BJIMT.js +49 -0
- package/dist/chunk-KEM4SDDP.js +3898 -0
- package/dist/chunk-KQDZXGNR.js +14 -0
- package/dist/chunk-L24C55VN.js +7923 -0
- package/dist/chunk-L6SGQIR3.js +471 -0
- package/dist/chunk-LR3R6Z2A.js +400 -0
- package/dist/chunk-M766ZJ6W.js +119 -0
- package/dist/chunk-MC3H7736.js +19910 -0
- package/dist/chunk-MGYZM27B.js +205 -0
- package/dist/chunk-MMOSZPWJ.js +273 -0
- package/dist/chunk-MVZVGYXH.js +1611 -0
- package/dist/chunk-N7IXEHK3.js +549 -0
- package/dist/chunk-NE2W3OO7.js +230 -0
- package/dist/chunk-NN2ZQEB4.js +38 -0
- package/dist/chunk-NXFPS2LR.js +182 -0
- package/dist/chunk-O3WI5KWW.js +31 -0
- package/dist/chunk-OLQJTJJU.js +76 -0
- package/dist/chunk-ONLS5QA5.js +102 -0
- package/dist/chunk-OT2O76OS.js +1496 -0
- package/dist/chunk-PAMPPUR6.js +4500 -0
- package/dist/chunk-PCOCACY3.js +385 -0
- package/dist/chunk-PCVDV3UZ.js +430 -0
- package/dist/chunk-PRPPGYZG.js +104 -0
- package/dist/chunk-Q4D5X7QB.js +264 -0
- package/dist/chunk-Q5AMFEWD.js +119 -0
- package/dist/chunk-QRAOANID.js +214 -0
- package/dist/chunk-QSXK2RGH.js +688 -0
- package/dist/chunk-QZSX3F7C.js +389 -0
- package/dist/chunk-R6RY4Q5T.js +51 -0
- package/dist/chunk-R6XF6UR3.js +149 -0
- package/dist/chunk-R77KLRB3.js +166 -0
- package/dist/chunk-RRTKKSLQ.js +251 -0
- package/dist/chunk-RTEYOSJY.js +29 -0
- package/dist/chunk-RUVFXE2L.js +666 -0
- package/dist/chunk-RVH5SUW5.js +69 -0
- package/dist/chunk-RZPYKDWS.js +26 -0
- package/dist/chunk-S2A7YC3Q.js +41433 -0
- package/dist/chunk-S3QNRWYR.js +25 -0
- package/dist/chunk-SJRGMG6A.js +174 -0
- package/dist/chunk-SP2I63OO.js +252 -0
- package/dist/chunk-T2UYZSCQ.js +545 -0
- package/dist/chunk-T6AIP7H5.js +3851 -0
- package/dist/chunk-T6XQL73F.js +94 -0
- package/dist/chunk-TANFV3MZ.js +531 -0
- package/dist/chunk-TDD55CJH.js +119 -0
- package/dist/chunk-TV45EUGM.js +51 -0
- package/dist/chunk-TXLH6WHQ.js +2078 -0
- package/dist/chunk-TYAID7B7.js +408 -0
- package/dist/chunk-UBBC2XSG.js +873 -0
- package/dist/chunk-UGLN7QHV.js +27 -0
- package/dist/chunk-UNM6ZOKP.js +878 -0
- package/dist/chunk-V2YTAIQO.js +1075 -0
- package/dist/chunk-VN3DP4LQ.js +43 -0
- package/dist/chunk-VOGCSYOI.js +16 -0
- package/dist/chunk-VYDBJ73M.js +32 -0
- package/dist/chunk-W3BQAZ3Q.js +34 -0
- package/dist/chunk-W5BVQF44.js +36 -0
- package/dist/chunk-WVPJSRRJ.js +52 -0
- package/dist/chunk-WY6PP6AA.js +53 -0
- package/dist/chunk-XAKWMCXI.js +285 -0
- package/dist/chunk-XPCM366L.js +70 -0
- package/dist/chunk-XPTOBS2D.js +48 -0
- package/dist/chunk-XY3P7JW7.js +3479 -0
- package/dist/chunk-XZRON6IV.js +131 -0
- package/dist/chunk-YEDDOYSC.js +27809 -0
- package/dist/chunk-YISXLPHN.js +496 -0
- package/dist/chunk-YMZ3TNYB.js +18 -0
- package/dist/chunk-YY6ZQQBP.js +203 -0
- package/dist/chunk-YYQN46FY.js +334 -0
- package/dist/chunk-Z6LF7HQN.js +22 -0
- package/dist/chunk-ZCJDOK2S.js +116 -0
- package/dist/chunk-ZDPAS56H.js +76 -0
- package/dist/chunk-ZMAINWBW.js +18 -0
- package/dist/chunk-ZMFPW66A.js +22 -0
- package/dist/chunk-ZNCEKR4J.js +296 -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,3479 @@
|
|
|
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
|
+
STORE_MARKER,
|
|
8
|
+
addStoreReference,
|
|
9
|
+
readStoreMarker,
|
|
10
|
+
readStoreVia
|
|
11
|
+
} from "./chunk-4AK7IBVP.js";
|
|
12
|
+
import {
|
|
13
|
+
AdapterError,
|
|
14
|
+
mergeBlockers,
|
|
15
|
+
requireApproval
|
|
16
|
+
} from "./chunk-7JUKDRSZ.js";
|
|
17
|
+
import {
|
|
18
|
+
readCatalog
|
|
19
|
+
} from "./chunk-27NKUS3X.js";
|
|
20
|
+
import {
|
|
21
|
+
createGit,
|
|
22
|
+
readChangeRepos
|
|
23
|
+
} from "./chunk-EFHQ3TWP.js";
|
|
24
|
+
import {
|
|
25
|
+
ChangeArtifact,
|
|
26
|
+
ChangeLifecycle,
|
|
27
|
+
ChangeTouches,
|
|
28
|
+
Finding,
|
|
29
|
+
PHASES,
|
|
30
|
+
Phase,
|
|
31
|
+
RecordId,
|
|
32
|
+
TestType,
|
|
33
|
+
changesDirOf,
|
|
34
|
+
changesRelOf,
|
|
35
|
+
external_exports,
|
|
36
|
+
fastGateOf,
|
|
37
|
+
fastPathOf,
|
|
38
|
+
handoffBranch,
|
|
39
|
+
isAssistantResolution,
|
|
40
|
+
loadOpenSpecSettings,
|
|
41
|
+
loadOrgPack,
|
|
42
|
+
openspecDirOf,
|
|
43
|
+
orgPackDirOf,
|
|
44
|
+
readFlow,
|
|
45
|
+
require_dist,
|
|
46
|
+
specFlowFiles,
|
|
47
|
+
toCanonicalYaml
|
|
48
|
+
} from "./chunk-S2A7YC3Q.js";
|
|
49
|
+
import {
|
|
50
|
+
writeFileAtomic
|
|
51
|
+
} from "./chunk-D3PO4DOZ.js";
|
|
52
|
+
import {
|
|
53
|
+
__toESM
|
|
54
|
+
} from "./chunk-TV45EUGM.js";
|
|
55
|
+
|
|
56
|
+
// packages/changes/dist/errors.js
|
|
57
|
+
var ChangeError = class extends Error {
|
|
58
|
+
code;
|
|
59
|
+
reasons;
|
|
60
|
+
constructor(code, message, reasons = []) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.code = code;
|
|
63
|
+
this.reasons = reasons;
|
|
64
|
+
this.name = "ChangeError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// packages/changes/dist/markdown.js
|
|
69
|
+
var SEPARATOR = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
|
|
70
|
+
function cells(line) {
|
|
71
|
+
let s = line.trim();
|
|
72
|
+
if (s.startsWith("|"))
|
|
73
|
+
s = s.slice(1);
|
|
74
|
+
if (s.endsWith("|"))
|
|
75
|
+
s = s.slice(0, -1);
|
|
76
|
+
return s.split("|").map((c) => c.trim());
|
|
77
|
+
}
|
|
78
|
+
var stripComments = (text) => text.replace(/<!--[\s\S]*?-->/g, "");
|
|
79
|
+
function readTables(text) {
|
|
80
|
+
const lines2 = stripComments(text).replace(/\r\n/g, "\n").split("\n");
|
|
81
|
+
const tables = [];
|
|
82
|
+
for (let i = 0; i + 1 < lines2.length; i++) {
|
|
83
|
+
if (!lines2[i].includes("|") || !SEPARATOR.test(lines2[i + 1]))
|
|
84
|
+
continue;
|
|
85
|
+
const header = cells(lines2[i]).map((h) => h.toLowerCase());
|
|
86
|
+
const rows = [];
|
|
87
|
+
let j = i + 2;
|
|
88
|
+
for (; j < lines2.length && lines2[j].includes("|"); j++) {
|
|
89
|
+
const v = cells(lines2[j]);
|
|
90
|
+
rows.push(Object.fromEntries(header.map((h, k) => [h, v[k] ?? ""])));
|
|
91
|
+
}
|
|
92
|
+
tables.push(rows);
|
|
93
|
+
i = j - 1;
|
|
94
|
+
}
|
|
95
|
+
return tables;
|
|
96
|
+
}
|
|
97
|
+
function tableWith(text, headers) {
|
|
98
|
+
return readTables(text).find((t) => t.length > 0 && headers.every((h) => h in t[0])) ?? [];
|
|
99
|
+
}
|
|
100
|
+
function isFilled(text) {
|
|
101
|
+
const lines2 = stripComments(text).replace(/\r\n/g, "\n").split("\n");
|
|
102
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
103
|
+
const line = lines2[i].trim();
|
|
104
|
+
if (line === "" || line.startsWith("#"))
|
|
105
|
+
continue;
|
|
106
|
+
if (SEPARATOR.test(line))
|
|
107
|
+
continue;
|
|
108
|
+
if (line.includes("|") && i + 1 < lines2.length && SEPARATOR.test(lines2[i + 1]))
|
|
109
|
+
continue;
|
|
110
|
+
if (line.includes("|") && cells(line).every((c) => c === ""))
|
|
111
|
+
continue;
|
|
112
|
+
if (/^([-*+]|\d+\.)(\s+\[[ xX]?\])?\s*$/.test(line))
|
|
113
|
+
continue;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// packages/changes/dist/policy.js
|
|
120
|
+
var import_yaml = __toESM(require_dist(), 1);
|
|
121
|
+
import { readFile } from "node:fs/promises";
|
|
122
|
+
import { join } from "node:path";
|
|
123
|
+
var CHANGE_POLICY_FILE = "process/change-policy.yaml";
|
|
124
|
+
var Ids = external_exports.array(RecordId);
|
|
125
|
+
var DEFAULT_ARTIFACT_PHASES = {
|
|
126
|
+
brief: "define",
|
|
127
|
+
proposal: "define",
|
|
128
|
+
specs: "define",
|
|
129
|
+
interfaces: "design",
|
|
130
|
+
mappings: "design",
|
|
131
|
+
data: "design",
|
|
132
|
+
design: "design",
|
|
133
|
+
ux: "design",
|
|
134
|
+
stories: "plan",
|
|
135
|
+
tasks: "plan",
|
|
136
|
+
snow: "plan"
|
|
137
|
+
};
|
|
138
|
+
var StageStep = external_exports.strictObject({
|
|
139
|
+
stage: RecordId,
|
|
140
|
+
phase: Phase,
|
|
141
|
+
/** The stage's loop state, relative to the change folder (ADR 0087, `stage/<stage>.yaml`). */
|
|
142
|
+
loop: external_exports.string().min(1),
|
|
143
|
+
/** The command that runs it; `<change>` stands for the change's id. */
|
|
144
|
+
command: external_exports.string().min(1),
|
|
145
|
+
/** Files it writes, relative to the change folder; a folder ends in "/". They ride with the hand-off. */
|
|
146
|
+
writes: external_exports.array(external_exports.string().min(1)).default([]),
|
|
147
|
+
/** The change type's artifacts of the phase that this stage writes: on the stage path, only these are due. */
|
|
148
|
+
artifacts: external_exports.array(ChangeArtifact).default([])
|
|
149
|
+
});
|
|
150
|
+
var DEFAULT_STAGES = [
|
|
151
|
+
{
|
|
152
|
+
stage: "feature",
|
|
153
|
+
phase: "define",
|
|
154
|
+
loop: "stage/feature.yaml",
|
|
155
|
+
command: "atticus change feature <change>",
|
|
156
|
+
writes: ["feature.yaml", "feature.md", "design/"],
|
|
157
|
+
artifacts: []
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
stage: "architecture",
|
|
161
|
+
phase: "design",
|
|
162
|
+
loop: "stage/architecture.yaml",
|
|
163
|
+
command: "atticus change architecture <change>",
|
|
164
|
+
writes: ["target-flows/"],
|
|
165
|
+
artifacts: ["design", "data"]
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
stage: "stories",
|
|
169
|
+
phase: "plan",
|
|
170
|
+
loop: "stage/stories.yaml",
|
|
171
|
+
command: "atticus plan stories <change>",
|
|
172
|
+
writes: ["requests.md", "stage/stories-answer.yaml"],
|
|
173
|
+
artifacts: ["stories", "tasks"]
|
|
174
|
+
}
|
|
175
|
+
];
|
|
176
|
+
var Privacy = external_exports.strictObject({
|
|
177
|
+
role: RecordId.default("privacy-officer"),
|
|
178
|
+
classifications: external_exports.array(external_exports.string().min(1)).default(["personal", "pii", "sensitive-personal", "special-category"]),
|
|
179
|
+
/** The first of these phases the change type has; a type with none uses its last phase. */
|
|
180
|
+
phases: external_exports.array(Phase).min(1).default(["design"])
|
|
181
|
+
});
|
|
182
|
+
var SIGNAL_FACTORS = [
|
|
183
|
+
"outside-call",
|
|
184
|
+
"auth",
|
|
185
|
+
"personal-data",
|
|
186
|
+
"schema",
|
|
187
|
+
"breaking",
|
|
188
|
+
"spread"
|
|
189
|
+
];
|
|
190
|
+
var DEFAULT_WEIGHTS = {
|
|
191
|
+
extraSystem: 1,
|
|
192
|
+
dataStore: 2,
|
|
193
|
+
vendor: 3,
|
|
194
|
+
interfaceModify: 1,
|
|
195
|
+
interfaceRetire: 2,
|
|
196
|
+
outsideCall: 3,
|
|
197
|
+
auth: 3,
|
|
198
|
+
personalData: 2,
|
|
199
|
+
schema: 2,
|
|
200
|
+
breaking: 3,
|
|
201
|
+
extraRepo: 1
|
|
202
|
+
};
|
|
203
|
+
var AUTH_WORDS = [
|
|
204
|
+
"401",
|
|
205
|
+
"403",
|
|
206
|
+
"auth",
|
|
207
|
+
"authn",
|
|
208
|
+
"authz",
|
|
209
|
+
"authenticate",
|
|
210
|
+
"authentication",
|
|
211
|
+
"authorise",
|
|
212
|
+
"authorize",
|
|
213
|
+
"authorisation",
|
|
214
|
+
"authorization",
|
|
215
|
+
"unauthorised",
|
|
216
|
+
"unauthorized",
|
|
217
|
+
"forbidden",
|
|
218
|
+
"permission",
|
|
219
|
+
"permissions",
|
|
220
|
+
"role",
|
|
221
|
+
"roles",
|
|
222
|
+
"scope",
|
|
223
|
+
"scopes",
|
|
224
|
+
"entitlement",
|
|
225
|
+
"entitlements",
|
|
226
|
+
"acl",
|
|
227
|
+
"rbac"
|
|
228
|
+
];
|
|
229
|
+
var PERSONAL_WORDS = [
|
|
230
|
+
"consent",
|
|
231
|
+
"consents",
|
|
232
|
+
"pii",
|
|
233
|
+
"personal",
|
|
234
|
+
"ssn",
|
|
235
|
+
"passport",
|
|
236
|
+
"dob",
|
|
237
|
+
"birth",
|
|
238
|
+
"birthdate",
|
|
239
|
+
"email",
|
|
240
|
+
"phone",
|
|
241
|
+
"nationality",
|
|
242
|
+
"gender",
|
|
243
|
+
"health",
|
|
244
|
+
"biometric",
|
|
245
|
+
"salary"
|
|
246
|
+
];
|
|
247
|
+
var DEFAULT_FACTOR_TESTS = {
|
|
248
|
+
"outside-call": ["contract", "integration"],
|
|
249
|
+
auth: ["security"],
|
|
250
|
+
"personal-data": ["security"],
|
|
251
|
+
schema: ["data-migration", "integration"],
|
|
252
|
+
breaking: ["contract", "regression"],
|
|
253
|
+
spread: ["integration"]
|
|
254
|
+
};
|
|
255
|
+
var Risk = external_exports.strictObject({
|
|
256
|
+
typeWeights: external_exports.record(RecordId, external_exports.number().int().min(0)).default({ "emergency-fix": 3, feature: 2, integration: 2 }),
|
|
257
|
+
defaultTypeWeight: external_exports.number().int().min(0).default(1),
|
|
258
|
+
weights: external_exports.strictObject({
|
|
259
|
+
/** Per system touched beyond the first. */
|
|
260
|
+
extraSystem: external_exports.number().int().min(0).default(1),
|
|
261
|
+
dataStore: external_exports.number().int().min(0).default(2),
|
|
262
|
+
/** Once, when any touched system is a vendor. */
|
|
263
|
+
vendor: external_exports.number().int().min(0).default(3),
|
|
264
|
+
interfaceModify: external_exports.number().int().min(0).default(1),
|
|
265
|
+
interfaceRetire: external_exports.number().int().min(0).default(2),
|
|
266
|
+
// What the stages' deltas do (ADR 0124 addendum B-change): each factor counts once however
|
|
267
|
+
// many steps raise it, and cites every step. Optional and additive (still version 1).
|
|
268
|
+
/** A step calls a system outside the estate: a vendor, a model provider. */
|
|
269
|
+
outsideCall: external_exports.number().int().min(0).default(3),
|
|
270
|
+
/** A step changes who may do what: an authorisation or authentication rule. */
|
|
271
|
+
auth: external_exports.number().int().min(0).default(3),
|
|
272
|
+
/** A step reads or writes personal, consent or classified data. */
|
|
273
|
+
personalData: external_exports.number().int().min(0).default(2),
|
|
274
|
+
/** A step adds a table, columns or a migration. */
|
|
275
|
+
schema: external_exports.number().int().min(0).default(2),
|
|
276
|
+
/** A step breaks who calls or reads it today. */
|
|
277
|
+
breaking: external_exports.number().int().min(0).default(3),
|
|
278
|
+
/** Per repo the change spans beyond the first: the deploy order matters. */
|
|
279
|
+
extraRepo: external_exports.number().int().min(0).default(1)
|
|
280
|
+
}).default({ ...DEFAULT_WEIGHTS }),
|
|
281
|
+
/**
|
|
282
|
+
* The words that mark a delta step as an authorisation change or as personal data (B-change).
|
|
283
|
+
* A step's words, field names, table and column names and file paths are split into words and
|
|
284
|
+
* compared whole. Optional and additive; a list a team writes replaces the built-in one.
|
|
285
|
+
*/
|
|
286
|
+
signals: external_exports.strictObject({
|
|
287
|
+
auth: external_exports.array(external_exports.string().min(1)).default([...AUTH_WORDS]),
|
|
288
|
+
personal: external_exports.array(external_exports.string().min(1)).default([...PERSONAL_WORDS])
|
|
289
|
+
}).default({ auth: [...AUTH_WORDS], personal: [...PERSONAL_WORDS] }),
|
|
290
|
+
/**
|
|
291
|
+
* The test kinds each factor asks on top of the test strategy's row (B-change). Optional and
|
|
292
|
+
* additive; a factor a team lists replaces the built-in kinds for it.
|
|
293
|
+
*/
|
|
294
|
+
tests: external_exports.partialRecord(external_exports.enum(SIGNAL_FACTORS), external_exports.array(TestType).min(1)).default({}).transform((t) => ({ ...DEFAULT_FACTOR_TESTS, ...t })),
|
|
295
|
+
/** Lowest score of each level; below medium is low. */
|
|
296
|
+
levels: external_exports.strictObject({
|
|
297
|
+
medium: external_exports.number().int().min(1).default(4),
|
|
298
|
+
high: external_exports.number().int().min(1).default(7)
|
|
299
|
+
}).refine((l) => l.high > l.medium, "high must be above medium").default({ medium: 4, high: 7 }),
|
|
300
|
+
/** Extra approver roles by risk level and phase. */
|
|
301
|
+
approvals: external_exports.partialRecord(external_exports.enum(["low", "medium", "high"]), external_exports.partialRecord(Phase, Ids.min(1))).default({ high: { release: ["architect"] } })
|
|
302
|
+
});
|
|
303
|
+
var SelfApproval = external_exports.strictObject({
|
|
304
|
+
/** Change types this applies to; empty means all. */
|
|
305
|
+
types: Ids.default([]),
|
|
306
|
+
phases: external_exports.array(Phase).min(1),
|
|
307
|
+
roles: Ids.min(1)
|
|
308
|
+
}).superRefine((s, ctx) => {
|
|
309
|
+
if (s.phases.includes("release"))
|
|
310
|
+
ctx.addIssue({
|
|
311
|
+
code: "custom",
|
|
312
|
+
path: ["phases"],
|
|
313
|
+
message: "nobody may approve their own change at release"
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
var ChangePolicy = external_exports.strictObject({
|
|
317
|
+
schemaVersion: external_exports.literal(1),
|
|
318
|
+
artifactPhases: external_exports.partialRecord(ChangeArtifact, Phase).default({}).transform((p) => ({ ...DEFAULT_ARTIFACT_PHASES, ...p })),
|
|
319
|
+
privacy: Privacy.default({
|
|
320
|
+
role: "privacy-officer",
|
|
321
|
+
classifications: ["personal", "pii", "sensitive-personal", "special-category"],
|
|
322
|
+
phases: ["design"]
|
|
323
|
+
}),
|
|
324
|
+
risk: Risk.default(() => Risk.parse({})),
|
|
325
|
+
/** The stage path (ADR 0087); a team's list replaces the built-in one. */
|
|
326
|
+
stages: external_exports.array(StageStep).default(() => DEFAULT_STAGES.map((x) => ({ ...x }))).refine((xs) => new Set(xs.map((x) => x.phase)).size === xs.length, "one stage per phase"),
|
|
327
|
+
/**
|
|
328
|
+
* At most this many questions go to a person when a stage stops (ADR 0117 addendum, U3); the
|
|
329
|
+
* rest wait and are asked once those are answered. Optional and additive (still version 1).
|
|
330
|
+
*/
|
|
331
|
+
questionsPerStop: external_exports.number().int().min(1).default(5),
|
|
332
|
+
/**
|
|
333
|
+
* How many times a stage's drafter may check its own draft with the stage's checks before it
|
|
334
|
+
* is handed in as it is (ADR 0087 addendum, W3a). Optional and additive (still version 1).
|
|
335
|
+
*/
|
|
336
|
+
draftSelfChecks: external_exports.number().int().min(1).default(3),
|
|
337
|
+
/** By default the product owner may approve the Define hand-off of their own brief. */
|
|
338
|
+
selfApproval: external_exports.array(SelfApproval).default([{ types: [], phases: ["define"], roles: ["product-owner"] }])
|
|
339
|
+
});
|
|
340
|
+
var DEFAULT_CHANGE_POLICY = ChangePolicy.parse({ schemaVersion: 1 });
|
|
341
|
+
async function loadChangePolicy(orgPackDir) {
|
|
342
|
+
let text;
|
|
343
|
+
try {
|
|
344
|
+
text = await readFile(join(orgPackDir, CHANGE_POLICY_FILE), "utf8");
|
|
345
|
+
} catch {
|
|
346
|
+
return { policy: DEFAULT_CHANGE_POLICY, problems: [] };
|
|
347
|
+
}
|
|
348
|
+
let data;
|
|
349
|
+
try {
|
|
350
|
+
data = (0, import_yaml.parse)(text);
|
|
351
|
+
} catch (e) {
|
|
352
|
+
return {
|
|
353
|
+
policy: DEFAULT_CHANGE_POLICY,
|
|
354
|
+
problems: [{ file: CHANGE_POLICY_FILE, message: `can't read YAML: ${e.message}` }]
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const r = ChangePolicy.safeParse(data);
|
|
358
|
+
if (r.success)
|
|
359
|
+
return { policy: r.data, problems: [] };
|
|
360
|
+
return {
|
|
361
|
+
policy: DEFAULT_CHANGE_POLICY,
|
|
362
|
+
problems: r.error.issues.map((i) => ({
|
|
363
|
+
file: CHANGE_POLICY_FILE,
|
|
364
|
+
message: `${i.path.map(String).join(".") || "(file)"}: ${i.message}`
|
|
365
|
+
}))
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function stagePathOf(policy) {
|
|
369
|
+
return policy.stages;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// packages/changes/dist/templates.js
|
|
373
|
+
var import_yaml2 = __toESM(require_dist(), 1);
|
|
374
|
+
import { readdir, readFile as readFile2 } from "node:fs/promises";
|
|
375
|
+
import { join as join2 } from "node:path";
|
|
376
|
+
var TemplateMeta = external_exports.strictObject({
|
|
377
|
+
id: RecordId,
|
|
378
|
+
name: external_exports.string().min(1),
|
|
379
|
+
/** The change type this template is normally used with. */
|
|
380
|
+
changeType: RecordId,
|
|
381
|
+
description: external_exports.string().min(1).optional()
|
|
382
|
+
});
|
|
383
|
+
var TEMPLATE_VARIABLES = ["title", "id", "type", "by", "date"];
|
|
384
|
+
var intro = "<!-- Brief for {{id}} ({{type}}), started by {{by}} on {{date}}. Replace every comment with your own words; the proposal is drafted from this file. -->";
|
|
385
|
+
function skeleton(sections) {
|
|
386
|
+
return ["# {{title}}", "", intro, ""].concat(...sections.map(([h, c]) => [`## ${h}`, "", `<!-- ${c} -->`, ""])).join("\n").replace(/\n+$/, "") + "\n";
|
|
387
|
+
}
|
|
388
|
+
var builtIn = (id, name, changeType, description, sections) => ({
|
|
389
|
+
id,
|
|
390
|
+
name,
|
|
391
|
+
changeType,
|
|
392
|
+
description,
|
|
393
|
+
body: skeleton(sections),
|
|
394
|
+
source: "built-in"
|
|
395
|
+
});
|
|
396
|
+
var BUILT_IN_TEMPLATES = [
|
|
397
|
+
builtIn("feature", "New feature", "feature", "Something new for customers or users.", [
|
|
398
|
+
["Problem", "Who has the problem, what it costs them today, and why now."],
|
|
399
|
+
["Outcome", "What is true when this is done, in the customer's or user's words."],
|
|
400
|
+
["Scope", "In scope, and what is out of scope."],
|
|
401
|
+
["Systems", "Systems you expect to be involved, by name or catalog id. Leave it if unsure."],
|
|
402
|
+
["Success measures", "How we will know it worked."],
|
|
403
|
+
["Open questions", "Anything you don't know yet, and who might."]
|
|
404
|
+
]),
|
|
405
|
+
builtIn("defect-fix", "Defect fix", "bug-fix", "Something that should work and doesn't.", [
|
|
406
|
+
["Defect", "What goes wrong, for whom, and since when."],
|
|
407
|
+
["Steps to reproduce", "Numbered steps, with the environment."],
|
|
408
|
+
["Expected and actual", "What should happen, and what happens instead."],
|
|
409
|
+
["Impact", "How many people or transactions, and any workaround."],
|
|
410
|
+
["Ticket", "The work-tracker or service-desk ticket."]
|
|
411
|
+
]),
|
|
412
|
+
builtIn("emergency-fix", "Emergency fix", "emergency-fix", "A fix that ships before the usual reviews; the skipped lenses run after release.", [
|
|
413
|
+
["Incident", "The incident or alert, when it started, and its ticket."],
|
|
414
|
+
["Impact", "Who is affected right now, and how badly."],
|
|
415
|
+
["Fix", "The smallest change that stops the harm."],
|
|
416
|
+
["Rollback", "How to undo the fix if it makes things worse."],
|
|
417
|
+
["Follow-up", "What the post-release review should look at."]
|
|
418
|
+
]),
|
|
419
|
+
builtIn("small-change", "Small change", "small-change", "A small change: one step from brief to tasks, checked and criticised at one gate (ADR 0113).", [
|
|
420
|
+
["Change", "What changes, for whom, in two or three sentences."],
|
|
421
|
+
["Where", "The flow, screen, endpoint or table it is in, if you know. Leave it if unsure."],
|
|
422
|
+
["Done when", "What a test can check when it is done."]
|
|
423
|
+
]),
|
|
424
|
+
builtIn("tech-debt", "Technical debt", "tech-debt", "Work that makes later change cheaper.", [
|
|
425
|
+
["Debt", "What is wrong with the code, design or platform today."],
|
|
426
|
+
["Cost of leaving it", "What it slows down or puts at risk, with examples."],
|
|
427
|
+
["Proposed change", "What you would change, and what stays the same for users."],
|
|
428
|
+
["Risk", "What could break, and how you will know."]
|
|
429
|
+
]),
|
|
430
|
+
builtIn("vulnerability-fix", "Vulnerability fix", "bug-fix", "A security weakness to close. Keep exploit details out of the brief.", [
|
|
431
|
+
["Vulnerability", "The advisory or scan id and a one-line description. No exploit details."],
|
|
432
|
+
["Affected components", "Components, versions and environments affected."],
|
|
433
|
+
["Severity", "The severity and the deadline that comes with it."],
|
|
434
|
+
["Fix", "Upgrade, patch or configuration change."],
|
|
435
|
+
["Verification", "How we prove it is closed, such as a rescan."]
|
|
436
|
+
]),
|
|
437
|
+
builtIn("config-flag", "Configuration or feature flag", "config-change", "A setting or flag changes; no code changes.", [
|
|
438
|
+
["Setting or flag", "Its name and where it lives."],
|
|
439
|
+
["Values", "Current and new value in each environment."],
|
|
440
|
+
["Why", "What the change achieves."],
|
|
441
|
+
["Rollback", "How to put the old value back, and how fast."]
|
|
442
|
+
]),
|
|
443
|
+
builtIn("data-fix", "Data fix", "bug-fix", "Stored data is wrong and must be corrected.", [
|
|
444
|
+
["Data problem", "What is wrong in which data store, and how it happened."],
|
|
445
|
+
["Records affected", "How many records, and how they are found."],
|
|
446
|
+
["Correction", "What the corrected data looks like, and the script or tool."],
|
|
447
|
+
["Verification", "How we check the correction, before and after."],
|
|
448
|
+
["Data owner", "Who owns the data and must approve the fix."]
|
|
449
|
+
]),
|
|
450
|
+
builtIn("decommission", "Decommission", "integration", "A system, interface or data store retires.", [
|
|
451
|
+
["What retires", "Systems, interfaces or data stores, by catalog id."],
|
|
452
|
+
["Consumers", "Who uses it today, from the catalog, and what they move to."],
|
|
453
|
+
["Replacement", "What replaces it, if anything."],
|
|
454
|
+
["Data", "What happens to the data: migrate, archive or delete, with retention."],
|
|
455
|
+
["Timeline", "Notice to consumers, switch-off and clean-up dates."]
|
|
456
|
+
])
|
|
457
|
+
];
|
|
458
|
+
async function loadTemplates(orgPackDir) {
|
|
459
|
+
const byId = new Map(BUILT_IN_TEMPLATES.map((t) => [t.id, t]));
|
|
460
|
+
const problems = [];
|
|
461
|
+
const dir = join2(orgPackDir, "process", "change-templates");
|
|
462
|
+
let names = [];
|
|
463
|
+
try {
|
|
464
|
+
names = (await readdir(dir)).filter((n) => n.endsWith(".md")).sort();
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
for (const name of names) {
|
|
468
|
+
const file = `process/change-templates/${name}`;
|
|
469
|
+
const text = (await readFile2(join2(dir, name), "utf8")).replace(/\r\n/g, "\n");
|
|
470
|
+
const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(text);
|
|
471
|
+
if (!m) {
|
|
472
|
+
problems.push({ file, message: "a change template starts with YAML front matter" });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
let meta;
|
|
476
|
+
try {
|
|
477
|
+
meta = (0, import_yaml2.parse)(m[1]);
|
|
478
|
+
} catch (e) {
|
|
479
|
+
problems.push({ file, message: `can't read the front matter: ${e.message}` });
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const r = TemplateMeta.safeParse(meta);
|
|
483
|
+
if (!r.success) {
|
|
484
|
+
for (const i of r.error.issues)
|
|
485
|
+
problems.push({ file, message: `${i.path.join(".") || "(front matter)"}: ${i.message}` });
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (`${r.data.id}.md` !== name) {
|
|
489
|
+
problems.push({ file, message: `says id ${r.data.id}; name the file ${r.data.id}.md` });
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const unknown = [...m[2].matchAll(/\{\{\s*([^}]*?)\s*\}\}/g)].map((x) => x[1]).filter((v) => !TEMPLATE_VARIABLES.includes(v));
|
|
493
|
+
if (unknown.length > 0) {
|
|
494
|
+
problems.push({
|
|
495
|
+
file,
|
|
496
|
+
message: `unknown placeholder ${unknown.map((u) => `{{${u}}}`).join(", ")}; use ${TEMPLATE_VARIABLES.map((v) => `{{${v}}}`).join(", ")}`
|
|
497
|
+
});
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
byId.set(r.data.id, { ...r.data, body: m[2].replace(/^\n+/, ""), source: file });
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
templates: [...byId.values()].sort((a, b) => a.id.localeCompare(b.id)),
|
|
504
|
+
problems
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function renderTemplate(t, vars) {
|
|
508
|
+
return t.body.replace(/\{\{\s*(title|id|type|by|date)\s*\}\}/g, (_, k) => vars[k]);
|
|
509
|
+
}
|
|
510
|
+
function pickTemplate(templates, type, wanted) {
|
|
511
|
+
if (wanted)
|
|
512
|
+
return templates.find((t) => t.id === wanted);
|
|
513
|
+
return templates.find((t) => t.id === type) ?? templates.find((t) => t.changeType === type) ?? templates.find((t) => t.id === "feature");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// packages/changes/dist/store.js
|
|
517
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
518
|
+
import { readdir as readdir2, readFile as readFile3 } from "node:fs/promises";
|
|
519
|
+
import { join as join3 } from "node:path";
|
|
520
|
+
var CHANGES_DIR = "openspec/changes";
|
|
521
|
+
var CHANGE_FILE = "change.yaml";
|
|
522
|
+
var LEADING = [
|
|
523
|
+
"schemaVersion",
|
|
524
|
+
"id",
|
|
525
|
+
"title",
|
|
526
|
+
"type",
|
|
527
|
+
"template",
|
|
528
|
+
"phase",
|
|
529
|
+
"state",
|
|
530
|
+
"from",
|
|
531
|
+
"to"
|
|
532
|
+
];
|
|
533
|
+
function changeFolder(root, id) {
|
|
534
|
+
return join3(changesDirOf(root), id);
|
|
535
|
+
}
|
|
536
|
+
function changeFilePath(root, id) {
|
|
537
|
+
return join3(changeFolder(root, id), CHANGE_FILE);
|
|
538
|
+
}
|
|
539
|
+
var ChangeFileError = class extends Error {
|
|
540
|
+
path;
|
|
541
|
+
constructor(message, path) {
|
|
542
|
+
super(message);
|
|
543
|
+
this.path = path;
|
|
544
|
+
this.name = "ChangeFileError";
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
var describe = (issues) => issues.map((i) => `${i.path.map(String).join(".") || "(file)"}: ${i.message}`).join("; ");
|
|
548
|
+
function serializeChange(value) {
|
|
549
|
+
const r = ChangeLifecycle.safeParse(value);
|
|
550
|
+
if (!r.success)
|
|
551
|
+
throw new Error(`change ${value.id}: ${describe(r.error.issues)}`);
|
|
552
|
+
return toCanonicalYaml(r.data, LEADING);
|
|
553
|
+
}
|
|
554
|
+
function changeStore(root) {
|
|
555
|
+
async function tryRead(id) {
|
|
556
|
+
const path = changeFilePath(root, id);
|
|
557
|
+
let text;
|
|
558
|
+
try {
|
|
559
|
+
text = await readFile3(path, "utf8");
|
|
560
|
+
} catch {
|
|
561
|
+
return void 0;
|
|
562
|
+
}
|
|
563
|
+
let data;
|
|
564
|
+
try {
|
|
565
|
+
data = (0, import_yaml3.parse)(text);
|
|
566
|
+
} catch (e) {
|
|
567
|
+
throw new ChangeFileError(`${path}: invalid YAML: ${e.message}`, path);
|
|
568
|
+
}
|
|
569
|
+
const r = ChangeLifecycle.safeParse(data);
|
|
570
|
+
if (!r.success)
|
|
571
|
+
throw new ChangeFileError(`${path}: ${describe(r.error.issues)}`, path);
|
|
572
|
+
if (r.data.id !== id)
|
|
573
|
+
throw new ChangeFileError(`${path}: says change ${r.data.id}, expected ${id}`, path);
|
|
574
|
+
return r.data;
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
root,
|
|
578
|
+
tryRead,
|
|
579
|
+
async read(id) {
|
|
580
|
+
const c = await tryRead(id);
|
|
581
|
+
if (!c) {
|
|
582
|
+
const path = changeFilePath(root, id);
|
|
583
|
+
throw new ChangeFileError(`No change ${id}: ${path} doesn't exist.`, path);
|
|
584
|
+
}
|
|
585
|
+
return c;
|
|
586
|
+
},
|
|
587
|
+
async write(value) {
|
|
588
|
+
const path = changeFilePath(root, value.id);
|
|
589
|
+
await writeFileAtomic(path, serializeChange(value));
|
|
590
|
+
return path;
|
|
591
|
+
},
|
|
592
|
+
async list() {
|
|
593
|
+
let names;
|
|
594
|
+
try {
|
|
595
|
+
names = (await readdir2(changesDirOf(root), { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== "archive").map((d) => d.name).sort();
|
|
596
|
+
} catch {
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
const out = [];
|
|
600
|
+
for (const n of names) {
|
|
601
|
+
const c = await tryRead(n);
|
|
602
|
+
if (c)
|
|
603
|
+
out.push(c);
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// packages/changes/dist/codeowners.js
|
|
611
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
612
|
+
import { join as join4 } from "node:path";
|
|
613
|
+
var CODEOWNERS_PATHS = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"];
|
|
614
|
+
function toRegex(pattern) {
|
|
615
|
+
let p = pattern;
|
|
616
|
+
const dirOnly = p.endsWith("/");
|
|
617
|
+
if (dirOnly)
|
|
618
|
+
p = p.slice(0, -1);
|
|
619
|
+
let anchored = p.includes("/");
|
|
620
|
+
if (p.startsWith("/")) {
|
|
621
|
+
anchored = true;
|
|
622
|
+
p = p.slice(1);
|
|
623
|
+
}
|
|
624
|
+
let body = "";
|
|
625
|
+
for (let i = 0; i < p.length; i++) {
|
|
626
|
+
const ch = p[i];
|
|
627
|
+
if (p.startsWith("**/", i)) {
|
|
628
|
+
body += "(?:.*/)?";
|
|
629
|
+
i += 2;
|
|
630
|
+
} else if (p.startsWith("/**", i) && i + 3 === p.length) {
|
|
631
|
+
body += "(?:/.*)?";
|
|
632
|
+
i += 2;
|
|
633
|
+
} else if (p.startsWith("**", i)) {
|
|
634
|
+
body += ".*";
|
|
635
|
+
i += 1;
|
|
636
|
+
} else if (ch === "*")
|
|
637
|
+
body += "[^/]*";
|
|
638
|
+
else if (ch === "?")
|
|
639
|
+
body += "[^/]";
|
|
640
|
+
else
|
|
641
|
+
body += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
642
|
+
}
|
|
643
|
+
return new RegExp(`${anchored ? "^" : "^(?:.*/)?"}${body}${dirOnly ? "/.*$" : "(?:/.*)?$"}`);
|
|
644
|
+
}
|
|
645
|
+
function parseCodeowners(text) {
|
|
646
|
+
const rules = [];
|
|
647
|
+
for (const raw of text.replace(/\r\n/g, "\n").split("\n")) {
|
|
648
|
+
const line = raw.replace(/(^|\s)#.*$/, "").trim();
|
|
649
|
+
if (!line)
|
|
650
|
+
continue;
|
|
651
|
+
const [pattern, ...owners] = line.split(/\s+/);
|
|
652
|
+
rules.push({
|
|
653
|
+
pattern,
|
|
654
|
+
regex: toRegex(pattern),
|
|
655
|
+
owners: owners.map((o) => o.startsWith("@") ? o.slice(1) : o)
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return rules;
|
|
659
|
+
}
|
|
660
|
+
function ownersFor(rules, path) {
|
|
661
|
+
for (let i = rules.length - 1; i >= 0; i--) {
|
|
662
|
+
if (rules[i].regex.test(path))
|
|
663
|
+
return [...rules[i].owners];
|
|
664
|
+
}
|
|
665
|
+
return [];
|
|
666
|
+
}
|
|
667
|
+
async function readCodeowners(root) {
|
|
668
|
+
for (const p of CODEOWNERS_PATHS) {
|
|
669
|
+
try {
|
|
670
|
+
return parseCodeowners(await readFile4(join4(root, p), "utf8"));
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return [];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// packages/changes/dist/context.js
|
|
678
|
+
import { join as join5 } from "node:path";
|
|
679
|
+
async function loadChangeContext(root) {
|
|
680
|
+
const orgPackDir = join5(orgPackDirOf(root));
|
|
681
|
+
const orgPack = await loadOrgPack(orgPackDir);
|
|
682
|
+
const policy = await loadChangePolicy(orgPackDir);
|
|
683
|
+
const templates = await loadTemplates(orgPackDir);
|
|
684
|
+
const catalog = await readCatalog(join5(root, orgPack.sources.layout.catalog, "generated"), {
|
|
685
|
+
cacheRoot: root
|
|
686
|
+
});
|
|
687
|
+
return {
|
|
688
|
+
root,
|
|
689
|
+
orgPack,
|
|
690
|
+
policy: policy.policy,
|
|
691
|
+
templates: templates.templates,
|
|
692
|
+
records: catalog.records,
|
|
693
|
+
codeowners: await readCodeowners(root),
|
|
694
|
+
problems: [
|
|
695
|
+
...orgPack.problems.map((p) => `org-pack/${p.file}: ${p.path ? `${p.path}: ` : ""}${p.message}`),
|
|
696
|
+
...policy.problems.map((p) => `org-pack/${p.file}: ${p.message}`),
|
|
697
|
+
...templates.problems.map((p) => `org-pack/${p.file}: ${p.message}`),
|
|
698
|
+
...catalog.problems.map((p) => `catalog ${p.file}: ${p.message}`)
|
|
699
|
+
]
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function changeTypeOf(ctx, type) {
|
|
703
|
+
const t = ctx.orgPack.changeTypes.types.find((x) => x.id === type);
|
|
704
|
+
if (!t) {
|
|
705
|
+
const known = ctx.orgPack.changeTypes.types.map((x) => x.id).join(", ");
|
|
706
|
+
throw new ChangeError("invalid", `Unknown change type ${type}. Use one of: ${known}.`);
|
|
707
|
+
}
|
|
708
|
+
return t;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// packages/changes/dist/footprint.js
|
|
712
|
+
var import_yaml4 = __toESM(require_dist(), 1);
|
|
713
|
+
import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
|
|
714
|
+
import { join as join6 } from "node:path";
|
|
715
|
+
function personalLevels(ctx) {
|
|
716
|
+
const pack = ctx.orgPack.pack?.classification ?? [
|
|
717
|
+
"public",
|
|
718
|
+
"internal",
|
|
719
|
+
"confidential",
|
|
720
|
+
"restricted"
|
|
721
|
+
];
|
|
722
|
+
const policy = ctx.policy.privacy.classifications;
|
|
723
|
+
if (policy.some((c) => pack.includes(c.toLowerCase())))
|
|
724
|
+
return { levels: policy, from: "change-policy" };
|
|
725
|
+
const i = pack.indexOf("internal");
|
|
726
|
+
return {
|
|
727
|
+
levels: [.../* @__PURE__ */ new Set([...policy, ...i >= 0 ? pack.slice(i + 1) : []])],
|
|
728
|
+
from: "pack-levels"
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
var STATUSES = /* @__PURE__ */ new Set(["planned-new", "planned-modify", "planned-retire"]);
|
|
732
|
+
async function read(path) {
|
|
733
|
+
try {
|
|
734
|
+
return await readFile5(path, "utf8");
|
|
735
|
+
} catch {
|
|
736
|
+
return "";
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
var cellId = (s) => s.replace(/[`*]/g, "").trim().split(/\s+/)[0].toLowerCase();
|
|
740
|
+
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9.]+/g, "-");
|
|
741
|
+
var sorted = (s) => [...new Set(s)].sort();
|
|
742
|
+
var wordsOf = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
743
|
+
function entryParts(entry) {
|
|
744
|
+
const m = /^\s*([A-Za-z]+)\s+(\S+)/.exec(entry);
|
|
745
|
+
const norm2 = (p) => p.replace(/\{[^}]*\}/g, "{}").replace(/:[A-Za-z_]\w*/g, "{}");
|
|
746
|
+
return m ? { method: m[1].toUpperCase(), path: norm2(m[2]) } : { path: norm2(entry.trim()) };
|
|
747
|
+
}
|
|
748
|
+
var str = (x) => typeof x === "string" && x ? x : void 0;
|
|
749
|
+
var strs = (x) => Array.isArray(x) ? x.filter((v) => typeof v === "string") : [];
|
|
750
|
+
var obj = (x) => x && typeof x === "object" && !Array.isArray(x) ? x : void 0;
|
|
751
|
+
var FIELD_LISTS = ["sends", "returns", "reads", "checks", "from", "to", "required"];
|
|
752
|
+
var quote = (s) => s.length > 90 ? `${s.slice(0, 87).trimEnd()}...` : s;
|
|
753
|
+
var specCache = /* @__PURE__ */ new WeakMap();
|
|
754
|
+
function specIndex(ctx) {
|
|
755
|
+
let p = specCache.get(ctx);
|
|
756
|
+
if (!p) {
|
|
757
|
+
p = (async () => {
|
|
758
|
+
const files = await specFlowFiles(ctx.root).catch(() => /* @__PURE__ */ new Map());
|
|
759
|
+
const read2 = /* @__PURE__ */ new Map();
|
|
760
|
+
const get = async (id) => {
|
|
761
|
+
if (read2.has(id))
|
|
762
|
+
return read2.get(id);
|
|
763
|
+
const f = files.get(id);
|
|
764
|
+
const flow = f ? await readFlow(ctx.root, f.repo, id).catch(() => void 0) : void 0;
|
|
765
|
+
const today = flow && f ? { repo: f.repo, flow } : void 0;
|
|
766
|
+
read2.set(id, today);
|
|
767
|
+
return today;
|
|
768
|
+
};
|
|
769
|
+
const ids = [...files.keys()];
|
|
770
|
+
return {
|
|
771
|
+
get,
|
|
772
|
+
hasComponent: async (id) => {
|
|
773
|
+
for (const flowId of ids) {
|
|
774
|
+
if (!flowId.startsWith(`${id}.`))
|
|
775
|
+
continue;
|
|
776
|
+
if ((await get(flowId))?.flow.component === id)
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
return false;
|
|
780
|
+
},
|
|
781
|
+
hasRepo: async (id) => {
|
|
782
|
+
for (const flowId of ids)
|
|
783
|
+
if (files.get(flowId).repo === id && await get(flowId))
|
|
784
|
+
return true;
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
})();
|
|
789
|
+
specCache.set(ctx, p);
|
|
790
|
+
}
|
|
791
|
+
return p;
|
|
792
|
+
}
|
|
793
|
+
async function changeFootprint(ctx, change) {
|
|
794
|
+
const dir = changeFolder(ctx.root, change.id);
|
|
795
|
+
const byId = new Map(ctx.records.map((r) => [r.id, r]));
|
|
796
|
+
const get = (kind, id) => {
|
|
797
|
+
const r = byId.get(id);
|
|
798
|
+
return r?.kind === kind ? r : void 0;
|
|
799
|
+
};
|
|
800
|
+
const profileSystems = new Map(ctx.orgPack.profiles.flatMap((p) => p.profile?.systems ?? []).map((s) => [s.id, s]));
|
|
801
|
+
const isSystem = (id) => get("System", id) !== void 0 || profileSystems.has(id);
|
|
802
|
+
const systems = new Set(change.systems);
|
|
803
|
+
const components = /* @__PURE__ */ new Set();
|
|
804
|
+
const interfaces = /* @__PURE__ */ new Map();
|
|
805
|
+
const dataStores = new Set(change.touches.dataStores);
|
|
806
|
+
const tables = new Set(change.touches.tables);
|
|
807
|
+
const repos = new Set(change.touches.repos);
|
|
808
|
+
const from = /* @__PURE__ */ new Set();
|
|
809
|
+
const ref = (id) => {
|
|
810
|
+
const c = get("Component", id);
|
|
811
|
+
if (c) {
|
|
812
|
+
components.add(c.id);
|
|
813
|
+
systems.add(c.system);
|
|
814
|
+
} else if (isSystem(id))
|
|
815
|
+
systems.add(id);
|
|
816
|
+
};
|
|
817
|
+
const proposal = await read(join6(dir, "proposal.md"));
|
|
818
|
+
const proposed = findSystemsTable(proposal);
|
|
819
|
+
if (proposed.length)
|
|
820
|
+
from.add("proposal.md");
|
|
821
|
+
for (const id of proposed)
|
|
822
|
+
if (isSystem(id))
|
|
823
|
+
systems.add(id);
|
|
824
|
+
for (const i of change.touches.interfaces)
|
|
825
|
+
interfaces.set(i.id, i.status);
|
|
826
|
+
const hops = tableWith(await read(join6(dir, "interfaces.md")), ["interface id"]);
|
|
827
|
+
if (hops.length)
|
|
828
|
+
from.add("interfaces.md");
|
|
829
|
+
for (const row of hops) {
|
|
830
|
+
const id = cellId(row["interface id"] ?? "");
|
|
831
|
+
const status = cellId(row.status ?? "");
|
|
832
|
+
for (const end of [row.caller, row.callee])
|
|
833
|
+
if (end)
|
|
834
|
+
ref(cellId(end));
|
|
835
|
+
if (get("Interface", id))
|
|
836
|
+
interfaces.set(id, STATUSES.has(status) ? status : "planned-modify");
|
|
837
|
+
}
|
|
838
|
+
const rows = tableWith(await read(join6(dir, "data.md")), ["data store", "table"]);
|
|
839
|
+
if (rows.length)
|
|
840
|
+
from.add("data.md");
|
|
841
|
+
for (const row of rows) {
|
|
842
|
+
const store = cellId(row["data store"] ?? "");
|
|
843
|
+
const table = cellId(row.table ?? "");
|
|
844
|
+
if (store)
|
|
845
|
+
dataStores.add(store);
|
|
846
|
+
if (table)
|
|
847
|
+
tables.add(table.includes(".") ? slug(table) : `${store}.${slug(table)}`);
|
|
848
|
+
}
|
|
849
|
+
const staged = await stageFootprint(ctx, dir, { get, isSystem, profileSystems, ref });
|
|
850
|
+
for (const f of staged.from)
|
|
851
|
+
from.add(f);
|
|
852
|
+
for (const r of staged.repos)
|
|
853
|
+
repos.add(r);
|
|
854
|
+
for (const t of staged.tables)
|
|
855
|
+
tables.add(t);
|
|
856
|
+
for (const [id, status] of staged.interfaces)
|
|
857
|
+
if (!interfaces.has(id))
|
|
858
|
+
interfaces.set(id, status);
|
|
859
|
+
const flows = [...staged.flows];
|
|
860
|
+
if (change.touches.records.length) {
|
|
861
|
+
const specs = await specIndex(ctx);
|
|
862
|
+
for (const id of change.touches.records) {
|
|
863
|
+
if (flows.some((f) => f.id === id))
|
|
864
|
+
continue;
|
|
865
|
+
const today = await specs.get(id);
|
|
866
|
+
if (!today)
|
|
867
|
+
continue;
|
|
868
|
+
flows.push({ id, repo: today.repo, change: "modify" });
|
|
869
|
+
repos.add(today.repo);
|
|
870
|
+
}
|
|
871
|
+
flows.sort((a, b) => a.id.localeCompare(b.id));
|
|
872
|
+
}
|
|
873
|
+
for (const id of interfaces.keys()) {
|
|
874
|
+
const i = get("Interface", id);
|
|
875
|
+
if (i) {
|
|
876
|
+
ref(i.from);
|
|
877
|
+
ref(i.to);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
for (const id of tables) {
|
|
881
|
+
const t = get("Table", id);
|
|
882
|
+
if (t) {
|
|
883
|
+
dataStores.add(t.dataStore);
|
|
884
|
+
if (t.ownerComponent)
|
|
885
|
+
ref(t.ownerComponent);
|
|
886
|
+
} else {
|
|
887
|
+
const store = id.split(".")[0];
|
|
888
|
+
if (get("DataStore", store))
|
|
889
|
+
dataStores.add(store);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
for (const id of dataStores) {
|
|
893
|
+
const s = get("DataStore", id);
|
|
894
|
+
if (s?.ownerComponent)
|
|
895
|
+
ref(s.ownerComponent);
|
|
896
|
+
}
|
|
897
|
+
for (const id of components) {
|
|
898
|
+
const c = get("Component", id);
|
|
899
|
+
if (c?.repo)
|
|
900
|
+
repos.add(c.repo);
|
|
901
|
+
}
|
|
902
|
+
try {
|
|
903
|
+
const cr = await readChangeRepos(ctx.root, change.id);
|
|
904
|
+
for (const r of cr?.repos ?? [])
|
|
905
|
+
repos.add(r.id);
|
|
906
|
+
} catch {
|
|
907
|
+
}
|
|
908
|
+
const vendors = [...systems].filter((s) => (get("System", s)?.boundary ?? profileSystems.get(s)?.boundary) === "vendor");
|
|
909
|
+
const personalSet = new Set(personalLevels(ctx).levels.map((c) => c.toLowerCase()));
|
|
910
|
+
const personal = [];
|
|
911
|
+
for (const s of staged.stores) {
|
|
912
|
+
for (const [col, level] of Object.entries(s.classification))
|
|
913
|
+
if (personalSet.has(level.toLowerCase()) && !personal.some((p) => p.id === s.table && p.field === col))
|
|
914
|
+
personal.push({ id: s.table, classification: level, field: col });
|
|
915
|
+
}
|
|
916
|
+
for (const r of ctx.records) {
|
|
917
|
+
if (r.kind !== "Column" || !r.classification)
|
|
918
|
+
continue;
|
|
919
|
+
if (!tables.has(r.table) && !change.touches.records.includes(r.id))
|
|
920
|
+
continue;
|
|
921
|
+
if (personalSet.has(r.classification.toLowerCase()))
|
|
922
|
+
personal.push({ id: r.id, classification: r.classification });
|
|
923
|
+
}
|
|
924
|
+
for (const id of interfaces.keys()) {
|
|
925
|
+
const ep = get("Interface", id)?.endpoint;
|
|
926
|
+
for (const s of ep && get("Endpoint", ep)?.schemaRefs || []) {
|
|
927
|
+
for (const f of get("Schema", s)?.fields ?? []) {
|
|
928
|
+
if (f.classification && personalSet.has(f.classification.toLowerCase()))
|
|
929
|
+
personal.push({ id: s, classification: f.classification, field: f.name });
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
personal.sort((a, b) => `${a.id}#${a.field ?? ""}`.localeCompare(`${b.id}#${b.field ?? ""}`));
|
|
934
|
+
const signals = [...staged.signals];
|
|
935
|
+
for (const s of staged.stores) {
|
|
936
|
+
if (signals.some((x) => x.factor === "personal-data" && x.cite === s.cite))
|
|
937
|
+
continue;
|
|
938
|
+
const classified = s.columns.filter((c) => {
|
|
939
|
+
const level = s.classification[c] ?? get("Column", `${s.table}.${c}`)?.classification ?? void 0;
|
|
940
|
+
return level !== void 0 && personalSet.has(level.toLowerCase());
|
|
941
|
+
});
|
|
942
|
+
if (classified.length)
|
|
943
|
+
signals.push({
|
|
944
|
+
factor: "personal-data",
|
|
945
|
+
detail: `${s.kind} of ${s.table} (${classified.join(", ")} classified personal)`,
|
|
946
|
+
cite: s.cite
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
const spread = sorted(staged.repos);
|
|
950
|
+
if (spread.length > 1)
|
|
951
|
+
signals.push({
|
|
952
|
+
factor: "spread",
|
|
953
|
+
detail: `${spread.length} repos (${spread.join(", ")})`,
|
|
954
|
+
cite: staged.deployFile ?? staged.from.find((f) => f.startsWith("target-flows/")) ?? ""
|
|
955
|
+
});
|
|
956
|
+
return {
|
|
957
|
+
systems: sorted(systems),
|
|
958
|
+
components: sorted(components),
|
|
959
|
+
interfaces: [...interfaces].map(([id, status]) => ({ id, status })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
960
|
+
dataStores: sorted(dataStores),
|
|
961
|
+
tables: sorted(tables),
|
|
962
|
+
records: sorted(change.touches.records),
|
|
963
|
+
repos: sorted(repos),
|
|
964
|
+
vendors: vendors.sort(),
|
|
965
|
+
personal,
|
|
966
|
+
flows,
|
|
967
|
+
entries: sorted(staged.entries),
|
|
968
|
+
newEntries: sorted(staged.newEntries),
|
|
969
|
+
calls: sorted([...staged.calls].filter((id) => !interfaces.has(id))),
|
|
970
|
+
columns: sorted(staged.columns),
|
|
971
|
+
fields: sorted(staged.fields),
|
|
972
|
+
outside: staged.outside,
|
|
973
|
+
signals,
|
|
974
|
+
from: sorted(from)
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
async function stageFootprint(ctx, dir, h) {
|
|
978
|
+
const out = {
|
|
979
|
+
from: [],
|
|
980
|
+
flows: [],
|
|
981
|
+
repos: /* @__PURE__ */ new Set(),
|
|
982
|
+
tables: /* @__PURE__ */ new Set(),
|
|
983
|
+
columns: /* @__PURE__ */ new Set(),
|
|
984
|
+
fields: /* @__PURE__ */ new Set(),
|
|
985
|
+
entries: /* @__PURE__ */ new Set(),
|
|
986
|
+
newEntries: /* @__PURE__ */ new Set(),
|
|
987
|
+
calls: /* @__PURE__ */ new Set(),
|
|
988
|
+
interfaces: /* @__PURE__ */ new Map(),
|
|
989
|
+
outside: [],
|
|
990
|
+
stores: [],
|
|
991
|
+
signals: []
|
|
992
|
+
};
|
|
993
|
+
const files = (await readdir3(join6(dir, "target-flows")).catch(() => [])).filter((f) => f.endsWith(".yaml")).sort();
|
|
994
|
+
const featureText = await read(join6(dir, "feature.yaml"));
|
|
995
|
+
const tasksText = await read(join6(dir, "tasks.md"));
|
|
996
|
+
if (!files.length && !featureText && !tasksText)
|
|
997
|
+
return out;
|
|
998
|
+
const { get, isSystem, profileSystems, ref } = h;
|
|
999
|
+
const specs = await specIndex(ctx);
|
|
1000
|
+
const catalogRepos = /* @__PURE__ */ new Set();
|
|
1001
|
+
for (const r of ctx.records)
|
|
1002
|
+
if (r.kind === "Component" && r.repo)
|
|
1003
|
+
catalogRepos.add(r.repo);
|
|
1004
|
+
const authWords = new Set(ctx.policy.risk.signals.auth.map((w) => w.toLowerCase()));
|
|
1005
|
+
const personalWords = new Set(ctx.policy.risk.signals.personal.map((w) => w.toLowerCase()));
|
|
1006
|
+
const endpoints = ctx.records.filter((r) => r.kind === "Endpoint");
|
|
1007
|
+
const ifaces = ctx.records.filter((r) => r.kind === "Interface");
|
|
1008
|
+
const flows = /* @__PURE__ */ new Map();
|
|
1009
|
+
const outsideOf = async (id) => {
|
|
1010
|
+
const c = get("Component", id);
|
|
1011
|
+
if (c)
|
|
1012
|
+
return get("System", c.system)?.boundary === "vendor";
|
|
1013
|
+
if (isSystem(id))
|
|
1014
|
+
return (get("System", id)?.boundary ?? profileSystems.get(id)?.boundary) === "vendor";
|
|
1015
|
+
if (catalogRepos.has(id))
|
|
1016
|
+
return false;
|
|
1017
|
+
return !await specs.hasComponent(id) && !await specs.hasRepo(id);
|
|
1018
|
+
};
|
|
1019
|
+
const endpointOf = (component, entry) => {
|
|
1020
|
+
const e = entryParts(entry);
|
|
1021
|
+
return endpoints.find((x) => x.component === component && (!e.method || !x.method || x.method.toUpperCase() === e.method) && (x.normalizedPath === e.path || entryParts(x.pathOrTopic ?? "").path === e.path));
|
|
1022
|
+
};
|
|
1023
|
+
const interfaceOf = (fromC, to, entry) => {
|
|
1024
|
+
const e = entryParts(entry);
|
|
1025
|
+
return ifaces.find((x) => x.from === fromC && x.to === to && (!e.method || !x.method || x.method.toUpperCase() === e.method) && x.normalizedPath === e.path);
|
|
1026
|
+
};
|
|
1027
|
+
const outside = /* @__PURE__ */ new Map();
|
|
1028
|
+
for (const f of files) {
|
|
1029
|
+
const rel = `target-flows/${f}`;
|
|
1030
|
+
const text = await read(join6(dir, "target-flows", f));
|
|
1031
|
+
const lines2 = new import_yaml4.LineCounter();
|
|
1032
|
+
let doc;
|
|
1033
|
+
try {
|
|
1034
|
+
doc = (0, import_yaml4.parseDocument)(text, { lineCounter: lines2 });
|
|
1035
|
+
} catch {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (doc.errors.length)
|
|
1039
|
+
continue;
|
|
1040
|
+
const d = obj(doc.toJS());
|
|
1041
|
+
const flowId = str(d?.flow);
|
|
1042
|
+
if (!d || !flowId)
|
|
1043
|
+
continue;
|
|
1044
|
+
out.from.push(rel);
|
|
1045
|
+
const lineOf = (node) => {
|
|
1046
|
+
const r = node?.range;
|
|
1047
|
+
return r ? lines2.linePos(r[0]).line : void 0;
|
|
1048
|
+
};
|
|
1049
|
+
const stepNodes = (() => {
|
|
1050
|
+
const top = doc.contents;
|
|
1051
|
+
const s = (0, import_yaml4.isMap)(top) ? top.get("steps", true) : void 0;
|
|
1052
|
+
return (0, import_yaml4.isSeq)(s) ? s.items : [];
|
|
1053
|
+
})();
|
|
1054
|
+
const keyLine = (key) => {
|
|
1055
|
+
const top = doc.contents;
|
|
1056
|
+
if (!(0, import_yaml4.isMap)(top))
|
|
1057
|
+
return void 0;
|
|
1058
|
+
const pair = top.items.find((p) => p.key?.value === key);
|
|
1059
|
+
return lineOf(pair?.key);
|
|
1060
|
+
};
|
|
1061
|
+
const today = await specs.get(flowId);
|
|
1062
|
+
const change = str(d.change) ?? "modify";
|
|
1063
|
+
const repo = str(d.repo) ?? today?.repo;
|
|
1064
|
+
const prefix = flowId.split(".")[0];
|
|
1065
|
+
const component = today?.flow.component ?? (get("Component", prefix) || await specs.hasComponent(prefix) ? prefix : void 0);
|
|
1066
|
+
flows.set(flowId, { id: flowId, ...repo ? { repo } : {}, change });
|
|
1067
|
+
if (repo)
|
|
1068
|
+
out.repos.add(repo);
|
|
1069
|
+
if (component)
|
|
1070
|
+
ref(component);
|
|
1071
|
+
const trigger = today?.flow.trigger.ref;
|
|
1072
|
+
if (trigger && get("Endpoint", trigger))
|
|
1073
|
+
out.entries.add(trigger);
|
|
1074
|
+
else {
|
|
1075
|
+
const entry = str(obj(d.trigger)?.entry);
|
|
1076
|
+
if (entry && component) {
|
|
1077
|
+
const ep = endpointOf(component, entry);
|
|
1078
|
+
if (ep)
|
|
1079
|
+
out.entries.add(ep.id);
|
|
1080
|
+
else
|
|
1081
|
+
out.newEntries.add(`${component} ${entry}`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (strs(d.consumers).length || change === "remove")
|
|
1085
|
+
out.signals.push({
|
|
1086
|
+
factor: "breaking",
|
|
1087
|
+
detail: change === "remove" ? `flow ${flowId} is removed` : `flow ${flowId} breaks ${strs(d.consumers).join(", ")}`,
|
|
1088
|
+
cite: `${rel}:${keyLine(strs(d.consumers).length ? "consumers" : "change") ?? 1}`
|
|
1089
|
+
});
|
|
1090
|
+
const steps = Array.isArray(d.steps) ? d.steps : [];
|
|
1091
|
+
for (const [i, raw] of steps.entries()) {
|
|
1092
|
+
const s = obj(raw);
|
|
1093
|
+
if (!s)
|
|
1094
|
+
continue;
|
|
1095
|
+
const cite = `${rel}:${lineOf(stepNodes[i]) ?? 1}`;
|
|
1096
|
+
const op = str(s.op) ?? "add";
|
|
1097
|
+
const n = typeof s.step === "number" ? s.step : void 0;
|
|
1098
|
+
const was = op !== "add" && n ? today?.flow.steps[n - 1] : void 0;
|
|
1099
|
+
const kind = str(s.kind) ?? was?.kind ?? "step";
|
|
1100
|
+
const what = str(s.what) ?? was?.what ?? "";
|
|
1101
|
+
const live = op === "add" || op === "change";
|
|
1102
|
+
const store = obj(s.store);
|
|
1103
|
+
const table = str(store?.table) ?? was?.store?.table;
|
|
1104
|
+
if (table) {
|
|
1105
|
+
out.tables.add(table);
|
|
1106
|
+
const cols = store ? strs(store.columns) : was?.store?.columns ?? [];
|
|
1107
|
+
for (const c of cols)
|
|
1108
|
+
out.columns.add(`${table}.${c}`);
|
|
1109
|
+
const classification = {};
|
|
1110
|
+
for (const [c, v] of Object.entries(obj(store?.classification) ?? {}))
|
|
1111
|
+
if (typeof v === "string")
|
|
1112
|
+
classification[c] = v;
|
|
1113
|
+
out.stores.push({ table, columns: cols, classification, kind, cite });
|
|
1114
|
+
if (live) {
|
|
1115
|
+
const hit = [...wordsOf(table), ...cols.flatMap(wordsOf)].filter((w) => personalWords.has(w));
|
|
1116
|
+
if (hit.length)
|
|
1117
|
+
out.signals.push({
|
|
1118
|
+
factor: "personal-data",
|
|
1119
|
+
detail: `${kind} of ${table} (${[...new Set(hit)].join(", ")})`,
|
|
1120
|
+
cite
|
|
1121
|
+
});
|
|
1122
|
+
const added = Object.keys(obj(store?.types) ?? {});
|
|
1123
|
+
if (store?.new === true || str(store?.migration) || added.length)
|
|
1124
|
+
out.signals.push({
|
|
1125
|
+
factor: "schema",
|
|
1126
|
+
detail: store?.new === true ? `new table ${table}` : added.length ? `${table} gains ${added.join(", ")}` : `migration ${str(store?.migration)} on ${table}`,
|
|
1127
|
+
cite
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const to = obj(s.to) ?? was?.to;
|
|
1132
|
+
const target = str(to?.component);
|
|
1133
|
+
const entry = str(to?.entry) ?? "";
|
|
1134
|
+
if (target) {
|
|
1135
|
+
if (await outsideOf(target)) {
|
|
1136
|
+
if (isSystem(target))
|
|
1137
|
+
ref(target);
|
|
1138
|
+
const o = outside.get(target) ?? { entries: /* @__PURE__ */ new Set(), cite };
|
|
1139
|
+
if (entry)
|
|
1140
|
+
o.entries.add(entry);
|
|
1141
|
+
outside.set(target, o);
|
|
1142
|
+
if (live)
|
|
1143
|
+
out.signals.push({
|
|
1144
|
+
factor: "outside-call",
|
|
1145
|
+
detail: `${target}${entry ? ` (${entry}${to?.new === true ? ", new" : ""})` : ""}`,
|
|
1146
|
+
cite
|
|
1147
|
+
});
|
|
1148
|
+
} else {
|
|
1149
|
+
ref(target);
|
|
1150
|
+
const ep = entry ? endpointOf(target, entry) : void 0;
|
|
1151
|
+
if (ep)
|
|
1152
|
+
out.entries.add(ep.id);
|
|
1153
|
+
else if (entry && to?.new === true)
|
|
1154
|
+
out.newEntries.add(`${target} ${entry}`);
|
|
1155
|
+
}
|
|
1156
|
+
const iface = component && entry ? interfaceOf(component, target, entry) : void 0;
|
|
1157
|
+
if (iface) {
|
|
1158
|
+
if (op === "remove")
|
|
1159
|
+
out.interfaces.set(iface.id, "planned-retire");
|
|
1160
|
+
else if (op === "change")
|
|
1161
|
+
out.interfaces.set(iface.id, "planned-modify");
|
|
1162
|
+
else
|
|
1163
|
+
out.calls.add(iface.id);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (!live)
|
|
1167
|
+
continue;
|
|
1168
|
+
const fields = obj(s.fields) ?? {};
|
|
1169
|
+
const names = FIELD_LISTS.flatMap((k) => strs(fields[k]));
|
|
1170
|
+
for (const name of names)
|
|
1171
|
+
out.fields.add(name);
|
|
1172
|
+
const paths = [str(s.at), str(s.new), ...strs(Array.isArray(s.uses) ? s.uses : [s.uses])].filter((p) => !!p).map((p) => p.replace(/:\d+$/, ""));
|
|
1173
|
+
const plain = (t) => t.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
1174
|
+
const authHit = [...plain(what), ...names.flatMap(wordsOf), ...paths.flatMap(plain)].filter((w) => authWords.has(w) && (kind === "validate" || !/^\d+$/.test(w)));
|
|
1175
|
+
if (authHit.length)
|
|
1176
|
+
out.signals.push({
|
|
1177
|
+
factor: "auth",
|
|
1178
|
+
detail: `${kind} step "${quote(what)}" (${[...new Set(authHit)].join(", ")})`,
|
|
1179
|
+
cite
|
|
1180
|
+
});
|
|
1181
|
+
const fieldHit = names.flatMap(wordsOf).filter((w) => personalWords.has(w));
|
|
1182
|
+
if (fieldHit.length && !out.signals.some((x) => x.factor === "personal-data" && x.cite === cite))
|
|
1183
|
+
out.signals.push({
|
|
1184
|
+
factor: "personal-data",
|
|
1185
|
+
detail: `${kind} step carries ${[...new Set(fieldHit)].join(", ")}`,
|
|
1186
|
+
cite
|
|
1187
|
+
});
|
|
1188
|
+
if (s.breaking === true || strs(fields.required).length)
|
|
1189
|
+
out.signals.push({
|
|
1190
|
+
factor: "breaking",
|
|
1191
|
+
detail: strs(fields.required).length ? `${kind} step now requires ${strs(fields.required).join(", ")}` : `${kind} step "${quote(what)}" is marked breaking`,
|
|
1192
|
+
cite
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (featureText) {
|
|
1197
|
+
let placed;
|
|
1198
|
+
try {
|
|
1199
|
+
placed = obj((0, import_yaml4.parse)(featureText));
|
|
1200
|
+
} catch {
|
|
1201
|
+
placed = void 0;
|
|
1202
|
+
}
|
|
1203
|
+
if (placed) {
|
|
1204
|
+
out.from.push("feature.yaml");
|
|
1205
|
+
for (const raw of Array.isArray(placed.flows) ? placed.flows : []) {
|
|
1206
|
+
const fl = obj(raw);
|
|
1207
|
+
const id = str(fl?.id);
|
|
1208
|
+
if (!id || flows.has(id))
|
|
1209
|
+
continue;
|
|
1210
|
+
const today = await specs.get(id);
|
|
1211
|
+
const read2 = str(fl?.how) === "read";
|
|
1212
|
+
flows.set(id, {
|
|
1213
|
+
id,
|
|
1214
|
+
...today ? { repo: today.repo } : {},
|
|
1215
|
+
change: read2 ? "read" : "modify"
|
|
1216
|
+
});
|
|
1217
|
+
if (read2 || !today)
|
|
1218
|
+
continue;
|
|
1219
|
+
out.repos.add(today.repo);
|
|
1220
|
+
ref(today.flow.component);
|
|
1221
|
+
const trigger = today.flow.trigger.ref;
|
|
1222
|
+
if (trigger && get("Endpoint", trigger))
|
|
1223
|
+
out.entries.add(trigger);
|
|
1224
|
+
}
|
|
1225
|
+
for (const raw of Array.isArray(placed.newFlows) ? placed.newFlows : []) {
|
|
1226
|
+
const repo = str(obj(raw)?.repo);
|
|
1227
|
+
if (repo)
|
|
1228
|
+
out.repos.add(repo);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
const taskRepos = [];
|
|
1233
|
+
for (const m of tasksText.matchAll(/^##\s+(\S+)\s*$/gm))
|
|
1234
|
+
if (catalogRepos.has(m[1]) || await specs.hasRepo(m[1]))
|
|
1235
|
+
taskRepos.push(m[1]);
|
|
1236
|
+
if (taskRepos.length) {
|
|
1237
|
+
out.from.push("tasks.md");
|
|
1238
|
+
for (const r of taskRepos)
|
|
1239
|
+
out.repos.add(r);
|
|
1240
|
+
}
|
|
1241
|
+
if (await readFile5(join6(dir, "stage", "architecture-deploy.yaml")).then(() => true, () => false))
|
|
1242
|
+
out.deployFile = "stage/architecture-deploy.yaml";
|
|
1243
|
+
out.flows = [...flows.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
1244
|
+
out.outside = [...outside].map(([id, o]) => ({ id, entries: sorted(o.entries), cite: o.cite })).sort((a, b) => a.id.localeCompare(b.id));
|
|
1245
|
+
return out;
|
|
1246
|
+
}
|
|
1247
|
+
function findSystemsTable(text) {
|
|
1248
|
+
for (const headerSet of [["system (catalog id)"], ["system"]]) {
|
|
1249
|
+
const rows = tableWith(text, headerSet);
|
|
1250
|
+
if (rows.length)
|
|
1251
|
+
return rows.map((r) => cellId(r[headerSet[0]] ?? "")).filter(Boolean);
|
|
1252
|
+
}
|
|
1253
|
+
return [];
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// packages/changes/dist/risk.js
|
|
1257
|
+
var SIGNAL_WORDS = {
|
|
1258
|
+
"outside-call": { one: "calls an outside system", many: "calls outside systems" },
|
|
1259
|
+
auth: { one: "changes who may do what", many: "changes who may do what" },
|
|
1260
|
+
"personal-data": {
|
|
1261
|
+
one: "reads or writes personal or consent data",
|
|
1262
|
+
many: "reads or writes personal or consent data"
|
|
1263
|
+
},
|
|
1264
|
+
schema: { one: "changes the schema", many: "changes the schema" },
|
|
1265
|
+
breaking: { one: "breaks who uses it today", many: "breaks who uses it today" },
|
|
1266
|
+
spread: { one: "spans several repos", many: "spans several repos" }
|
|
1267
|
+
};
|
|
1268
|
+
var WEIGHT = {
|
|
1269
|
+
"outside-call": "outsideCall",
|
|
1270
|
+
auth: "auth",
|
|
1271
|
+
"personal-data": "personalData",
|
|
1272
|
+
schema: "schema",
|
|
1273
|
+
breaking: "breaking",
|
|
1274
|
+
spread: "extraRepo"
|
|
1275
|
+
};
|
|
1276
|
+
function detailsOf(signals) {
|
|
1277
|
+
const ds = [...new Set(signals.map((s) => s.detail))];
|
|
1278
|
+
return ds.length > 3 ? `${ds.slice(0, 3).join("; ")}; and ${ds.length - 3} more` : ds.join("; ");
|
|
1279
|
+
}
|
|
1280
|
+
function scoreRisk(input, policy) {
|
|
1281
|
+
const { risk } = policy;
|
|
1282
|
+
const w = risk.weights;
|
|
1283
|
+
const f = input.footprint;
|
|
1284
|
+
const factors = [];
|
|
1285
|
+
const add = (factor, detail, points, cites) => {
|
|
1286
|
+
if (points > 0)
|
|
1287
|
+
factors.push({ factor, detail, points, ...cites ? { cites } : {} });
|
|
1288
|
+
};
|
|
1289
|
+
add("type", `change type ${input.type}`, risk.typeWeights[input.type] ?? risk.defaultTypeWeight);
|
|
1290
|
+
if (f.systems.length > 1)
|
|
1291
|
+
add("systems", `${f.systems.length} systems touched (${f.systems.join(", ")})`, (f.systems.length - 1) * w.extraSystem);
|
|
1292
|
+
for (const s of f.dataStores)
|
|
1293
|
+
add("data-store", `data store ${s}`, w.dataStore);
|
|
1294
|
+
const crossed = f.vendors.filter((v) => !(f.outside ?? []).some((o) => o.id === v));
|
|
1295
|
+
if (crossed.length > 0)
|
|
1296
|
+
add("vendor", `crosses the vendor boundary (${crossed.join(", ")})`, w.vendor);
|
|
1297
|
+
for (const i of f.interfaces) {
|
|
1298
|
+
if (i.status === "planned-modify")
|
|
1299
|
+
add("interface", `interface ${i.id} planned-modify`, w.interfaceModify);
|
|
1300
|
+
if (i.status === "planned-retire")
|
|
1301
|
+
add("interface", `interface ${i.id} planned-retire`, w.interfaceRetire);
|
|
1302
|
+
}
|
|
1303
|
+
for (const factor of SIGNAL_FACTORS) {
|
|
1304
|
+
const signals = (f.signals ?? []).filter((s) => s.factor === factor);
|
|
1305
|
+
if (!signals.length)
|
|
1306
|
+
continue;
|
|
1307
|
+
const words = SIGNAL_WORDS[factor];
|
|
1308
|
+
const points = factor === "spread" ? (f.repos.length - 1) * w[WEIGHT[factor]] : w[WEIGHT[factor]];
|
|
1309
|
+
add(factor, `${signals.length > 1 ? words.many : words.one}: ${detailsOf(signals)}`, points, [
|
|
1310
|
+
...new Set(signals.map((s) => s.cite))
|
|
1311
|
+
]);
|
|
1312
|
+
}
|
|
1313
|
+
const score = factors.reduce((n, x) => n + x.points, 0);
|
|
1314
|
+
const level = score >= risk.levels.high ? "high" : score >= risk.levels.medium ? "medium" : "low";
|
|
1315
|
+
return { score, level, factors };
|
|
1316
|
+
}
|
|
1317
|
+
var LEVELS = ["low", "medium", "high"];
|
|
1318
|
+
function testsFor(strategy, type, level) {
|
|
1319
|
+
const rows = strategy.matrix.filter((m) => m.changeType === type);
|
|
1320
|
+
if (rows.length === 0)
|
|
1321
|
+
return { required: [], note: `test-strategy.yaml has no row for ${type}.` };
|
|
1322
|
+
const at = LEVELS.indexOf(level);
|
|
1323
|
+
const order = [
|
|
1324
|
+
at,
|
|
1325
|
+
...LEVELS.slice(0, at).map((_, i) => at - 1 - i).filter((i) => i >= 0),
|
|
1326
|
+
...LEVELS.slice(at + 1).map((_, i) => at + 1 + i)
|
|
1327
|
+
];
|
|
1328
|
+
for (const i of order) {
|
|
1329
|
+
const row = rows.find((r) => r.risk === LEVELS[i]);
|
|
1330
|
+
if (!row)
|
|
1331
|
+
continue;
|
|
1332
|
+
const plan = {
|
|
1333
|
+
required: [...row.required],
|
|
1334
|
+
row: { changeType: type, risk: row.risk }
|
|
1335
|
+
};
|
|
1336
|
+
if (row.risk !== level)
|
|
1337
|
+
plan.note = `test-strategy.yaml has no ${type}/${level} row; using ${type}/${row.risk}.`;
|
|
1338
|
+
return plan;
|
|
1339
|
+
}
|
|
1340
|
+
return { required: [], note: `test-strategy.yaml has no row for ${type}.` };
|
|
1341
|
+
}
|
|
1342
|
+
function whyOf(factor, f) {
|
|
1343
|
+
const what = f.detail.replace(/^[^:]*: /, "");
|
|
1344
|
+
switch (factor) {
|
|
1345
|
+
case "outside-call":
|
|
1346
|
+
return `against a stub of ${what}, with its failure paths (an error, a timeout, an answer it can't use)`;
|
|
1347
|
+
case "auth":
|
|
1348
|
+
return "of who may and who may not: each caller the rule lets in and each it refuses";
|
|
1349
|
+
case "personal-data":
|
|
1350
|
+
return "that the personal or consent data is read only where it may be, and never logged or sent on";
|
|
1351
|
+
case "schema":
|
|
1352
|
+
return "that the migration runs on today's data and the code reads what it writes";
|
|
1353
|
+
case "breaking":
|
|
1354
|
+
return "that today's callers and readers still work, or are changed with it";
|
|
1355
|
+
case "spread":
|
|
1356
|
+
return `across the repos in their deploy order (${what})`;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
function testPlanFor(ctx, type, risk) {
|
|
1360
|
+
const plan = testsFor(ctx.orgPack.testStrategy, type, risk.level);
|
|
1361
|
+
const added = [];
|
|
1362
|
+
for (const f of risk.factors) {
|
|
1363
|
+
const factor = SIGNAL_FACTORS.includes(f.factor) ? f.factor : void 0;
|
|
1364
|
+
const kinds = factor ? ctx.policy.risk.tests[factor] : void 0;
|
|
1365
|
+
if (!factor || !kinds?.length)
|
|
1366
|
+
continue;
|
|
1367
|
+
added.push({ factor, kinds: [...kinds], why: whyOf(factor, f), cites: f.cites ?? [] });
|
|
1368
|
+
}
|
|
1369
|
+
if (!added.length)
|
|
1370
|
+
return plan;
|
|
1371
|
+
const matrix = [...plan.required];
|
|
1372
|
+
const required = [...matrix];
|
|
1373
|
+
for (const a of added)
|
|
1374
|
+
for (const k of a.kinds)
|
|
1375
|
+
if (!required.includes(k))
|
|
1376
|
+
required.push(k);
|
|
1377
|
+
return { ...plan, required, matrix, added };
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// packages/changes/dist/lifecycle.js
|
|
1381
|
+
var import_yaml5 = __toESM(require_dist(), 1);
|
|
1382
|
+
import { createHash } from "node:crypto";
|
|
1383
|
+
import { readdir as readdir4, readFile as readFile6 } from "node:fs/promises";
|
|
1384
|
+
import { join as join7 } from "node:path";
|
|
1385
|
+
function phaseState(phase) {
|
|
1386
|
+
return phase === "release" ? "release-ready" : phase;
|
|
1387
|
+
}
|
|
1388
|
+
function statePhase(state) {
|
|
1389
|
+
if (state === "release-ready")
|
|
1390
|
+
return "release";
|
|
1391
|
+
return PHASES.includes(state) ? state : void 0;
|
|
1392
|
+
}
|
|
1393
|
+
function nextState(type, phase) {
|
|
1394
|
+
const i = type.phases.indexOf(phase);
|
|
1395
|
+
const next = type.phases[i + 1];
|
|
1396
|
+
return next ? phaseState(next) : "released";
|
|
1397
|
+
}
|
|
1398
|
+
var ARRIVE = {
|
|
1399
|
+
define: "definition",
|
|
1400
|
+
design: "architecture",
|
|
1401
|
+
plan: "planning",
|
|
1402
|
+
build: "build",
|
|
1403
|
+
qa: "QA",
|
|
1404
|
+
release: "release"
|
|
1405
|
+
};
|
|
1406
|
+
function handoffName(type, phase) {
|
|
1407
|
+
switch (phase) {
|
|
1408
|
+
case "define": {
|
|
1409
|
+
const next = type.phases[type.phases.indexOf(phase) + 1];
|
|
1410
|
+
return next ? `Send to ${ARRIVE[next]}` : "Go/no-go";
|
|
1411
|
+
}
|
|
1412
|
+
case "design":
|
|
1413
|
+
return "Approve design";
|
|
1414
|
+
case "plan":
|
|
1415
|
+
return "Plan ready";
|
|
1416
|
+
case "build":
|
|
1417
|
+
return "Raise PR";
|
|
1418
|
+
case "qa":
|
|
1419
|
+
return "QA sign-off";
|
|
1420
|
+
case "release":
|
|
1421
|
+
return "Go/no-go";
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function artifactPhase(ctx, type, artifact) {
|
|
1425
|
+
const want = PHASES.indexOf(ctx.policy.artifactPhases[artifact]);
|
|
1426
|
+
return type.phases.find((p) => PHASES.indexOf(p) >= want) ?? type.phases.at(-1);
|
|
1427
|
+
}
|
|
1428
|
+
function phaseArtifacts(ctx, type, phase) {
|
|
1429
|
+
const out = type.artifacts.filter((a) => artifactPhase(ctx, type, a) === phase);
|
|
1430
|
+
if (phase === type.phases[0] && !out.includes("brief"))
|
|
1431
|
+
out.unshift("brief");
|
|
1432
|
+
return out;
|
|
1433
|
+
}
|
|
1434
|
+
function requiredArtifacts(ctx, type, phase, footprint) {
|
|
1435
|
+
return phaseArtifacts(ctx, type, phase).filter((a) => {
|
|
1436
|
+
if (!type.artifacts.includes(a))
|
|
1437
|
+
return false;
|
|
1438
|
+
if (a === "data")
|
|
1439
|
+
return footprint.from.some((f) => f.startsWith("target-flows/")) ? footprint.signals.some((s) => s.factor === "schema") : footprint.dataStores.length + footprint.tables.length > 0;
|
|
1440
|
+
return true;
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
async function phaseStage(ctx, change, phase) {
|
|
1444
|
+
const step = ctx.policy.stages.find((x) => x.phase === phase);
|
|
1445
|
+
if (!step)
|
|
1446
|
+
return void 0;
|
|
1447
|
+
const dir = changeFolder(ctx.root, change.id);
|
|
1448
|
+
for (const x of ctx.policy.stages)
|
|
1449
|
+
if (await readFile6(join7(dir, ...x.loop.split("/"))).then(() => true, () => false))
|
|
1450
|
+
return step;
|
|
1451
|
+
return void 0;
|
|
1452
|
+
}
|
|
1453
|
+
var stageCommand = (step, change) => step.command.replaceAll("<change>", change);
|
|
1454
|
+
async function checkStage(root, change, step) {
|
|
1455
|
+
const dir = changeFolder(root, change.id);
|
|
1456
|
+
const run = stageCommand(step, change.id);
|
|
1457
|
+
const reason = (message) => [{ code: "stage-not-done", message }];
|
|
1458
|
+
const text = await readFile6(join7(dir, ...step.loop.split("/")), "utf8").catch(() => void 0);
|
|
1459
|
+
if (text === void 0)
|
|
1460
|
+
return reason(`The ${step.stage} stage hasn't run yet: run ${run}.`);
|
|
1461
|
+
let loop;
|
|
1462
|
+
try {
|
|
1463
|
+
loop = (0, import_yaml5.parse)(text);
|
|
1464
|
+
} catch {
|
|
1465
|
+
loop = null;
|
|
1466
|
+
}
|
|
1467
|
+
const state = typeof loop?.state === "string" ? loop.state : void 0;
|
|
1468
|
+
switch (state) {
|
|
1469
|
+
case "done":
|
|
1470
|
+
break;
|
|
1471
|
+
case "stopped":
|
|
1472
|
+
return reason(`The ${step.stage} stage stopped and isn't done: see what stopped it, then run ${run} again.`);
|
|
1473
|
+
case "questions":
|
|
1474
|
+
return reason(`The ${step.stage} stage waits for a person to answer its questions: answer them (atticus questions), then run ${run} again.`);
|
|
1475
|
+
case "waiting":
|
|
1476
|
+
return reason(`The ${step.stage} stage is with your assistant${typeof loop?.run === "string" ? ` (run ${loop.run})` : ""}: finish the hand-off, then run ${run} again.`);
|
|
1477
|
+
case "drafting":
|
|
1478
|
+
return reason(`The ${step.stage} stage is still drafting and isn't done: run ${run}.`);
|
|
1479
|
+
default:
|
|
1480
|
+
return reason(`${step.loop} doesn't read as a stage's loop state: run ${run} again.`);
|
|
1481
|
+
}
|
|
1482
|
+
const out = [];
|
|
1483
|
+
for (const w of step.writes.filter((x) => !x.endsWith("/") && !x.startsWith("stage/")))
|
|
1484
|
+
if (!await readFile6(join7(dir, ...w.split("/"))).then(() => true, () => false))
|
|
1485
|
+
out.push(...reason(`${w} is missing, though the ${step.stage} stage is done: run ${run} again.`));
|
|
1486
|
+
return out;
|
|
1487
|
+
}
|
|
1488
|
+
async function stageFiles(dir, step) {
|
|
1489
|
+
const out = [];
|
|
1490
|
+
for (const w of [...step.writes, step.loop]) {
|
|
1491
|
+
if (w.endsWith("/"))
|
|
1492
|
+
out.push(...await walk(dir, w.slice(0, -1)));
|
|
1493
|
+
else if (await readFile6(join7(dir, ...w.split("/"))).then(() => true, () => false))
|
|
1494
|
+
out.push(w);
|
|
1495
|
+
}
|
|
1496
|
+
return [...new Set(out)].sort();
|
|
1497
|
+
}
|
|
1498
|
+
async function walk(dir, rel = "") {
|
|
1499
|
+
let entries;
|
|
1500
|
+
try {
|
|
1501
|
+
entries = await readdir4(join7(dir, rel), { withFileTypes: true });
|
|
1502
|
+
} catch {
|
|
1503
|
+
return [];
|
|
1504
|
+
}
|
|
1505
|
+
const out = [];
|
|
1506
|
+
for (const e of entries) {
|
|
1507
|
+
const p = rel ? `${rel}/${e.name}` : e.name;
|
|
1508
|
+
if (e.isDirectory())
|
|
1509
|
+
out.push(...await walk(dir, p));
|
|
1510
|
+
else
|
|
1511
|
+
out.push(p);
|
|
1512
|
+
}
|
|
1513
|
+
return out.sort();
|
|
1514
|
+
}
|
|
1515
|
+
async function artifactFiles(dir, artifact) {
|
|
1516
|
+
if (artifact === "specs")
|
|
1517
|
+
return (await walk(dir, "specs")).filter((f2) => f2.endsWith(".md"));
|
|
1518
|
+
if (artifact === "stories") {
|
|
1519
|
+
const index = (await walk(dir)).includes("stories.md") ? ["stories.md"] : [];
|
|
1520
|
+
return [...index, ...(await walk(dir, "stories")).filter((f2) => f2.endsWith(".md"))];
|
|
1521
|
+
}
|
|
1522
|
+
const f = `${artifact}.md`;
|
|
1523
|
+
try {
|
|
1524
|
+
await readFile6(join7(dir, f));
|
|
1525
|
+
return [f];
|
|
1526
|
+
} catch {
|
|
1527
|
+
return [];
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
var FILE = {
|
|
1531
|
+
brief: "brief.md",
|
|
1532
|
+
proposal: "proposal.md",
|
|
1533
|
+
specs: "specs/<capability>/spec.md",
|
|
1534
|
+
interfaces: "interfaces.md",
|
|
1535
|
+
mappings: "mappings.md",
|
|
1536
|
+
data: "data.md",
|
|
1537
|
+
design: "design.md",
|
|
1538
|
+
ux: "ux.md",
|
|
1539
|
+
stories: "stories.md",
|
|
1540
|
+
tasks: "tasks.md",
|
|
1541
|
+
snow: "snow.md"
|
|
1542
|
+
};
|
|
1543
|
+
async function checkArtifacts(root, change, artifacts) {
|
|
1544
|
+
const dir = changeFolder(root, change.id);
|
|
1545
|
+
const reasons = [];
|
|
1546
|
+
for (const a of artifacts) {
|
|
1547
|
+
let files = await artifactFiles(dir, a);
|
|
1548
|
+
if (a === "stories")
|
|
1549
|
+
files = files.filter((f) => f === "stories.md");
|
|
1550
|
+
if (files.length === 0) {
|
|
1551
|
+
reasons.push({ code: "missing-artifact", artifact: a, message: `${FILE[a]} is missing.` });
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
const texts = await Promise.all(files.map((f) => readFile6(join7(dir, f), "utf8")));
|
|
1555
|
+
if (!texts.some(isFilled)) {
|
|
1556
|
+
reasons.push({
|
|
1557
|
+
code: "unfilled-artifact",
|
|
1558
|
+
artifact: a,
|
|
1559
|
+
message: `${files.length === 1 ? files[0] : FILE[a]} still holds only the template.`
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return reasons;
|
|
1564
|
+
}
|
|
1565
|
+
async function checkFindings(root, change, gates) {
|
|
1566
|
+
const reasons = [];
|
|
1567
|
+
for (const gate of gates) {
|
|
1568
|
+
const base = join7(changeFolder(root, change.id), "review", gate);
|
|
1569
|
+
let rounds;
|
|
1570
|
+
try {
|
|
1571
|
+
rounds = (await readdir4(base, { withFileTypes: true })).filter((d) => d.isDirectory() && /^\d+$/.test(d.name)).map((d) => Number(d.name));
|
|
1572
|
+
} catch {
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
if (rounds.length === 0)
|
|
1576
|
+
continue;
|
|
1577
|
+
const round2 = Math.max(...rounds);
|
|
1578
|
+
const dir = join7(base, String(round2));
|
|
1579
|
+
try {
|
|
1580
|
+
const meta = (0, import_yaml5.parse)(await readFile6(join7(dir, "meta", "round.yaml"), "utf8"));
|
|
1581
|
+
for (const l of meta?.lenses ?? [])
|
|
1582
|
+
if (l.state === "waiting")
|
|
1583
|
+
reasons.push({
|
|
1584
|
+
code: "lens-waiting",
|
|
1585
|
+
message: `The ${l.lens ?? "AI"} lens on the ${gate} gate (round ${round2}) is waiting for its hand-off answer${l.runId ? ` (atticus handoff done ${l.runId})` : ""}.`
|
|
1586
|
+
});
|
|
1587
|
+
else if (l.state === "failed")
|
|
1588
|
+
reasons.push({
|
|
1589
|
+
code: "lens-failed",
|
|
1590
|
+
message: `The ${l.lens ?? "AI"} lens on the ${gate} gate (round ${round2}) didn't review${l.reason ? ` (${l.reason})` : ""}: run atticus critic ${change.id} --gate ${gate} again.`
|
|
1591
|
+
});
|
|
1592
|
+
} catch {
|
|
1593
|
+
}
|
|
1594
|
+
const files = (await readdir4(dir)).filter((f) => /\.ya?ml$/.test(f)).sort();
|
|
1595
|
+
for (const f of files) {
|
|
1596
|
+
const rel = `review/${gate}/${round2}/${f}`;
|
|
1597
|
+
let data;
|
|
1598
|
+
try {
|
|
1599
|
+
data = (0, import_yaml5.parse)(await readFile6(join7(dir, f), "utf8"));
|
|
1600
|
+
} catch {
|
|
1601
|
+
data = void 0;
|
|
1602
|
+
}
|
|
1603
|
+
const r = Finding.safeParse(data);
|
|
1604
|
+
if (!r.success) {
|
|
1605
|
+
reasons.push({
|
|
1606
|
+
code: "bad-finding",
|
|
1607
|
+
message: `${rel} is not a valid finding (${r.error.issues[0]?.message ?? "unreadable"}), so the ${gate} gate can't be checked.`
|
|
1608
|
+
});
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
const x = r.data;
|
|
1612
|
+
const byAssistant = x.severity === "blocking" && isAssistantResolution(x.resolution);
|
|
1613
|
+
if (x.state !== "open" && !byAssistant)
|
|
1614
|
+
continue;
|
|
1615
|
+
if (x.severity === "blocking")
|
|
1616
|
+
reasons.push({
|
|
1617
|
+
code: "blocking-finding",
|
|
1618
|
+
finding: x.id,
|
|
1619
|
+
message: `Blocking finding ${x.id} on the ${gate} gate (round ${round2}): ${x.summary}`
|
|
1620
|
+
});
|
|
1621
|
+
else if (x.severity === "gap")
|
|
1622
|
+
reasons.push({
|
|
1623
|
+
code: "open-gap",
|
|
1624
|
+
finding: x.id,
|
|
1625
|
+
message: `Knowledge gap ${x.id} on the ${gate} gate (round ${round2}) needs an answer or a fix: ${x.summary}`
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return reasons;
|
|
1630
|
+
}
|
|
1631
|
+
function mandatoryGates(ctx) {
|
|
1632
|
+
return [...new Set(ctx.orgPack.profiles.flatMap((p) => p.profile?.gates ?? []))].sort();
|
|
1633
|
+
}
|
|
1634
|
+
async function checkMandatoryRounds(root, change, gates) {
|
|
1635
|
+
const reasons = [];
|
|
1636
|
+
for (const gate of gates) {
|
|
1637
|
+
const rounds = await readdir4(join7(changeFolder(root, change.id), "review", gate), {
|
|
1638
|
+
withFileTypes: true
|
|
1639
|
+
}).then((ds) => ds.filter((d) => d.isDirectory() && /^\d+$/.test(d.name)), () => []);
|
|
1640
|
+
if (rounds.length === 0)
|
|
1641
|
+
reasons.push({
|
|
1642
|
+
code: "critic-round-missing",
|
|
1643
|
+
message: `The ${gate} gate is mandatory on this platform and the critic hasn't reviewed it yet: run atticus critic ${change.id} --gate ${gate}.`
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
return reasons;
|
|
1647
|
+
}
|
|
1648
|
+
function phaseGates(ctx, phase) {
|
|
1649
|
+
return ctx.orgPack.gates.gates.filter((g) => g.phase === phase).map((g) => g.id);
|
|
1650
|
+
}
|
|
1651
|
+
var BUILT_IN_GATES = ["drift", "hand-up", "trace", "conform"];
|
|
1652
|
+
var BUILT_IN_GATE_PHASES = ["build", "qa", "release"];
|
|
1653
|
+
function phaseFindingGates(ctx, phase) {
|
|
1654
|
+
const own = phaseGates(ctx, phase);
|
|
1655
|
+
if (!BUILT_IN_GATE_PHASES.includes(phase))
|
|
1656
|
+
return own;
|
|
1657
|
+
return [...own, ...BUILT_IN_GATES.filter((g) => !own.includes(g))];
|
|
1658
|
+
}
|
|
1659
|
+
function sha256(text) {
|
|
1660
|
+
return createHash("sha256").update(text).digest("hex");
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// packages/changes/dist/progress.js
|
|
1664
|
+
var import_yaml6 = __toESM(require_dist(), 1);
|
|
1665
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
1666
|
+
import { join as join8 } from "node:path";
|
|
1667
|
+
async function stageCompletion(ctx, change) {
|
|
1668
|
+
const dir = changeFolder(ctx.root, change.id);
|
|
1669
|
+
const out = [];
|
|
1670
|
+
for (const s of ctx.policy.stages) {
|
|
1671
|
+
const text = await readFile7(join8(dir, ...s.loop.split("/")), "utf8").catch(() => void 0);
|
|
1672
|
+
let state = "not-started";
|
|
1673
|
+
if (text !== void 0) {
|
|
1674
|
+
try {
|
|
1675
|
+
const y = (0, import_yaml6.parse)(text);
|
|
1676
|
+
state = typeof y?.state === "string" ? y.state : "unknown";
|
|
1677
|
+
} catch {
|
|
1678
|
+
state = "unknown";
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
out.push({ stage: s.stage, phase: s.phase, state });
|
|
1682
|
+
}
|
|
1683
|
+
return out;
|
|
1684
|
+
}
|
|
1685
|
+
var cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
1686
|
+
var and = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs.at(-1)}`;
|
|
1687
|
+
function workProgress(type, state, stages, raised = false) {
|
|
1688
|
+
if (stages.every((s) => s.state === "not-started"))
|
|
1689
|
+
return void 0;
|
|
1690
|
+
const phase = state === "returned" ? type.phases[0] : statePhase(state);
|
|
1691
|
+
if (!phase)
|
|
1692
|
+
return void 0;
|
|
1693
|
+
const at = type.phases.indexOf(phase);
|
|
1694
|
+
const own = stages.filter((s) => type.phases.includes(s.phase));
|
|
1695
|
+
if (own.length && own.every((s) => type.phases.indexOf(s.phase) < at))
|
|
1696
|
+
return void 0;
|
|
1697
|
+
const done = own.filter((s) => s.state === "done").map((s) => s.stage);
|
|
1698
|
+
const ahead = own.filter((s) => s.state === "done" && type.phases.indexOf(s.phase) > at).map((s) => s.phase);
|
|
1699
|
+
const current = own.find((s) => s.phase === phase);
|
|
1700
|
+
const under = own.find((s) => s.state !== "done" && s.state !== "not-started");
|
|
1701
|
+
const last = own.filter((s) => s.state === "done").at(-1);
|
|
1702
|
+
const name = handoffName(type, phase);
|
|
1703
|
+
let summary;
|
|
1704
|
+
if (under)
|
|
1705
|
+
summary = `The ${under.stage} stage is ${under.state === "questions" ? "waiting on answers" : under.state}${done.length ? ` (${and(done)} done)` : ""}; the change is in ${state}.`;
|
|
1706
|
+
else if (!current || current.state !== "done")
|
|
1707
|
+
summary = `${done.length ? `${cap(and(done))} done; ` : ""}the ${current?.stage ?? phase} stage hasn't run yet; the change is in ${state}.`;
|
|
1708
|
+
else if (ahead.length)
|
|
1709
|
+
summary = `${cap(last.stage)} done; the change is still in ${state}: ${raised ? "merge" : "raise"} "${name}" (the ${phase} hand-off), then the ${and(ahead)} hand-offs.`;
|
|
1710
|
+
else
|
|
1711
|
+
summary = `${cap(current.stage)} done; waiting on "${name}" (the ${phase} hand-off${raised ? " PR to merge" : ", not raised yet"}).`;
|
|
1712
|
+
return { done, ahead, summary };
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// packages/changes/dist/approvals.js
|
|
1716
|
+
var strip = (h) => h.startsWith("@") ? h.slice(1) : h;
|
|
1717
|
+
function privacyImpact(ctx, type, footprint) {
|
|
1718
|
+
const { phases } = ctx.policy.privacy;
|
|
1719
|
+
return {
|
|
1720
|
+
touched: footprint.personal.length > 0,
|
|
1721
|
+
records: footprint.personal,
|
|
1722
|
+
phase: phases.find((p) => type.phases.includes(p)) ?? type.phases.at(-1),
|
|
1723
|
+
counted: personalLevels(ctx)
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
function prApprovers(handoff2, approvers) {
|
|
1727
|
+
const raiser = strip(handoff2.by);
|
|
1728
|
+
return [...new Set(approvers.map(strip))].filter((p) => p !== raiser).sort();
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// packages/changes/dist/assess.js
|
|
1732
|
+
var liveHandoff = (c) => c.handoffs.find((h) => h.state === "open" || h.state === "changes-requested");
|
|
1733
|
+
async function assess(ctx, change) {
|
|
1734
|
+
const type = changeTypeOf(ctx, change.type);
|
|
1735
|
+
const footprint = await changeFootprint(ctx, change);
|
|
1736
|
+
const risk = scoreRisk({ type: type.id, footprint }, ctx.policy);
|
|
1737
|
+
const tests = testPlanFor(ctx, type.id, risk);
|
|
1738
|
+
const privacy = privacyImpact(ctx, type, footprint);
|
|
1739
|
+
const progress = workProgress(type, change.state, await stageCompletion(ctx, change), change.state !== "returned" && liveHandoff(change) !== void 0);
|
|
1740
|
+
const base = {
|
|
1741
|
+
change,
|
|
1742
|
+
type,
|
|
1743
|
+
state: change.state,
|
|
1744
|
+
footprint,
|
|
1745
|
+
risk,
|
|
1746
|
+
tests,
|
|
1747
|
+
privacy,
|
|
1748
|
+
...progress ? { progress } : {}
|
|
1749
|
+
};
|
|
1750
|
+
const phase = change.state === "returned" ? type.phases[0] : statePhase(change.state);
|
|
1751
|
+
if (!phase) {
|
|
1752
|
+
if (change.state === "released")
|
|
1753
|
+
return { ...base, next: "archived", blockers: [], reasons: [] };
|
|
1754
|
+
if (change.state === "post-release-review") {
|
|
1755
|
+
const blockers2 = await checkFindings(ctx.root, change, ["post-release-review"]);
|
|
1756
|
+
return { ...base, next: "archived", blockers: blockers2, reasons: blockers2 };
|
|
1757
|
+
}
|
|
1758
|
+
const reasons2 = [
|
|
1759
|
+
{
|
|
1760
|
+
code: "wrong-state",
|
|
1761
|
+
message: change.state === "archived" ? `${change.id} is archived.` : `${change.id} is a draft; create it with atticus change new.`
|
|
1762
|
+
}
|
|
1763
|
+
];
|
|
1764
|
+
return { ...base, blockers: reasons2, reasons: reasons2 };
|
|
1765
|
+
}
|
|
1766
|
+
const stage = await phaseStage(ctx, change, phase);
|
|
1767
|
+
const due = requiredArtifacts(ctx, type, phase, footprint).filter((a) => !stage || stage.artifacts.includes(a));
|
|
1768
|
+
const stageReasons = stage ? await checkStage(ctx.root, change, stage) : [];
|
|
1769
|
+
const blockers = [
|
|
1770
|
+
...stageReasons,
|
|
1771
|
+
...await checkArtifacts(ctx.root, change, due),
|
|
1772
|
+
...await checkFindings(ctx.root, change, phaseFindingGates(ctx, phase)),
|
|
1773
|
+
...stageReasons.length ? [] : await checkMandatoryRounds(ctx.root, change, phaseGates(ctx, phase).filter((g) => mandatoryGates(ctx).includes(g)))
|
|
1774
|
+
];
|
|
1775
|
+
const next = change.state === "returned" ? phaseState(phase) : nextState(type, phase);
|
|
1776
|
+
const live = change.state === "returned" ? void 0 : liveHandoff(change);
|
|
1777
|
+
const reasons = [...blockers];
|
|
1778
|
+
if (!live) {
|
|
1779
|
+
reasons.push({
|
|
1780
|
+
code: "no-handoff",
|
|
1781
|
+
message: `No hand-off PR yet: raise "${handoffName(type, phase)}" when the ${phase} work is done.`
|
|
1782
|
+
});
|
|
1783
|
+
} else if (live.state === "changes-requested") {
|
|
1784
|
+
reasons.push({
|
|
1785
|
+
code: "changes-requested",
|
|
1786
|
+
message: `Changes were requested on PR #${live.pr.number}; update the artifacts and hand off again.`
|
|
1787
|
+
});
|
|
1788
|
+
} else {
|
|
1789
|
+
const approved = prApprovers(live, live.approvals.map((a) => a.by));
|
|
1790
|
+
reasons.push({
|
|
1791
|
+
code: "pr-not-merged",
|
|
1792
|
+
message: approved.length ? `PR #${live.pr.number} is approved (${approved.join(", ")}) but not merged yet: ${live.pr.url}` : `Waiting for PR #${live.pr.number} to be approved and merged on the Git host: ${live.pr.url}`
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
return { ...base, phase, next, ...live ? { handoff: live } : {}, blockers, reasons };
|
|
1796
|
+
}
|
|
1797
|
+
async function canAdvance(change, root) {
|
|
1798
|
+
const ctx = await loadChangeContext(root);
|
|
1799
|
+
const c = typeof change === "string" ? await changeStore(root).read(change) : change;
|
|
1800
|
+
const a = await assess(ctx, c);
|
|
1801
|
+
return {
|
|
1802
|
+
ok: a.reasons.length === 0,
|
|
1803
|
+
state: a.state,
|
|
1804
|
+
...a.phase ? { phase: a.phase } : {},
|
|
1805
|
+
...a.next ? { next: a.next } : {},
|
|
1806
|
+
reasons: a.reasons
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
async function changeReport(root, id) {
|
|
1810
|
+
const ctx = await loadChangeContext(root);
|
|
1811
|
+
const a = await assess(ctx, await changeStore(root).read(id));
|
|
1812
|
+
return { ...a, nextStep: nextStep(a), problems: ctx.problems };
|
|
1813
|
+
}
|
|
1814
|
+
function nextStep(a) {
|
|
1815
|
+
switch (a.state) {
|
|
1816
|
+
case "draft":
|
|
1817
|
+
return "Create the change with atticus change new.";
|
|
1818
|
+
case "archived":
|
|
1819
|
+
return "Nothing: the change is archived.";
|
|
1820
|
+
case "released":
|
|
1821
|
+
return "Archive the change.";
|
|
1822
|
+
case "post-release-review":
|
|
1823
|
+
return a.reasons.length ? "Resolve the post-release review findings, then archive the change." : "Review the skipped lenses, then archive the change.";
|
|
1824
|
+
case "returned":
|
|
1825
|
+
return `Revise the work, then hand off ${a.phase} again.`;
|
|
1826
|
+
}
|
|
1827
|
+
const name = handoffName(a.type, a.phase);
|
|
1828
|
+
if (!a.handoff)
|
|
1829
|
+
return a.blockers.length ? `Finish the ${a.phase} work, then raise "${name}".` : `Ready to raise "${name}".`;
|
|
1830
|
+
if (a.handoff.state === "changes-requested")
|
|
1831
|
+
return `Update the artifacts and hand off again on PR #${a.handoff.pr.number}.`;
|
|
1832
|
+
return `Merge PR #${a.handoff.pr.number} once it is approved (${a.handoff.pr.url}); syncing then moves the change on.`;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// packages/changes/dist/new-change.js
|
|
1836
|
+
var import_yaml7 = __toESM(require_dist(), 1);
|
|
1837
|
+
import { mkdir, readFile as readFile8, writeFile } from "node:fs/promises";
|
|
1838
|
+
import { join as join9 } from "node:path";
|
|
1839
|
+
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1840
|
+
var strip2 = (h) => h.startsWith("@") ? h.slice(1) : h;
|
|
1841
|
+
function slugify(title) {
|
|
1842
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/, "");
|
|
1843
|
+
}
|
|
1844
|
+
async function exists(path) {
|
|
1845
|
+
try {
|
|
1846
|
+
await readFile8(path);
|
|
1847
|
+
return true;
|
|
1848
|
+
} catch {
|
|
1849
|
+
return false;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
async function defaultSchema(root) {
|
|
1853
|
+
try {
|
|
1854
|
+
const cfg = (0, import_yaml7.parse)(await readFile8(join9(openspecDirOf(root), "config.yaml"), "utf8"));
|
|
1855
|
+
if (cfg && typeof cfg.schema === "string")
|
|
1856
|
+
return cfg.schema;
|
|
1857
|
+
} catch {
|
|
1858
|
+
}
|
|
1859
|
+
return "spec-driven";
|
|
1860
|
+
}
|
|
1861
|
+
async function newChange(root, opts) {
|
|
1862
|
+
const ctx = await loadChangeContext(root);
|
|
1863
|
+
const type = changeTypeOf(ctx, opts.type);
|
|
1864
|
+
const id = opts.id ?? slugify(opts.title);
|
|
1865
|
+
if (!RecordId.safeParse(id).success)
|
|
1866
|
+
throw new ChangeError("invalid", `"${id}" can't be a change id; use a lowercase slug such as add-transfer-limit.`);
|
|
1867
|
+
const template = pickTemplate(ctx.templates, type.id, opts.template);
|
|
1868
|
+
if (!template)
|
|
1869
|
+
throw new ChangeError("invalid", `Unknown change template ${opts.template}. Use one of: ${ctx.templates.map((t) => t.id).join(", ")}.`);
|
|
1870
|
+
const store = changeStore(root);
|
|
1871
|
+
if (await store.tryRead(id).catch(() => true))
|
|
1872
|
+
throw new ChangeError("conflict", `Change ${id} already exists.`);
|
|
1873
|
+
const now = opts.now ?? nowIso();
|
|
1874
|
+
const by = strip2(opts.by);
|
|
1875
|
+
const dir = changeFolder(root, id);
|
|
1876
|
+
if (opts.openspec && !await exists(join9(dir, ".openspec.yaml"))) {
|
|
1877
|
+
await opts.openspec.newChange(id, { description: opts.title });
|
|
1878
|
+
} else {
|
|
1879
|
+
await mkdir(dir, { recursive: true });
|
|
1880
|
+
if (!await exists(join9(dir, ".openspec.yaml")))
|
|
1881
|
+
await writeFile(join9(dir, ".openspec.yaml"), `schema: ${await defaultSchema(root)}
|
|
1882
|
+
created: ${now.slice(0, 10)}
|
|
1883
|
+
`, "utf8");
|
|
1884
|
+
}
|
|
1885
|
+
if (!await exists(join9(dir, "brief.md")))
|
|
1886
|
+
await writeFileAtomic(join9(dir, "brief.md"), renderTemplate(template, {
|
|
1887
|
+
title: opts.title,
|
|
1888
|
+
id,
|
|
1889
|
+
type: type.id,
|
|
1890
|
+
by,
|
|
1891
|
+
date: now.slice(0, 10)
|
|
1892
|
+
}));
|
|
1893
|
+
const owners = {};
|
|
1894
|
+
for (const [role, people] of Object.entries(opts.owners ?? {}))
|
|
1895
|
+
if (people.length)
|
|
1896
|
+
owners[role] = [...new Set(people.map(strip2))];
|
|
1897
|
+
if (!owners["product-owner"] && type.phases.includes("define"))
|
|
1898
|
+
owners["product-owner"] = [by];
|
|
1899
|
+
const first = phaseState(type.phases[0]);
|
|
1900
|
+
const touches = ChangeTouches.parse(opts.touches ?? {});
|
|
1901
|
+
const change = {
|
|
1902
|
+
schemaVersion: 1,
|
|
1903
|
+
id,
|
|
1904
|
+
title: opts.title,
|
|
1905
|
+
type: type.id,
|
|
1906
|
+
template: template.id,
|
|
1907
|
+
state: first,
|
|
1908
|
+
owners,
|
|
1909
|
+
systems: [...new Set(opts.systems ?? [])].sort(),
|
|
1910
|
+
touches,
|
|
1911
|
+
authors: {},
|
|
1912
|
+
createdAt: now,
|
|
1913
|
+
createdBy: by,
|
|
1914
|
+
history: [
|
|
1915
|
+
{ from: "draft", to: first, at: now, by, reason: `Created from the ${template.id} template` }
|
|
1916
|
+
],
|
|
1917
|
+
handoffs: []
|
|
1918
|
+
};
|
|
1919
|
+
await store.write(change);
|
|
1920
|
+
return change;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// packages/changes/dist/publish.js
|
|
1924
|
+
import { rm } from "node:fs/promises";
|
|
1925
|
+
import { join as join10 } from "node:path";
|
|
1926
|
+
function gitHandoffPublisher(opts = {}) {
|
|
1927
|
+
const remote = opts.remote ?? "origin";
|
|
1928
|
+
const env = opts.env ?? process.env;
|
|
1929
|
+
return {
|
|
1930
|
+
async publish({ root, branch, base, files, message, approval }) {
|
|
1931
|
+
requireApproval("git", approval);
|
|
1932
|
+
const git = createGit({ env });
|
|
1933
|
+
const out = async (args) => (await git(args, root)).stdout.trim();
|
|
1934
|
+
const tryOut = (args) => out(args).catch(() => void 0);
|
|
1935
|
+
const gitDir = await out(["rev-parse", "--absolute-git-dir"]);
|
|
1936
|
+
await tryOut(["fetch", "--quiet", remote, base]);
|
|
1937
|
+
await tryOut(["fetch", "--quiet", remote, branch]);
|
|
1938
|
+
const start = await tryOut([
|
|
1939
|
+
"rev-parse",
|
|
1940
|
+
"--verify",
|
|
1941
|
+
"--quiet",
|
|
1942
|
+
`refs/remotes/${remote}/${branch}^{commit}`
|
|
1943
|
+
]) ?? await tryOut([
|
|
1944
|
+
"rev-parse",
|
|
1945
|
+
"--verify",
|
|
1946
|
+
"--quiet",
|
|
1947
|
+
`refs/remotes/${remote}/${base}^{commit}`
|
|
1948
|
+
]) ?? await out(["rev-parse", "--verify", `refs/heads/${base}^{commit}`]);
|
|
1949
|
+
const index = join10(gitDir, "atticus-handoff-index");
|
|
1950
|
+
const staged = createGit({ env: { ...env, GIT_INDEX_FILE: index } });
|
|
1951
|
+
try {
|
|
1952
|
+
await staged(["read-tree", start], root);
|
|
1953
|
+
if (files.length > 0)
|
|
1954
|
+
await staged(["add", "-f", "--", ...files], root);
|
|
1955
|
+
const tree = (await staged(["write-tree"], root)).stdout.trim();
|
|
1956
|
+
const startTree = await out(["rev-parse", `${start}^{tree}`]);
|
|
1957
|
+
const commit = tree === startTree ? start : await out(["commit-tree", tree, "-p", start, "-m", message]);
|
|
1958
|
+
await out(["update-ref", `refs/heads/${branch}`, commit]);
|
|
1959
|
+
await out(["push", "--quiet", remote, `${commit}:refs/heads/${branch}`]);
|
|
1960
|
+
return { commit };
|
|
1961
|
+
} finally {
|
|
1962
|
+
await rm(index, { force: true });
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// packages/changes/dist/handoff.js
|
|
1969
|
+
import { readdir as readdir6, readFile as readFile10 } from "node:fs/promises";
|
|
1970
|
+
import { join as join12 } from "node:path";
|
|
1971
|
+
|
|
1972
|
+
// packages/changes/dist/drafted.js
|
|
1973
|
+
var import_yaml8 = __toESM(require_dist(), 1);
|
|
1974
|
+
import { mkdir as mkdir2, readdir as readdir5, readFile as readFile9, rm as rm2 } from "node:fs/promises";
|
|
1975
|
+
import { dirname, join as join11, relative, sep } from "node:path";
|
|
1976
|
+
var CAPTURE_SCHEMA_VERSION = 1;
|
|
1977
|
+
var DraftCapture = external_exports.object({
|
|
1978
|
+
schemaVersion: external_exports.literal(CAPTURE_SCHEMA_VERSION),
|
|
1979
|
+
change: external_exports.string().min(1),
|
|
1980
|
+
stage: external_exports.string().min(1),
|
|
1981
|
+
phase: external_exports.string().min(1),
|
|
1982
|
+
pr: external_exports.object({ number: external_exports.number().int().positive(), url: external_exports.string().min(1) }),
|
|
1983
|
+
branch: external_exports.string().min(1),
|
|
1984
|
+
base: external_exports.string().min(1),
|
|
1985
|
+
/** captured: both sides kept. waiting-for-merge: the merge isn't in the clone yet. no-draft: the drafted files are gone. */
|
|
1986
|
+
state: external_exports.enum(["captured", "waiting-for-merge", "no-draft"]),
|
|
1987
|
+
/** When Atticus saw the PR merged (the sync's time). */
|
|
1988
|
+
mergedAt: external_exports.string().min(1),
|
|
1989
|
+
capturedAt: external_exports.string().min(1),
|
|
1990
|
+
drafted: external_exports.object({
|
|
1991
|
+
from: external_exports.enum(["git", "working-tree", "missing"]),
|
|
1992
|
+
commit: external_exports.string().optional()
|
|
1993
|
+
}),
|
|
1994
|
+
merged: external_exports.object({
|
|
1995
|
+
commit: external_exports.string().min(1),
|
|
1996
|
+
/** The merge commit's committer date. */
|
|
1997
|
+
at: external_exports.string().min(1),
|
|
1998
|
+
how: external_exports.enum(["message", "ancestry"])
|
|
1999
|
+
}).optional(),
|
|
2000
|
+
files: external_exports.array(external_exports.object({
|
|
2001
|
+
path: external_exports.string().min(1),
|
|
2002
|
+
status: external_exports.enum(["same", "edited", "added", "removed"]),
|
|
2003
|
+
drafted: external_exports.string().optional(),
|
|
2004
|
+
merged: external_exports.string().optional()
|
|
2005
|
+
})).default([]),
|
|
2006
|
+
note: external_exports.string().optional()
|
|
2007
|
+
});
|
|
2008
|
+
var capturePaths = (stage) => ({
|
|
2009
|
+
capture: `stage/${stage}.capture.yaml`,
|
|
2010
|
+
drafted: `stage/${stage}.drafted`,
|
|
2011
|
+
merged: `stage/${stage}.merged`
|
|
2012
|
+
});
|
|
2013
|
+
var HEADER = "# What Atticus drafted beside what was merged (H1, ADR 0087 addendum). Atticus writes this file.\n";
|
|
2014
|
+
function machinery(path) {
|
|
2015
|
+
return path === "change.yaml" || path === "brief.md" || path.startsWith("stage/") || path.startsWith("review/") || path.startsWith("questions/") || path.startsWith("design/") || path.startsWith("inputs/");
|
|
2016
|
+
}
|
|
2017
|
+
function artifactMatch(path, a) {
|
|
2018
|
+
if (a === "specs")
|
|
2019
|
+
return path.startsWith("specs/") && path.endsWith(".md");
|
|
2020
|
+
if (a === "stories")
|
|
2021
|
+
return path === "stories.md" || path.startsWith("stories/") && path.endsWith(".md");
|
|
2022
|
+
return path === `${a}.md`;
|
|
2023
|
+
}
|
|
2024
|
+
function draftedPath(path, step, artifacts) {
|
|
2025
|
+
if (machinery(path))
|
|
2026
|
+
return false;
|
|
2027
|
+
if (step) {
|
|
2028
|
+
for (const w of step.writes)
|
|
2029
|
+
if (w.endsWith("/") ? path.startsWith(w) : path === w)
|
|
2030
|
+
return !machinery(path);
|
|
2031
|
+
return step.artifacts.some((a) => artifactMatch(path, a));
|
|
2032
|
+
}
|
|
2033
|
+
return artifacts.some((a) => a !== "brief" && artifactMatch(path, a));
|
|
2034
|
+
}
|
|
2035
|
+
async function openGit(root, runner) {
|
|
2036
|
+
const top = await runner(["rev-parse", "--show-toplevel"], root).then((r) => r.stdout.trim(), () => void 0);
|
|
2037
|
+
if (!top)
|
|
2038
|
+
return void 0;
|
|
2039
|
+
const prefix = (await runner(["rev-parse", "--show-prefix"], root)).stdout.trim();
|
|
2040
|
+
const run = async (args) => (await runner(args, top)).stdout;
|
|
2041
|
+
return {
|
|
2042
|
+
git: { run, tryRun: (args) => run(args).catch(() => void 0) },
|
|
2043
|
+
prefix
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
var sameText = (text, recorded) => sha256(text) === recorded || sha256(text.replace(/\r?\n/g, "\r\n")) === recorded || sha256(text.replace(/\r\n/g, "\n")) === recorded;
|
|
2047
|
+
async function walk2(dir, rel = "") {
|
|
2048
|
+
let entries;
|
|
2049
|
+
try {
|
|
2050
|
+
entries = await readdir5(join11(dir, rel), { withFileTypes: true });
|
|
2051
|
+
} catch {
|
|
2052
|
+
return [];
|
|
2053
|
+
}
|
|
2054
|
+
const out = [];
|
|
2055
|
+
for (const e of entries) {
|
|
2056
|
+
const p = rel ? `${rel}/${e.name}` : e.name;
|
|
2057
|
+
if (e.isDirectory())
|
|
2058
|
+
out.push(...await walk2(dir, p));
|
|
2059
|
+
else
|
|
2060
|
+
out.push(p);
|
|
2061
|
+
}
|
|
2062
|
+
return out.sort();
|
|
2063
|
+
}
|
|
2064
|
+
async function firstParentLog(git, ref) {
|
|
2065
|
+
const out = await git.tryRun([
|
|
2066
|
+
"log",
|
|
2067
|
+
"--first-parent",
|
|
2068
|
+
"-n",
|
|
2069
|
+
"500",
|
|
2070
|
+
"--format=%H%x1f%cI%x1f%B%x1e",
|
|
2071
|
+
ref
|
|
2072
|
+
]);
|
|
2073
|
+
if (!out)
|
|
2074
|
+
return [];
|
|
2075
|
+
return out.split("").map((x) => x.replace(/^\s+/, "")).filter(Boolean).map((x) => {
|
|
2076
|
+
const [sha, at, message] = x.split("");
|
|
2077
|
+
return { sha: sha.trim(), at: (at ?? "").trim(), message: message ?? "" };
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2081
|
+
async function findMerge(git, refs, branch, pr, draft) {
|
|
2082
|
+
const names = new RegExp(`(^|[^\\w.-])${escape(branch)}($|[^\\w/.-])`);
|
|
2083
|
+
const number = new RegExp(`(#|!|\\bPR\\s*)${pr}(?!\\d)`);
|
|
2084
|
+
for (const ref of refs) {
|
|
2085
|
+
const log = await firstParentLog(git, ref);
|
|
2086
|
+
if (!log.length)
|
|
2087
|
+
continue;
|
|
2088
|
+
const oldest = [...log].reverse();
|
|
2089
|
+
const byBranch = oldest.find((e) => names.test(e.message));
|
|
2090
|
+
const byNumber = byBranch ?? oldest.find((e) => number.test(e.message));
|
|
2091
|
+
if (byNumber)
|
|
2092
|
+
return { commit: byNumber.sha, at: byNumber.at, how: "message" };
|
|
2093
|
+
if (draft) {
|
|
2094
|
+
const after = await git.tryRun(["rev-list", "--ancestry-path", `${draft}..${ref}`]);
|
|
2095
|
+
const descendants = new Set((after ?? "").split("\n").map((s) => s.trim()).filter(Boolean));
|
|
2096
|
+
const merge = oldest.find((e) => descendants.has(e.sha));
|
|
2097
|
+
if (merge)
|
|
2098
|
+
return { commit: merge.sha, at: merge.at, how: "ancestry" };
|
|
2099
|
+
const ff = oldest.find((e) => e.sha === draft);
|
|
2100
|
+
if (ff)
|
|
2101
|
+
return { commit: ff.sha, at: ff.at, how: "ancestry" };
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
return void 0;
|
|
2105
|
+
}
|
|
2106
|
+
async function existing(git, refs) {
|
|
2107
|
+
const out = [];
|
|
2108
|
+
for (const r of refs)
|
|
2109
|
+
if (await git.tryRun(["rev-parse", "--verify", "--quiet", `${r}^{commit}`]))
|
|
2110
|
+
out.push(r);
|
|
2111
|
+
return out;
|
|
2112
|
+
}
|
|
2113
|
+
async function blob(git, commit, path) {
|
|
2114
|
+
return git.tryRun(["cat-file", "blob", `${commit}:${path}`]);
|
|
2115
|
+
}
|
|
2116
|
+
async function findDraft(git, ref, dirInTree, recorded) {
|
|
2117
|
+
const commits = await git.tryRun(["rev-list", "-n", "100", ref, "--", dirInTree]);
|
|
2118
|
+
for (const c of (commits ?? "").split("\n").map((s) => s.trim()).filter(Boolean)) {
|
|
2119
|
+
let all = true;
|
|
2120
|
+
for (const r of recorded) {
|
|
2121
|
+
const text = await blob(git, c, `${dirInTree}/${r.path}`);
|
|
2122
|
+
if (text === void 0 || !sameText(text, r.sha256)) {
|
|
2123
|
+
all = false;
|
|
2124
|
+
break;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (all)
|
|
2128
|
+
return c;
|
|
2129
|
+
}
|
|
2130
|
+
return void 0;
|
|
2131
|
+
}
|
|
2132
|
+
async function captureStageOf(ctx, change, phase) {
|
|
2133
|
+
const step = await phaseStage(ctx, change, phase);
|
|
2134
|
+
return step ? { name: step.stage, step } : { name: phase };
|
|
2135
|
+
}
|
|
2136
|
+
async function captureHandoff(input) {
|
|
2137
|
+
const { ctx, change, handoff: handoff2 } = input;
|
|
2138
|
+
const root = ctx.root;
|
|
2139
|
+
const opened = await openGit(root, input.git ?? createGit());
|
|
2140
|
+
if (!opened)
|
|
2141
|
+
return void 0;
|
|
2142
|
+
const { git, prefix } = opened;
|
|
2143
|
+
const remote = input.remote ?? "origin";
|
|
2144
|
+
const phase = handoff2.phase;
|
|
2145
|
+
const { name, step } = await captureStageOf(ctx, change, phase);
|
|
2146
|
+
const artifacts = phaseArtifacts(ctx, input.type, phase);
|
|
2147
|
+
const isDraft = (p2) => draftedPath(p2, step, artifacts);
|
|
2148
|
+
const recorded = handoff2.artifacts.filter((a) => isDraft(a.path));
|
|
2149
|
+
if (!recorded.length)
|
|
2150
|
+
return void 0;
|
|
2151
|
+
const dirInTree = `${prefix}${changesRelOf(root)}/${change.id}`;
|
|
2152
|
+
const branchRefs = await existing(git, [
|
|
2153
|
+
`refs/heads/${handoff2.branch}`,
|
|
2154
|
+
`refs/remotes/${remote}/${handoff2.branch}`
|
|
2155
|
+
]);
|
|
2156
|
+
let draft;
|
|
2157
|
+
for (const ref of branchRefs) {
|
|
2158
|
+
draft = await findDraft(git, ref, dirInTree, recorded);
|
|
2159
|
+
if (draft)
|
|
2160
|
+
break;
|
|
2161
|
+
}
|
|
2162
|
+
const baseRefs = await existing(git, [
|
|
2163
|
+
`refs/remotes/${remote}/${input.base}`,
|
|
2164
|
+
`refs/heads/${input.base}`
|
|
2165
|
+
]);
|
|
2166
|
+
const merged = await findMerge(git, baseRefs, handoff2.branch, handoff2.pr.number, draft);
|
|
2167
|
+
if (!draft && merged && await git.tryRun(["rev-parse", "--verify", "--quiet", `${merged.commit}^2`]))
|
|
2168
|
+
draft = await findDraft(git, `${merged.commit}^2`, dirInTree, recorded);
|
|
2169
|
+
const folder = changeFolder(root, change.id);
|
|
2170
|
+
const draftedFiles = /* @__PURE__ */ new Map();
|
|
2171
|
+
let from = "missing";
|
|
2172
|
+
if (draft) {
|
|
2173
|
+
from = "git";
|
|
2174
|
+
const listed = await git.tryRun([
|
|
2175
|
+
"ls-tree",
|
|
2176
|
+
"-r",
|
|
2177
|
+
"--name-only",
|
|
2178
|
+
"--full-tree",
|
|
2179
|
+
draft,
|
|
2180
|
+
"--",
|
|
2181
|
+
dirInTree
|
|
2182
|
+
]);
|
|
2183
|
+
const paths2 = new Set(recorded.map((r) => r.path));
|
|
2184
|
+
for (const full of (listed ?? "").split("\n").map((s) => s.trim()).filter(Boolean)) {
|
|
2185
|
+
const p2 = full.slice(dirInTree.length + 1);
|
|
2186
|
+
if (isDraft(p2) && paths2.has(p2)) {
|
|
2187
|
+
const t = await blob(git, draft, full);
|
|
2188
|
+
if (t !== void 0)
|
|
2189
|
+
draftedFiles.set(p2, t);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
} else {
|
|
2193
|
+
const texts = await Promise.all(recorded.map(async (r) => [
|
|
2194
|
+
r,
|
|
2195
|
+
await readFile9(join11(folder, ...r.path.split("/")), "utf8").catch(() => void 0)
|
|
2196
|
+
]));
|
|
2197
|
+
if (texts.every(([r, t]) => t !== void 0 && sameText(t, r.sha256))) {
|
|
2198
|
+
from = "working-tree";
|
|
2199
|
+
for (const [r, t] of texts)
|
|
2200
|
+
draftedFiles.set(r.path, t);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
const mergedFiles = /* @__PURE__ */ new Map();
|
|
2204
|
+
if (merged) {
|
|
2205
|
+
const listed = await git.tryRun([
|
|
2206
|
+
"ls-tree",
|
|
2207
|
+
"-r",
|
|
2208
|
+
"--name-only",
|
|
2209
|
+
"--full-tree",
|
|
2210
|
+
merged.commit,
|
|
2211
|
+
"--",
|
|
2212
|
+
dirInTree
|
|
2213
|
+
]);
|
|
2214
|
+
for (const full of (listed ?? "").split("\n").map((s) => s.trim()).filter(Boolean)) {
|
|
2215
|
+
const p2 = full.slice(dirInTree.length + 1);
|
|
2216
|
+
if (!isDraft(p2))
|
|
2217
|
+
continue;
|
|
2218
|
+
const t = await blob(git, merged.commit, full);
|
|
2219
|
+
if (t !== void 0)
|
|
2220
|
+
mergedFiles.set(p2, t);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
const state = from === "missing" ? "no-draft" : merged ? "captured" : "waiting-for-merge";
|
|
2224
|
+
const paths = [
|
|
2225
|
+
.../* @__PURE__ */ new Set([...draftedFiles.keys(), ...merged ? mergedFiles.keys() : []])
|
|
2226
|
+
].sort();
|
|
2227
|
+
const files = paths.map((path) => {
|
|
2228
|
+
const d = draftedFiles.get(path);
|
|
2229
|
+
const m = merged ? mergedFiles.get(path) : void 0;
|
|
2230
|
+
const status = !merged || d === m ? "same" : d === void 0 ? "added" : m === void 0 ? "removed" : "edited";
|
|
2231
|
+
return {
|
|
2232
|
+
path,
|
|
2233
|
+
status,
|
|
2234
|
+
...d !== void 0 ? { drafted: sha256(d) } : {},
|
|
2235
|
+
...m !== void 0 ? { merged: sha256(m) } : {}
|
|
2236
|
+
};
|
|
2237
|
+
});
|
|
2238
|
+
const capture = {
|
|
2239
|
+
schemaVersion: CAPTURE_SCHEMA_VERSION,
|
|
2240
|
+
change: change.id,
|
|
2241
|
+
stage: name,
|
|
2242
|
+
phase,
|
|
2243
|
+
pr: handoff2.pr,
|
|
2244
|
+
branch: handoff2.branch,
|
|
2245
|
+
base: input.base,
|
|
2246
|
+
state,
|
|
2247
|
+
mergedAt: handoff2.updatedAt ?? input.now,
|
|
2248
|
+
capturedAt: input.now,
|
|
2249
|
+
drafted: { from, ...draft ? { commit: draft } : {} },
|
|
2250
|
+
...merged ? { merged } : {},
|
|
2251
|
+
files: state === "captured" ? files : [],
|
|
2252
|
+
...state === "waiting-for-merge" ? {
|
|
2253
|
+
note: `The merge of PR #${handoff2.pr.number} isn't in this clone yet: pull ${input.base}, then run atticus change learn.`
|
|
2254
|
+
} : state === "no-draft" ? {
|
|
2255
|
+
note: `No commit on ${handoff2.branch} and no file on disk has the drafted files' hashes any more, so the draft can't be compared.`
|
|
2256
|
+
} : {}
|
|
2257
|
+
};
|
|
2258
|
+
const p = capturePaths(name);
|
|
2259
|
+
await rm2(join11(folder, ...p.drafted.split("/")), { recursive: true, force: true });
|
|
2260
|
+
await rm2(join11(folder, ...p.merged.split("/")), { recursive: true, force: true });
|
|
2261
|
+
const put = async (base, rel, text) => {
|
|
2262
|
+
const f = join11(folder, ...base.split("/"), ...rel.split("/"));
|
|
2263
|
+
await mkdir2(dirname(f), { recursive: true });
|
|
2264
|
+
await writeFileAtomic(f, text);
|
|
2265
|
+
};
|
|
2266
|
+
for (const [rel, t] of draftedFiles)
|
|
2267
|
+
await put(p.drafted, rel, t);
|
|
2268
|
+
if (state === "captured")
|
|
2269
|
+
for (const [rel, t] of mergedFiles)
|
|
2270
|
+
await put(p.merged, rel, t);
|
|
2271
|
+
await mkdir2(join11(folder, "stage"), { recursive: true });
|
|
2272
|
+
await writeFileAtomic(join11(folder, ...p.capture.split("/")), HEADER + (0, import_yaml8.stringify)(capture, { lineWidth: 0 }));
|
|
2273
|
+
return { capture, file: p.capture };
|
|
2274
|
+
}
|
|
2275
|
+
async function readFolderFiles(dir) {
|
|
2276
|
+
const out = /* @__PURE__ */ new Map();
|
|
2277
|
+
for (const f of await walk2(dir))
|
|
2278
|
+
out.set(f, (await readFile9(join11(dir, ...f.split("/")), "utf8")).replace(/\r\n/g, "\n"));
|
|
2279
|
+
return out;
|
|
2280
|
+
}
|
|
2281
|
+
async function changeFolders(root) {
|
|
2282
|
+
const base = join11(root, ...changesRelOf(root).split("/"));
|
|
2283
|
+
const out = [];
|
|
2284
|
+
const list = async (dir) => (await readdir5(dir, { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
2285
|
+
for (const name of await list(base)) {
|
|
2286
|
+
if (name === "archive")
|
|
2287
|
+
for (const a of await list(join11(base, "archive")))
|
|
2288
|
+
out.push(join11(base, "archive", a));
|
|
2289
|
+
else
|
|
2290
|
+
out.push(join11(base, name));
|
|
2291
|
+
}
|
|
2292
|
+
return out;
|
|
2293
|
+
}
|
|
2294
|
+
async function readDraftPairs(root) {
|
|
2295
|
+
const out = [];
|
|
2296
|
+
for (const folder of await changeFolders(root)) {
|
|
2297
|
+
const names = (await readdir5(join11(folder, "stage")).catch(() => [])).filter((f) => f.endsWith(".capture.yaml")).sort();
|
|
2298
|
+
for (const f of names) {
|
|
2299
|
+
let capture;
|
|
2300
|
+
try {
|
|
2301
|
+
capture = DraftCapture.parse((0, import_yaml8.parse)(await readFile9(join11(folder, "stage", f), "utf8")));
|
|
2302
|
+
} catch {
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
const p = capturePaths(capture.stage);
|
|
2306
|
+
out.push({
|
|
2307
|
+
capture,
|
|
2308
|
+
folder: relative(root, folder).split(sep).join("/"),
|
|
2309
|
+
drafted: await readFolderFiles(join11(folder, ...p.drafted.split("/"))),
|
|
2310
|
+
merged: await readFolderFiles(join11(folder, ...p.merged.split("/")))
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
return out;
|
|
2315
|
+
}
|
|
2316
|
+
async function completeCaptures(root, ctx, o) {
|
|
2317
|
+
const out = [];
|
|
2318
|
+
let changes;
|
|
2319
|
+
try {
|
|
2320
|
+
changes = await changeStore(root).list();
|
|
2321
|
+
} catch {
|
|
2322
|
+
return out;
|
|
2323
|
+
}
|
|
2324
|
+
for (const change of changes) {
|
|
2325
|
+
const type = ctx.orgPack.changeTypes.types.find((t) => t.id === change.type);
|
|
2326
|
+
if (!type)
|
|
2327
|
+
continue;
|
|
2328
|
+
for (const h of change.handoffs.filter((x) => x.state === "merged")) {
|
|
2329
|
+
const { name } = await captureStageOf(ctx, change, h.phase);
|
|
2330
|
+
const file = join11(changeFolder(root, change.id), ...capturePaths(name).capture.split("/"));
|
|
2331
|
+
const prior = await readFile9(file, "utf8").then((t) => DraftCapture.safeParse((0, import_yaml8.parse)(t)), () => void 0);
|
|
2332
|
+
if (prior?.success && prior.data.pr.number === h.pr.number && prior.data.state !== "waiting-for-merge")
|
|
2333
|
+
continue;
|
|
2334
|
+
const base = prior?.success ? prior.data.base : o.defaultBase ?? "main";
|
|
2335
|
+
const r = await captureHandoff({
|
|
2336
|
+
ctx,
|
|
2337
|
+
change,
|
|
2338
|
+
type,
|
|
2339
|
+
handoff: h,
|
|
2340
|
+
base,
|
|
2341
|
+
now: o.now,
|
|
2342
|
+
...o.git ? { git: o.git } : {},
|
|
2343
|
+
...o.remote ? { remote: o.remote } : {}
|
|
2344
|
+
});
|
|
2345
|
+
if (r)
|
|
2346
|
+
out.push(r);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
return out;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// packages/changes/dist/handoff.js
|
|
2353
|
+
var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
2354
|
+
var strip3 = (h) => h.startsWith("@") ? h.slice(1) : h;
|
|
2355
|
+
function metarepo(ctx, repo) {
|
|
2356
|
+
const fromRuntime = ctx.orgPack.runtime.adapters.gitHost?.settings.metarepo;
|
|
2357
|
+
const r = repo ?? (typeof fromRuntime === "string" ? fromRuntime : void 0);
|
|
2358
|
+
if (!r)
|
|
2359
|
+
throw new ChangeError("invalid", "Say which repo on the Git host is the metarepo: pass --repo, or set metarepo under adapters.gitHost.settings in runtime.yaml.");
|
|
2360
|
+
return r;
|
|
2361
|
+
}
|
|
2362
|
+
async function load(root, id) {
|
|
2363
|
+
const ctx = await loadChangeContext(root);
|
|
2364
|
+
const change = await changeStore(root).read(id).catch((e) => {
|
|
2365
|
+
throw new ChangeError("not-found", e.message);
|
|
2366
|
+
});
|
|
2367
|
+
return { ctx, change };
|
|
2368
|
+
}
|
|
2369
|
+
async function walkReview(dir, gate) {
|
|
2370
|
+
const out = [];
|
|
2371
|
+
const base = join12(dir, "review", gate);
|
|
2372
|
+
let rounds;
|
|
2373
|
+
try {
|
|
2374
|
+
rounds = (await readdir6(base)).filter((r) => /^\d+$/.test(r)).sort((a, b) => Number(a) - Number(b));
|
|
2375
|
+
} catch {
|
|
2376
|
+
return [];
|
|
2377
|
+
}
|
|
2378
|
+
for (const r of rounds) {
|
|
2379
|
+
try {
|
|
2380
|
+
for (const f of (await readdir6(join12(base, r))).filter((x) => /\.ya?ml$/.test(x)).sort())
|
|
2381
|
+
out.push(`review/${gate}/${r}/${f}`);
|
|
2382
|
+
} catch {
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
return out;
|
|
2386
|
+
}
|
|
2387
|
+
async function keptPairFiles(dir) {
|
|
2388
|
+
const out = [];
|
|
2389
|
+
let entries;
|
|
2390
|
+
try {
|
|
2391
|
+
entries = await readdir6(join12(dir, "stage"), { withFileTypes: true });
|
|
2392
|
+
} catch {
|
|
2393
|
+
return out;
|
|
2394
|
+
}
|
|
2395
|
+
const walk3 = async (rel) => {
|
|
2396
|
+
for (const e of await readdir6(join12(dir, ...rel.split("/")), { withFileTypes: true }).catch(() => [])) {
|
|
2397
|
+
const p = `${rel}/${e.name}`;
|
|
2398
|
+
if (e.isDirectory())
|
|
2399
|
+
await walk3(p);
|
|
2400
|
+
else
|
|
2401
|
+
out.push(p);
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
for (const e of entries) {
|
|
2405
|
+
if (e.isFile() && e.name.endsWith(".capture.yaml"))
|
|
2406
|
+
out.push(`stage/${e.name}`);
|
|
2407
|
+
else if (e.isDirectory() && /\.(drafted|merged)$/.test(e.name))
|
|
2408
|
+
await walk3(`stage/${e.name}`);
|
|
2409
|
+
}
|
|
2410
|
+
return out.sort();
|
|
2411
|
+
}
|
|
2412
|
+
async function handoffFiles(ctx, a, phase) {
|
|
2413
|
+
const dir = changeFolder(ctx.root, a.change.id);
|
|
2414
|
+
const artifacts = [];
|
|
2415
|
+
const files = /* @__PURE__ */ new Set();
|
|
2416
|
+
for (const art of phaseArtifacts(ctx, a.type, phase)) {
|
|
2417
|
+
const f = await artifactFiles(dir, art);
|
|
2418
|
+
if (f.length === 0)
|
|
2419
|
+
continue;
|
|
2420
|
+
artifacts.push(art);
|
|
2421
|
+
for (const x of f)
|
|
2422
|
+
files.add(x);
|
|
2423
|
+
}
|
|
2424
|
+
const stage = await phaseStage(ctx, a.change, phase);
|
|
2425
|
+
if (stage)
|
|
2426
|
+
for (const f of await stageFiles(dir, stage))
|
|
2427
|
+
files.add(f);
|
|
2428
|
+
if (phase === "build") {
|
|
2429
|
+
for (const extra of ["tasks.md", "repos.yaml"]) {
|
|
2430
|
+
try {
|
|
2431
|
+
await readFile10(join12(dir, extra));
|
|
2432
|
+
files.add(extra);
|
|
2433
|
+
} catch {
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
for (const gate of phaseGates(ctx, phase))
|
|
2438
|
+
for (const f of await walkReview(dir, gate))
|
|
2439
|
+
files.add(f);
|
|
2440
|
+
for (const f of await keptPairFiles(dir))
|
|
2441
|
+
files.add(f);
|
|
2442
|
+
return { files: [...files].sort(), artifacts };
|
|
2443
|
+
}
|
|
2444
|
+
function prBody(a, phase, next, paths, reviewers, review) {
|
|
2445
|
+
const c = a.change;
|
|
2446
|
+
const lines2 = [
|
|
2447
|
+
`## Hand-off: ${handoffName(a.type, phase)}`,
|
|
2448
|
+
"",
|
|
2449
|
+
`Change ${c.id} (${c.title}), type ${c.type}, moves from ${phase} to ${next} when this pull request merges.`,
|
|
2450
|
+
"",
|
|
2451
|
+
"Merging this pull request approves the hand-off. The Git host's own rules (branch protection, CODEOWNERS) decide who may approve it.",
|
|
2452
|
+
"",
|
|
2453
|
+
"### Files",
|
|
2454
|
+
"",
|
|
2455
|
+
...paths.length ? paths.map((f) => `- ${f}`) : ["- None"],
|
|
2456
|
+
"",
|
|
2457
|
+
"### Reviewers",
|
|
2458
|
+
"",
|
|
2459
|
+
...reviewers.length ? reviewers.map((r) => `- @${r}`) : ["- None"],
|
|
2460
|
+
"",
|
|
2461
|
+
"### Risk",
|
|
2462
|
+
"",
|
|
2463
|
+
`${a.risk.level} (${a.risk.score})`,
|
|
2464
|
+
"",
|
|
2465
|
+
...a.risk.factors.map((f) => `- ${f.detail}: +${f.points}`),
|
|
2466
|
+
"",
|
|
2467
|
+
"### Privacy",
|
|
2468
|
+
"",
|
|
2469
|
+
a.privacy.touched ? `Touches personal data (${a.privacy.records.map((r) => r.id).join(", ")}); review it with the ${a.privacy.phase} hand-off.` : "No personal data touched.",
|
|
2470
|
+
...review?.trim() ? ["", review.trim()] : []
|
|
2471
|
+
];
|
|
2472
|
+
return lines2.join("\n") + "\n";
|
|
2473
|
+
}
|
|
2474
|
+
async function handoff(root, id, opts) {
|
|
2475
|
+
requireApproval("changes", opts.approval);
|
|
2476
|
+
const { ctx, change } = await load(root, id);
|
|
2477
|
+
const repo = metarepo(ctx, opts.repo);
|
|
2478
|
+
const now = opts.now ?? nowIso2();
|
|
2479
|
+
const by = strip3(opts.by);
|
|
2480
|
+
const a = await assess(ctx, change);
|
|
2481
|
+
const phase = a.phase;
|
|
2482
|
+
if (!phase)
|
|
2483
|
+
throw new ChangeError("refused", `${id} is ${change.state}; there is nothing to hand off.`, a.reasons);
|
|
2484
|
+
if (a.blockers.length > 0)
|
|
2485
|
+
throw new ChangeError("refused", `${id} isn't ready for "${handoffName(a.type, phase)}".`, a.blockers);
|
|
2486
|
+
if (opts.openspec && phase === "plan")
|
|
2487
|
+
await requireValid(opts.openspec, id);
|
|
2488
|
+
const { files, artifacts } = await handoffFiles(ctx, a, phase);
|
|
2489
|
+
const dir = changeFolder(root, id);
|
|
2490
|
+
const hashed = await Promise.all(files.map(async (path) => ({ path, sha256: sha256(await readFile10(join12(dir, path))) })));
|
|
2491
|
+
const authors = { ...change.authors };
|
|
2492
|
+
for (const art of artifacts) {
|
|
2493
|
+
const people = (opts.authors?.[art] ?? [by]).map(strip3);
|
|
2494
|
+
authors[art] = [.../* @__PURE__ */ new Set([...authors[art] ?? [], ...people])];
|
|
2495
|
+
}
|
|
2496
|
+
const branch = handoffBranch(id, phase);
|
|
2497
|
+
const rel = changesRelOf(root);
|
|
2498
|
+
const metaFiles = [...files.map((f) => `${rel}/${id}/${f}`), `${rel}/${id}/change.yaml`];
|
|
2499
|
+
const reviewers = [
|
|
2500
|
+
...new Set(files.flatMap((f) => ownersFor(ctx.codeowners, `${rel}/${id}/${f}`)))
|
|
2501
|
+
].filter((p) => p !== by).sort();
|
|
2502
|
+
const live = change.state === "returned" ? void 0 : liveHandoff(change);
|
|
2503
|
+
if (live && live.phase === phase) {
|
|
2504
|
+
const pr2 = await opts.gitHost.getPullRequest(repo, live.pr.number);
|
|
2505
|
+
if (pr2.state === "open" || pr2.state === "draft") {
|
|
2506
|
+
await opts.publisher?.publish({
|
|
2507
|
+
root,
|
|
2508
|
+
branch,
|
|
2509
|
+
base: pr2.base,
|
|
2510
|
+
files: metaFiles,
|
|
2511
|
+
message: `${handoffName(a.type, phase)}: update`,
|
|
2512
|
+
approval: opts.approval
|
|
2513
|
+
});
|
|
2514
|
+
await opts.gitHost.comment(repo, live.pr.number, `${by} updated the hand-off for another review.`, opts.approval);
|
|
2515
|
+
const updated = {
|
|
2516
|
+
...live,
|
|
2517
|
+
state: "open",
|
|
2518
|
+
updatedAt: now,
|
|
2519
|
+
reviewers,
|
|
2520
|
+
artifacts: hashed,
|
|
2521
|
+
approvals: []
|
|
2522
|
+
};
|
|
2523
|
+
await changeStore(root).write({
|
|
2524
|
+
...change,
|
|
2525
|
+
authors,
|
|
2526
|
+
handoffs: change.handoffs.map((h2) => h2 === live ? updated : h2)
|
|
2527
|
+
});
|
|
2528
|
+
return updated;
|
|
2529
|
+
}
|
|
2530
|
+
if (pr2.state === "merged")
|
|
2531
|
+
throw new ChangeError("refused", `PR #${pr2.number} is already merged on the Git host; sync the change instead.`);
|
|
2532
|
+
live.state = "closed";
|
|
2533
|
+
live.updatedAt = now;
|
|
2534
|
+
}
|
|
2535
|
+
const base = opts.base ?? (await opts.gitHost.getRepo(repo)).defaultBranch;
|
|
2536
|
+
const next = a.next;
|
|
2537
|
+
await opts.publisher?.publish({
|
|
2538
|
+
root,
|
|
2539
|
+
branch,
|
|
2540
|
+
base,
|
|
2541
|
+
files: metaFiles,
|
|
2542
|
+
message: `${handoffName(a.type, phase)}: ${change.title}`,
|
|
2543
|
+
approval: opts.approval
|
|
2544
|
+
});
|
|
2545
|
+
const pr = await opts.gitHost.createPullRequest({
|
|
2546
|
+
repo,
|
|
2547
|
+
head: branch,
|
|
2548
|
+
base,
|
|
2549
|
+
title: `${change.title}: ${handoffName(a.type, phase)}`,
|
|
2550
|
+
body: prBody(a, phase, change.state === "returned" ? nextAfterResubmit(a, phase) : next, files.map((f) => `${rel}/${id}/${f}`), reviewers, opts.review)
|
|
2551
|
+
}, opts.approval);
|
|
2552
|
+
const h = {
|
|
2553
|
+
phase,
|
|
2554
|
+
branch,
|
|
2555
|
+
pr: { number: pr.number, url: pr.url },
|
|
2556
|
+
state: "open",
|
|
2557
|
+
by,
|
|
2558
|
+
openedAt: now,
|
|
2559
|
+
reviewers,
|
|
2560
|
+
approvals: [],
|
|
2561
|
+
artifacts: hashed
|
|
2562
|
+
};
|
|
2563
|
+
const history = [...change.history];
|
|
2564
|
+
let state = change.state;
|
|
2565
|
+
if (change.state === "returned") {
|
|
2566
|
+
history.push({ from: "returned", to: phaseState(phase), at: now, by, reason: "Resubmitted" });
|
|
2567
|
+
state = phaseState(phase);
|
|
2568
|
+
}
|
|
2569
|
+
await changeStore(root).write({
|
|
2570
|
+
...change,
|
|
2571
|
+
state,
|
|
2572
|
+
authors,
|
|
2573
|
+
history,
|
|
2574
|
+
handoffs: [...change.handoffs, h]
|
|
2575
|
+
});
|
|
2576
|
+
return h;
|
|
2577
|
+
}
|
|
2578
|
+
function nextAfterResubmit(a, phase) {
|
|
2579
|
+
const next = a.type.phases[a.type.phases.indexOf(phase) + 1];
|
|
2580
|
+
return next ? phaseState(next) : "released";
|
|
2581
|
+
}
|
|
2582
|
+
async function syncHandoff(root, id, opts) {
|
|
2583
|
+
const { ctx, change } = await load(root, id);
|
|
2584
|
+
const repo = metarepo(ctx, opts.repo);
|
|
2585
|
+
const now = opts.now ?? nowIso2();
|
|
2586
|
+
const live = liveHandoff(change);
|
|
2587
|
+
if (!live)
|
|
2588
|
+
throw new ChangeError("refused", `${id} has no open hand-off PR to sync.`, [
|
|
2589
|
+
{ code: "no-handoff", message: `${id} has no open hand-off PR.` }
|
|
2590
|
+
]);
|
|
2591
|
+
const a = await assess(ctx, change);
|
|
2592
|
+
const pr = await opts.gitHost.getPullRequest(repo, live.pr.number);
|
|
2593
|
+
const current = pr.url !== live.pr.url || pr.number !== live.pr.number ? { ...live, pr: { number: pr.number, url: pr.url } } : live;
|
|
2594
|
+
const store = changeStore(root);
|
|
2595
|
+
const save = async (h2, extra = {}) => {
|
|
2596
|
+
const updated = {
|
|
2597
|
+
...change,
|
|
2598
|
+
...extra,
|
|
2599
|
+
handoffs: change.handoffs.map((x) => x === live ? h2 : x)
|
|
2600
|
+
};
|
|
2601
|
+
await store.write(updated);
|
|
2602
|
+
return updated;
|
|
2603
|
+
};
|
|
2604
|
+
if (pr.state === "closed") {
|
|
2605
|
+
const h2 = { ...current, state: "closed", updatedAt: now };
|
|
2606
|
+
await save(h2);
|
|
2607
|
+
return {
|
|
2608
|
+
merged: false,
|
|
2609
|
+
ready: false,
|
|
2610
|
+
state: change.state,
|
|
2611
|
+
handoff: h2,
|
|
2612
|
+
reasons: [{ code: "pr-closed", message: `PR #${pr.number} was closed on the Git host.` }]
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
const latest = /* @__PURE__ */ new Map();
|
|
2616
|
+
for (const r of pr.reviews)
|
|
2617
|
+
if (r.state !== "commented")
|
|
2618
|
+
latest.set(strip3(r.by), r.state);
|
|
2619
|
+
const approvers = [...latest].filter(([, s]) => s === "approved").map(([p]) => p);
|
|
2620
|
+
const requested = [...latest].filter(([, s]) => s === "changes-requested").map(([p]) => p).sort();
|
|
2621
|
+
const counted = prApprovers(live, approvers);
|
|
2622
|
+
const approvals = counted.map((by) => ({ by, at: now }));
|
|
2623
|
+
const advance = async (by) => {
|
|
2624
|
+
const h2 = { ...current, state: "merged", updatedAt: now, approvals };
|
|
2625
|
+
const next = a.next;
|
|
2626
|
+
const history = [
|
|
2627
|
+
...change.history,
|
|
2628
|
+
{
|
|
2629
|
+
from: change.state,
|
|
2630
|
+
to: next,
|
|
2631
|
+
at: now,
|
|
2632
|
+
by,
|
|
2633
|
+
pr: pr.number,
|
|
2634
|
+
reason: `${handoffName(a.type, live.phase)} merged`
|
|
2635
|
+
}
|
|
2636
|
+
];
|
|
2637
|
+
let state = next;
|
|
2638
|
+
if (next === "released" && a.type.postReleaseReview) {
|
|
2639
|
+
history.push({
|
|
2640
|
+
from: "released",
|
|
2641
|
+
to: "post-release-review",
|
|
2642
|
+
at: now,
|
|
2643
|
+
by,
|
|
2644
|
+
reason: "Released first; the skipped lenses are reviewed now"
|
|
2645
|
+
});
|
|
2646
|
+
state = "post-release-review";
|
|
2647
|
+
}
|
|
2648
|
+
const saved = await save(h2, { state, history });
|
|
2649
|
+
const kept = await captureHandoff({
|
|
2650
|
+
ctx,
|
|
2651
|
+
change: saved,
|
|
2652
|
+
type: a.type,
|
|
2653
|
+
handoff: h2,
|
|
2654
|
+
base: pr.base,
|
|
2655
|
+
now,
|
|
2656
|
+
...opts.git ? { git: opts.git } : {},
|
|
2657
|
+
...opts.remote ? { remote: opts.remote } : {}
|
|
2658
|
+
}).catch(() => void 0);
|
|
2659
|
+
return {
|
|
2660
|
+
merged: true,
|
|
2661
|
+
ready: true,
|
|
2662
|
+
state,
|
|
2663
|
+
handoff: h2,
|
|
2664
|
+
reasons: [],
|
|
2665
|
+
...kept ? { capture: kept.capture } : {}
|
|
2666
|
+
};
|
|
2667
|
+
};
|
|
2668
|
+
if (pr.state === "merged")
|
|
2669
|
+
return advance(strip3(opts.approval?.by ?? live.by));
|
|
2670
|
+
if (requested.length > 0) {
|
|
2671
|
+
const h2 = { ...current, state: "changes-requested", updatedAt: now, approvals };
|
|
2672
|
+
await save(h2);
|
|
2673
|
+
return {
|
|
2674
|
+
merged: false,
|
|
2675
|
+
ready: false,
|
|
2676
|
+
state: change.state,
|
|
2677
|
+
handoff: h2,
|
|
2678
|
+
reasons: requested.map((p) => ({
|
|
2679
|
+
code: "changes-requested",
|
|
2680
|
+
person: p,
|
|
2681
|
+
message: `${p} requested changes on PR #${pr.number}.`
|
|
2682
|
+
}))
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
const h = { ...current, state: "open", updatedAt: now, approvals };
|
|
2686
|
+
const reasons = [];
|
|
2687
|
+
if (counted.length === 0)
|
|
2688
|
+
reasons.push(approvers.length > 0 ? {
|
|
2689
|
+
code: "self-approval",
|
|
2690
|
+
person: strip3(live.by),
|
|
2691
|
+
message: `PR #${pr.number} is approved only by ${strip3(live.by)}, who raised it; it needs someone else's approval.`
|
|
2692
|
+
} : {
|
|
2693
|
+
code: "approval-missing",
|
|
2694
|
+
message: `PR #${pr.number} isn't approved yet: ask a reviewer on the Git host (${pr.url}).`
|
|
2695
|
+
});
|
|
2696
|
+
if (reasons.length === 0) {
|
|
2697
|
+
reasons.push(...a.blockers);
|
|
2698
|
+
}
|
|
2699
|
+
if (reasons.length === 0) {
|
|
2700
|
+
const blockers = mergeBlockers(pr);
|
|
2701
|
+
if (blockers.length > 0)
|
|
2702
|
+
reasons.push({
|
|
2703
|
+
code: "host-refused",
|
|
2704
|
+
message: `The Git host won't merge PR #${pr.number} yet: ${blockers.join("; ")}.`
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
if (reasons.length > 0 || !opts.approval) {
|
|
2708
|
+
await save(h);
|
|
2709
|
+
return { merged: false, ready: reasons.length === 0, state: change.state, handoff: h, reasons };
|
|
2710
|
+
}
|
|
2711
|
+
const approval = requireApproval("changes", opts.approval);
|
|
2712
|
+
try {
|
|
2713
|
+
await opts.gitHost.merge(repo, pr.number, opts.approval);
|
|
2714
|
+
} catch (e) {
|
|
2715
|
+
if (!(e instanceof AdapterError))
|
|
2716
|
+
throw e;
|
|
2717
|
+
await save(h);
|
|
2718
|
+
return {
|
|
2719
|
+
merged: false,
|
|
2720
|
+
ready: false,
|
|
2721
|
+
state: change.state,
|
|
2722
|
+
handoff: h,
|
|
2723
|
+
reasons: [{ code: "host-refused", message: `The Git host refused the merge: ${e.message}` }]
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
return advance(strip3(approval.by));
|
|
2727
|
+
}
|
|
2728
|
+
async function returnToPo(root, id, opts) {
|
|
2729
|
+
const { ctx, change } = await load(root, id);
|
|
2730
|
+
const live = liveHandoff(change);
|
|
2731
|
+
if (!live)
|
|
2732
|
+
throw new ChangeError("refused", `${id} has no open hand-off PR to return.`, [
|
|
2733
|
+
{ code: "no-handoff", message: `${id} has no open hand-off PR.` }
|
|
2734
|
+
]);
|
|
2735
|
+
requireApproval("changes", opts.approval);
|
|
2736
|
+
const repo = metarepo(ctx, opts.repo);
|
|
2737
|
+
const now = opts.now ?? nowIso2();
|
|
2738
|
+
const by = strip3(opts.by);
|
|
2739
|
+
const host = opts.gitHost;
|
|
2740
|
+
await host.comment(repo, live.pr.number, `Returned to the product owner by ${by}: ${opts.reason}`, opts.approval);
|
|
2741
|
+
const closedOnHost = host.closePullRequest ? (await host.closePullRequest(repo, live.pr.number, opts.approval)).state === "closed" : false;
|
|
2742
|
+
const h = { ...live, state: "closed", updatedAt: now };
|
|
2743
|
+
await changeStore(root).write({
|
|
2744
|
+
...change,
|
|
2745
|
+
state: "returned",
|
|
2746
|
+
history: [
|
|
2747
|
+
...change.history,
|
|
2748
|
+
{
|
|
2749
|
+
from: change.state,
|
|
2750
|
+
to: "returned",
|
|
2751
|
+
at: now,
|
|
2752
|
+
by,
|
|
2753
|
+
pr: live.pr.number,
|
|
2754
|
+
reason: `Returned to the product owner (${opts.reason})`
|
|
2755
|
+
}
|
|
2756
|
+
],
|
|
2757
|
+
handoffs: change.handoffs.map((x) => x === live ? h : x)
|
|
2758
|
+
});
|
|
2759
|
+
return { state: "returned", closedOnHost, handoff: h };
|
|
2760
|
+
}
|
|
2761
|
+
async function requireValid(openspec, id) {
|
|
2762
|
+
const r = await openspec.validate({ item: id, type: "change", strict: true });
|
|
2763
|
+
const item = r.items[0];
|
|
2764
|
+
if (!item || item.valid)
|
|
2765
|
+
return;
|
|
2766
|
+
throw new ChangeError("refused", `${id} doesn't pass openspec validate --strict.`, item.issues.filter((i) => i.level !== "INFO").map((i) => ({ code: "openspec-invalid", message: `${i.path}: ${i.message}` })));
|
|
2767
|
+
}
|
|
2768
|
+
async function archiveChange(root, id, opts) {
|
|
2769
|
+
const { ctx, change } = await load(root, id);
|
|
2770
|
+
const a = await assess(ctx, change);
|
|
2771
|
+
if (change.state !== "released" && change.state !== "post-release-review")
|
|
2772
|
+
throw new ChangeError("refused", `${id} is ${change.state}; only a released change can be archived.`, [{ code: "wrong-state", message: `${id} is ${change.state}.` }]);
|
|
2773
|
+
if (a.reasons.length > 0)
|
|
2774
|
+
throw new ChangeError("refused", `${id} can't be archived yet.`, a.reasons);
|
|
2775
|
+
if (opts.openspec)
|
|
2776
|
+
await requireValid(opts.openspec, id);
|
|
2777
|
+
const now = opts.now ?? nowIso2();
|
|
2778
|
+
const updated = {
|
|
2779
|
+
...change,
|
|
2780
|
+
state: "archived",
|
|
2781
|
+
history: [
|
|
2782
|
+
...change.history,
|
|
2783
|
+
{ from: change.state, to: "archived", at: now, by: strip3(opts.by), reason: "Archived" }
|
|
2784
|
+
]
|
|
2785
|
+
};
|
|
2786
|
+
await changeStore(root).write(updated);
|
|
2787
|
+
if (opts.openspec)
|
|
2788
|
+
await opts.openspec.archive(id);
|
|
2789
|
+
return updated;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// packages/changes/dist/learn.js
|
|
2793
|
+
var import_yaml9 = __toESM(require_dist(), 1);
|
|
2794
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
2795
|
+
import { join as join13 } from "node:path";
|
|
2796
|
+
var LEARN_SCHEMA_VERSION = 1;
|
|
2797
|
+
var MAX_CELLS = 4e6;
|
|
2798
|
+
var lines = (text) => {
|
|
2799
|
+
if (text === void 0 || text === "")
|
|
2800
|
+
return [];
|
|
2801
|
+
const ls = text.replace(/\r\n/g, "\n").split("\n");
|
|
2802
|
+
if (ls[ls.length - 1] === "")
|
|
2803
|
+
ls.pop();
|
|
2804
|
+
return ls;
|
|
2805
|
+
};
|
|
2806
|
+
function lineDiff(a, b) {
|
|
2807
|
+
const n = a.length;
|
|
2808
|
+
const m = b.length;
|
|
2809
|
+
if (n * m > MAX_CELLS) {
|
|
2810
|
+
const left = /* @__PURE__ */ new Map();
|
|
2811
|
+
for (const l of b)
|
|
2812
|
+
left.set(l, (left.get(l) ?? 0) + 1);
|
|
2813
|
+
const out2 = [];
|
|
2814
|
+
for (const l of a) {
|
|
2815
|
+
const k = left.get(l) ?? 0;
|
|
2816
|
+
if (k > 0) {
|
|
2817
|
+
left.set(l, k - 1);
|
|
2818
|
+
out2.push({ op: "=", line: l });
|
|
2819
|
+
} else
|
|
2820
|
+
out2.push({ op: "-", line: l });
|
|
2821
|
+
}
|
|
2822
|
+
for (const [l, k] of left)
|
|
2823
|
+
for (let i2 = 0; i2 < k; i2++)
|
|
2824
|
+
out2.push({ op: "+", line: l });
|
|
2825
|
+
return out2;
|
|
2826
|
+
}
|
|
2827
|
+
const w = m + 1;
|
|
2828
|
+
const t = new Uint32Array((n + 1) * w);
|
|
2829
|
+
for (let i2 = n - 1; i2 >= 0; i2--)
|
|
2830
|
+
for (let j2 = m - 1; j2 >= 0; j2--)
|
|
2831
|
+
t[i2 * w + j2] = a[i2] === b[j2] ? t[(i2 + 1) * w + j2 + 1] + 1 : Math.max(t[(i2 + 1) * w + j2], t[i2 * w + j2 + 1]);
|
|
2832
|
+
const out = [];
|
|
2833
|
+
let i = 0;
|
|
2834
|
+
let j = 0;
|
|
2835
|
+
while (i < n && j < m) {
|
|
2836
|
+
if (a[i] === b[j]) {
|
|
2837
|
+
out.push({ op: "=", line: a[i] });
|
|
2838
|
+
i++;
|
|
2839
|
+
j++;
|
|
2840
|
+
} else if (t[(i + 1) * w + j] >= t[i * w + j + 1])
|
|
2841
|
+
out.push({ op: "-", line: a[i++] });
|
|
2842
|
+
else
|
|
2843
|
+
out.push({ op: "+", line: b[j++] });
|
|
2844
|
+
}
|
|
2845
|
+
while (i < n)
|
|
2846
|
+
out.push({ op: "-", line: a[i++] });
|
|
2847
|
+
while (j < m)
|
|
2848
|
+
out.push({ op: "+", line: b[j++] });
|
|
2849
|
+
return out;
|
|
2850
|
+
}
|
|
2851
|
+
function hunksOf(drafted, merged) {
|
|
2852
|
+
const out = [];
|
|
2853
|
+
let cur;
|
|
2854
|
+
let line = 1;
|
|
2855
|
+
for (const o of lineDiff(lines(drafted), lines(merged))) {
|
|
2856
|
+
if (o.op === "=") {
|
|
2857
|
+
cur = void 0;
|
|
2858
|
+
line++;
|
|
2859
|
+
continue;
|
|
2860
|
+
}
|
|
2861
|
+
if (!cur)
|
|
2862
|
+
out.push(cur = { at: line, drafted: [], merged: [] });
|
|
2863
|
+
if (o.op === "-") {
|
|
2864
|
+
cur.drafted.push(o.line);
|
|
2865
|
+
line++;
|
|
2866
|
+
} else
|
|
2867
|
+
cur.merged.push(o.line);
|
|
2868
|
+
}
|
|
2869
|
+
return out;
|
|
2870
|
+
}
|
|
2871
|
+
function countLines(drafted, merged) {
|
|
2872
|
+
let added = 0;
|
|
2873
|
+
let removed = 0;
|
|
2874
|
+
let changed2 = 0;
|
|
2875
|
+
for (const h of hunksOf(drafted, merged)) {
|
|
2876
|
+
const c = Math.min(h.drafted.length, h.merged.length);
|
|
2877
|
+
changed2 += c;
|
|
2878
|
+
removed += h.drafted.length - c;
|
|
2879
|
+
added += h.merged.length - c;
|
|
2880
|
+
}
|
|
2881
|
+
return { drafted: lines(drafted).length, merged: lines(merged).length, added, removed, changed: changed2 };
|
|
2882
|
+
}
|
|
2883
|
+
var norm = (s) => s.trim().replace(/\s+\((Entry|Validation|Error|Checks?|Outcome)[^)]*\)\s*$/i, "").replace(/[.\s]+$/, "").replace(/\s+/g, " ");
|
|
2884
|
+
function readStory(text) {
|
|
2885
|
+
const ls = lines(text);
|
|
2886
|
+
const sections = /* @__PURE__ */ new Map();
|
|
2887
|
+
let title = "";
|
|
2888
|
+
let statement = "";
|
|
2889
|
+
let section = "";
|
|
2890
|
+
let body = [];
|
|
2891
|
+
const close = () => {
|
|
2892
|
+
if (section)
|
|
2893
|
+
sections.set(section, body.join("\n").trim());
|
|
2894
|
+
};
|
|
2895
|
+
for (const l of ls) {
|
|
2896
|
+
if (!title && /^# /.test(l)) {
|
|
2897
|
+
title = l.slice(2).trim();
|
|
2898
|
+
continue;
|
|
2899
|
+
}
|
|
2900
|
+
const h = /^## (.+)$/.exec(l);
|
|
2901
|
+
if (h) {
|
|
2902
|
+
close();
|
|
2903
|
+
section = h[1].trim();
|
|
2904
|
+
body = [];
|
|
2905
|
+
continue;
|
|
2906
|
+
}
|
|
2907
|
+
if (!section && !statement && /^As an? /.test(l))
|
|
2908
|
+
statement = l.trim();
|
|
2909
|
+
if (section)
|
|
2910
|
+
body.push(l);
|
|
2911
|
+
}
|
|
2912
|
+
close();
|
|
2913
|
+
const bullets = (s, indent) => (s ?? "").split("\n").filter((l) => indent.test(l)).map((l) => norm(l.replace(indent, "")));
|
|
2914
|
+
let criteria = bullets(sections.get("Acceptance criteria"), /^- /);
|
|
2915
|
+
if (!criteria.length) {
|
|
2916
|
+
const tasks = sections.get("Tasks") ?? "";
|
|
2917
|
+
let inCriteria = false;
|
|
2918
|
+
for (const l of tasks.split("\n")) {
|
|
2919
|
+
if (/^- Acceptance criteria:/.test(l))
|
|
2920
|
+
inCriteria = true;
|
|
2921
|
+
else if (/^- /.test(l) || /^#/.test(l))
|
|
2922
|
+
inCriteria = false;
|
|
2923
|
+
else if (inCriteria && /^ {2}- /.test(l))
|
|
2924
|
+
criteria.push(norm(l.replace(/^ {2}- /, "")));
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
criteria = [...new Set(criteria.filter(Boolean))];
|
|
2928
|
+
const fields = /* @__PURE__ */ new Set();
|
|
2929
|
+
for (const l of (sections.get("Tasks") ?? "").split("\n")) {
|
|
2930
|
+
const f = /^- Fields: (.+)$/.exec(l);
|
|
2931
|
+
if (f) {
|
|
2932
|
+
for (const x of f[1].split(","))
|
|
2933
|
+
if (x.trim() && x.trim() !== "none")
|
|
2934
|
+
fields.add(x.trim());
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
for (const name of ["Interface changes", "Data changes"]) {
|
|
2938
|
+
let col = -1;
|
|
2939
|
+
for (const l of (sections.get(name) ?? "").split("\n")) {
|
|
2940
|
+
if (!l.startsWith("|")) {
|
|
2941
|
+
col = -1;
|
|
2942
|
+
continue;
|
|
2943
|
+
}
|
|
2944
|
+
const cells2 = l.split("|").slice(1, -1).map((c) => c.trim());
|
|
2945
|
+
if (cells2.every((c) => /^-+$/.test(c)))
|
|
2946
|
+
continue;
|
|
2947
|
+
const head = cells2.findIndex((c) => c === "Field" || c === "Column");
|
|
2948
|
+
if (head >= 0) {
|
|
2949
|
+
col = head;
|
|
2950
|
+
continue;
|
|
2951
|
+
}
|
|
2952
|
+
if (col >= 0 && cells2[col])
|
|
2953
|
+
fields.add(cells2[col]);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
return { title, statement, criteria, fields: [...fields].sort(), sections };
|
|
2957
|
+
}
|
|
2958
|
+
function measureStory(file, d, m) {
|
|
2959
|
+
const id = file.replace(/^stories\//, "").replace(/\.md$/, "");
|
|
2960
|
+
const a = d === void 0 ? void 0 : readStory(d);
|
|
2961
|
+
const b = m === void 0 ? void 0 : readStory(m);
|
|
2962
|
+
const status = d === m ? "same" : !a ? "added" : !b ? "removed" : "edited";
|
|
2963
|
+
const minus = (x, y) => x.filter((v) => !y.includes(v));
|
|
2964
|
+
const changed2 = [];
|
|
2965
|
+
if (a && b) {
|
|
2966
|
+
if (a.title !== b.title)
|
|
2967
|
+
changed2.push("title");
|
|
2968
|
+
if (a.statement !== b.statement)
|
|
2969
|
+
changed2.push("statement");
|
|
2970
|
+
for (const s of [.../* @__PURE__ */ new Set([...a.sections.keys(), ...b.sections.keys()])])
|
|
2971
|
+
if ((a.sections.get(s) ?? "") !== (b.sections.get(s) ?? ""))
|
|
2972
|
+
changed2.push(s);
|
|
2973
|
+
}
|
|
2974
|
+
return {
|
|
2975
|
+
id,
|
|
2976
|
+
file,
|
|
2977
|
+
status,
|
|
2978
|
+
criteria: {
|
|
2979
|
+
added: minus(b?.criteria ?? [], a?.criteria ?? []),
|
|
2980
|
+
removed: minus(a?.criteria ?? [], b?.criteria ?? [])
|
|
2981
|
+
},
|
|
2982
|
+
fields: {
|
|
2983
|
+
added: minus(b?.fields ?? [], a?.fields ?? []),
|
|
2984
|
+
removed: minus(a?.fields ?? [], b?.fields ?? [])
|
|
2985
|
+
},
|
|
2986
|
+
changed: changed2
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
var isStoryFile = (p) => p.startsWith("stories/") && p.endsWith(".md");
|
|
2990
|
+
var TREND_MIN = 3;
|
|
2991
|
+
var TREND_STEP = 0.02;
|
|
2992
|
+
var round = (x) => Math.round(x * 1e4) / 1e4;
|
|
2993
|
+
function measurePair(p) {
|
|
2994
|
+
const c = p.capture;
|
|
2995
|
+
const paths = [.../* @__PURE__ */ new Set([...p.drafted.keys(), ...p.merged.keys()])].sort();
|
|
2996
|
+
const files = paths.map((path) => {
|
|
2997
|
+
const d = p.drafted.get(path);
|
|
2998
|
+
const m = p.merged.get(path);
|
|
2999
|
+
const status = d === m ? "same" : d === void 0 ? "added" : m === void 0 ? "removed" : "edited";
|
|
3000
|
+
return { path, status, ...countLines(d, m) };
|
|
3001
|
+
});
|
|
3002
|
+
const sum = (k) => files.reduce((n, f) => n + f[k], 0);
|
|
3003
|
+
const t = {
|
|
3004
|
+
drafted: sum("drafted"),
|
|
3005
|
+
merged: sum("merged"),
|
|
3006
|
+
added: sum("added"),
|
|
3007
|
+
removed: sum("removed"),
|
|
3008
|
+
changed: sum("changed")
|
|
3009
|
+
};
|
|
3010
|
+
return {
|
|
3011
|
+
stage: c.stage,
|
|
3012
|
+
phase: c.phase,
|
|
3013
|
+
pr: c.pr.number,
|
|
3014
|
+
mergedAt: c.merged?.at ?? c.mergedAt,
|
|
3015
|
+
drafted: { from: c.drafted.from, ...c.drafted.commit ? { commit: c.drafted.commit } : {} },
|
|
3016
|
+
merged: { commit: c.merged?.commit ?? "" },
|
|
3017
|
+
files,
|
|
3018
|
+
stories: paths.filter(isStoryFile).map((f) => measureStory(f, p.drafted.get(f), p.merged.get(f))),
|
|
3019
|
+
totals: {
|
|
3020
|
+
...t,
|
|
3021
|
+
editRate: round((t.added + t.removed + t.changed) / Math.max(1, t.drafted, t.merged)),
|
|
3022
|
+
files: files.length,
|
|
3023
|
+
filesEdited: files.filter((f) => f.status !== "same").length
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
var byTime = (a, b) => a.mergedAt.localeCompare(b.mergedAt) || a.change.localeCompare(b.change) || a.stage.localeCompare(b.stage);
|
|
3028
|
+
function trendOf(points) {
|
|
3029
|
+
const ps = [...points].sort(byTime);
|
|
3030
|
+
if (ps.length < TREND_MIN)
|
|
3031
|
+
return { points: ps, direction: "too-few" };
|
|
3032
|
+
const half = Math.floor(ps.length / 2);
|
|
3033
|
+
const mean = (xs) => round(xs.reduce((n, x) => n + x.editRate, 0) / xs.length);
|
|
3034
|
+
const earlier = mean(ps.slice(0, half));
|
|
3035
|
+
const later = mean(ps.slice(ps.length - half));
|
|
3036
|
+
const d = later - earlier;
|
|
3037
|
+
return {
|
|
3038
|
+
points: ps,
|
|
3039
|
+
earlier,
|
|
3040
|
+
later,
|
|
3041
|
+
direction: d <= -TREND_STEP ? "falling" : d >= TREND_STEP ? "rising" : "flat"
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
function learnFromPairs(pairs) {
|
|
3045
|
+
const changes = /* @__PURE__ */ new Map();
|
|
3046
|
+
const pending = [];
|
|
3047
|
+
for (const p of pairs) {
|
|
3048
|
+
const c = p.capture;
|
|
3049
|
+
if (c.state !== "captured") {
|
|
3050
|
+
pending.push({
|
|
3051
|
+
change: c.change,
|
|
3052
|
+
stage: c.stage,
|
|
3053
|
+
pr: c.pr.number,
|
|
3054
|
+
state: c.state,
|
|
3055
|
+
note: c.note ?? ""
|
|
3056
|
+
});
|
|
3057
|
+
continue;
|
|
3058
|
+
}
|
|
3059
|
+
const list = changes.get(c.change) ?? [];
|
|
3060
|
+
list.push(measurePair(p));
|
|
3061
|
+
changes.set(c.change, list);
|
|
3062
|
+
}
|
|
3063
|
+
const out = [...changes].map(([change, stages]) => ({
|
|
3064
|
+
change,
|
|
3065
|
+
stages: stages.sort((a, b) => a.mergedAt.localeCompare(b.mergedAt) || a.stage.localeCompare(b.stage))
|
|
3066
|
+
})).sort((a, b) => a.stages[0].mergedAt.localeCompare(b.stages[0].mergedAt) || a.change.localeCompare(b.change));
|
|
3067
|
+
const points = out.flatMap((c) => c.stages.map((s) => ({
|
|
3068
|
+
change: c.change,
|
|
3069
|
+
stage: s.stage,
|
|
3070
|
+
mergedAt: s.mergedAt,
|
|
3071
|
+
editRate: s.totals.editRate
|
|
3072
|
+
})));
|
|
3073
|
+
const byStage = {};
|
|
3074
|
+
for (const s of [...new Set(points.map((p) => p.stage))].sort())
|
|
3075
|
+
byStage[s] = trendOf(points.filter((p) => p.stage === s));
|
|
3076
|
+
return {
|
|
3077
|
+
schemaVersion: LEARN_SCHEMA_VERSION,
|
|
3078
|
+
changes: out,
|
|
3079
|
+
pending: pending.sort((a, b) => a.change.localeCompare(b.change) || a.stage.localeCompare(b.stage)),
|
|
3080
|
+
trend: { overall: trendOf(points), byStage }
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
async function learnReport(root) {
|
|
3084
|
+
return learnFromPairs(await readDraftPairs(root));
|
|
3085
|
+
}
|
|
3086
|
+
var pct = (x) => `${Math.round(x * 100)}%`;
|
|
3087
|
+
var day = (iso) => iso.slice(0, 10);
|
|
3088
|
+
function trendLine(name, t) {
|
|
3089
|
+
if (t.direction === "too-few")
|
|
3090
|
+
return ` ${name}: ${t.points.length} merged so far; a trend needs ${TREND_MIN}.`;
|
|
3091
|
+
const words = { falling: "falling", rising: "rising", flat: "flat" }[t.direction];
|
|
3092
|
+
return ` ${name}: ${pct(t.earlier)} of lines edited in the earlier half, ${pct(t.later)} in the later half, over ${t.points.length} merged: ${words}.`;
|
|
3093
|
+
}
|
|
3094
|
+
function formatLearn(r) {
|
|
3095
|
+
const out = [];
|
|
3096
|
+
const stages = r.changes.reduce((n, c) => n + c.stages.length, 0);
|
|
3097
|
+
if (!stages && !r.pending.length)
|
|
3098
|
+
return [
|
|
3099
|
+
"No merged hand-off has a kept draft yet. When a stage's hand-off PR merges, atticus change sync keeps what Atticus drafted beside what merged (stage/<stage>.drafted/ and .merged/)."
|
|
3100
|
+
];
|
|
3101
|
+
out.push(`How much people changed Atticus's drafts, from ${stages} merged hand-off${stages === 1 ? "" : "s"} in ${r.changes.length} change${r.changes.length === 1 ? "" : "s"}:`);
|
|
3102
|
+
for (const c of r.changes) {
|
|
3103
|
+
out.push("", c.change);
|
|
3104
|
+
for (const s of c.stages) {
|
|
3105
|
+
const t = s.totals;
|
|
3106
|
+
out.push(` ${s.stage} (PR #${s.pr}, merged ${day(s.mergedAt)}): ${t.added + t.removed + t.changed} of ${Math.max(t.drafted, t.merged)} lines edited (${pct(t.editRate)}); ${t.filesEdited} of ${t.files} files`);
|
|
3107
|
+
for (const f of s.files.filter((x) => x.status !== "same"))
|
|
3108
|
+
out.push(` ${f.path} (${f.status}): +${f.added} -${f.removed} ~${f.changed}`);
|
|
3109
|
+
for (const st of s.stories.filter((x) => x.status !== "same")) {
|
|
3110
|
+
const c2 = st.criteria;
|
|
3111
|
+
const bits = [
|
|
3112
|
+
c2.added.length || c2.removed.length ? `criteria +${c2.added.length} -${c2.removed.length}` : "",
|
|
3113
|
+
st.fields.added.length ? `fields added: ${st.fields.added.join(", ")}` : "",
|
|
3114
|
+
st.fields.removed.length ? `fields removed: ${st.fields.removed.join(", ")}` : "",
|
|
3115
|
+
st.changed.length ? `changed: ${st.changed.join(", ")}` : ""
|
|
3116
|
+
].filter(Boolean);
|
|
3117
|
+
out.push(` story ${st.id} (${st.status}): ${bits.join("; ") || "reworded"}`);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
out.push("", "Trend (share of lines people edited, by merge date):");
|
|
3122
|
+
out.push(trendLine("all stages", r.trend.overall));
|
|
3123
|
+
for (const [s, t] of Object.entries(r.trend.byStage))
|
|
3124
|
+
out.push(trendLine(s, t));
|
|
3125
|
+
if (r.pending.length) {
|
|
3126
|
+
out.push("", "Not counted yet:");
|
|
3127
|
+
for (const p of r.pending)
|
|
3128
|
+
out.push(` ${p.change} ${p.stage} (PR #${p.pr}): ${p.note}`);
|
|
3129
|
+
}
|
|
3130
|
+
return out;
|
|
3131
|
+
}
|
|
3132
|
+
var uniq = (xs) => [...new Set(xs.filter(Boolean))].sort();
|
|
3133
|
+
async function pairTouches(root, p) {
|
|
3134
|
+
const files = p.merged.size ? p.merged : p.drafted;
|
|
3135
|
+
const repos = [];
|
|
3136
|
+
const tables = [];
|
|
3137
|
+
const flows = [];
|
|
3138
|
+
const systems = [];
|
|
3139
|
+
let entry;
|
|
3140
|
+
const yaml = (t) => {
|
|
3141
|
+
try {
|
|
3142
|
+
const v = (0, import_yaml9.parse)(t);
|
|
3143
|
+
return v && typeof v === "object" ? v : void 0;
|
|
3144
|
+
} catch {
|
|
3145
|
+
return void 0;
|
|
3146
|
+
}
|
|
3147
|
+
};
|
|
3148
|
+
const arr = (v) => Array.isArray(v) ? v.filter((x) => !!x && typeof x === "object") : [];
|
|
3149
|
+
for (const [path, text] of files) {
|
|
3150
|
+
if (path === "feature.yaml") {
|
|
3151
|
+
const d = yaml(text);
|
|
3152
|
+
if (typeof d?.entry === "string")
|
|
3153
|
+
entry = d.entry;
|
|
3154
|
+
for (const f of arr(d?.flows))
|
|
3155
|
+
if (typeof f.id === "string") {
|
|
3156
|
+
flows.push(f.id);
|
|
3157
|
+
if (f.id.includes("."))
|
|
3158
|
+
repos.push(f.id.slice(0, f.id.indexOf(".")));
|
|
3159
|
+
}
|
|
3160
|
+
for (const f of arr(d?.newFlows))
|
|
3161
|
+
if (typeof f.repo === "string")
|
|
3162
|
+
repos.push(f.repo);
|
|
3163
|
+
} else if (path.startsWith("target-flows/") && path.endsWith(".yaml")) {
|
|
3164
|
+
const d = yaml(text);
|
|
3165
|
+
if (typeof d?.repo === "string")
|
|
3166
|
+
repos.push(d.repo);
|
|
3167
|
+
if (typeof d?.flow === "string")
|
|
3168
|
+
flows.push(d.flow);
|
|
3169
|
+
for (const s of arr(d?.steps)) {
|
|
3170
|
+
const st = s.store;
|
|
3171
|
+
if (st && typeof st.table === "string")
|
|
3172
|
+
tables.push(st.table);
|
|
3173
|
+
}
|
|
3174
|
+
} else if (path.startsWith("stories/") && path.endsWith(".md")) {
|
|
3175
|
+
const sys = /^## Systems and repos\n\n(.+)$/m.exec(text);
|
|
3176
|
+
if (sys)
|
|
3177
|
+
repos.push(...sys[1].split(",").map((s) => s.trim()));
|
|
3178
|
+
for (const m of text.matchAll(/^- Store: ([^:\s]+):/gm))
|
|
3179
|
+
tables.push(m[1]);
|
|
3180
|
+
for (const m of text.matchAll(/^target-flows\/([^\s]+)\.yaml/gm))
|
|
3181
|
+
flows.push(m[1]);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
const change = yaml(await readFile11(join13(root, ...p.folder.split("/"), "change.yaml"), "utf8").catch(() => ""));
|
|
3185
|
+
const touches = change?.touches ?? {};
|
|
3186
|
+
for (const r of Array.isArray(touches.repos) ? touches.repos : [])
|
|
3187
|
+
if (typeof r === "string")
|
|
3188
|
+
repos.push(r);
|
|
3189
|
+
for (const t of Array.isArray(touches.tables) ? touches.tables : [])
|
|
3190
|
+
if (typeof t === "string")
|
|
3191
|
+
tables.push(t);
|
|
3192
|
+
for (const s of Array.isArray(change?.systems) ? change.systems : [])
|
|
3193
|
+
if (typeof s === "string")
|
|
3194
|
+
systems.push(s);
|
|
3195
|
+
return {
|
|
3196
|
+
repos: uniq(repos),
|
|
3197
|
+
tables: uniq(tables),
|
|
3198
|
+
flows: uniq(flows),
|
|
3199
|
+
systems: uniq(systems),
|
|
3200
|
+
...entry ? { entry } : {}
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
async function pastCorrections(root, q) {
|
|
3204
|
+
const pairs = (await readDraftPairs(root)).filter((p) => p.capture.state === "captured" && p.capture.stage === q.stage && p.capture.change !== q.change);
|
|
3205
|
+
const out = [];
|
|
3206
|
+
for (const p of pairs) {
|
|
3207
|
+
const files = [.../* @__PURE__ */ new Set([...p.drafted.keys(), ...p.merged.keys()])].sort().map((path) => {
|
|
3208
|
+
const d = p.drafted.get(path);
|
|
3209
|
+
const m = p.merged.get(path);
|
|
3210
|
+
const status = d === m ? "same" : d === void 0 ? "added" : m === void 0 ? "removed" : "edited";
|
|
3211
|
+
return { path, status, hunks: status === "same" ? [] : hunksOf(d, m) };
|
|
3212
|
+
}).filter((f) => f.hunks.length > 0);
|
|
3213
|
+
if (!files.length)
|
|
3214
|
+
continue;
|
|
3215
|
+
const t = await pairTouches(root, p);
|
|
3216
|
+
const shares = [];
|
|
3217
|
+
const share = (kind, xs, ys) => {
|
|
3218
|
+
for (const x of xs)
|
|
3219
|
+
if (ys.includes(x))
|
|
3220
|
+
shares.push(`${kind} ${x}`);
|
|
3221
|
+
};
|
|
3222
|
+
share("repo", q.touches.repos, t.repos);
|
|
3223
|
+
share("table", q.touches.tables, t.tables);
|
|
3224
|
+
share("flow", q.touches.flows, t.flows);
|
|
3225
|
+
share("system", q.touches.systems, t.systems);
|
|
3226
|
+
const strong = shares.length;
|
|
3227
|
+
if (q.touches.entry && t.entry === q.touches.entry)
|
|
3228
|
+
shares.push(`entry ${t.entry}`);
|
|
3229
|
+
if (!shares.length)
|
|
3230
|
+
continue;
|
|
3231
|
+
out.push({
|
|
3232
|
+
change: p.capture.change,
|
|
3233
|
+
folder: p.folder,
|
|
3234
|
+
stage: p.capture.stage,
|
|
3235
|
+
pr: p.capture.pr.number,
|
|
3236
|
+
mergedAt: p.capture.merged?.at ?? p.capture.mergedAt,
|
|
3237
|
+
shares,
|
|
3238
|
+
// A shared repo, table, flow or system counts for more than the same kind of entry.
|
|
3239
|
+
score: strong * 2 + (shares.length - strong),
|
|
3240
|
+
files
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
3243
|
+
return out.sort((a, b) => b.score - a.score || b.mergedAt.localeCompare(a.mergedAt) || a.change.localeCompare(b.change)).slice(0, q.max ?? 2);
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
// packages/changes/dist/conflicts.js
|
|
3247
|
+
var both = (a, b) => a.filter((x) => b.includes(x));
|
|
3248
|
+
var changed = (f) => f.flows.filter((x) => x.change !== "read").map((x) => x.id);
|
|
3249
|
+
async function detectConflicts(root) {
|
|
3250
|
+
const ctx = await loadChangeContext(root);
|
|
3251
|
+
const open = (await changeStore(root).list()).filter((c) => c.state !== "archived");
|
|
3252
|
+
const prints = [];
|
|
3253
|
+
for (const c of open)
|
|
3254
|
+
prints.push([c.id, await changeFootprint(ctx, c)]);
|
|
3255
|
+
const out = [];
|
|
3256
|
+
for (let i = 0; i < prints.length; i++) {
|
|
3257
|
+
for (let j = i + 1; j < prints.length; j++) {
|
|
3258
|
+
const [ia, a] = prints[i];
|
|
3259
|
+
const [ib, b] = prints[j];
|
|
3260
|
+
const overlap = {
|
|
3261
|
+
systems: both(a.systems, b.systems),
|
|
3262
|
+
interfaces: both(a.interfaces.map((x) => x.id), b.interfaces.map((x) => x.id)),
|
|
3263
|
+
dataStores: both(a.dataStores, b.dataStores),
|
|
3264
|
+
tables: both(a.tables, b.tables),
|
|
3265
|
+
records: both(a.records, b.records),
|
|
3266
|
+
repos: both(a.repos, b.repos),
|
|
3267
|
+
flows: both(changed(a), changed(b))
|
|
3268
|
+
};
|
|
3269
|
+
if (Object.values(overlap).some((v) => v.length > 0))
|
|
3270
|
+
out.push({ changes: [ia, ib], overlap });
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
return out;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
// packages/changes/dist/size.js
|
|
3277
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
3278
|
+
import { join as join14 } from "node:path";
|
|
3279
|
+
async function changeSize(ctx, change) {
|
|
3280
|
+
const type = ctx.orgPack.changeTypes.types.find((t) => t.id === change.type);
|
|
3281
|
+
if (!type)
|
|
3282
|
+
return {
|
|
3283
|
+
fast: false,
|
|
3284
|
+
why: "standard",
|
|
3285
|
+
words: `${change.type} isn't a change type in change-types.yaml, so ${change.id} takes the full path.`
|
|
3286
|
+
};
|
|
3287
|
+
const brief = await readFile12(join14(changeFolder(ctx.root, change.id), "brief.md"), "utf8").catch(() => "");
|
|
3288
|
+
const f = fastPathOf(type, brief);
|
|
3289
|
+
const gate = f.fast ? fastGateOf(type, ctx.orgPack.gates) : void 0;
|
|
3290
|
+
const words = {
|
|
3291
|
+
type: `${change.id} is a ${type.name.toLowerCase()} (${type.id}), a small type: one step from brief to tasks.`,
|
|
3292
|
+
brief: `${change.id}'s brief says it is small, and ${type.id} lets a brief say so: one step from brief to tasks.`,
|
|
3293
|
+
"brief-standard": `${change.id}'s brief says it is standard, so it takes the full path though ${type.id} is small.`,
|
|
3294
|
+
"brief-not-allowed": `${change.id}'s brief says it is small, but ${type.id} doesn't let a brief choose the fast path (smallWhenBriefSays in change-types.yaml), so it takes the full path.`,
|
|
3295
|
+
standard: `${change.id} is a ${type.id} change: the feature, architecture and stories stages.`
|
|
3296
|
+
};
|
|
3297
|
+
return { fast: f.fast, why: f.why, ...gate ? { gate } : {}, words: words[f.why] };
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// packages/changes/dist/openspec-stores.js
|
|
3301
|
+
import { existsSync } from "node:fs";
|
|
3302
|
+
import { readFile as readFile13, writeFile as writeFile2 } from "node:fs/promises";
|
|
3303
|
+
import { homedir } from "node:os";
|
|
3304
|
+
import { basename, isAbsolute, join as join15, relative as relative2, resolve, sep as sep2 } from "node:path";
|
|
3305
|
+
function storeDirOf(root, path) {
|
|
3306
|
+
if (path === "~" || path.startsWith("~/") || path.startsWith("~\\"))
|
|
3307
|
+
return join15(homedir(), path.slice(2));
|
|
3308
|
+
return isAbsolute(path) ? resolve(path) : resolve(root, path);
|
|
3309
|
+
}
|
|
3310
|
+
function packRel(root) {
|
|
3311
|
+
return relative2(root, orgPackDirOf(root)).split(sep2).join("/");
|
|
3312
|
+
}
|
|
3313
|
+
async function openspecStores(root) {
|
|
3314
|
+
const { settings, problem } = await loadOpenSpecSettings(orgPackDirOf(root));
|
|
3315
|
+
const problems = problem ? [`${packRel(root)}/${problem}`] : [];
|
|
3316
|
+
const stores = [];
|
|
3317
|
+
for (const s of settings.stores) {
|
|
3318
|
+
const dir = storeDirOf(root, s.path);
|
|
3319
|
+
const view = await readStoreVia(dir);
|
|
3320
|
+
stores.push({ ...view, label: s.label ?? view.id ?? basename(dir), path: s.path });
|
|
3321
|
+
}
|
|
3322
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3323
|
+
for (const s of stores) {
|
|
3324
|
+
if (seen.has(s.label))
|
|
3325
|
+
problems.push(`${packRel(root)}/openspec.yaml: two stores are labelled ${s.label}; give one a label`);
|
|
3326
|
+
seen.add(s.label);
|
|
3327
|
+
}
|
|
3328
|
+
return { stores, problems };
|
|
3329
|
+
}
|
|
3330
|
+
async function storeSpecLookup(root, capability) {
|
|
3331
|
+
const { stores } = await openspecStores(root);
|
|
3332
|
+
return stores.flatMap((s) => s.specs.filter((x) => x.id === capability).map((x) => ({ store: s.label, ...x })));
|
|
3333
|
+
}
|
|
3334
|
+
var REMOTE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
3335
|
+
var SCP = /^[^/\\]+@[^:/\\]+:/;
|
|
3336
|
+
async function metarepoOrigin(root, git = createGit()) {
|
|
3337
|
+
if (!existsSync(join15(root, ".git")))
|
|
3338
|
+
return void 0;
|
|
3339
|
+
try {
|
|
3340
|
+
const url = (await git(["remote", "get-url", "origin"], root)).stdout.trim();
|
|
3341
|
+
if (!url || url.startsWith("file:"))
|
|
3342
|
+
return void 0;
|
|
3343
|
+
return REMOTE.test(url) || SCP.test(url) ? url : void 0;
|
|
3344
|
+
} catch {
|
|
3345
|
+
return void 0;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
async function referenceMetarepoIn(root, repoDir, o = {}) {
|
|
3349
|
+
const { settings } = await loadOpenSpecSettings(orgPackDirOf(root));
|
|
3350
|
+
if (!settings.referenceInRepos)
|
|
3351
|
+
return { action: "off" };
|
|
3352
|
+
const file = "openspec/config.yaml";
|
|
3353
|
+
const marker = await readStoreMarker(root);
|
|
3354
|
+
if (!marker.marker)
|
|
3355
|
+
return {
|
|
3356
|
+
action: "skipped",
|
|
3357
|
+
file,
|
|
3358
|
+
why: marker.missing ? `this metarepo isn't an OpenSpec store yet (no ${STORE_MARKER}): run atticus upgrade --openspec-store, commit the file, then hand down again` : `this metarepo's ${STORE_MARKER} can't be read (${marker.notes.join("; ")})`
|
|
3359
|
+
};
|
|
3360
|
+
const path = join15(repoDir, "openspec", "config.yaml");
|
|
3361
|
+
const text = await readFile13(path, "utf8").catch(() => void 0);
|
|
3362
|
+
if (text === void 0)
|
|
3363
|
+
return {
|
|
3364
|
+
action: "skipped",
|
|
3365
|
+
file,
|
|
3366
|
+
why: "the repo has no openspec/config.yaml, so there is nowhere to add the metarepo as an OpenSpec reference"
|
|
3367
|
+
};
|
|
3368
|
+
const remote = marker.marker.remote ?? await metarepoOrigin(root, o.git);
|
|
3369
|
+
const r = addStoreReference(text, { id: marker.marker.id, ...remote ? { remote } : {} });
|
|
3370
|
+
if ("problem" in r)
|
|
3371
|
+
return { action: "skipped", file, why: r.problem };
|
|
3372
|
+
if (r.action === "added")
|
|
3373
|
+
await writeFile2(path, r.text, "utf8");
|
|
3374
|
+
return { action: r.action, file };
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
export {
|
|
3378
|
+
ChangeError,
|
|
3379
|
+
readTables,
|
|
3380
|
+
tableWith,
|
|
3381
|
+
isFilled,
|
|
3382
|
+
CHANGE_POLICY_FILE,
|
|
3383
|
+
StageStep,
|
|
3384
|
+
SIGNAL_FACTORS,
|
|
3385
|
+
ChangePolicy,
|
|
3386
|
+
DEFAULT_CHANGE_POLICY,
|
|
3387
|
+
loadChangePolicy,
|
|
3388
|
+
stagePathOf,
|
|
3389
|
+
TemplateMeta,
|
|
3390
|
+
TEMPLATE_VARIABLES,
|
|
3391
|
+
BUILT_IN_TEMPLATES,
|
|
3392
|
+
loadTemplates,
|
|
3393
|
+
renderTemplate,
|
|
3394
|
+
pickTemplate,
|
|
3395
|
+
CHANGES_DIR,
|
|
3396
|
+
CHANGE_FILE,
|
|
3397
|
+
changeFolder,
|
|
3398
|
+
changeFilePath,
|
|
3399
|
+
ChangeFileError,
|
|
3400
|
+
serializeChange,
|
|
3401
|
+
changeStore,
|
|
3402
|
+
CODEOWNERS_PATHS,
|
|
3403
|
+
parseCodeowners,
|
|
3404
|
+
ownersFor,
|
|
3405
|
+
readCodeowners,
|
|
3406
|
+
loadChangeContext,
|
|
3407
|
+
changeTypeOf,
|
|
3408
|
+
personalLevels,
|
|
3409
|
+
wordsOf,
|
|
3410
|
+
changeFootprint,
|
|
3411
|
+
scoreRisk,
|
|
3412
|
+
testsFor,
|
|
3413
|
+
testPlanFor,
|
|
3414
|
+
phaseState,
|
|
3415
|
+
statePhase,
|
|
3416
|
+
nextState,
|
|
3417
|
+
handoffName,
|
|
3418
|
+
artifactPhase,
|
|
3419
|
+
phaseArtifacts,
|
|
3420
|
+
requiredArtifacts,
|
|
3421
|
+
phaseStage,
|
|
3422
|
+
stageCommand,
|
|
3423
|
+
checkStage,
|
|
3424
|
+
stageFiles,
|
|
3425
|
+
artifactFiles,
|
|
3426
|
+
checkArtifacts,
|
|
3427
|
+
checkFindings,
|
|
3428
|
+
mandatoryGates,
|
|
3429
|
+
checkMandatoryRounds,
|
|
3430
|
+
phaseGates,
|
|
3431
|
+
BUILT_IN_GATES,
|
|
3432
|
+
phaseFindingGates,
|
|
3433
|
+
sha256,
|
|
3434
|
+
stageCompletion,
|
|
3435
|
+
workProgress,
|
|
3436
|
+
privacyImpact,
|
|
3437
|
+
prApprovers,
|
|
3438
|
+
liveHandoff,
|
|
3439
|
+
assess,
|
|
3440
|
+
canAdvance,
|
|
3441
|
+
changeReport,
|
|
3442
|
+
slugify,
|
|
3443
|
+
newChange,
|
|
3444
|
+
gitHandoffPublisher,
|
|
3445
|
+
CAPTURE_SCHEMA_VERSION,
|
|
3446
|
+
DraftCapture,
|
|
3447
|
+
capturePaths,
|
|
3448
|
+
draftedPath,
|
|
3449
|
+
captureStageOf,
|
|
3450
|
+
captureHandoff,
|
|
3451
|
+
readDraftPairs,
|
|
3452
|
+
completeCaptures,
|
|
3453
|
+
handoff,
|
|
3454
|
+
syncHandoff,
|
|
3455
|
+
returnToPo,
|
|
3456
|
+
requireValid,
|
|
3457
|
+
archiveChange,
|
|
3458
|
+
LEARN_SCHEMA_VERSION,
|
|
3459
|
+
lineDiff,
|
|
3460
|
+
hunksOf,
|
|
3461
|
+
countLines,
|
|
3462
|
+
readStory,
|
|
3463
|
+
TREND_MIN,
|
|
3464
|
+
TREND_STEP,
|
|
3465
|
+
measurePair,
|
|
3466
|
+
trendOf,
|
|
3467
|
+
learnFromPairs,
|
|
3468
|
+
learnReport,
|
|
3469
|
+
formatLearn,
|
|
3470
|
+
pairTouches,
|
|
3471
|
+
pastCorrections,
|
|
3472
|
+
detectConflicts,
|
|
3473
|
+
changeSize,
|
|
3474
|
+
storeDirOf,
|
|
3475
|
+
openspecStores,
|
|
3476
|
+
storeSpecLookup,
|
|
3477
|
+
metarepoOrigin,
|
|
3478
|
+
referenceMetarepoIn
|
|
3479
|
+
};
|