@maycombhq/atticus 0.0.0-stage → 2.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +177 -0
- package/README.md +17 -2
- package/THIRD-PARTY-LICENSES.txt +2607 -0
- package/dist/bin.js +47 -0
- package/dist/chunk-23W7VWKR.js +2609 -0
- package/dist/chunk-2C5B464C.js +106 -0
- package/dist/chunk-2LLFKP7C.js +53 -0
- package/dist/chunk-2MD7TXSG.js +166 -0
- package/dist/chunk-2UIQBDJO.js +131 -0
- package/dist/chunk-32MPK524.js +378 -0
- package/dist/chunk-3ALD2W2A.js +95 -0
- package/dist/chunk-3G4YI4ZU.js +16 -0
- package/dist/chunk-4AK7IBVP.js +915 -0
- package/dist/chunk-4BMDGTED.js +69 -0
- package/dist/chunk-4NDAAFQT.js +103 -0
- package/dist/chunk-4Q3NN7VF.js +32 -0
- package/dist/chunk-4RS5KZQD.js +25 -0
- package/dist/chunk-4SKZDI62.js +22 -0
- package/dist/chunk-55YODD3N.js +385 -0
- package/dist/chunk-56YQ6ROQ.js +3479 -0
- package/dist/chunk-5A5L3T2U.js +583 -0
- package/dist/chunk-5FEOD73Z.js +374 -0
- package/dist/chunk-5GDRCDQB.js +32 -0
- package/dist/chunk-5WS2YOGD.js +95 -0
- package/dist/chunk-65NYPGRC.js +766 -0
- package/dist/chunk-6BVONWJ7.js +50 -0
- package/dist/chunk-6D4FRTFF.js +53 -0
- package/dist/chunk-6GPJOFV4.js +149 -0
- package/dist/chunk-6JCPI55B.js +92 -0
- package/dist/chunk-6JUBZBMW.js +56 -0
- package/dist/chunk-6SCVQZX5.js +2522 -0
- package/dist/chunk-6TACQN3N.js +41 -0
- package/dist/chunk-72TXNLOC.js +496 -0
- package/dist/chunk-76WKMA4J.js +14 -0
- package/dist/chunk-7EFILTCI.js +67 -0
- package/dist/chunk-7EKXJZ2Y.js +2902 -0
- package/dist/chunk-7EOBHUMC.js +133 -0
- package/dist/chunk-7JUKDRSZ.js +1726 -0
- package/dist/chunk-7NN5VWDC.js +43 -0
- package/dist/chunk-7UFQZXZA.js +5949 -0
- package/dist/chunk-A3KPZJ5M.js +131 -0
- package/dist/chunk-A6AC72A5.js +48 -0
- package/dist/chunk-A7KZKIZX.js +166 -0
- package/dist/chunk-ABWHYWNI.js +101 -0
- package/dist/chunk-AFFIK6JI.js +46 -0
- package/dist/chunk-AHOQZ4OK.js +37 -0
- package/dist/chunk-AVJDSJ2F.js +18 -0
- package/dist/chunk-AXXCPAP6.js +203 -0
- package/dist/chunk-AY3QXYMB.js +41 -0
- package/dist/chunk-B2OKWJIU.js +143 -0
- package/dist/chunk-B632FH3I.js +251 -0
- package/dist/chunk-BLW4SXCU.js +38 -0
- package/dist/chunk-BVS2K73A.js +33 -0
- package/dist/chunk-BWZ77RQT.js +25 -0
- package/dist/chunk-BYBUDX4T.js +163 -0
- package/dist/chunk-BYZCY6SP.js +230 -0
- package/dist/chunk-C6EKFH7J.js +169 -0
- package/dist/chunk-C6MZFGN7.js +271 -0
- package/dist/chunk-CAQZUNOH.js +1150 -0
- package/dist/chunk-CD4XHYGY.js +856 -0
- package/dist/chunk-CEZQOLEO.js +1217 -0
- package/dist/chunk-CHWTETW7.js +128 -0
- package/dist/chunk-CMV7TGU6.js +31 -0
- package/dist/chunk-CNUKCS2T.js +2741 -0
- package/dist/chunk-CP3L3FH7.js +549 -0
- package/dist/chunk-CPGCA6GZ.js +41 -0
- package/dist/chunk-CWM6KM5G.js +315 -0
- package/dist/chunk-CXDSTRUJ.js +873 -0
- package/dist/chunk-D3PO4DOZ.js +261 -0
- package/dist/chunk-DFBCSHVT.js +69 -0
- package/dist/chunk-DHPACDG7.js +104 -0
- package/dist/chunk-DPC5Y5DL.js +3898 -0
- package/dist/chunk-DPUYDDQG.js +164 -0
- package/dist/chunk-DQN7TF5D.js +325 -0
- package/dist/chunk-EADP3PEN.js +450 -0
- package/dist/chunk-ED2LWWIP.js +878 -0
- package/dist/chunk-EHVTK3UE.js +116 -0
- package/dist/chunk-EPTBZ3GP.js +101 -0
- package/dist/chunk-EXIMEJ2X.js +545 -0
- package/dist/chunk-EYWIDJFU.js +130 -0
- package/dist/chunk-F44OMYIN.js +54 -0
- package/dist/chunk-F7HYOUFF.js +287 -0
- package/dist/chunk-FALXWAMD.js +182 -0
- package/dist/chunk-FBNPE7HS.js +49 -0
- package/dist/chunk-FSWUD5QB.js +108 -0
- package/dist/chunk-FVQISWPW.js +296 -0
- package/dist/chunk-G2B4ZQGH.js +240 -0
- package/dist/chunk-GK6XK5SP.js +1227 -0
- package/dist/chunk-GLEBK5L3.js +34 -0
- package/dist/chunk-GNB5QDDI.js +25 -0
- package/dist/chunk-GQ4N4WKF.js +1142 -0
- package/dist/chunk-GRMR5TRQ.js +119 -0
- package/dist/chunk-HC6XKREB.js +334 -0
- package/dist/chunk-HFQCFSJR.js +60 -0
- package/dist/chunk-HJDICQA4.js +102 -0
- package/dist/chunk-HLJAXMM4.js +233 -0
- package/dist/chunk-HVIMMDVB.js +162 -0
- package/dist/chunk-HZVLAPN5.js +32 -0
- package/dist/chunk-I3E7CX4H.js +155 -0
- package/dist/chunk-IQFTZ37C.js +2919 -0
- package/dist/chunk-ITY3VJRX.js +285 -0
- package/dist/chunk-JBTK5OP2.js +55 -0
- package/dist/chunk-JPGCERE2.js +26 -0
- package/dist/chunk-JQDNHIBC.js +38 -0
- package/dist/chunk-JRBCWLOR.js +307 -0
- package/dist/chunk-JRZQA2JM.js +212 -0
- package/dist/chunk-KCBJCA66.js +45 -0
- package/dist/chunk-KJPMEAI4.js +705 -0
- package/dist/chunk-KQDZXGNR.js +14 -0
- package/dist/chunk-KUUQR4HA.js +264 -0
- package/dist/chunk-L24C55VN.js +7923 -0
- package/dist/chunk-L3BX5ZIB.js +400 -0
- package/dist/chunk-L6SGQIR3.js +471 -0
- package/dist/chunk-LBENYT3H.js +66 -0
- package/dist/chunk-LNAJW6FB.js +47 -0
- package/dist/chunk-MC3H7736.js +19910 -0
- package/dist/chunk-MMOSZPWJ.js +273 -0
- package/dist/chunk-MXCHWULV.js +285 -0
- package/dist/chunk-N3E56YPN.js +344 -0
- package/dist/chunk-NHFQMOVK.js +180 -0
- package/dist/chunk-NQU6KNQF.js +27 -0
- package/dist/chunk-OIK5YPTN.js +391 -0
- package/dist/chunk-OLWFG75W.js +4330 -0
- package/dist/chunk-ONPUIE7Y.js +29 -0
- package/dist/chunk-OW7D2PJL.js +376 -0
- package/dist/chunk-P63VL4HE.js +2357 -0
- package/dist/chunk-PA4YNUMU.js +45 -0
- package/dist/chunk-PAMPPUR6.js +4500 -0
- package/dist/chunk-PBS3OFNN.js +113 -0
- package/dist/chunk-PFT3VTS4.js +214 -0
- package/dist/chunk-PFTA6BZF.js +49 -0
- package/dist/chunk-PNN5KLAY.js +119 -0
- package/dist/chunk-PRPPGYZG.js +104 -0
- package/dist/chunk-PUKEZ37F.js +666 -0
- package/dist/chunk-Q3HOFXZJ.js +76 -0
- package/dist/chunk-Q5AMFEWD.js +119 -0
- package/dist/chunk-QKRNMBJO.js +80 -0
- package/dist/chunk-QSU47IQ5.js +36 -0
- package/dist/chunk-QSXK2RGH.js +688 -0
- package/dist/chunk-QXNWYUYI.js +52 -0
- package/dist/chunk-R6RY4Q5T.js +51 -0
- package/dist/chunk-RCU3MPY2.js +1496 -0
- package/dist/chunk-RLIB4JUN.js +174 -0
- package/dist/chunk-RYSCLMU3.js +1991 -0
- package/dist/chunk-RZUZ4RJJ.js +415 -0
- package/dist/chunk-S2A7YC3Q.js +41433 -0
- package/dist/chunk-S2FBBX7D.js +540 -0
- package/dist/chunk-S34B4QPI.js +430 -0
- package/dist/chunk-SETWVXOJ.js +2078 -0
- package/dist/chunk-SMA4N4AC.js +211 -0
- package/dist/chunk-SP2I63OO.js +252 -0
- package/dist/chunk-STHSY74Q.js +242 -0
- package/dist/chunk-STUPFGXY.js +3206 -0
- package/dist/chunk-T5I4S2W3.js +34 -0
- package/dist/chunk-T6AIP7H5.js +3851 -0
- package/dist/chunk-TANFV3MZ.js +531 -0
- package/dist/chunk-TAVDMUV3.js +189 -0
- package/dist/chunk-TBSQJ6IV.js +324 -0
- package/dist/chunk-TIZHP7KE.js +408 -0
- package/dist/chunk-TV45EUGM.js +51 -0
- package/dist/chunk-TW2KPPUC.js +4087 -0
- package/dist/chunk-V2YTAIQO.js +1075 -0
- package/dist/chunk-V5PKHHQQ.js +1963 -0
- package/dist/chunk-VF4IOZZO.js +184 -0
- package/dist/chunk-VJTINHZI.js +48 -0
- package/dist/chunk-VKLILA2D.js +205 -0
- package/dist/chunk-VOGCSYOI.js +16 -0
- package/dist/chunk-VPTTCYOC.js +16101 -0
- package/dist/chunk-WY6PP6AA.js +53 -0
- package/dist/chunk-WYFIXP54.js +174 -0
- package/dist/chunk-WZ2BXIGG.js +34 -0
- package/dist/chunk-XJNDSYGO.js +147 -0
- package/dist/chunk-XPCM366L.js +70 -0
- package/dist/chunk-XSQKGVYA.js +69 -0
- package/dist/chunk-XUPVB7TL.js +207 -0
- package/dist/chunk-XZRON6IV.js +131 -0
- package/dist/chunk-Y4NG2FP5.js +94 -0
- package/dist/chunk-YEDDOYSC.js +27809 -0
- package/dist/chunk-YGMCXNTY.js +41 -0
- package/dist/chunk-YJKPTX6J.js +228 -0
- package/dist/chunk-YMZ3TNYB.js +18 -0
- package/dist/chunk-Z6LF7HQN.js +22 -0
- package/dist/chunk-Z7SG32ZY.js +55 -0
- package/dist/chunk-ZDPAS56H.js +76 -0
- package/dist/chunk-ZJSP24X4.js +1247 -0
- package/dist/chunk-ZMAINWBW.js +18 -0
- package/dist/chunk-ZMFPW66A.js +22 -0
- package/dist/chunk-ZUIAOKXI.js +1611 -0
- package/dist/chunk-ZVM6AKKM.js +47 -0
- package/dist/files.json +1888 -0
- package/dist/fixtures/forge/README.md +19 -0
- package/dist/fixtures/forge/expected/discrepancies.yaml +54 -0
- package/dist/fixtures/forge/expected/inputs.yaml +38 -0
- package/dist/fixtures/forge/expected/scan.yaml +342 -0
- package/dist/fixtures/forge/inputs/diagrams/forge-context.drawio +23 -0
- package/dist/fixtures/forge/inputs/partner-specs/northwind-pay-openapi.yaml +11 -0
- package/dist/fixtures/forge/inputs/tickets/ado-stories.json +4 -0
- package/dist/fixtures/forge/inputs/tickets/closed-service-requests.json +5 -0
- package/dist/fixtures/forge/inputs/wiki/integration-patterns.md +11 -0
- package/dist/fixtures/forge/inputs/wiki/ledger-solution-spec.md +9 -0
- package/dist/fixtures/forge/inputs/wiki/snow-firewall-request.md +4 -0
- package/dist/fixtures/forge/metarepo/judgments/reaches/84af5b6af24f7ca8c23013f3.yaml +17 -0
- package/dist/fixtures/forge/metarepo/judgments/reaches/c3917555edebddce8cecb647.yaml +17 -0
- package/dist/fixtures/forge/metarepo/judgments/reaches/c5fafd6601f8ca9e34e0bbd0.yaml +17 -0
- package/dist/fixtures/forge/metarepo/org-pack/agents/architecture-critic.yaml +9 -0
- package/dist/fixtures/forge/metarepo/org-pack/agents/story-writer.yaml +10 -0
- package/dist/fixtures/forge/metarepo/org-pack/checklists/architecture.yaml +115 -0
- package/dist/fixtures/forge/metarepo/org-pack/dialects/forge.yaml +111 -0
- package/dist/fixtures/forge/metarepo/org-pack/gates.yaml +22 -0
- package/dist/fixtures/forge/metarepo/org-pack/pack.yaml +11 -0
- package/dist/fixtures/forge/metarepo/org-pack/patterns/A.EB.PUB.yaml +12 -0
- package/dist/fixtures/forge/metarepo/org-pack/patterns/D.JDBC.yaml +12 -0
- package/dist/fixtures/forge/metarepo/org-pack/patterns/S.DIR.BFF.yaml +12 -0
- package/dist/fixtures/forge/metarepo/org-pack/patterns/S.GW.SOR.yaml +13 -0
- package/dist/fixtures/forge/metarepo/org-pack/patterns/S.GW.VND.yaml +14 -0
- package/dist/fixtures/forge/metarepo/org-pack/process/change-templates/feature.md +37 -0
- package/dist/fixtures/forge/metarepo/org-pack/process/skills/add-forge-endpoint/SKILL.md +17 -0
- package/dist/fixtures/forge/metarepo/org-pack/process/snow/firewall-rule.yaml +14 -0
- package/dist/fixtures/forge/metarepo/org-pack/process/snow/gateway-route.yaml +13 -0
- package/dist/fixtures/forge/metarepo/org-pack/profiles/forge/profile.yaml +12 -0
- package/dist/fixtures/forge/metarepo/org-pack/profiles/forge/repos.manifest.yaml +44 -0
- package/dist/fixtures/forge/metarepo/org-pack/roles.yaml +14 -0
- package/dist/fixtures/forge/metarepo/org-pack/runtime.yaml +14 -0
- package/dist/fixtures/forge/metarepo/org-pack/sources.yaml +3 -0
- package/dist/fixtures/forge/repos/forge-events/topics/payments.initiated.yaml +4 -0
- package/dist/fixtures/forge/repos/forge-events/topics/transfers.completed.yaml +4 -0
- package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/ledger-v1.xml +3 -0
- package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/policies/SpikeArrest.xml +3 -0
- package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/policies/VerifyOAuth.xml +4 -0
- package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/proxies/default.xml +14 -0
- package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/targets/ledger.xml +5 -0
- package/dist/fixtures/forge/repos/forge-infra/alerts/ledger-api.yaml +5 -0
- package/dist/fixtures/forge/repos/forge-infra/k8s/ledger-api.yaml +15 -0
- package/dist/fixtures/forge/repos/forge-infra/k8s/portal-bff.yaml +7 -0
- package/dist/fixtures/forge/repos/forge-infra/terraform/ledger-db.tf +9 -0
- package/dist/fixtures/forge/repos/forge-ledger/api/ledger-openapi.yaml +31 -0
- package/dist/fixtures/forge/repos/forge-ledger/forge.yaml +4 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/java/forge/ledger/LedgerEndpoints.java +13 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/java/forge/ledger/LedgerEvents.java +11 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/java/forge/ledger/NorthwindClient.java +10 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/resources/application.yml +3 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/resources/db/migration/V1__create_account.sql +5 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/resources/db/migration/V2__create_entry.sql +7 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/pom.xml +1 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/AuditPublisher.java +12 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/BalanceController.java +16 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/LedgerClient.java +13 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/PaymentEvents.java +13 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/src/main/resources/application.yml +8 -0
- package/dist/fixtures/forge/repos/forge-portal-ui/package.json +1 -0
- package/dist/fixtures/forge/repos/forge-portal-ui/src/api/balance.ts +5 -0
- package/dist/fixtures/forge/repos/forge-reports-job/src/main/java/forge/reports/DailyEntriesJob.java +19 -0
- package/dist/fixtures/forge/repos/forge-reports-job/src/main/resources/application.yml +3 -0
- package/dist/fixtures/forge-change/README.md +41 -0
- package/dist/fixtures/forge-change/expected/findings.yaml +301 -0
- package/dist/fixtures/forge-change/expected/stories.yaml +101 -0
- package/dist/fixtures/forge-change/judgments/personal-data/0b5f3dbf5257a22275d8c74b.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/2759a66770134c417440bf2d.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/31b1ea2f14d0737f1aca79fd.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/49afd70c08228e65ad6ca1ed.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/761cabcb8a39f5e5866d2dd6.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/adae902b90cc8ffa0c765145.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/ba11bb933105ec9493e065dd.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/bdbcd1cdac3021c68063c3e7.yaml +18 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/brief.md +35 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/change.yaml +120 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/data.md +18 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/design.md +29 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/interfaces.md +17 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/mappings.md +26 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/proposal.md +35 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/snow.md +12 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/specs/transfer-limits/spec.md +19 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s1-refuse-over-limit.md +17 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s2-show-remaining.md +16 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s3-audit-refusals.md +18 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s4-velocity-check.md +16 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories.md +8 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/tasks.md +12 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/ux.md +7 -0
- package/dist/fixtures/forge-change/org-pack/agents/code-and-test-critic.yaml +13 -0
- package/dist/fixtures/forge-change/org-pack/agents/complete-critic.yaml +12 -0
- package/dist/fixtures/forge-change/org-pack/agents/safe-critic.yaml +12 -0
- package/dist/fixtures/forge-change/org-pack/agents/ship-critic.yaml +11 -0
- package/dist/fixtures/forge-change/org-pack/agents/true-critic.yaml +11 -0
- package/dist/fixtures/forge-change/org-pack/checklists/review.yaml +54 -0
- package/dist/fixtures/forge-change/org-pack/gates.yaml +25 -0
- package/dist/fixtures/forge-change/org-pack/teams/team-ledger/checklists/ledger-team.yaml +227 -0
- package/dist/fixtures/forge-change/recordings/complete-critic/2ad84f84bf39c1d5412697e69fe23034d6983ec45a00314bf0975e627589a3c5.json +12 -0
- package/dist/fixtures/forge-change/recordings/complete-critic/417c98d8e54a4387d0fd93441c49f44c2868958739070d855c1dce0c41e24db3.json +12 -0
- package/dist/fixtures/forge-change/recordings/complete-critic/50203ee22e9cb3ef047295edb6538fe7d5695e1e4b139f3c929013dc6ce47021.json +12 -0
- package/dist/fixtures/forge-change/recordings/complete-critic/89f18d67f5d9c325bf35e2a9e020f9ddb6ee13b8523315037f0905cc57c040b1.json +12 -0
- package/dist/fixtures/forge-change/recordings/complete-critic/b4097867d26ae8eadc8b9efa8cdd86897013358035f0b40086c299a59c4ba121.json +12 -0
- package/dist/fixtures/forge-change/recordings/safe-critic/302d06f49664dd67ad2e21ab9cf74c1b0f7d3f750709c6ed8eff2b70983274e2.json +12 -0
- package/dist/fixtures/forge-change/recordings/safe-critic/32f1049c9a5ab4a759543290db4644ec6e221cc45fdfdcf06193837c6a8ce0b9.json +12 -0
- package/dist/fixtures/forge-change/recordings/safe-critic/4e4a84bfc04b9f26ca4250319a879901ff7a1c081c71e46a117675fee05026b0.json +12 -0
- package/dist/fixtures/forge-change/recordings/safe-critic/604af0fcdd6df6b76208fa88305fdc15c37606c2a4d1ae816477e46ac8c2dd04.json +12 -0
- package/dist/fixtures/forge-change/recordings/safe-critic/f0bf50af11f14c50d07904f0dffe1c8c20fb0cd89770645a8824c1d30a443955.json +12 -0
- package/dist/fixtures/forge-change/recordings/ship-critic/350954b58833b2c04880c67db45a1b328b7fd541d9f0d943b63905b8875488a6.json +12 -0
- package/dist/fixtures/forge-change/recordings/ship-critic/4af95c6c96a206777cec9de1f9c2d8c55a36ddc9b8db2f81d6eb83217b835efa.json +12 -0
- package/dist/fixtures/forge-change/recordings/ship-critic/76c34522a49cf7587a4e5689a5030d384603fa780b8aba97d27815cbc829683e.json +12 -0
- package/dist/fixtures/forge-change/recordings/ship-critic/a3b2cd1ace48587e5994333db03b34c3a0f9dec4f1932f1730ddfb687f6918c8.json +12 -0
- package/dist/fixtures/forge-change/recordings/ship-critic/de288a4fa64d0fc20890f9a787080ce6eac5203b7b9b72d884df11e8cf111835.json +12 -0
- package/dist/fixtures/forge-change/recordings/story-writer/1ddb00eac2aaf91a5ec5d9dfd220116be9f75aec6546a4a3a3b050ed89af2864.json +12 -0
- package/dist/packages/assistant-kit/frameworks-kit/README.md +86 -0
- package/dist/packages/assistant-kit/frameworks-kit/checklist.md +11 -0
- package/dist/packages/assistant-kit/frameworks-kit/examples/tide-lantern.yaml +56 -0
- package/dist/packages/assistant-kit/frameworks-kit/examples/tide-platform.yaml +43 -0
- package/dist/packages/assistant-kit/frameworks-kit/prompts/write-framework-model.md +12 -0
- package/dist/packages/assistant-kit/kit/README.md +98 -0
- package/dist/packages/assistant-kit/kit/c2-reference.md +334 -0
- package/dist/packages/assistant-kit/kit/checklist.md +39 -0
- package/dist/packages/assistant-kit/kit/examples/house-http-client.yaml +102 -0
- package/dist/packages/assistant-kit/kit/examples/tide-apigee.yaml +56 -0
- package/dist/packages/assistant-kit/kit/examples/tide-apim.yaml +32 -0
- package/dist/packages/assistant-kit/kit/examples/tide-kong.yaml +44 -0
- package/dist/packages/assistant-kit/kit/prompts/write-dialect.md +34 -0
- package/dist/packages/assistant-kit/kit/worked-example.md +198 -0
- package/dist/packages/conformance/sample/README.md +4 -0
- package/dist/packages/conformance/sample/inputs/notes.md +3 -0
- package/dist/packages/conformance/sample/inputs/systems.csv +3 -0
- package/dist/packages/conformance/sample/repo/api/greeter.proto +6 -0
- package/dist/packages/conformance/sample/repo/api/openapi.yaml +10 -0
- package/dist/packages/conformance/sample/repo/api/schema.graphql +3 -0
- package/dist/packages/conformance/sample/repo/db/V1__create_greeting.sql +4 -0
- package/dist/packages/conformance/sample/repo/package.json +7 -0
- package/dist/packages/conformance/sample/repo/src/main/java/sample/HelloController.java +10 -0
- package/dist/packages/conformance/sample/repo/web/app.ts +4 -0
- package/dist/packages/evals/datasets/critic-lenses.yaml +62 -0
- package/dist/packages/evals/datasets/dialect-writer.yaml +14 -0
- package/dist/packages/evals/datasets/feature-namer.yaml +34 -0
- package/dist/packages/evals/datasets/glossary-definer.yaml +25 -0
- package/dist/packages/evals/datasets/story-writer.yaml +13 -0
- package/dist/packages/evals/datasets/template-writer.yaml +16 -0
- package/dist/packages/evals/datasets/test-writer.yaml +18 -0
- package/dist/packages/executors/reference/reference-executor.mjs +233 -0
- package/dist/packages/extractors/dialects/argocd.yaml +45 -0
- package/dist/packages/extractors/dialects/fixtures/jaxrs/accounts-service/src/main/java/forge/accounts/AccountResource.java +47 -0
- package/dist/packages/extractors/dialects/fixtures/jaxrs/accounts-service/src/main/java/forge/accounts/AuditClient.java +12 -0
- package/dist/packages/extractors/dialects/fixtures/jaxrs/accounts-service/src/main/java/forge/accounts/LedgerClient.java +17 -0
- package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/java/forge/statements/LedgerClient.java +14 -0
- package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/java/forge/statements/RatesClient.java +11 -0
- package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/java/forge/statements/StatementController.java +36 -0
- package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/resources/application.yml +7 -0
- package/dist/packages/extractors/dialects/fixtures/quarkus-reactive-messaging/fraud-service/src/main/java/forge/fraud/FraudScorer.java +27 -0
- package/dist/packages/extractors/dialects/fixtures/quarkus-reactive-messaging/fraud-service/src/main/resources/application.properties +4 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/api.tf +38 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/main.tf +95 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/terraform.tfvars +1 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/variables.tf +4 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-azure/forge-azure-infra/main.tf +102 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-gcp/forge-gcp-infra/main.tf +51 -0
- package/dist/packages/extractors/dialects/harness.yaml +101 -0
- package/dist/packages/extractors/dialects/jaxrs.yaml +155 -0
- package/dist/packages/extractors/dialects/micronaut.yaml +207 -0
- package/dist/packages/extractors/dialects/quarkus-reactive-messaging.yaml +158 -0
- package/dist/packages/extractors/dialects/tekton.yaml +71 -0
- package/dist/packages/extractors/dialects/terraform-aws.yaml +267 -0
- package/dist/packages/extractors/dialects/terraform-azure.yaml +212 -0
- package/dist/packages/extractors/dialects/terraform-gcp.yaml +109 -0
- package/dist/packages/extractors/grammars/README.md +11 -0
- package/dist/packages/extractors/grammars/tree-sitter-groovy.LICENSE +21 -0
- package/dist/packages/extractors/grammars/tree-sitter-groovy.wasm +0 -0
- package/dist/packages/extractors/grammars/vendored.json +11 -0
- package/dist/packages/learn/lessons/architect-diagrams-and-code.yaml +69 -0
- package/dist/packages/learn/lessons/business-analyst-story-bar.yaml +94 -0
- package/dist/packages/learn/lessons/delivery-lead-release-readiness.yaml +44 -0
- package/dist/packages/learn/lessons/designer-features-in-user-words.yaml +57 -0
- package/dist/packages/learn/lessons/developer-find-your-way.yaml +60 -0
- package/dist/packages/learn/lessons/everyone-first-hour.yaml +74 -0
- package/dist/packages/learn/lessons/platform-lead-coverage-and-defences.yaml +58 -0
- package/dist/packages/learn/lessons/product-owner-first-change.yaml +167 -0
- package/dist/packages/learn/lessons/qa-tests-and-trace.yaml +53 -0
- package/dist/packages/learn/lessons/team-lead-initiative.yaml +146 -0
- package/dist/packages/naysayer/constitution/constitution.yaml +286 -0
- package/dist/packages/naysayer/starter/review.yaml +256 -0
- package/dist/packages/sdk/npm/index.js +8892 -0
- package/dist/packages/speckit/rules/drift.yaml +78 -0
- package/dist/packages/taskrun/rules/conform.yaml +38 -0
- package/dist/packages/testgen/rules/trace.yaml +49 -0
- package/dist/packages/ui/dist/assets/elk-api-tma0MCuh.js +2 -0
- package/dist/packages/ui/dist/assets/elk-worker.min--dAJroGl.js +6401 -0
- package/dist/packages/ui/dist/assets/elk-worker.min-DdYmS5kP.js +2 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- package/dist/packages/ui/dist/assets/index-B54fm9FQ.js +29 -0
- package/dist/packages/ui/dist/assets/index-DD_hufQz.css +1 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-devanagari-600-normal-BtY48IV_.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-devanagari-600-normal-DhS7ScYx.woff2 +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-devanagari-700-normal-BQQOj9BB.woff2 +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-devanagari-700-normal-DBRwWuxA.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-600-normal-CXCVEoA9.woff2 +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-600-normal-Dx02qI2_.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-700-normal-BPDUZo87.woff2 +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-700-normal-e0Uj8ZlN.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-ext-600-normal-BleIgkPp.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-ext-600-normal-C9ZjJQAt.woff2 +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-ext-700-normal-BPaQ7tlD.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-ext-700-normal-S6aYESQK.woff2 +0 -0
- package/dist/packages/ui/dist/favicon.svg +1 -0
- package/dist/packages/ui/dist/index.html +15 -0
- package/dist/third-party.json +575 -0
- package/dist/vendor/@fission-ai/openspec/bin/openspec.js +85986 -0
- package/dist/vendor/@fission-ai/openspec/package.json +82 -0
- package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/schema.yaml +248 -0
- package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/design.md +21 -0
- package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/proposal.md +34 -0
- package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/spec.md +13 -0
- package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/tasks.md +11 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-c_sharp.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-go.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-java.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-javascript.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-kotlin.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-php.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-python.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-ruby.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-rust.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-scala.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-tsx.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-typescript.wasm +0 -0
- package/dist/vendor/web-tree-sitter/tree-sitter.wasm +0 -0
- package/package.json +36 -3
|
@@ -0,0 +1,4330 @@
|
|
|
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
|
+
featuresForScan,
|
|
8
|
+
namingHint,
|
|
9
|
+
syncSources
|
|
10
|
+
} from "./chunk-L24C55VN.js";
|
|
11
|
+
import {
|
|
12
|
+
dirSource,
|
|
13
|
+
sameCommit,
|
|
14
|
+
verifyFacts
|
|
15
|
+
} from "./chunk-PAMPPUR6.js";
|
|
16
|
+
import {
|
|
17
|
+
scanInputs
|
|
18
|
+
} from "./chunk-23W7VWKR.js";
|
|
19
|
+
import {
|
|
20
|
+
listIngestors
|
|
21
|
+
} from "./chunk-7EKXJZ2Y.js";
|
|
22
|
+
import {
|
|
23
|
+
BUILT_IN_EXTRACTORS,
|
|
24
|
+
MODEL_READERS,
|
|
25
|
+
PREFIX_CONVENTIONS,
|
|
26
|
+
SHIPPED_DIALECTS_DIR,
|
|
27
|
+
applyPipelineModels,
|
|
28
|
+
detectModules,
|
|
29
|
+
dialectExtractor,
|
|
30
|
+
loadShippedDialects,
|
|
31
|
+
modelCovers,
|
|
32
|
+
repoImports,
|
|
33
|
+
runDialectTests,
|
|
34
|
+
runFrameworkTests,
|
|
35
|
+
unmodelledFrameworks
|
|
36
|
+
} from "./chunk-MC3H7736.js";
|
|
37
|
+
import {
|
|
38
|
+
baseClonePath,
|
|
39
|
+
changesSince,
|
|
40
|
+
checkoutState
|
|
41
|
+
} from "./chunk-KJPMEAI4.js";
|
|
42
|
+
import {
|
|
43
|
+
DEFAULT_IGNORES,
|
|
44
|
+
configHash,
|
|
45
|
+
createParseCache,
|
|
46
|
+
fsRepo,
|
|
47
|
+
fsScanCache,
|
|
48
|
+
globToRegExp,
|
|
49
|
+
readPinnedEntry,
|
|
50
|
+
sandboxedExtractor,
|
|
51
|
+
scanRepo,
|
|
52
|
+
withParseCache
|
|
53
|
+
} from "./chunk-GQ4N4WKF.js";
|
|
54
|
+
import {
|
|
55
|
+
buildContextPack,
|
|
56
|
+
toRunSummary
|
|
57
|
+
} from "./chunk-CNUKCS2T.js";
|
|
58
|
+
import {
|
|
59
|
+
AgentDefinition,
|
|
60
|
+
DECLARED_CALLER,
|
|
61
|
+
DEFAULT_SPEC_TEMPLATE,
|
|
62
|
+
HOSTS_NONE,
|
|
63
|
+
IsoDateTime,
|
|
64
|
+
NEVER_CHECKED_STEPS,
|
|
65
|
+
PluginExpected,
|
|
66
|
+
REPO_TYPE_SCHEMA_VERSION,
|
|
67
|
+
RecordId,
|
|
68
|
+
SCAN_SETTINGS_SCHEMA_VERSION,
|
|
69
|
+
applyCurated,
|
|
70
|
+
callTargetQuestion,
|
|
71
|
+
callerRecords,
|
|
72
|
+
catalogDirOf,
|
|
73
|
+
declaredCallsOf,
|
|
74
|
+
deserializeRecord,
|
|
75
|
+
external_exports,
|
|
76
|
+
factIdPart,
|
|
77
|
+
factsDirOf,
|
|
78
|
+
factsRelOf,
|
|
79
|
+
fieldKey,
|
|
80
|
+
flowFiles,
|
|
81
|
+
hostMatches,
|
|
82
|
+
hostQuestionText,
|
|
83
|
+
identifierWords,
|
|
84
|
+
isPrototypeType,
|
|
85
|
+
joinCallers,
|
|
86
|
+
joinFacts,
|
|
87
|
+
joinRecords,
|
|
88
|
+
lineNamesField,
|
|
89
|
+
lineNamesRoute,
|
|
90
|
+
lineNamesWord,
|
|
91
|
+
listQuestions,
|
|
92
|
+
loadOrgPack,
|
|
93
|
+
mergeCatalog,
|
|
94
|
+
normalizePath,
|
|
95
|
+
orgPackDirOf,
|
|
96
|
+
parseFacts,
|
|
97
|
+
parseRecord,
|
|
98
|
+
projectFacts,
|
|
99
|
+
questionId,
|
|
100
|
+
readParsedFiles,
|
|
101
|
+
recordPath,
|
|
102
|
+
require_dist,
|
|
103
|
+
serializeRecord,
|
|
104
|
+
slugify,
|
|
105
|
+
specFileHash,
|
|
106
|
+
specFindingFingerprint,
|
|
107
|
+
throwIfCancelled,
|
|
108
|
+
toCanonicalYaml,
|
|
109
|
+
urlHostPort
|
|
110
|
+
} from "./chunk-S2A7YC3Q.js";
|
|
111
|
+
import {
|
|
112
|
+
SCHEMA_VERSION,
|
|
113
|
+
writeFileAtomic
|
|
114
|
+
} from "./chunk-D3PO4DOZ.js";
|
|
115
|
+
import {
|
|
116
|
+
__toESM
|
|
117
|
+
} from "./chunk-TV45EUGM.js";
|
|
118
|
+
|
|
119
|
+
// packages/glossary/dist/proposals.js
|
|
120
|
+
var import_yaml = __toESM(require_dist(), 1);
|
|
121
|
+
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
122
|
+
import { join } from "node:path";
|
|
123
|
+
var GLOSSARY_DEFINER = "glossary-definer";
|
|
124
|
+
var GLOSSARY_CHANGE = "glossary";
|
|
125
|
+
var GLOSSARY_OUTPUT = "catalog/proposals/glossary-definitions.yaml";
|
|
126
|
+
var DefinitionProposalFile = external_exports.strictObject({
|
|
127
|
+
schemaVersion: external_exports.literal(1),
|
|
128
|
+
term: RecordId,
|
|
129
|
+
definition: external_exports.string().min(1),
|
|
130
|
+
evidence: external_exports.array(external_exports.string().min(1)).min(1),
|
|
131
|
+
agent: RecordId,
|
|
132
|
+
runId: external_exports.string().optional(),
|
|
133
|
+
at: IsoDateTime
|
|
134
|
+
});
|
|
135
|
+
var LEADING = ["schemaVersion", "term", "definition", "evidence"];
|
|
136
|
+
async function writeDefinitionProposals(dir, proposals) {
|
|
137
|
+
const written = [];
|
|
138
|
+
if (!proposals.length)
|
|
139
|
+
return written;
|
|
140
|
+
await mkdir(join(dir, "glossaryterm"), { recursive: true });
|
|
141
|
+
for (const p of proposals) {
|
|
142
|
+
const file = `glossaryterm/${p.term}.yaml`;
|
|
143
|
+
const body = DefinitionProposalFile.parse({ schemaVersion: 1, ...p });
|
|
144
|
+
await writeFile(join(dir, file), toCanonicalYaml(body, LEADING), "utf8");
|
|
145
|
+
written.push(file);
|
|
146
|
+
}
|
|
147
|
+
return written;
|
|
148
|
+
}
|
|
149
|
+
async function readDefinitionProposals(dir) {
|
|
150
|
+
const proposals = [];
|
|
151
|
+
const problems = [];
|
|
152
|
+
let files;
|
|
153
|
+
try {
|
|
154
|
+
files = (await readdir(join(dir, "glossaryterm"))).filter((f) => /\.ya?ml$/.test(f)).sort();
|
|
155
|
+
} catch {
|
|
156
|
+
return { proposals, problems };
|
|
157
|
+
}
|
|
158
|
+
for (const f of files) {
|
|
159
|
+
const file = `glossaryterm/${f}`;
|
|
160
|
+
let data;
|
|
161
|
+
try {
|
|
162
|
+
data = (0, import_yaml.parse)(await readFile(join(dir, file), "utf8"));
|
|
163
|
+
} catch (e) {
|
|
164
|
+
problems.push({ file, message: `invalid YAML: ${e.message.split("\n")[0]}` });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const p = DefinitionProposalFile.safeParse(data);
|
|
168
|
+
if (!p.success) {
|
|
169
|
+
problems.push({
|
|
170
|
+
file,
|
|
171
|
+
message: p.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const { schemaVersion: _v, ...rest } = p.data;
|
|
176
|
+
void _v;
|
|
177
|
+
proposals.push(rest);
|
|
178
|
+
}
|
|
179
|
+
return { proposals, problems };
|
|
180
|
+
}
|
|
181
|
+
function glossaryDefinerAgent(orgPack) {
|
|
182
|
+
const own = orgPack?.agents.find((a) => a.id === GLOSSARY_DEFINER);
|
|
183
|
+
if (own)
|
|
184
|
+
return own;
|
|
185
|
+
return AgentDefinition.parse({
|
|
186
|
+
schemaVersion: 1,
|
|
187
|
+
id: GLOSSARY_DEFINER,
|
|
188
|
+
role: "business-analyst",
|
|
189
|
+
description: "Proposes a one-sentence business definition for each glossary term found only in code, from where the code uses it.",
|
|
190
|
+
inputs: ["glossary", "catalog-slice"],
|
|
191
|
+
// C5 has no output kind for catalog proposals (see ADR 0031); a definition seeds specs.
|
|
192
|
+
outputArtifact: "specs",
|
|
193
|
+
tokenBudget: orgPack?.runtime.budgets.defaultTokens ?? 15e4,
|
|
194
|
+
allowedRuntimes: ["hand-off", "model", "headless-claude-code", "mock", "replay"]
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
var INSTRUCTIONS = `# Glossary definer
|
|
198
|
+
|
|
199
|
+
You propose business definitions for words an estate's code uses that no wiki page defines. Each term below is a data block: its id, the word, and the records (endpoints, topics, tables, columns, schema fields) that use it. For each term propose:
|
|
200
|
+
|
|
201
|
+
- definition: one sentence a product owner would accept, saying what the word means to the business, only from the evidence;
|
|
202
|
+
- evidence: the record ids from that term's block your definition relies on.
|
|
203
|
+
|
|
204
|
+
Don't invent systems, rules or numbers. A proposal that cites no record id from its term is discarded. Write YAML and nothing else:
|
|
205
|
+
|
|
206
|
+
terms:
|
|
207
|
+
- id: <term id>
|
|
208
|
+
definition: <one sentence>
|
|
209
|
+
evidence: [<record id>, ...]`;
|
|
210
|
+
var Proposal = external_exports.object({
|
|
211
|
+
id: external_exports.string().min(1),
|
|
212
|
+
definition: external_exports.string().trim().min(3).max(400),
|
|
213
|
+
evidence: external_exports.array(external_exports.coerce.string()).default([])
|
|
214
|
+
});
|
|
215
|
+
function block(e) {
|
|
216
|
+
return toCanonicalYaml({
|
|
217
|
+
id: e.id,
|
|
218
|
+
term: e.term,
|
|
219
|
+
usedBy: (e.code?.uses ?? []).map((u) => ({
|
|
220
|
+
id: u.record,
|
|
221
|
+
kind: u.kind,
|
|
222
|
+
name: u.name,
|
|
223
|
+
...u.evidence.path ? {
|
|
224
|
+
code: `${u.evidence.repo ? `${u.evidence.repo}:` : ""}${u.evidence.path}${u.evidence.line ? `:${u.evidence.line}` : ""}`
|
|
225
|
+
} : {}
|
|
226
|
+
}))
|
|
227
|
+
}, ["id", "term", "usedBy", "id", "kind", "name", "code"]).trimEnd();
|
|
228
|
+
}
|
|
229
|
+
function parseDefinerOutput(output, entries, meta) {
|
|
230
|
+
let data;
|
|
231
|
+
try {
|
|
232
|
+
data = (0, import_yaml.parse)(output.replace(/^```(?:ya?ml)?\s*\n|\n```\s*$/g, ""));
|
|
233
|
+
} catch (e) {
|
|
234
|
+
return {
|
|
235
|
+
proposals: [],
|
|
236
|
+
discarded: [{ reason: `the output is not YAML: ${e.message.split("\n")[0]}` }]
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const list = data?.terms;
|
|
240
|
+
if (!Array.isArray(list))
|
|
241
|
+
return { proposals: [], discarded: [{ reason: "the output has no terms list" }] };
|
|
242
|
+
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
243
|
+
const out = { proposals: [], discarded: [] };
|
|
244
|
+
for (const raw of list) {
|
|
245
|
+
const p = Proposal.safeParse(raw);
|
|
246
|
+
const id = typeof raw?.id === "string" ? raw.id : void 0;
|
|
247
|
+
if (!p.success) {
|
|
248
|
+
out.discarded.push({ ...id ? { term: id } : {}, reason: "incomplete proposal" });
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const e = byId.get(p.data.id);
|
|
252
|
+
if (!e) {
|
|
253
|
+
out.discarded.push({
|
|
254
|
+
term: p.data.id,
|
|
255
|
+
reason: `${p.data.id} isn't one of the terms it was given`
|
|
256
|
+
});
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (out.proposals.some((x) => x.term === e.id)) {
|
|
260
|
+
out.discarded.push({ term: e.id, reason: "a second proposal for the same term" });
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const known = new Set((e.code?.uses ?? []).map((u) => u.record));
|
|
264
|
+
const evidence = [...new Set(p.data.evidence.filter((x) => known.has(x)))];
|
|
265
|
+
if (!evidence.length) {
|
|
266
|
+
out.discarded.push({ term: e.id, reason: "it cites no record that uses the term" });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
out.proposals.push({
|
|
270
|
+
term: e.id,
|
|
271
|
+
definition: p.data.definition.replace(/\s+/g, " "),
|
|
272
|
+
evidence,
|
|
273
|
+
agent: meta.agent,
|
|
274
|
+
at: meta.at,
|
|
275
|
+
...meta.runId ? { runId: meta.runId } : {}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
function undefinedCodeTerms(entries) {
|
|
281
|
+
return entries.filter((e) => e.sources.length === 1 && e.sources[0] === "code" && (e.code?.uses.length ?? 0) > 0);
|
|
282
|
+
}
|
|
283
|
+
async function proposeDefinitions(entries, runtime, o = {}) {
|
|
284
|
+
const todo = [...entries].sort((a, b) => a.id.localeCompare(b.id));
|
|
285
|
+
if (!todo.length)
|
|
286
|
+
return { proposals: [], discarded: [] };
|
|
287
|
+
const agent = glossaryDefinerAgent(o.orgPack);
|
|
288
|
+
const now2 = o.now ?? /* @__PURE__ */ new Date();
|
|
289
|
+
const at3 = now2.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
290
|
+
const levels = o.orgPack?.pack?.classification ?? [
|
|
291
|
+
"public",
|
|
292
|
+
"internal",
|
|
293
|
+
"confidential",
|
|
294
|
+
"restricted"
|
|
295
|
+
];
|
|
296
|
+
const sources = [
|
|
297
|
+
{
|
|
298
|
+
id: "0-instructions",
|
|
299
|
+
kind: "instruction",
|
|
300
|
+
origin: "atticus/glossary",
|
|
301
|
+
text: INSTRUCTIONS,
|
|
302
|
+
classification: "public",
|
|
303
|
+
trusted: true
|
|
304
|
+
},
|
|
305
|
+
...todo.map((e) => ({
|
|
306
|
+
id: e.id,
|
|
307
|
+
kind: "catalog",
|
|
308
|
+
origin: `catalog/generated/glossaryterm/${e.id}.yaml`,
|
|
309
|
+
text: block(e),
|
|
310
|
+
classification: "internal",
|
|
311
|
+
priority: 10
|
|
312
|
+
}))
|
|
313
|
+
];
|
|
314
|
+
const pack = buildContextPack({
|
|
315
|
+
change: GLOSSARY_CHANGE,
|
|
316
|
+
agent: agent.id,
|
|
317
|
+
budget: o.orgPack?.runtime.budgets.perAgent[agent.id] ?? agent.tokenBudget,
|
|
318
|
+
sources,
|
|
319
|
+
levels,
|
|
320
|
+
maxClassification: levels.includes("internal") ? "internal" : levels.at(-1),
|
|
321
|
+
now: now2
|
|
322
|
+
});
|
|
323
|
+
const sent = todo.filter((e) => pack.manifest.sources.some((s) => s.id === e.id));
|
|
324
|
+
const discarded = todo.filter((e) => !sent.includes(e)).map((e) => ({
|
|
325
|
+
term: e.id,
|
|
326
|
+
reason: "left out of the context pack (budget or classification)"
|
|
327
|
+
}));
|
|
328
|
+
const prepared = await runtime.prepare({
|
|
329
|
+
change: GLOSSARY_CHANGE,
|
|
330
|
+
agent,
|
|
331
|
+
pack,
|
|
332
|
+
outputPath: GLOSSARY_OUTPUT
|
|
333
|
+
});
|
|
334
|
+
const run = await runtime.run(prepared.runId);
|
|
335
|
+
if (run.state === "waiting" || run.state === "prepared")
|
|
336
|
+
return {
|
|
337
|
+
proposals: [],
|
|
338
|
+
discarded,
|
|
339
|
+
waiting: run.runId,
|
|
340
|
+
promptSha256: pack.manifest.promptSha256
|
|
341
|
+
};
|
|
342
|
+
if (run.state !== "validated" || run.output === void 0)
|
|
343
|
+
return {
|
|
344
|
+
proposals: [],
|
|
345
|
+
discarded: [
|
|
346
|
+
...discarded,
|
|
347
|
+
{ reason: `the glossary definer ${run.state}: ${run.errors.join("; ") || "no output"}` }
|
|
348
|
+
],
|
|
349
|
+
run: toRunSummary(run),
|
|
350
|
+
promptSha256: pack.manifest.promptSha256
|
|
351
|
+
};
|
|
352
|
+
const parsed = parseDefinerOutput(run.output, sent, { agent: agent.id, at: at3, runId: run.runId });
|
|
353
|
+
return {
|
|
354
|
+
proposals: parsed.proposals,
|
|
355
|
+
discarded: [...discarded, ...parsed.discarded],
|
|
356
|
+
run: toRunSummary(run),
|
|
357
|
+
promptSha256: pack.manifest.promptSha256
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// packages/glossary/dist/decisions.js
|
|
362
|
+
import { createHash } from "node:crypto";
|
|
363
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
364
|
+
import { dirname, join as join2 } from "node:path";
|
|
365
|
+
|
|
366
|
+
// packages/glossary/dist/words.js
|
|
367
|
+
var STOP_TOKENS = /* @__PURE__ */ new Set([
|
|
368
|
+
"a",
|
|
369
|
+
"an",
|
|
370
|
+
"and",
|
|
371
|
+
"api",
|
|
372
|
+
"at",
|
|
373
|
+
"by",
|
|
374
|
+
"create",
|
|
375
|
+
"created",
|
|
376
|
+
"delete",
|
|
377
|
+
"for",
|
|
378
|
+
"get",
|
|
379
|
+
"id",
|
|
380
|
+
"ids",
|
|
381
|
+
"in",
|
|
382
|
+
"is",
|
|
383
|
+
"list",
|
|
384
|
+
"of",
|
|
385
|
+
"on",
|
|
386
|
+
"or",
|
|
387
|
+
"patch",
|
|
388
|
+
"post",
|
|
389
|
+
"put",
|
|
390
|
+
"the",
|
|
391
|
+
"to",
|
|
392
|
+
"ts",
|
|
393
|
+
"update",
|
|
394
|
+
"updated",
|
|
395
|
+
"uuid"
|
|
396
|
+
]);
|
|
397
|
+
var QUALIFIERS = /* @__PURE__ */ new Set([
|
|
398
|
+
"cur",
|
|
399
|
+
"curr",
|
|
400
|
+
"current",
|
|
401
|
+
"has",
|
|
402
|
+
"max",
|
|
403
|
+
"min",
|
|
404
|
+
"new",
|
|
405
|
+
"old",
|
|
406
|
+
"prev",
|
|
407
|
+
"previous"
|
|
408
|
+
]);
|
|
409
|
+
var splitWords = (s) => identifierWords(s);
|
|
410
|
+
function singular(w) {
|
|
411
|
+
if (w.length <= 3)
|
|
412
|
+
return w;
|
|
413
|
+
if (/ies$/.test(w))
|
|
414
|
+
return `${w.slice(0, -3)}y`;
|
|
415
|
+
if (/(ss|us|is)$/.test(w))
|
|
416
|
+
return w;
|
|
417
|
+
if (/(sses|shes|ches|xes|zes)$/.test(w))
|
|
418
|
+
return w.slice(0, -2);
|
|
419
|
+
if (/ses$/.test(w))
|
|
420
|
+
return w.slice(0, -1);
|
|
421
|
+
if (/s$/.test(w))
|
|
422
|
+
return w.slice(0, -1);
|
|
423
|
+
return w;
|
|
424
|
+
}
|
|
425
|
+
var isNoise = (w) => STOP_TOKENS.has(w) || /^v\d+$/.test(w) || /^\d+$/.test(w);
|
|
426
|
+
function businessWords(s) {
|
|
427
|
+
return splitWords(s).filter((w) => !isNoise(w)).map(singular);
|
|
428
|
+
}
|
|
429
|
+
function compoundOf(name) {
|
|
430
|
+
const w = splitWords(name).filter((x) => !/^\d+$/.test(x));
|
|
431
|
+
while (w.length && (isNoise(w[0]) || QUALIFIERS.has(w[0])))
|
|
432
|
+
w.shift();
|
|
433
|
+
while (w.length && isNoise(w.at(-1)))
|
|
434
|
+
w.pop();
|
|
435
|
+
if (w.length < 2 || w.some(isNoise))
|
|
436
|
+
return void 0;
|
|
437
|
+
return [...w.slice(0, -1), singular(w.at(-1))].join(" ");
|
|
438
|
+
}
|
|
439
|
+
function singleOf(name) {
|
|
440
|
+
const w = businessWords(name).filter((x) => !QUALIFIERS.has(x));
|
|
441
|
+
return w.length === 1 ? w[0] : void 0;
|
|
442
|
+
}
|
|
443
|
+
function termKey(term) {
|
|
444
|
+
const w = term.toLowerCase().replace(/[`*_"'’]/g, "").split(/[^a-z0-9]+/).filter(Boolean);
|
|
445
|
+
if (!w.length)
|
|
446
|
+
return "";
|
|
447
|
+
return [...w.slice(0, -1), singular(w.at(-1))].join(" ");
|
|
448
|
+
}
|
|
449
|
+
function termId(term) {
|
|
450
|
+
return `term.${termKey(term).replace(/\s+/g, "-")}`.slice(0, 120);
|
|
451
|
+
}
|
|
452
|
+
function displayTerm(term) {
|
|
453
|
+
const t = term.trim().replace(/\s+/g, " ");
|
|
454
|
+
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
455
|
+
}
|
|
456
|
+
function definitionKey(d) {
|
|
457
|
+
return d.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// packages/glossary/dist/decisions.js
|
|
461
|
+
var GlossaryError = class extends Error {
|
|
462
|
+
code;
|
|
463
|
+
constructor(code, message) {
|
|
464
|
+
super(message);
|
|
465
|
+
this.code = code;
|
|
466
|
+
this.name = "GlossaryError";
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var now = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
470
|
+
function decisionId(prefix, key) {
|
|
471
|
+
const id = `${prefix}.${key}`;
|
|
472
|
+
if (id.length <= 120)
|
|
473
|
+
return id;
|
|
474
|
+
return `${id.slice(0, 110)}-${createHash("sha256").update(id).digest("hex").slice(0, 8)}`;
|
|
475
|
+
}
|
|
476
|
+
function decision(id, target, action, value, reason, o) {
|
|
477
|
+
if (!o.by.trim())
|
|
478
|
+
throw new GlossaryError("invalid", "Say who decides (--by <handle>).");
|
|
479
|
+
const p = parseRecord({
|
|
480
|
+
schemaVersion: 2,
|
|
481
|
+
kind: "CuratedDecision",
|
|
482
|
+
id,
|
|
483
|
+
status: "current",
|
|
484
|
+
confidence: "asserted",
|
|
485
|
+
evidence: [{ source: "manual", by: o.by }],
|
|
486
|
+
target,
|
|
487
|
+
action,
|
|
488
|
+
value,
|
|
489
|
+
reason: o.reason?.trim() || reason,
|
|
490
|
+
by: o.by,
|
|
491
|
+
at: o.at ?? now()
|
|
492
|
+
});
|
|
493
|
+
if (!p.ok)
|
|
494
|
+
throw new GlossaryError("invalid", `That decision isn't valid: ${p.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
|
|
495
|
+
return p.record;
|
|
496
|
+
}
|
|
497
|
+
function resolveTermId(termOrId) {
|
|
498
|
+
const t = termOrId.trim();
|
|
499
|
+
if (/^term\.[a-z0-9][a-z0-9._-]*$/.test(t))
|
|
500
|
+
return t;
|
|
501
|
+
const id = termId(t);
|
|
502
|
+
if (id === "term.")
|
|
503
|
+
throw new GlossaryError("invalid", `"${termOrId}" is not a term.`);
|
|
504
|
+
return id;
|
|
505
|
+
}
|
|
506
|
+
function defineDecision(term, definition, o) {
|
|
507
|
+
const def = definition.trim().replace(/\s+/g, " ");
|
|
508
|
+
if (def.length < 3)
|
|
509
|
+
throw new GlossaryError("invalid", "A definition needs a sentence.");
|
|
510
|
+
const id = resolveTermId(term);
|
|
511
|
+
const name = o.existing?.term ?? (term.startsWith("term.") ? void 0 : displayTerm(term));
|
|
512
|
+
return decision(decisionId("glossary.define", id.slice("term.".length)), id, "assert", { ...name ? { term: name } : {}, definition: def, ...o.owner ? { owner: o.owner } : {} }, `Glossary definition of ${name ?? id}`, o);
|
|
513
|
+
}
|
|
514
|
+
function aliasDecision(term, aliases, o) {
|
|
515
|
+
const id = resolveTermId(term);
|
|
516
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
517
|
+
for (const a of aliases.map((x) => x.trim().replace(/\s+/g, " ")).filter(Boolean))
|
|
518
|
+
if (!byKey.has(termKey(a)))
|
|
519
|
+
byKey.set(termKey(a), a);
|
|
520
|
+
const names = [...byKey.values()].sort();
|
|
521
|
+
if (!names.length)
|
|
522
|
+
throw new GlossaryError("invalid", "Give at least one other name.");
|
|
523
|
+
return decision(decisionId("glossary.alias", id.slice("term.".length)), id, "alias", names, `Other names for ${id}`, o);
|
|
524
|
+
}
|
|
525
|
+
function linkDecision(term, recordId, records, o) {
|
|
526
|
+
const byId = new Map(records.map((r) => [r.id, r]));
|
|
527
|
+
const [base2, field] = recordId.split("#");
|
|
528
|
+
const target = byId.get(base2);
|
|
529
|
+
if (!target)
|
|
530
|
+
throw new GlossaryError("not-found", `There is no record ${base2} in the catalog.`);
|
|
531
|
+
if (field !== void 0) {
|
|
532
|
+
if (target.kind !== "Schema" || !target.fields.some((f) => f.name === field))
|
|
533
|
+
throw new GlossaryError("not-found", `${base2} has no schema field ${field}.`);
|
|
534
|
+
} else if (target.kind === "Column") {
|
|
535
|
+
return decision(decisionId("glossary.link", target.id), target.id, "assert", { glossaryTerm: term.id }, `${target.id} holds the glossary term ${term.term}`, o);
|
|
536
|
+
}
|
|
537
|
+
const fields = [.../* @__PURE__ */ new Set([...term.fields, recordId])].sort();
|
|
538
|
+
return decision(decisionId("glossary.fields", term.id.slice("term.".length)), term.id, "assert", { fields }, `What the glossary term ${term.term} is linked to`, o);
|
|
539
|
+
}
|
|
540
|
+
async function writeDecisions(curatedDir, decisions) {
|
|
541
|
+
const out = [];
|
|
542
|
+
for (const d of decisions) {
|
|
543
|
+
const rel = recordPath(d);
|
|
544
|
+
const full = join2(curatedDir, ...rel.split("/"));
|
|
545
|
+
await mkdir2(dirname(full), { recursive: true });
|
|
546
|
+
await writeFile2(full, serializeRecord(d), "utf8");
|
|
547
|
+
out.push(rel);
|
|
548
|
+
}
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// packages/glossary/dist/scan.js
|
|
553
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
|
|
554
|
+
import { dirname as dirname2, join as join3, relative, sep } from "node:path";
|
|
555
|
+
|
|
556
|
+
// packages/glossary/dist/code.js
|
|
557
|
+
var firstEvidence = (r) => r.evidence[0];
|
|
558
|
+
function named(records) {
|
|
559
|
+
const out = [];
|
|
560
|
+
const endpointIds = new Set(records.filter((r) => r.kind === "Endpoint").map((r) => r.id));
|
|
561
|
+
for (const r of records) {
|
|
562
|
+
if (r.status === "retired")
|
|
563
|
+
continue;
|
|
564
|
+
if (r.kind === "Endpoint") {
|
|
565
|
+
const e = r;
|
|
566
|
+
out.push({
|
|
567
|
+
record: e.id,
|
|
568
|
+
kind: e.kind,
|
|
569
|
+
name: e.pathOrTopic,
|
|
570
|
+
words: businessWords(e.pathOrTopic),
|
|
571
|
+
isField: false,
|
|
572
|
+
evidence: firstEvidence(e)
|
|
573
|
+
});
|
|
574
|
+
} else if (r.kind === "Interface") {
|
|
575
|
+
const i = r;
|
|
576
|
+
if (i.endpoint && endpointIds.has(i.endpoint))
|
|
577
|
+
continue;
|
|
578
|
+
out.push({
|
|
579
|
+
record: i.id,
|
|
580
|
+
kind: i.kind,
|
|
581
|
+
name: i.normalizedPath,
|
|
582
|
+
words: businessWords(i.normalizedPath),
|
|
583
|
+
isField: false,
|
|
584
|
+
evidence: firstEvidence(i)
|
|
585
|
+
});
|
|
586
|
+
} else if (r.kind === "Table") {
|
|
587
|
+
const t = r;
|
|
588
|
+
const compound = compoundOf(t.name);
|
|
589
|
+
const single = compound ? void 0 : singleOf(t.name);
|
|
590
|
+
out.push({
|
|
591
|
+
record: t.id,
|
|
592
|
+
kind: t.kind,
|
|
593
|
+
name: t.name,
|
|
594
|
+
words: businessWords(t.name),
|
|
595
|
+
...compound ? { compound } : {},
|
|
596
|
+
...single ? { single } : {},
|
|
597
|
+
isField: false,
|
|
598
|
+
evidence: firstEvidence(t)
|
|
599
|
+
});
|
|
600
|
+
} else if (r.kind === "Column") {
|
|
601
|
+
const c = r;
|
|
602
|
+
const compound = compoundOf(c.name);
|
|
603
|
+
const single = compound ? void 0 : singleOf(c.name);
|
|
604
|
+
out.push({
|
|
605
|
+
record: c.id,
|
|
606
|
+
kind: c.kind,
|
|
607
|
+
name: c.name,
|
|
608
|
+
words: businessWords(c.name),
|
|
609
|
+
...compound ? { compound } : {},
|
|
610
|
+
...single ? { single } : {},
|
|
611
|
+
type: c.type,
|
|
612
|
+
isField: true,
|
|
613
|
+
evidence: firstEvidence(c)
|
|
614
|
+
});
|
|
615
|
+
} else if (r.kind === "Schema") {
|
|
616
|
+
const s = r;
|
|
617
|
+
for (const f of s.fields) {
|
|
618
|
+
const compound = compoundOf(f.name);
|
|
619
|
+
const single = compound ? void 0 : singleOf(f.name);
|
|
620
|
+
out.push({
|
|
621
|
+
record: `${s.id}#${f.name}`,
|
|
622
|
+
kind: "SchemaField",
|
|
623
|
+
name: f.name,
|
|
624
|
+
words: businessWords(f.name),
|
|
625
|
+
...compound ? { compound } : {},
|
|
626
|
+
...single ? { single } : {},
|
|
627
|
+
...f.type ? { type: f.type } : {},
|
|
628
|
+
isField: true,
|
|
629
|
+
evidence: firstEvidence(s)
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return out.sort((a, b) => a.record < b.record ? -1 : a.record > b.record ? 1 : 0);
|
|
635
|
+
}
|
|
636
|
+
function codeTerms(records, aliasOf = (k) => k) {
|
|
637
|
+
const items = named(records);
|
|
638
|
+
const byWord = /* @__PURE__ */ new Map();
|
|
639
|
+
for (const n of items)
|
|
640
|
+
if (!n.compound)
|
|
641
|
+
for (const w of new Set(n.words))
|
|
642
|
+
byWord.set(w, (byWord.get(w) ?? /* @__PURE__ */ new Set()).add(n.record));
|
|
643
|
+
const tableWords = new Set(items.filter((n) => n.kind === "Table" && n.single).map((n) => n.single));
|
|
644
|
+
const keys = /* @__PURE__ */ new Set();
|
|
645
|
+
for (const [w, recs] of byWord)
|
|
646
|
+
if (recs.size >= 2 || tableWords.has(w))
|
|
647
|
+
keys.add(w);
|
|
648
|
+
for (const n of items)
|
|
649
|
+
if (n.compound)
|
|
650
|
+
keys.add(n.compound);
|
|
651
|
+
const out = /* @__PURE__ */ new Map();
|
|
652
|
+
const get = (key) => {
|
|
653
|
+
const k = aliasOf(key);
|
|
654
|
+
let t = out.get(k);
|
|
655
|
+
if (!t) {
|
|
656
|
+
t = { key: k, uses: [], fields: [], types: {} };
|
|
657
|
+
out.set(k, t);
|
|
658
|
+
}
|
|
659
|
+
return t;
|
|
660
|
+
};
|
|
661
|
+
for (const key of [...keys].sort()) {
|
|
662
|
+
for (const n of items) {
|
|
663
|
+
const usesWord = !key.includes(" ") && !n.compound && n.words.includes(key);
|
|
664
|
+
const isWhole = n.compound === key || !n.compound && n.single === key;
|
|
665
|
+
if (!usesWord && !isWhole)
|
|
666
|
+
continue;
|
|
667
|
+
const t = get(key);
|
|
668
|
+
if (!t.uses.some((u) => u.record === n.record))
|
|
669
|
+
t.uses.push({ record: n.record, kind: n.kind, name: n.name, evidence: n.evidence });
|
|
670
|
+
if (n.isField && isWhole && !t.fields.includes(n.record)) {
|
|
671
|
+
t.fields.push(n.record);
|
|
672
|
+
if (n.type)
|
|
673
|
+
t.types[n.record] = n.type;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
for (const n of items) {
|
|
678
|
+
const whole = n.compound ?? n.single;
|
|
679
|
+
if (!n.isField || !whole)
|
|
680
|
+
continue;
|
|
681
|
+
const k = aliasOf(whole);
|
|
682
|
+
if (k === whole)
|
|
683
|
+
continue;
|
|
684
|
+
const t = get(whole);
|
|
685
|
+
if (!t.fields.includes(n.record)) {
|
|
686
|
+
t.fields.push(n.record);
|
|
687
|
+
if (n.type)
|
|
688
|
+
t.types[n.record] = n.type;
|
|
689
|
+
}
|
|
690
|
+
if (!t.uses.some((u) => u.record === n.record))
|
|
691
|
+
t.uses.push({ record: n.record, kind: n.kind, name: n.name, evidence: n.evidence });
|
|
692
|
+
}
|
|
693
|
+
for (const t of out.values()) {
|
|
694
|
+
t.uses.sort((a, b) => a.record < b.record ? -1 : a.record > b.record ? 1 : 0);
|
|
695
|
+
t.fields.sort();
|
|
696
|
+
}
|
|
697
|
+
return out;
|
|
698
|
+
}
|
|
699
|
+
function fieldNames(records) {
|
|
700
|
+
return named(records).filter((n) => n.isField).map((n) => ({ record: n.record, name: n.name }));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// packages/glossary/dist/wiki.js
|
|
704
|
+
var GLOSSARY = /\b(glossary|glossaries|definitions|terminology|terms|vocabulary)\b/i;
|
|
705
|
+
var META = /^(owner|owners|last updated|updated|status|version|author|date|classification|approver|approvers|reviewed by|reviewer|lead time|typical lead time|title|tags|see also|note|notes|example|examples|source|link|links|contact|team)$/i;
|
|
706
|
+
var strip = (s) => s.replace(/^\*\*(.+)\*\*$/, "$1").replace(/^__(.+)__$/, "$1").replace(/[`]/g, "").trim();
|
|
707
|
+
function isTermLike(raw) {
|
|
708
|
+
const t = strip(raw);
|
|
709
|
+
if (!t || t.length > 60 || !/^[A-Za-z]/.test(t))
|
|
710
|
+
return false;
|
|
711
|
+
if (/:\/\/|[|?!;]/.test(t) || /[.,]$/.test(t))
|
|
712
|
+
return false;
|
|
713
|
+
if (META.test(t))
|
|
714
|
+
return false;
|
|
715
|
+
const words2 = t.split(/\s+/);
|
|
716
|
+
return words2.length >= 1 && words2.length <= 5;
|
|
717
|
+
}
|
|
718
|
+
var cleanDefinition = (s) => s.replace(/\s+/g, " ").trim();
|
|
719
|
+
var BOLD = /^(?:\*\*([^*]+?)\*\*|__([^_]+?)__)\s*(?::|\s[—–-]\s)\s*(.+)$/;
|
|
720
|
+
var PLAIN = /^([A-Za-z][^:]{0,59}?)\s*(?::|\s[—–-]\s)\s*(.+)$/;
|
|
721
|
+
function lineDefinition(text, scoped) {
|
|
722
|
+
const b = BOLD.exec(text);
|
|
723
|
+
if (b) {
|
|
724
|
+
const term = (b[1] ?? b[2]).trim();
|
|
725
|
+
const definition = cleanDefinition(b[3]);
|
|
726
|
+
if (isTermLike(term) && definition.length >= 3)
|
|
727
|
+
return { term, definition, markup: "bold" };
|
|
728
|
+
return void 0;
|
|
729
|
+
}
|
|
730
|
+
if (!scoped)
|
|
731
|
+
return void 0;
|
|
732
|
+
const p = PLAIN.exec(text);
|
|
733
|
+
if (p && isTermLike(p[1])) {
|
|
734
|
+
const definition = cleanDefinition(p[2]);
|
|
735
|
+
if (definition.length >= 3)
|
|
736
|
+
return { term: strip(p[1]), definition, markup: "line" };
|
|
737
|
+
}
|
|
738
|
+
return void 0;
|
|
739
|
+
}
|
|
740
|
+
var TERM_HEADER = /^(term|terms|name|word|phrase|concept)$/i;
|
|
741
|
+
var DEF_HEADER = /^(definition|definitions|meaning|description|what it means)$/i;
|
|
742
|
+
function scopes(page) {
|
|
743
|
+
const fmType = [page.frontMatter.type, page.frontMatter.kind].filter((v) => typeof v === "string").join(" ");
|
|
744
|
+
const pageWide = GLOSSARY.test(page.title) || GLOSSARY.test(fmType);
|
|
745
|
+
const out = [];
|
|
746
|
+
let openLevel;
|
|
747
|
+
for (const s of page.sections) {
|
|
748
|
+
if (openLevel !== void 0 && s.level > 0 && s.level <= openLevel)
|
|
749
|
+
openLevel = void 0;
|
|
750
|
+
if (openLevel === void 0 && s.level > 0 && GLOSSARY.test(s.heading))
|
|
751
|
+
openLevel = s.level;
|
|
752
|
+
out.push(pageWide || openLevel !== void 0);
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
function pageDefinitions(page) {
|
|
757
|
+
const out = [];
|
|
758
|
+
const add = (term, definition, line, markup) => {
|
|
759
|
+
const t = displayTerm(strip(term));
|
|
760
|
+
const key = termKey(t);
|
|
761
|
+
if (!key)
|
|
762
|
+
return;
|
|
763
|
+
out.push({
|
|
764
|
+
term: t,
|
|
765
|
+
key,
|
|
766
|
+
definition,
|
|
767
|
+
path: page.path,
|
|
768
|
+
line,
|
|
769
|
+
markup,
|
|
770
|
+
pageTitle: page.title,
|
|
771
|
+
...page.owner ? { owner: page.owner } : {}
|
|
772
|
+
});
|
|
773
|
+
};
|
|
774
|
+
const inScope = scopes(page);
|
|
775
|
+
page.sections.forEach((s, si) => {
|
|
776
|
+
const scoped = inScope[si];
|
|
777
|
+
if (scoped && s.level >= 2 && !GLOSSARY.test(s.heading) && isTermLike(s.heading)) {
|
|
778
|
+
const first = s.blocks[0];
|
|
779
|
+
if (first?.type === "paragraph" && !lineDefinition(first.text.split("\n")[0], true))
|
|
780
|
+
add(s.heading, cleanDefinition(first.text), first.line, "heading");
|
|
781
|
+
}
|
|
782
|
+
for (const b of s.blocks) {
|
|
783
|
+
if (b.type === "paragraph") {
|
|
784
|
+
const lines = b.text.split("\n");
|
|
785
|
+
for (let k = 0; k < lines.length; k++) {
|
|
786
|
+
const l = lines[k];
|
|
787
|
+
const next = lines[k + 1];
|
|
788
|
+
if (next !== void 0 && /^:\s+\S/.test(next) && isTermLike(l)) {
|
|
789
|
+
add(l, cleanDefinition(next.replace(/^:\s+/, "")), b.line + k, "definition-list");
|
|
790
|
+
k++;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const d = lineDefinition(l, scoped);
|
|
794
|
+
if (d)
|
|
795
|
+
add(d.term, d.definition, b.line + k, d.markup);
|
|
796
|
+
}
|
|
797
|
+
} else if (b.type === "list") {
|
|
798
|
+
for (const it of b.items) {
|
|
799
|
+
const d = lineDefinition(it.text.split("\n")[0], scoped);
|
|
800
|
+
if (d)
|
|
801
|
+
add(d.term, cleanDefinition([d.definition, ...it.text.split("\n").slice(1)].join(" ")), it.line, d.markup === "line" ? "list" : d.markup);
|
|
802
|
+
}
|
|
803
|
+
} else if (b.type === "table" && scoped) {
|
|
804
|
+
const ti = b.header.findIndex((h) => TERM_HEADER.test(strip(h)));
|
|
805
|
+
const di = b.header.findIndex((h) => DEF_HEADER.test(strip(h)));
|
|
806
|
+
if (ti < 0 || di < 0)
|
|
807
|
+
continue;
|
|
808
|
+
for (const r of b.rows) {
|
|
809
|
+
const term = r.cells[ti] ?? "";
|
|
810
|
+
const def = cleanDefinition(r.cells[di] ?? "");
|
|
811
|
+
if (isTermLike(term) && def.length >= 3)
|
|
812
|
+
add(term, def, r.line, "table");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
return out.sort((a, b) => a.line - b.line);
|
|
818
|
+
}
|
|
819
|
+
function wikiDefinitions(pages) {
|
|
820
|
+
return pages.flatMap(pageDefinitions).sort((a, b) => a.path === b.path ? a.line - b.line : a.path < b.path ? -1 : 1);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// packages/glossary/dist/build.js
|
|
824
|
+
var isDecision = (r) => r.kind === "CuratedDecision" && r.status === "current";
|
|
825
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
826
|
+
function typeSort(type) {
|
|
827
|
+
const t = type.toLowerCase();
|
|
828
|
+
if (/^(numeric|decimal|number|money|int|integer|bigint|smallint|tinyint|float|double|real|long|short)\b/.test(t))
|
|
829
|
+
return "number";
|
|
830
|
+
if (/^(date|time|timestamp|datetime|timestamptz|instant|localdate)/.test(t))
|
|
831
|
+
return "time";
|
|
832
|
+
if (/^(bool|boolean|bit)\b/.test(t))
|
|
833
|
+
return "boolean";
|
|
834
|
+
if (/^(uuid|guid)\b/.test(t))
|
|
835
|
+
return "identifier";
|
|
836
|
+
if (/^(varchar|char|character|nvarchar|nchar|text|string|clob|citext)/.test(t))
|
|
837
|
+
return "text";
|
|
838
|
+
return t.replace(/\(.*$/, "");
|
|
839
|
+
}
|
|
840
|
+
function definitionSort(d) {
|
|
841
|
+
if (/\b(amount|sum|total|count|number of|quantity)\b/i.test(d))
|
|
842
|
+
return "number";
|
|
843
|
+
if (/\b(date|timestamp|time at which|day on which)\b/i.test(d))
|
|
844
|
+
return "time";
|
|
845
|
+
return void 0;
|
|
846
|
+
}
|
|
847
|
+
var ALIASES = (d) => (Array.isArray(d.value) ? d.value : [d.value]).map(String);
|
|
848
|
+
function buildGlossary(input) {
|
|
849
|
+
const problems = [];
|
|
850
|
+
const decisions = (input.decisions ?? []).filter(isDecision);
|
|
851
|
+
const records = input.records.filter((r) => r.kind !== "GlossaryTerm");
|
|
852
|
+
const byId = new Map(records.map((r) => [r.id, r]));
|
|
853
|
+
const aliasTo = /* @__PURE__ */ new Map();
|
|
854
|
+
const aliasesOf = /* @__PURE__ */ new Map();
|
|
855
|
+
for (const d of decisions.filter((x) => x.action === "alias" && x.target.startsWith("term."))) {
|
|
856
|
+
for (const a of ALIASES(d)) {
|
|
857
|
+
const k = termKey(a);
|
|
858
|
+
if (!k)
|
|
859
|
+
continue;
|
|
860
|
+
aliasTo.set(k, d.target);
|
|
861
|
+
aliasesOf.set(d.target, [.../* @__PURE__ */ new Set([...aliasesOf.get(d.target) ?? [], a])]);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const defined = /* @__PURE__ */ new Map();
|
|
865
|
+
const termFieldDecisions = /* @__PURE__ */ new Map();
|
|
866
|
+
for (const d of decisions.filter((x) => x.action === "assert" && x.target.startsWith("term."))) {
|
|
867
|
+
if (!isObj(d.value))
|
|
868
|
+
continue;
|
|
869
|
+
if (typeof d.value.definition === "string")
|
|
870
|
+
defined.set(d.target, d);
|
|
871
|
+
if (Array.isArray(d.value.fields))
|
|
872
|
+
termFieldDecisions.set(d.target, d.value.fields.map(String));
|
|
873
|
+
}
|
|
874
|
+
const columnDecisions = /* @__PURE__ */ new Map();
|
|
875
|
+
for (const d of decisions.filter((x) => x.action === "assert")) {
|
|
876
|
+
if (!isObj(d.value) || typeof d.value.glossaryTerm !== "string")
|
|
877
|
+
continue;
|
|
878
|
+
if (byId.get(d.target)?.kind === "Column")
|
|
879
|
+
columnDecisions.set(d.target, d.value.glossaryTerm);
|
|
880
|
+
}
|
|
881
|
+
const idOfKey = (key) => aliasTo.get(key) ?? termId(key);
|
|
882
|
+
const keyOfAlias = (key) => {
|
|
883
|
+
const id = aliasTo.get(key);
|
|
884
|
+
return id ? id.slice("term.".length).replace(/-/g, " ") : key;
|
|
885
|
+
};
|
|
886
|
+
const defs = /* @__PURE__ */ new Map();
|
|
887
|
+
for (const d of wikiDefinitions(input.pages)) {
|
|
888
|
+
const id = idOfKey(d.key);
|
|
889
|
+
defs.set(id, [...defs.get(id) ?? [], d]);
|
|
890
|
+
}
|
|
891
|
+
const code = /* @__PURE__ */ new Map();
|
|
892
|
+
for (const t of codeTerms(records, keyOfAlias).values())
|
|
893
|
+
code.set(idOfKey(t.key), t);
|
|
894
|
+
const proposals = new Map((input.proposals ?? []).map((p) => [p.term, p]));
|
|
895
|
+
const known = [.../* @__PURE__ */ new Set([...defs.keys(), ...code.keys(), ...defined.keys()])].map((id) => ({
|
|
896
|
+
id,
|
|
897
|
+
term: defs.get(id)?.[0]?.term ?? id.slice("term.".length).replace(/-/g, " "),
|
|
898
|
+
aliases: aliasesOf.get(id) ?? []
|
|
899
|
+
}));
|
|
900
|
+
for (const [id, c] of [...code]) {
|
|
901
|
+
if (defs.has(id) || defined.has(id) || !c.key.includes(" "))
|
|
902
|
+
continue;
|
|
903
|
+
const h = namingHint(id, c.key, known.filter((k) => k.id !== id));
|
|
904
|
+
if (h?.kind === "abbreviation")
|
|
905
|
+
code.delete(id);
|
|
906
|
+
}
|
|
907
|
+
const ids = /* @__PURE__ */ new Set([...defs.keys(), ...code.keys(), ...defined.keys()]);
|
|
908
|
+
const entries = [];
|
|
909
|
+
const terms = [];
|
|
910
|
+
const conflicts = [];
|
|
911
|
+
const columnLinks = [];
|
|
912
|
+
for (const id of [...ids].sort()) {
|
|
913
|
+
const ds = defs.get(id) ?? [];
|
|
914
|
+
const c = code.get(id);
|
|
915
|
+
const person = defined.get(id);
|
|
916
|
+
const proposal = c && !ds.length && !person ? proposals.get(id) : void 0;
|
|
917
|
+
const personTerm = person && isObj(person.value) && typeof person.value.term === "string" ? person.value.term : void 0;
|
|
918
|
+
const term = ds[0]?.term ?? personTerm ?? displayTerm(id.slice("term.".length).replace(/-/g, " "));
|
|
919
|
+
const linkedColumns = [...columnDecisions].filter(([, t]) => t === id).map(([col]) => col);
|
|
920
|
+
const linked = [.../* @__PURE__ */ new Set([...linkedColumns, ...termFieldDecisions.get(id) ?? []])].sort();
|
|
921
|
+
const sources = [];
|
|
922
|
+
if (ds.length)
|
|
923
|
+
sources.push("wiki");
|
|
924
|
+
if (c)
|
|
925
|
+
sources.push("code");
|
|
926
|
+
if (person)
|
|
927
|
+
sources.push("person");
|
|
928
|
+
if (proposal)
|
|
929
|
+
sources.push("ai");
|
|
930
|
+
entries.push({
|
|
931
|
+
id,
|
|
932
|
+
term,
|
|
933
|
+
sources,
|
|
934
|
+
definitions: ds,
|
|
935
|
+
...c ? { code: c } : {},
|
|
936
|
+
aliases: (aliasesOf.get(id) ?? []).sort(),
|
|
937
|
+
linked
|
|
938
|
+
});
|
|
939
|
+
const flags = [];
|
|
940
|
+
const distinct = [...new Map(ds.map((d) => [definitionKey(d.definition), d])).values()];
|
|
941
|
+
if (distinct.length > 1) {
|
|
942
|
+
if (!person)
|
|
943
|
+
flags.push("conflict:definition");
|
|
944
|
+
conflicts.push({
|
|
945
|
+
term: id,
|
|
946
|
+
kind: "definition",
|
|
947
|
+
message: `${term} has ${distinct.length} definitions: ${distinct.map((d) => `"${d.definition}" (${d.path}:${d.line})`).join(" and ")}.`,
|
|
948
|
+
evidence: distinct.map((d) => ({ source: "document", path: d.path, line: d.line })),
|
|
949
|
+
resolved: !!person
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
const definition = person && isObj(person.value) && typeof person.value.definition === "string" ? person.value.definition : ds[0]?.definition ?? proposal?.definition ?? `Not defined yet. Found in code: ${(c?.uses ?? []).slice(0, 5).map((u) => u.record).join(", ")}.`;
|
|
953
|
+
if (!ds.length && !person)
|
|
954
|
+
flags.push(proposal ? "ai-proposed" : "undefined");
|
|
955
|
+
const fields = [.../* @__PURE__ */ new Set([...c?.fields ?? [], ...linkedColumns])].sort();
|
|
956
|
+
const types = { ...c?.types ?? {} };
|
|
957
|
+
for (const col of linkedColumns) {
|
|
958
|
+
const r = byId.get(col);
|
|
959
|
+
if (r?.kind === "Column")
|
|
960
|
+
types[col] = r.type;
|
|
961
|
+
}
|
|
962
|
+
const sorts = /* @__PURE__ */ new Map();
|
|
963
|
+
for (const [f, t] of Object.entries(types))
|
|
964
|
+
sorts.set(typeSort(t), [...sorts.get(typeSort(t)) ?? [], f]);
|
|
965
|
+
const expected = ds.length || person || proposal ? definitionSort(definition) : void 0;
|
|
966
|
+
const off = expected ? Object.entries(types).filter(([, t]) => typeSort(t) !== expected) : [];
|
|
967
|
+
if (sorts.size > 1 || off.length) {
|
|
968
|
+
flags.push("conflict:code-usage");
|
|
969
|
+
const parts = [];
|
|
970
|
+
if (expected && off.length)
|
|
971
|
+
parts.push(`the definition says it is ${expected === "number" ? "an amount or count" : "a date or time"}, but ${off.map(([f, t]) => `${f} is ${t}`).join(", ")}`);
|
|
972
|
+
if (sorts.size > 1)
|
|
973
|
+
parts.push(`its fields have different types: ${Object.entries(types).sort(([a], [b]) => a.localeCompare(b)).map(([f, t]) => `${f} ${t}`).join(", ")}`);
|
|
974
|
+
conflicts.push({
|
|
975
|
+
term: id,
|
|
976
|
+
kind: "code-usage",
|
|
977
|
+
message: `${term} is used differently in code: ${parts.join("; ")}.`,
|
|
978
|
+
evidence: Object.keys(types).sort().map((f) => {
|
|
979
|
+
const r = byId.get(f.split("#")[0]);
|
|
980
|
+
const e = r?.evidence[0];
|
|
981
|
+
return e ?? { source: "code" };
|
|
982
|
+
}),
|
|
983
|
+
resolved: false
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
const evidence = [
|
|
987
|
+
...ds.map((d) => ({ source: "document", path: d.path, line: d.line })),
|
|
988
|
+
...(c?.uses ?? []).slice(0, 5).map((u) => u.evidence)
|
|
989
|
+
];
|
|
990
|
+
if (!evidence.length && person)
|
|
991
|
+
evidence.push({ source: "manual", by: person.by });
|
|
992
|
+
if (!evidence.length)
|
|
993
|
+
evidence.push({ source: "manual" });
|
|
994
|
+
const allFields = [.../* @__PURE__ */ new Set([...fields, ...termFieldDecisions.get(id) ?? []])].sort();
|
|
995
|
+
const owner = ds.find((d) => d.owner)?.owner;
|
|
996
|
+
const p = parseRecord({
|
|
997
|
+
schemaVersion: 2,
|
|
998
|
+
kind: "GlossaryTerm",
|
|
999
|
+
id,
|
|
1000
|
+
status: "current",
|
|
1001
|
+
confidence: "inferred",
|
|
1002
|
+
term,
|
|
1003
|
+
definition,
|
|
1004
|
+
fields: allFields,
|
|
1005
|
+
...owner ? { owner } : {},
|
|
1006
|
+
evidence,
|
|
1007
|
+
...flags.length ? { flags: [...new Set(flags)].sort() } : {}
|
|
1008
|
+
});
|
|
1009
|
+
if (!p.ok) {
|
|
1010
|
+
problems.push(`${id}: ${p.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
terms.push(p.record);
|
|
1014
|
+
for (const f of c?.fields ?? []) {
|
|
1015
|
+
if (byId.get(f)?.kind !== "Column")
|
|
1016
|
+
continue;
|
|
1017
|
+
const r = byId.get(f);
|
|
1018
|
+
const whole = compoundOf(r.name) ?? singleOf(r.name) ?? "";
|
|
1019
|
+
columnLinks.push({ column: f, term: id, how: aliasTo.has(whole) ? "alias" : "name" });
|
|
1020
|
+
}
|
|
1021
|
+
for (const col of linkedColumns)
|
|
1022
|
+
columnLinks.push({ column: col, term: id, how: "decision" });
|
|
1023
|
+
}
|
|
1024
|
+
const decided = new Set(columnLinks.filter((l) => l.how === "decision").map((l) => l.column));
|
|
1025
|
+
const links = columnLinks.filter((l) => l.how === "decision" || !decided.has(l.column)).sort((a, b) => a.column.localeCompare(b.column) || a.term.localeCompare(b.term));
|
|
1026
|
+
const covered = /* @__PURE__ */ new Set([...terms.flatMap((t) => t.fields), ...links.map((l) => l.column)]);
|
|
1027
|
+
const names = entries.map((e) => ({ id: e.id, term: e.term, aliases: e.aliases }));
|
|
1028
|
+
const hints = fieldNames(records).filter((f) => !covered.has(f.record)).map((f) => namingHint(f.record, f.name, names)).filter((h) => !!h);
|
|
1029
|
+
return {
|
|
1030
|
+
terms,
|
|
1031
|
+
entries,
|
|
1032
|
+
columnLinks: links,
|
|
1033
|
+
conflicts: conflicts.sort((a, b) => a.term.localeCompare(b.term) || a.kind.localeCompare(b.kind)),
|
|
1034
|
+
hints,
|
|
1035
|
+
problems
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function applyGlossary(records, build) {
|
|
1039
|
+
const link = /* @__PURE__ */ new Map();
|
|
1040
|
+
for (const l of build.columnLinks)
|
|
1041
|
+
if (!link.has(l.column))
|
|
1042
|
+
link.set(l.column, l.term);
|
|
1043
|
+
const out = [];
|
|
1044
|
+
for (const r of records) {
|
|
1045
|
+
if (r.kind === "GlossaryTerm")
|
|
1046
|
+
continue;
|
|
1047
|
+
if (r.kind === "Column" && !r.glossaryTerm && link.has(r.id))
|
|
1048
|
+
out.push({ ...r, glossaryTerm: link.get(r.id) });
|
|
1049
|
+
else
|
|
1050
|
+
out.push(r);
|
|
1051
|
+
}
|
|
1052
|
+
return [...out, ...build.terms];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// packages/glossary/dist/scan.js
|
|
1056
|
+
async function readRecords(dir) {
|
|
1057
|
+
const records = [];
|
|
1058
|
+
const problems = [];
|
|
1059
|
+
async function walk(d) {
|
|
1060
|
+
let entries;
|
|
1061
|
+
try {
|
|
1062
|
+
entries = await readdir2(d, { withFileTypes: true });
|
|
1063
|
+
} catch {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1067
|
+
const full = join3(d, e.name);
|
|
1068
|
+
if (e.isDirectory())
|
|
1069
|
+
await walk(full);
|
|
1070
|
+
else if (/\.ya?ml$/.test(e.name)) {
|
|
1071
|
+
const r = deserializeRecord(await readFile2(full, "utf8"));
|
|
1072
|
+
if (r.ok)
|
|
1073
|
+
records.push(r.record);
|
|
1074
|
+
else
|
|
1075
|
+
problems.push({
|
|
1076
|
+
file: relative(dir, full).split(sep).join("/"),
|
|
1077
|
+
message: r.issues.map((i) => `${i.path}: ${i.message}`).join("; ")
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
await walk(dir);
|
|
1083
|
+
return { records, problems };
|
|
1084
|
+
}
|
|
1085
|
+
async function glossaryForScan(i) {
|
|
1086
|
+
const problems = [];
|
|
1087
|
+
let pages = i.pages;
|
|
1088
|
+
if (!pages) {
|
|
1089
|
+
const s = await syncSources({
|
|
1090
|
+
root: i.root,
|
|
1091
|
+
orgPack: i.orgPack,
|
|
1092
|
+
orgPackDir: i.orgPackDir ?? orgPackDirOf(i.root),
|
|
1093
|
+
records: [...i.records]
|
|
1094
|
+
});
|
|
1095
|
+
pages = s.pages;
|
|
1096
|
+
for (const p of s.problems)
|
|
1097
|
+
problems.push(`${p.source}: ${p.message}`);
|
|
1098
|
+
}
|
|
1099
|
+
const prop = await readDefinitionProposals(i.proposalsDir);
|
|
1100
|
+
for (const p of prop.problems)
|
|
1101
|
+
problems.push(`proposals/${p.file}: ${p.message}`);
|
|
1102
|
+
const build = buildGlossary({
|
|
1103
|
+
pages,
|
|
1104
|
+
records: i.records,
|
|
1105
|
+
decisions: i.decisions,
|
|
1106
|
+
proposals: prop.proposals
|
|
1107
|
+
});
|
|
1108
|
+
problems.push(...build.problems);
|
|
1109
|
+
return { build, records: applyGlossary(i.records, build), problems };
|
|
1110
|
+
}
|
|
1111
|
+
var isGlossaryDecision = (d, columns) => d.kind === "CuratedDecision" && (d.target.startsWith("term.") || columns.has(d.target) && d.action === "assert" && typeof d.value === "object" && d.value !== null && "glossaryTerm" in d.value);
|
|
1112
|
+
async function loadGlossary(root) {
|
|
1113
|
+
const orgPack = await loadOrgPack(orgPackDirOf(root));
|
|
1114
|
+
const problems = orgPack.problems.map((p) => `org-pack/${p.file}: ${p.path ? `${p.path}: ` : ""}${p.message}`);
|
|
1115
|
+
const catalogDir = catalogDirOf(root);
|
|
1116
|
+
const generated = await readRecords(join3(catalogDir, "generated"));
|
|
1117
|
+
const curated = await readRecords(join3(catalogDir, "curated"));
|
|
1118
|
+
for (const p of [...generated.problems, ...curated.problems])
|
|
1119
|
+
problems.push(`catalog ${p.file}: ${p.message}`);
|
|
1120
|
+
const base2 = generated.records.filter((r) => r.kind !== "GlossaryTerm");
|
|
1121
|
+
const s = await syncSources({ root, orgPack, orgPackDir: orgPackDirOf(root), records: base2 });
|
|
1122
|
+
for (const p of s.problems)
|
|
1123
|
+
problems.push(`${p.source}: ${p.message}`);
|
|
1124
|
+
const g = await glossaryForScan({
|
|
1125
|
+
root,
|
|
1126
|
+
orgPack,
|
|
1127
|
+
records: base2,
|
|
1128
|
+
decisions: curated.records,
|
|
1129
|
+
proposalsDir: join3(catalogDir, "proposals"),
|
|
1130
|
+
pages: s.pages
|
|
1131
|
+
});
|
|
1132
|
+
problems.push(...g.problems);
|
|
1133
|
+
const columns = new Set(base2.filter((r) => r.kind === "Column").map((r) => r.id));
|
|
1134
|
+
const mine = curated.records.filter((d) => isGlossaryDecision(d, columns));
|
|
1135
|
+
const applied = applyCurated(g.records, mine);
|
|
1136
|
+
for (const p of applied.problems)
|
|
1137
|
+
problems.push(`${p.decision}: ${p.message}`);
|
|
1138
|
+
return {
|
|
1139
|
+
orgPack,
|
|
1140
|
+
catalogDir,
|
|
1141
|
+
base: base2,
|
|
1142
|
+
decisions: curated.records,
|
|
1143
|
+
pages: s.pages,
|
|
1144
|
+
build: g.build,
|
|
1145
|
+
records: applied.records,
|
|
1146
|
+
terms: applied.records.filter((r) => r.kind === "GlossaryTerm"),
|
|
1147
|
+
problems
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
async function scanGlossary(root, o = {}) {
|
|
1151
|
+
let g = await loadGlossary(root);
|
|
1152
|
+
let propose;
|
|
1153
|
+
if (o.runtime) {
|
|
1154
|
+
const todo = undefinedCodeTerms(g.build.entries);
|
|
1155
|
+
propose = await proposeDefinitions(todo, o.runtime, {
|
|
1156
|
+
orgPack: g.orgPack,
|
|
1157
|
+
...o.now ? { now: o.now } : {}
|
|
1158
|
+
});
|
|
1159
|
+
if (propose.proposals.length) {
|
|
1160
|
+
await writeDefinitionProposals(join3(g.catalogDir, "proposals"), propose.proposals);
|
|
1161
|
+
g = await loadGlossary(root);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const generated = join3(g.catalogDir, "generated");
|
|
1165
|
+
const written = [];
|
|
1166
|
+
const removed = [];
|
|
1167
|
+
const want = new Map(g.terms.map((t) => [recordPath(t), serializeRecord(t)]));
|
|
1168
|
+
const termDir = join3(generated, "glossaryterm");
|
|
1169
|
+
for (const f of (await readdir2(termDir).catch(() => [])).sort()) {
|
|
1170
|
+
const rel = `glossaryterm/${f}`;
|
|
1171
|
+
if (!want.has(rel)) {
|
|
1172
|
+
await rm2(join3(termDir, f), { force: true });
|
|
1173
|
+
removed.push(rel);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
const before = new Map(g.base.map((r) => [`${r.kind}/${r.id}`, r]));
|
|
1177
|
+
const put = async (rel, text) => {
|
|
1178
|
+
const full = join3(generated, ...rel.split("/"));
|
|
1179
|
+
await mkdir3(dirname2(full), { recursive: true });
|
|
1180
|
+
await writeFile3(full, text, "utf8");
|
|
1181
|
+
written.push(rel);
|
|
1182
|
+
};
|
|
1183
|
+
for (const [rel, text] of want) {
|
|
1184
|
+
const cur = await readFile2(join3(generated, ...rel.split("/")), "utf8").catch(() => void 0);
|
|
1185
|
+
if (cur !== text)
|
|
1186
|
+
await put(rel, text);
|
|
1187
|
+
}
|
|
1188
|
+
const termIds = new Set(g.terms.map((t) => t.id));
|
|
1189
|
+
for (const r of g.records) {
|
|
1190
|
+
if (r.kind !== "Column")
|
|
1191
|
+
continue;
|
|
1192
|
+
const old = before.get(`Column/${r.id}`);
|
|
1193
|
+
if (!old || old.kind !== "Column")
|
|
1194
|
+
continue;
|
|
1195
|
+
const next = r.glossaryTerm && termIds.has(r.glossaryTerm) ? r : { ...r, glossaryTerm: void 0 };
|
|
1196
|
+
if ((old.glossaryTerm ?? "") === (next.glossaryTerm ?? ""))
|
|
1197
|
+
continue;
|
|
1198
|
+
const { glossaryTerm, ...rest } = next;
|
|
1199
|
+
await put(recordPath(r), serializeRecord(glossaryTerm ? next : rest));
|
|
1200
|
+
}
|
|
1201
|
+
return { ...g, written: written.sort(), removed, ...propose ? { propose } : {} };
|
|
1202
|
+
}
|
|
1203
|
+
async function completeGlossaryDefinitions(root, run, o = {}) {
|
|
1204
|
+
const g = await loadGlossary(root);
|
|
1205
|
+
const at3 = (o.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1206
|
+
const sent = new Set(run.request.pack.manifest.sources.map((s) => s.id));
|
|
1207
|
+
const todo = undefinedCodeTerms(g.build.entries).filter((e) => sent.has(e.id));
|
|
1208
|
+
const parsed = parseDefinerOutput(run.output ?? "", todo, {
|
|
1209
|
+
agent: run.request.agent.id,
|
|
1210
|
+
at: at3,
|
|
1211
|
+
runId: run.runId
|
|
1212
|
+
});
|
|
1213
|
+
if (parsed.proposals.length)
|
|
1214
|
+
await writeDefinitionProposals(join3(g.catalogDir, "proposals"), parsed.proposals);
|
|
1215
|
+
const propose = {
|
|
1216
|
+
...parsed,
|
|
1217
|
+
...run.state ? { run: toRunSummary(run) } : {},
|
|
1218
|
+
promptSha256: run.request.pack.manifest.promptSha256
|
|
1219
|
+
};
|
|
1220
|
+
const scanned = await scanGlossary(root);
|
|
1221
|
+
return { ...scanned, propose };
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// packages/scanner/dist/files.js
|
|
1225
|
+
import { readdir as readdir3, readFile as readFile3, rm as rm3 } from "node:fs/promises";
|
|
1226
|
+
import { join as join4, relative as relative2, resolve, sep as sep2 } from "node:path";
|
|
1227
|
+
var kept = /* @__PURE__ */ new Map();
|
|
1228
|
+
var keeping = false;
|
|
1229
|
+
function keepCatalogsInMemory(on = true) {
|
|
1230
|
+
keeping = on;
|
|
1231
|
+
if (!on)
|
|
1232
|
+
kept.clear();
|
|
1233
|
+
}
|
|
1234
|
+
function forgetCatalogs() {
|
|
1235
|
+
kept.clear();
|
|
1236
|
+
}
|
|
1237
|
+
async function readCatalog(dir, o = {}) {
|
|
1238
|
+
if (!keeping)
|
|
1239
|
+
return readCatalogFiles(dir, o.cacheRoot);
|
|
1240
|
+
const key = resolve(dir);
|
|
1241
|
+
let read = kept.get(key);
|
|
1242
|
+
if (!read) {
|
|
1243
|
+
read = readCatalogFiles(dir, o.cacheRoot);
|
|
1244
|
+
kept.set(key, read);
|
|
1245
|
+
read.catch(() => kept.delete(key));
|
|
1246
|
+
}
|
|
1247
|
+
const r = await read;
|
|
1248
|
+
return { records: structuredClone(r.records), problems: r.problems.map((p) => ({ ...p })) };
|
|
1249
|
+
}
|
|
1250
|
+
var CATALOG_CACHE_VERSION = `c1@${SCHEMA_VERSION}`;
|
|
1251
|
+
async function readCatalogFiles(dir, cacheRoot) {
|
|
1252
|
+
const records = [];
|
|
1253
|
+
const problems = [];
|
|
1254
|
+
const files = [];
|
|
1255
|
+
async function walk(d) {
|
|
1256
|
+
let entries;
|
|
1257
|
+
try {
|
|
1258
|
+
entries = await readdir3(d, { withFileTypes: true });
|
|
1259
|
+
} catch {
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1263
|
+
const full = join4(d, e.name);
|
|
1264
|
+
if (e.isDirectory())
|
|
1265
|
+
await walk(full);
|
|
1266
|
+
else if (/\.ya?ml$/.test(e.name))
|
|
1267
|
+
files.push(relative2(dir, full).split(sep2).join("/"));
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
await walk(dir);
|
|
1271
|
+
for (const p of await readParsedFiles({
|
|
1272
|
+
dir,
|
|
1273
|
+
files,
|
|
1274
|
+
name: "catalog",
|
|
1275
|
+
version: CATALOG_CACHE_VERSION,
|
|
1276
|
+
cacheRoot,
|
|
1277
|
+
parse: (text) => {
|
|
1278
|
+
const r = deserializeRecord(text);
|
|
1279
|
+
return r.ok ? { ok: true, value: r.record } : { ok: false, message: r.issues.map((i) => `${i.path}: ${i.message}`).join("; ") };
|
|
1280
|
+
}
|
|
1281
|
+
})) {
|
|
1282
|
+
if (p.problem === void 0)
|
|
1283
|
+
records.push(p.value);
|
|
1284
|
+
else
|
|
1285
|
+
problems.push({ file: p.rel, message: p.problem });
|
|
1286
|
+
}
|
|
1287
|
+
records.sort((a, b) => a.kind === b.kind ? a.id.localeCompare(b.id) : a.kind.localeCompare(b.kind));
|
|
1288
|
+
return { records, problems };
|
|
1289
|
+
}
|
|
1290
|
+
async function writeCatalog(dir, records) {
|
|
1291
|
+
forgetCatalogs();
|
|
1292
|
+
const wanted = new Map(records.map((r) => [recordPath(r), serializeRecord(r)]));
|
|
1293
|
+
let written = 0;
|
|
1294
|
+
let unchanged = 0;
|
|
1295
|
+
for (const [rel, text] of wanted) {
|
|
1296
|
+
const full = join4(dir, ...rel.split("/"));
|
|
1297
|
+
const current = await readFile3(full, "utf8").catch(() => void 0);
|
|
1298
|
+
if (current === text) {
|
|
1299
|
+
unchanged++;
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
await writeFileAtomic(full, text);
|
|
1303
|
+
written++;
|
|
1304
|
+
}
|
|
1305
|
+
let removed = 0;
|
|
1306
|
+
const existing = await readCatalogPaths(dir);
|
|
1307
|
+
for (const rel of existing) {
|
|
1308
|
+
if (!wanted.has(rel)) {
|
|
1309
|
+
await rm3(join4(dir, ...rel.split("/")), { force: true });
|
|
1310
|
+
removed++;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return { written, unchanged, removed };
|
|
1314
|
+
}
|
|
1315
|
+
async function readCatalogPaths(dir) {
|
|
1316
|
+
const out = [];
|
|
1317
|
+
async function walk(d) {
|
|
1318
|
+
let entries;
|
|
1319
|
+
try {
|
|
1320
|
+
entries = await readdir3(d, { withFileTypes: true });
|
|
1321
|
+
} catch {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
for (const e of entries) {
|
|
1325
|
+
const full = join4(d, e.name);
|
|
1326
|
+
if (e.isDirectory())
|
|
1327
|
+
await walk(full);
|
|
1328
|
+
else if (/\.ya?ml$/.test(e.name))
|
|
1329
|
+
out.push(relative2(dir, full).split(sep2).join("/"));
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
await walk(dir);
|
|
1333
|
+
return out;
|
|
1334
|
+
}
|
|
1335
|
+
function canonical(v) {
|
|
1336
|
+
return JSON.stringify(v, (_k, x) => x && typeof x === "object" && !Array.isArray(x) ? Object.fromEntries(Object.entries(x).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : x);
|
|
1337
|
+
}
|
|
1338
|
+
function diffCatalogs(before, after) {
|
|
1339
|
+
const key = (r) => `${r.kind}|${r.id}`;
|
|
1340
|
+
const a = new Map(before.map((r) => [key(r), r]));
|
|
1341
|
+
const b = new Map(after.map((r) => [key(r), r]));
|
|
1342
|
+
const diff = { added: [], removed: [], changed: [], evidenceOnly: [] };
|
|
1343
|
+
for (const [k, r] of b) {
|
|
1344
|
+
const old = a.get(k);
|
|
1345
|
+
if (!old) {
|
|
1346
|
+
diff.added.push({ kind: r.kind, id: r.id });
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
const fields = [.../* @__PURE__ */ new Set([...Object.keys(old), ...Object.keys(r)])].filter((f) => f !== "evidence" && canonical(old[f]) !== canonical(r[f])).sort();
|
|
1350
|
+
if (fields.length > 0)
|
|
1351
|
+
diff.changed.push({ kind: r.kind, id: r.id, fields });
|
|
1352
|
+
else if (canonical(old.evidence) !== canonical(r.evidence))
|
|
1353
|
+
diff.evidenceOnly.push({ kind: r.kind, id: r.id });
|
|
1354
|
+
}
|
|
1355
|
+
for (const [k, r] of a)
|
|
1356
|
+
if (!b.has(k))
|
|
1357
|
+
diff.removed.push({ kind: r.kind, id: r.id });
|
|
1358
|
+
const sort = (xs) => xs.sort((x, y) => x.kind === y.kind ? x.id.localeCompare(y.id) : x.kind.localeCompare(y.kind));
|
|
1359
|
+
sort(diff.added);
|
|
1360
|
+
sort(diff.removed);
|
|
1361
|
+
sort(diff.changed);
|
|
1362
|
+
sort(diff.evidenceOnly);
|
|
1363
|
+
return diff;
|
|
1364
|
+
}
|
|
1365
|
+
function diffIsEmpty(d) {
|
|
1366
|
+
return d.added.length + d.removed.length + d.changed.length + d.evidenceOnly.length === 0;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// packages/scanner/dist/merge-input.js
|
|
1370
|
+
import { join as join5 } from "node:path";
|
|
1371
|
+
function estateRepos(op, only) {
|
|
1372
|
+
return op.profiles.flatMap((p) => (p.repos?.repos ?? []).map((r) => ({ ...r, platform: p.platform }))).filter((r) => !r.archived && (!only || only.includes(r.id)));
|
|
1373
|
+
}
|
|
1374
|
+
function productionRepos(op, only) {
|
|
1375
|
+
return estateRepos(op, only).filter((r) => !isPrototypeType(r.type));
|
|
1376
|
+
}
|
|
1377
|
+
function prototypeScanNotes(op, only) {
|
|
1378
|
+
return estateRepos(op, only).filter((r) => isPrototypeType(r.type)).map((r) => `${r.id}: a prototype, so not scanned into the catalog; compare it with the estate: atticus change attach <change> --prototype ${r.id}.`);
|
|
1379
|
+
}
|
|
1380
|
+
function isPrototypeRepo(op, repo) {
|
|
1381
|
+
return estateRepos(op).some((r) => r.id === repo && isPrototypeType(r.type));
|
|
1382
|
+
}
|
|
1383
|
+
function estateMergeInput(op, repos, records, modulesOf = (id) => repos.find((r) => r.id === id)?.modules ?? []) {
|
|
1384
|
+
return {
|
|
1385
|
+
records,
|
|
1386
|
+
repos: repos.flatMap((r) => [
|
|
1387
|
+
{
|
|
1388
|
+
id: r.id,
|
|
1389
|
+
type: r.type,
|
|
1390
|
+
...r.component ? { component: r.component } : {},
|
|
1391
|
+
...r.system ? { system: r.system } : {},
|
|
1392
|
+
hosts: r.hosts
|
|
1393
|
+
},
|
|
1394
|
+
// A module's hosts join calls to its own component (ADR 0057).
|
|
1395
|
+
...modulesOf(r.id).filter((m) => m.hosts.length > 0).map((m) => ({
|
|
1396
|
+
id: r.id,
|
|
1397
|
+
type: r.type,
|
|
1398
|
+
component: m.component,
|
|
1399
|
+
...r.system ? { system: r.system } : {},
|
|
1400
|
+
hosts: m.hosts
|
|
1401
|
+
}))
|
|
1402
|
+
]),
|
|
1403
|
+
systems: op.profiles.flatMap((p) => p.profile?.systems ?? []),
|
|
1404
|
+
patterns: op.patterns,
|
|
1405
|
+
profilePath: op.profiles[0] ? `org-pack/profiles/${op.profiles[0].platform}/profile.yaml` : void 0
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function mergeEstate(op, repos, records, modulesOf) {
|
|
1409
|
+
return mergeCatalog(estateMergeInput(op, repos, records, modulesOf));
|
|
1410
|
+
}
|
|
1411
|
+
function curatedDirOf(root, op) {
|
|
1412
|
+
return join5(root, ...op.sources.layout.catalog.split("/"), "curated");
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// packages/scanner/dist/facts-catalog.js
|
|
1416
|
+
var TARGETS_STEP = "facts-targets";
|
|
1417
|
+
var TARGETS_NONE = "None of these";
|
|
1418
|
+
var byKindId = (a, b) => a.kind === b.kind ? a.id.localeCompare(b.id) : a.kind.localeCompare(b.kind);
|
|
1419
|
+
var URL_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
1420
|
+
var joinUrl = (base2, path) => `${base2.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
1421
|
+
function urlOf(f, facts) {
|
|
1422
|
+
if (f.host)
|
|
1423
|
+
return joinUrl(URL_RE.test(f.host) ? f.host : `http://${f.host}`, f.path);
|
|
1424
|
+
if (f.base) {
|
|
1425
|
+
const cfg = facts.find((x) => x.kind === "config" && x.key === f.base && !!x.value);
|
|
1426
|
+
if (cfg?.value && URL_RE.test(cfg.value))
|
|
1427
|
+
return joinUrl(cfg.value, f.path);
|
|
1428
|
+
}
|
|
1429
|
+
return void 0;
|
|
1430
|
+
}
|
|
1431
|
+
var HTTP_CALLS = /* @__PURE__ */ new Set(["rest", "soap"]);
|
|
1432
|
+
var EVENT_CALLS = /* @__PURE__ */ new Set(["event-topic", "mq-queue"]);
|
|
1433
|
+
function needsTarget(f, facts) {
|
|
1434
|
+
return f.kind === "call" && !f.to && !EVENT_CALLS.has(f.callType) && !(HTTP_CALLS.has(f.callType) && urlOf(f, facts));
|
|
1435
|
+
}
|
|
1436
|
+
function targetQuestionFor(f) {
|
|
1437
|
+
if (f.kind !== "call")
|
|
1438
|
+
return void 0;
|
|
1439
|
+
return {
|
|
1440
|
+
question: callTargetQuestion(f, factIdPart(f.component ?? f.repo)),
|
|
1441
|
+
evidence: [`${f.cite.file}:${f.cite.line}`]
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
var SERVES = {
|
|
1445
|
+
rest: ["rest"],
|
|
1446
|
+
soap: ["soap"],
|
|
1447
|
+
graphql: ["graphql"],
|
|
1448
|
+
grpc: ["grpc"],
|
|
1449
|
+
webhook: ["webhook"],
|
|
1450
|
+
sftp: ["file-drop"]
|
|
1451
|
+
};
|
|
1452
|
+
function targetQuestionDrafts(facts, records, op, asks) {
|
|
1453
|
+
const repoOf = /* @__PURE__ */ new Map();
|
|
1454
|
+
for (const r of records)
|
|
1455
|
+
if (r.kind === "Component" && r.repo)
|
|
1456
|
+
repoOf.set(r.id, r.repo);
|
|
1457
|
+
const vendors = op.profiles.flatMap((p) => p.profile?.systems ?? []).filter((s) => s.boundary === "vendor").sort((a, b) => a.id.localeCompare(b.id));
|
|
1458
|
+
const out = [];
|
|
1459
|
+
for (const f of [...facts].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
1460
|
+
const ask = asks?.find((a) => a.reason === "ambiguous" && a.call.repo === f.repo && a.call.fact === f.id);
|
|
1461
|
+
if (f.kind !== "call" || !needsTarget(f, facts) && !ask)
|
|
1462
|
+
continue;
|
|
1463
|
+
const q = targetQuestionFor(f);
|
|
1464
|
+
const own = slugify(f.component ?? f.repo).replace(/\./g, "-");
|
|
1465
|
+
const types = new Set(SERVES[f.callType] ?? []);
|
|
1466
|
+
const servers = ask ? [...new Set(ask.candidates.map((c) => c.component))].sort() : [
|
|
1467
|
+
...new Set(records.filter((r) => r.kind === "Endpoint" && types.has(r.endpointType) && r.component !== own).filter((r) => repoOf.get(r.component) !== f.repo).map((r) => r.component))
|
|
1468
|
+
].sort();
|
|
1469
|
+
const what = [f.method?.toUpperCase(), f.path].filter(Boolean).join(" ");
|
|
1470
|
+
const draft = {
|
|
1471
|
+
question: q.question,
|
|
1472
|
+
options: [
|
|
1473
|
+
...servers.map((c) => ({
|
|
1474
|
+
label: c,
|
|
1475
|
+
means: `${c}${repoOf.get(c) ? `, built in ${repoOf.get(c)},` : ""} answers ${what}`,
|
|
1476
|
+
changes: `The call joins ${c} when the facts are next written (atticus verify ${f.repo} --write)`
|
|
1477
|
+
})),
|
|
1478
|
+
...vendors.map((s) => ({
|
|
1479
|
+
label: s.id,
|
|
1480
|
+
means: `${s.name}, an outside system, answers ${what}`,
|
|
1481
|
+
changes: `The call goes to ${s.name} when the facts are next written`
|
|
1482
|
+
})),
|
|
1483
|
+
{
|
|
1484
|
+
label: TARGETS_NONE,
|
|
1485
|
+
means: "Nothing in the estate or the org pack's systems answers it",
|
|
1486
|
+
changes: "The call stays a loose end, and this isn't asked again"
|
|
1487
|
+
}
|
|
1488
|
+
],
|
|
1489
|
+
evidence: q.evidence,
|
|
1490
|
+
scope: "repo"
|
|
1491
|
+
};
|
|
1492
|
+
out.push({
|
|
1493
|
+
factId: f.id,
|
|
1494
|
+
id: questionId({
|
|
1495
|
+
step: TARGETS_STEP,
|
|
1496
|
+
repo: f.repo,
|
|
1497
|
+
question: q.question,
|
|
1498
|
+
evidence: q.evidence
|
|
1499
|
+
}),
|
|
1500
|
+
repo: f.repo,
|
|
1501
|
+
draft
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
return out;
|
|
1505
|
+
}
|
|
1506
|
+
function targetsFromAnswers(repo, facts, questions) {
|
|
1507
|
+
const byId = new Map(questions.map((q) => [q.id, q]));
|
|
1508
|
+
const out = {};
|
|
1509
|
+
for (const f of [...facts].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
1510
|
+
if (f.kind !== "call")
|
|
1511
|
+
continue;
|
|
1512
|
+
const q = targetQuestionFor(f);
|
|
1513
|
+
if (!q)
|
|
1514
|
+
continue;
|
|
1515
|
+
const asked = byId.get(questionId({ step: TARGETS_STEP, repo, ...q }));
|
|
1516
|
+
if (!asked || asked.state !== "answered" || !asked.answer)
|
|
1517
|
+
continue;
|
|
1518
|
+
const label2 = asked.answer.option !== void 0 ? asked.options[asked.answer.option - 1]?.label : asked.answer.text;
|
|
1519
|
+
if (!label2 || label2 === TARGETS_NONE)
|
|
1520
|
+
continue;
|
|
1521
|
+
const to = slugify(label2);
|
|
1522
|
+
if (to)
|
|
1523
|
+
out[f.id] = to;
|
|
1524
|
+
}
|
|
1525
|
+
return out;
|
|
1526
|
+
}
|
|
1527
|
+
function componentRenames(repos, op, taken = []) {
|
|
1528
|
+
const manifests = estateRepos(op);
|
|
1529
|
+
const users = /* @__PURE__ */ new Map();
|
|
1530
|
+
const use = (id, repo) => users.set(id, (users.get(id) ?? /* @__PURE__ */ new Set()).add(repo));
|
|
1531
|
+
for (const { repo, facts } of repos) {
|
|
1532
|
+
const fallback = manifests.find((r) => r.id === repo)?.component;
|
|
1533
|
+
for (const f of facts)
|
|
1534
|
+
if (f.kind !== "field")
|
|
1535
|
+
use(factIdPart(f.component ?? fallback ?? repo), repo);
|
|
1536
|
+
}
|
|
1537
|
+
for (const t of taken)
|
|
1538
|
+
if (users.has(t.id))
|
|
1539
|
+
use(t.id, t.repo);
|
|
1540
|
+
const out = {};
|
|
1541
|
+
for (const [id, set] of [...users].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
1542
|
+
if (set.size < 2)
|
|
1543
|
+
continue;
|
|
1544
|
+
for (const repo of repos.map((r) => r.repo).filter((r) => set.has(r))) {
|
|
1545
|
+
if (factIdPart(repo) === id)
|
|
1546
|
+
continue;
|
|
1547
|
+
(out[repo] ??= {})[id] = factIdPart(`${repo}-${id}`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return out;
|
|
1551
|
+
}
|
|
1552
|
+
function withComponents(repo, facts, op, renames) {
|
|
1553
|
+
const fallback = estateRepos(op).find((r) => r.id === repo)?.component;
|
|
1554
|
+
const mine = renames?.[repo] ?? {};
|
|
1555
|
+
return facts.map((f) => {
|
|
1556
|
+
const c = f.component ?? fallback;
|
|
1557
|
+
if (!c)
|
|
1558
|
+
return f;
|
|
1559
|
+
const to = mine[factIdPart(c)];
|
|
1560
|
+
return to ? { ...f, component: to } : f.component ? f : { ...f, component: c };
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
function factsRecords(input) {
|
|
1564
|
+
const { repo, op } = input;
|
|
1565
|
+
const manifest = estateRepos(op).find((r) => r.id === repo);
|
|
1566
|
+
const facts = withComponents(repo, input.facts, op, input.renames).map((f) => {
|
|
1567
|
+
let g = f;
|
|
1568
|
+
const to = input.targets?.[g.id];
|
|
1569
|
+
if (g.kind === "call" && !g.to && to)
|
|
1570
|
+
g = { ...g, to };
|
|
1571
|
+
return g;
|
|
1572
|
+
});
|
|
1573
|
+
const projected = projectFacts(facts, manifest?.system ? { system: manifest.system } : {});
|
|
1574
|
+
const factById = new Map(facts.map((f) => [f.id, f]));
|
|
1575
|
+
const loose = /* @__PURE__ */ new Map();
|
|
1576
|
+
const looseFact = /* @__PURE__ */ new Map();
|
|
1577
|
+
const extra = [];
|
|
1578
|
+
for (const [factId, id] of Object.entries(projected.ids)) {
|
|
1579
|
+
const f = factById.get(factId);
|
|
1580
|
+
const rec = projected.records.find((r) => r.id === id && r.kind === "Unresolved");
|
|
1581
|
+
if (f.kind === "call" && rec) {
|
|
1582
|
+
looseFact.set(id, factId);
|
|
1583
|
+
const url = HTTP_CALLS.has(f.callType) ? urlOf(f, facts) : void 0;
|
|
1584
|
+
if (url)
|
|
1585
|
+
loose.set(id, {
|
|
1586
|
+
...rec,
|
|
1587
|
+
unresolvedKind: "http-call",
|
|
1588
|
+
raw: `${f.method?.toUpperCase() ?? "ANY"} ${url}`
|
|
1589
|
+
});
|
|
1590
|
+
else if (EVENT_CALLS.has(f.callType))
|
|
1591
|
+
loose.set(id, { ...rec, unresolvedKind: "event-publish", raw: f.path });
|
|
1592
|
+
}
|
|
1593
|
+
if (f.kind === "entry" && EVENT_CALLS.has(f.entryType)) {
|
|
1594
|
+
const ep = projected.records.find((r) => r.id === id && r.kind === "Endpoint");
|
|
1595
|
+
extra.push({
|
|
1596
|
+
kind: "Unresolved",
|
|
1597
|
+
schemaVersion: 2,
|
|
1598
|
+
id: `unresolved.${id}`,
|
|
1599
|
+
status: "current",
|
|
1600
|
+
confidence: ep.confidence,
|
|
1601
|
+
unresolvedKind: "event-subscribe",
|
|
1602
|
+
raw: f.path,
|
|
1603
|
+
owner: ep.component,
|
|
1604
|
+
evidence: ep.evidence.map((e) => ({ ...e, side: "callee" }))
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const newRecords = [
|
|
1609
|
+
...projected.records.filter((r) => r.kind !== "Flow").map((r) => loose.get(r.id) ?? r),
|
|
1610
|
+
...extra
|
|
1611
|
+
];
|
|
1612
|
+
const newFlows = projected.records.filter((r) => r.kind === "Flow");
|
|
1613
|
+
return { facts, projected, factById, newRecords, newFlows, loose, looseFact };
|
|
1614
|
+
}
|
|
1615
|
+
function outsideSystems(op) {
|
|
1616
|
+
return op.profiles.flatMap((p) => p.profile?.systems ?? []).filter((s) => s.boundary === "vendor").flatMap((s) => [s.id, s.name, ...s.aliases]).sort();
|
|
1617
|
+
}
|
|
1618
|
+
function joinReposOf(op, list, renames) {
|
|
1619
|
+
const manifests = estateRepos(op);
|
|
1620
|
+
return list.map(({ repo, facts }) => {
|
|
1621
|
+
const m = manifests.find((r) => r.id === repo);
|
|
1622
|
+
const modules = (m?.modules ?? []).filter((x) => x.hosts.length > 0);
|
|
1623
|
+
const component = m?.component ? renames?.[repo]?.[factIdPart(m.component)] ?? m.component : void 0;
|
|
1624
|
+
return {
|
|
1625
|
+
repo,
|
|
1626
|
+
facts: renames?.[repo] ? withComponents(repo, facts, op, renames) : facts,
|
|
1627
|
+
...component ? { component } : {},
|
|
1628
|
+
...m?.hosts.length ? { hosts: m.hosts } : {},
|
|
1629
|
+
...modules.length ? { modules: modules.map((x) => ({ component: x.component, hosts: x.hosts })) } : {},
|
|
1630
|
+
// An API gateway's entries are routes the join follows through (ADR 0124 addendum A-gateway).
|
|
1631
|
+
// A type that says which side it faces (`gateway-external`, `gateway-internal`) marks each
|
|
1632
|
+
// hop through it (A-gateway2); a plain `gateway` leaves that to the proxy's virtual host.
|
|
1633
|
+
...m && /^gateway(-|$)/.test(m.type) ? { gateway: true } : {},
|
|
1634
|
+
...m?.type === "gateway-external" ? { facing: "external" } : m?.type === "gateway-internal" ? { facing: "internal" } : {}
|
|
1635
|
+
};
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
function applyFactJoins(records, join10, repos, withCallers) {
|
|
1639
|
+
const calls = /* @__PURE__ */ new Map();
|
|
1640
|
+
for (const j of join10.joins) {
|
|
1641
|
+
const k = `${j.call.repo}|${j.call.at}|${j.call.component}`;
|
|
1642
|
+
if (!calls.has(k))
|
|
1643
|
+
calls.set(k, j.interface);
|
|
1644
|
+
}
|
|
1645
|
+
const ifaces = new Set(join10.joins.map((j) => j.interface));
|
|
1646
|
+
const madeByCall = (r) => {
|
|
1647
|
+
if (r.kind === "Interface" && ifaces.has(r.id))
|
|
1648
|
+
return r.id;
|
|
1649
|
+
if (r.kind !== "Interface" && r.kind !== "Unresolved")
|
|
1650
|
+
return void 0;
|
|
1651
|
+
const owner = r.kind === "Interface" ? r.from : r.owner;
|
|
1652
|
+
for (const e of r.evidence) {
|
|
1653
|
+
if (e.side === "callee")
|
|
1654
|
+
continue;
|
|
1655
|
+
const to = calls.get(`${e.repo}|${e.path}:${e.line}|${owner}`);
|
|
1656
|
+
if (to)
|
|
1657
|
+
return to;
|
|
1658
|
+
}
|
|
1659
|
+
return void 0;
|
|
1660
|
+
};
|
|
1661
|
+
const was = /* @__PURE__ */ new Map();
|
|
1662
|
+
const kept2 = [];
|
|
1663
|
+
for (const r of records) {
|
|
1664
|
+
const to = madeByCall(r);
|
|
1665
|
+
if (to) {
|
|
1666
|
+
if (to !== r.id)
|
|
1667
|
+
was.set(r.id, to);
|
|
1668
|
+
} else if (!(r.kind === "FieldMapping" && ifaces.has(r.interface)))
|
|
1669
|
+
kept2.push(r);
|
|
1670
|
+
}
|
|
1671
|
+
const re = (id) => {
|
|
1672
|
+
for (const [from, to] of was)
|
|
1673
|
+
if (id === from || id.startsWith(`${from}.`))
|
|
1674
|
+
return to + id.slice(from.length);
|
|
1675
|
+
return id;
|
|
1676
|
+
};
|
|
1677
|
+
const joined = joinRecords(join10, repos);
|
|
1678
|
+
const callers = withCallers ? callerRecords(withCallers.callers, repos, withCallers.join, {
|
|
1679
|
+
...withCallers.only ? { only: withCallers.only } : {},
|
|
1680
|
+
existing: joined
|
|
1681
|
+
}) : [];
|
|
1682
|
+
const filled = new Set(callers.map((r) => `${r.kind}|${r.id}`));
|
|
1683
|
+
return [
|
|
1684
|
+
...kept2.map((r) => {
|
|
1685
|
+
if (r.kind === "Schema" && re(r.ref) !== r.ref)
|
|
1686
|
+
return { ...r, id: re(r.id), ref: re(r.ref) };
|
|
1687
|
+
if (r.kind === "FieldMapping" && re(r.interface) !== r.interface)
|
|
1688
|
+
return { ...r, id: re(r.id), interface: re(r.interface) };
|
|
1689
|
+
return r;
|
|
1690
|
+
}),
|
|
1691
|
+
...joined.filter((r) => !filled.has(`${r.kind}|${r.id}`)),
|
|
1692
|
+
...callers
|
|
1693
|
+
];
|
|
1694
|
+
}
|
|
1695
|
+
var entryText = (j) => j.method ? `${j.method} ${j.path}` : j.path;
|
|
1696
|
+
function joinAbout(all, repo) {
|
|
1697
|
+
const joins = all.joins.filter((j) => j.call.repo === repo || j.entry.repo === repo || !!j.gateway?.some((h) => h.entry.repo === repo));
|
|
1698
|
+
const ifaces = new Set(joins.map((j) => j.interface));
|
|
1699
|
+
return {
|
|
1700
|
+
joins,
|
|
1701
|
+
asks: all.asks.filter((a) => a.call.repo === repo),
|
|
1702
|
+
misses: all.misses.filter((m) => m.call.repo === repo),
|
|
1703
|
+
lineage: all.lineage.filter((l) => ifaces.has(l.interface)),
|
|
1704
|
+
gaps: all.gaps.filter((g) => ifaces.has(g.interface))
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function catalogWithFacts(input) {
|
|
1708
|
+
const { repo, op } = input;
|
|
1709
|
+
if (isPrototypeRepo(op, repo))
|
|
1710
|
+
return prototypeLeftOut(input);
|
|
1711
|
+
const others = (input.estate ?? []).filter((x) => x.repo !== repo && !isPrototypeRepo(op, x.repo));
|
|
1712
|
+
const renames = componentRenames([{ repo, facts: input.facts }, ...others], op, input.existing.flatMap((r) => r.kind === "Component" && r.repo && r.repo !== repo ? [{ id: r.id, repo: r.repo }] : []));
|
|
1713
|
+
const { facts, projected, newRecords, newFlows, looseFact } = factsRecords({
|
|
1714
|
+
...input,
|
|
1715
|
+
renames
|
|
1716
|
+
});
|
|
1717
|
+
const mine = (e) => e.repo === repo;
|
|
1718
|
+
const oldComponents = new Set(input.existing.filter((r) => r.kind === "Component" && r.repo === repo).map((r) => r.id));
|
|
1719
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
1720
|
+
const kept2 = [];
|
|
1721
|
+
const mixed = [];
|
|
1722
|
+
for (const r of input.existing) {
|
|
1723
|
+
if (r.kind === "Flow" || r.kind === "CuratedDecision")
|
|
1724
|
+
continue;
|
|
1725
|
+
const ev = r.evidence;
|
|
1726
|
+
const m = ev.filter(mine).length;
|
|
1727
|
+
if (m === 0)
|
|
1728
|
+
kept2.push(r);
|
|
1729
|
+
else if (m === ev.length || r.flags?.includes(DECLARED_CALLER))
|
|
1730
|
+
dropped.add(`${r.kind}|${r.id}`);
|
|
1731
|
+
else
|
|
1732
|
+
mixed.push(r);
|
|
1733
|
+
}
|
|
1734
|
+
const droppedEndpoint = (id) => typeof id === "string" && dropped.has(`Endpoint|${id}`);
|
|
1735
|
+
for (const r of mixed) {
|
|
1736
|
+
const ev = r.evidence;
|
|
1737
|
+
const out = { ...r, evidence: ev.filter((e) => !mine(e)) };
|
|
1738
|
+
if (r.kind === "Interface") {
|
|
1739
|
+
if (droppedEndpoint(out.endpoint))
|
|
1740
|
+
delete out.endpoint;
|
|
1741
|
+
if (out.confidence === "verified" && ev.some((e) => mine(e) && e.side === "callee"))
|
|
1742
|
+
out.confidence = "inferred";
|
|
1743
|
+
}
|
|
1744
|
+
if (r.kind === "Table") {
|
|
1745
|
+
out.readers = out.readers.filter((c) => !oldComponents.has(c));
|
|
1746
|
+
out.writers = out.writers.filter((c) => !oldComponents.has(c));
|
|
1747
|
+
}
|
|
1748
|
+
kept2.push(out);
|
|
1749
|
+
}
|
|
1750
|
+
const projectedComponents = new Set(newRecords.filter((r) => r.kind === "Component").map((r) => r.id));
|
|
1751
|
+
const profileSystems = new Set(op.profiles.flatMap((p) => p.profile?.systems ?? []).map((x) => x.id));
|
|
1752
|
+
for (let i = kept2.length - 1; i >= 0; i--) {
|
|
1753
|
+
const r = kept2[i];
|
|
1754
|
+
if (r.kind === "Component" && projectedComponents.has(r.id) && r.confidence === "inferred" && !r.repo || input.estate && r.kind === "System" && profileSystems.has(r.id))
|
|
1755
|
+
kept2.splice(i, 1);
|
|
1756
|
+
}
|
|
1757
|
+
const remap = /* @__PURE__ */ new Map();
|
|
1758
|
+
const stepTo = /* @__PURE__ */ new Map();
|
|
1759
|
+
let join10;
|
|
1760
|
+
let records0 = [...kept2, ...newRecords];
|
|
1761
|
+
if (input.estate) {
|
|
1762
|
+
const repos = joinReposOf(op, [{ repo, facts }, ...others], renames);
|
|
1763
|
+
const whole = joinFacts({
|
|
1764
|
+
repos,
|
|
1765
|
+
targets: { ...input.estateTargets, [repo]: input.targets ?? {} },
|
|
1766
|
+
outside: outsideSystems(op)
|
|
1767
|
+
});
|
|
1768
|
+
join10 = joinAbout(whole, repo);
|
|
1769
|
+
records0 = applyFactJoins(records0, join10, repos, {
|
|
1770
|
+
callers: joinCallers({ repos, join: whole, declared: declaredCallsOf(op.profiles) }),
|
|
1771
|
+
join: whole,
|
|
1772
|
+
only: [repo]
|
|
1773
|
+
});
|
|
1774
|
+
for (const j of join10.joins) {
|
|
1775
|
+
const id = j.call.repo === repo ? projected.ids[j.call.fact] : void 0;
|
|
1776
|
+
if (!id || remap.has(id))
|
|
1777
|
+
continue;
|
|
1778
|
+
remap.set(id, j.interface);
|
|
1779
|
+
stepTo.set(id, { component: j.entry.component, entry: entryText(j) });
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
const merged0 = mergeEstate(op, estateRepos(op), records0.sort(byKindId));
|
|
1783
|
+
const heard = new Set((join10?.joins ?? []).filter((j) => j.how === "topic").map((j) => `${j.entry.repo}|${j.entry.at}|${j.entry.component}`));
|
|
1784
|
+
const merged = {
|
|
1785
|
+
...merged0,
|
|
1786
|
+
records: merged0.records.filter((r) => !(r.kind === "Unresolved" && r.unresolvedKind === "event-subscribe" && r.evidence.some((e) => heard.has(`${e.repo}|${e.path}:${e.line}|${r.owner}`))))
|
|
1787
|
+
};
|
|
1788
|
+
const endpoints = merged.records.filter((r) => r.kind === "Endpoint");
|
|
1789
|
+
for (const r of projected.records) {
|
|
1790
|
+
if (r.kind !== "Endpoint")
|
|
1791
|
+
continue;
|
|
1792
|
+
const hit = r.endpointType === "rest" ? endpoints.find((e) => e.endpointType === "rest" && e.component === r.component && (e.method ?? "") === (r.method ?? "") && e.normalizedPath === normalizePath(r.normalizedPath ?? r.pathOrTopic)) : endpoints.find((e) => e.id === r.id) ?? endpoints.find((e) => e.endpointType === r.endpointType && e.pathOrTopic === r.pathOrTopic);
|
|
1793
|
+
if (hit && hit.id !== r.id)
|
|
1794
|
+
remap.set(r.id, hit.id);
|
|
1795
|
+
}
|
|
1796
|
+
const stillLoose = new Set(merged.records.filter((r) => r.kind === "Unresolved").map((r) => r.id));
|
|
1797
|
+
const joinedTo = /* @__PURE__ */ new Map();
|
|
1798
|
+
for (const [id] of looseFact) {
|
|
1799
|
+
if (stillLoose.has(id))
|
|
1800
|
+
continue;
|
|
1801
|
+
const rec = newRecords.find((r) => r.id === id);
|
|
1802
|
+
const at3 = rec.evidence[0];
|
|
1803
|
+
const iface = merged.records.find((r) => r.kind === "Interface" && r.from === rec.owner && r.evidence.some((e) => e.repo === repo && e.path === at3?.path && e.line === at3?.line));
|
|
1804
|
+
if (iface) {
|
|
1805
|
+
remap.set(id, iface.id);
|
|
1806
|
+
joinedTo.set(id, iface.to);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
const re = (id) => {
|
|
1810
|
+
const exact = remap.get(id);
|
|
1811
|
+
if (exact)
|
|
1812
|
+
return exact;
|
|
1813
|
+
for (const [from, to] of remap)
|
|
1814
|
+
if (id.startsWith(`${from}.`))
|
|
1815
|
+
return to + id.slice(from.length);
|
|
1816
|
+
return id;
|
|
1817
|
+
};
|
|
1818
|
+
const records = merged.records.map((r) => {
|
|
1819
|
+
if (r.kind === "Schema")
|
|
1820
|
+
return { ...r, id: re(r.id), ref: re(r.ref) };
|
|
1821
|
+
if (r.kind === "Endpoint" && r.schemaRefs.length)
|
|
1822
|
+
return { ...r, schemaRefs: r.schemaRefs.map(re).sort() };
|
|
1823
|
+
if (r.kind === "FieldMapping")
|
|
1824
|
+
return { ...r, interface: re(r.interface) };
|
|
1825
|
+
return r;
|
|
1826
|
+
});
|
|
1827
|
+
const applied = applyCurated([...records].sort(byKindId), input.decisions);
|
|
1828
|
+
const newIds = new Set(applied.records.filter((r) => r.evidence.some(mine)).map((r) => r.id));
|
|
1829
|
+
const oldIds = new Set([...dropped].map((k) => k.slice(k.indexOf("|") + 1)));
|
|
1830
|
+
const flows = newFlows.map((f) => {
|
|
1831
|
+
const next = {
|
|
1832
|
+
...f,
|
|
1833
|
+
trigger: { ...f.trigger, ...f.trigger.ref ? { ref: re(f.trigger.ref) } : {} },
|
|
1834
|
+
steps: f.steps.map((s) => {
|
|
1835
|
+
const was = s.to?.component;
|
|
1836
|
+
const direct = s.refs.map((r) => stepTo.get(r)).find((x) => !!x);
|
|
1837
|
+
const to = s.to && direct ? { ...s.to, ...direct } : s.to && was && joinedTo.has(was) ? { ...s.to, component: joinedTo.get(was) } : s.to;
|
|
1838
|
+
return { ...s, refs: s.refs.map(re), ...to ? { to } : {} };
|
|
1839
|
+
})
|
|
1840
|
+
};
|
|
1841
|
+
const p = parseRecord(next);
|
|
1842
|
+
return p.ok ? p.record : next;
|
|
1843
|
+
});
|
|
1844
|
+
const unresolved = [];
|
|
1845
|
+
for (const r of applied.records) {
|
|
1846
|
+
if (r.kind !== "Unresolved" || !looseFact.has(r.id))
|
|
1847
|
+
continue;
|
|
1848
|
+
unresolved.push({
|
|
1849
|
+
factId: looseFact.get(r.id),
|
|
1850
|
+
id: r.id,
|
|
1851
|
+
unresolvedKind: r.unresolvedKind,
|
|
1852
|
+
raw: r.raw
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
unresolved.sort((a, b) => a.factId.localeCompare(b.factId));
|
|
1856
|
+
return {
|
|
1857
|
+
records: applied.records.sort(byKindId),
|
|
1858
|
+
flows: flows.sort((a, b) => a.id.localeCompare(b.id)),
|
|
1859
|
+
unresolved,
|
|
1860
|
+
curated: {
|
|
1861
|
+
applied: applied.applied,
|
|
1862
|
+
suppressed: applied.suppressed,
|
|
1863
|
+
problems: applied.problems.filter((p) => newIds.has(p.target) || oldIds.has(p.target))
|
|
1864
|
+
},
|
|
1865
|
+
projectionIssues: projected.issues.filter((i) => i.code !== "unresolved-target"),
|
|
1866
|
+
invalid: merged.invalid,
|
|
1867
|
+
notes: merged.notes,
|
|
1868
|
+
...join10 ? { join: join10 } : {}
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function prototypeLeftOut(input) {
|
|
1872
|
+
const { repo } = input;
|
|
1873
|
+
const records = input.existing.filter((r) => {
|
|
1874
|
+
if (r.kind === "Flow" || r.kind === "CuratedDecision")
|
|
1875
|
+
return false;
|
|
1876
|
+
const ev = r.evidence;
|
|
1877
|
+
return !(ev.length && ev.every((e) => e.repo === repo));
|
|
1878
|
+
});
|
|
1879
|
+
return {
|
|
1880
|
+
records: [...records].sort(byKindId),
|
|
1881
|
+
flows: [],
|
|
1882
|
+
unresolved: [],
|
|
1883
|
+
curated: { applied: [], suppressed: [], problems: [] },
|
|
1884
|
+
projectionIssues: [],
|
|
1885
|
+
invalid: [],
|
|
1886
|
+
notes: [
|
|
1887
|
+
`${repo} is a prototype: its facts are kept in facts/${repo}.jsonl and compared with the estate (atticus change attach <change> --prototype ${repo}), never written into the catalog.`
|
|
1888
|
+
]
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// packages/scanner/dist/facts-scan.js
|
|
1893
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1894
|
+
import { join as join6 } from "node:path";
|
|
1895
|
+
async function factsForScan(root, op, repo, commit, readerRecords, at3 = {}) {
|
|
1896
|
+
const rel = `${factsRelOf(root)}/${repo}.jsonl`;
|
|
1897
|
+
const text = await readFile4(join6(factsDirOf(root), `${repo}.jsonl`), "utf8").catch(() => void 0);
|
|
1898
|
+
if (text === void 0 || text.trim() === "")
|
|
1899
|
+
return {};
|
|
1900
|
+
const parsed = parseFacts(text);
|
|
1901
|
+
if (!parsed.ok)
|
|
1902
|
+
return {
|
|
1903
|
+
note: `${repo}: ${rel} doesn't read as facts (${parsed.issues[0]?.message ?? "invalid"}); its readers' records are used. Run atticus verify ${repo}.`
|
|
1904
|
+
};
|
|
1905
|
+
let facts = parsed.facts;
|
|
1906
|
+
const unverified = facts.filter((f) => f.verified === "none").length;
|
|
1907
|
+
if (unverified)
|
|
1908
|
+
return {
|
|
1909
|
+
note: `${repo}: ${unverified} of its facts aren't verified yet; its readers' records are used. Run atticus verify ${repo}.`
|
|
1910
|
+
};
|
|
1911
|
+
const older = commit ? facts.filter((f) => !sameCommit(f.commit, commit)) : [];
|
|
1912
|
+
let carriedSaid = "";
|
|
1913
|
+
if (older.length) {
|
|
1914
|
+
const diff = at3.dir ? await changesSince(at3.dir, older.map((f) => f.commit), commit, at3.git) : void 0;
|
|
1915
|
+
if (!diff || diff.missing.length)
|
|
1916
|
+
return {
|
|
1917
|
+
note: `${repo}: its facts are of commit ${(diff?.missing[0] ?? older[0].commit).slice(0, 7)}, the checkout is at ${commit.slice(0, 7)}, and the two can't be compared here; its readers' records are used until it is read again (/atticus-map ${repo}, then atticus verify ${repo} --write).`
|
|
1918
|
+
};
|
|
1919
|
+
const report = await verifyFacts({
|
|
1920
|
+
facts,
|
|
1921
|
+
source: dirSource(at3.dir),
|
|
1922
|
+
commit,
|
|
1923
|
+
carry: diff.changes
|
|
1924
|
+
});
|
|
1925
|
+
const from = [...new Set(older.map((f) => f.commit.slice(0, 7)))].join(", ");
|
|
1926
|
+
if (!report.accepted.length)
|
|
1927
|
+
return {
|
|
1928
|
+
note: `${repo}: every one of its facts (of commit ${from}) cites a file changed since, or names a fact that does; its readers' records are used until it is read again (/atticus-map ${repo}, then atticus verify ${repo} --write).`
|
|
1929
|
+
};
|
|
1930
|
+
facts = report.accepted;
|
|
1931
|
+
const left = report.rejected.length;
|
|
1932
|
+
carriedSaid = `; ${report.carried.length} carried from commit ${from} to ${commit.slice(0, 7)} (the files they cite are unchanged, their lines checked again)${left ? `, ${left} left out until read again (they cite a file changed since, or name a fact that does: /atticus-map ${repo}, then atticus verify ${repo} --write)` : ""}`;
|
|
1933
|
+
}
|
|
1934
|
+
const questions = (await listQuestions(root, { repo }).catch(() => ({ questions: [] }))).questions;
|
|
1935
|
+
const targets = targetsFromAnswers(repo, facts, questions);
|
|
1936
|
+
const others = (await estateFacts(root, op, repo)).repos;
|
|
1937
|
+
const renames = componentRenames([{ repo, facts }, ...others], op);
|
|
1938
|
+
const { newRecords, facts: used } = factsRecords({ repo, facts, op, targets, renames });
|
|
1939
|
+
const keyOf = (r) => {
|
|
1940
|
+
const x = r;
|
|
1941
|
+
if (r.kind === "Endpoint")
|
|
1942
|
+
return `${String(x.method ?? "").toUpperCase()} ${normalizePath(String(x.pathOrTopic))}`;
|
|
1943
|
+
if (r.kind === "Interface")
|
|
1944
|
+
return `${String(x.method ?? "").toUpperCase()} ${String(x.normalizedPath)}`;
|
|
1945
|
+
if (r.kind === "Table")
|
|
1946
|
+
return String(x.name).toLowerCase();
|
|
1947
|
+
if (r.kind === "Column")
|
|
1948
|
+
return `${String(x.table).split(".").pop()}.${String(x.name)}`.toLowerCase();
|
|
1949
|
+
if (r.kind === "Setting")
|
|
1950
|
+
return String(x.key);
|
|
1951
|
+
return void 0;
|
|
1952
|
+
};
|
|
1953
|
+
const have = new Set(newRecords.map((r) => `${r.kind}|${keyOf(r)}`));
|
|
1954
|
+
const extra = /* @__PURE__ */ new Map();
|
|
1955
|
+
for (const r of readerRecords) {
|
|
1956
|
+
const k = keyOf(r);
|
|
1957
|
+
if (k === void 0 || have.has(`${r.kind}|${k}`))
|
|
1958
|
+
continue;
|
|
1959
|
+
extra.set(r.kind, (extra.get(r.kind) ?? 0) + 1);
|
|
1960
|
+
}
|
|
1961
|
+
const said = [...extra.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([k, n]) => `${n} ${k}`).join(", ");
|
|
1962
|
+
return {
|
|
1963
|
+
records: newRecords,
|
|
1964
|
+
facts: used,
|
|
1965
|
+
targets,
|
|
1966
|
+
note: `${repo}: read from its facts (${facts.length}, ${rel}${carriedSaid})${said ? `; its readers found ${said} the facts don't have (a cross-check: read those lines again, or a reader misreads them)` : "; its readers agree"}.`
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
async function estateFacts(root, op, except) {
|
|
1970
|
+
const repos = [];
|
|
1971
|
+
const targets = {};
|
|
1972
|
+
const notes = [];
|
|
1973
|
+
for (const r of productionRepos(op)) {
|
|
1974
|
+
if (r.id === except)
|
|
1975
|
+
continue;
|
|
1976
|
+
const text = await readFile4(join6(factsDirOf(root), `${r.id}.jsonl`), "utf8").catch(() => void 0);
|
|
1977
|
+
if (text === void 0 || text.trim() === "")
|
|
1978
|
+
continue;
|
|
1979
|
+
const parsed = parseFacts(text);
|
|
1980
|
+
if (!parsed.ok) {
|
|
1981
|
+
notes.push(`${r.id}: its facts don't read, so the estate join leaves them out.`);
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
if (parsed.facts.some((f) => f.verified === "none")) {
|
|
1985
|
+
notes.push(`${r.id}: some of its facts aren't verified yet, so the estate join leaves them out (atticus verify ${r.id}).`);
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
repos.push({ repo: r.id, facts: parsed.facts });
|
|
1989
|
+
const questions = (await listQuestions(root, { repo: r.id }).catch(() => ({ questions: [] }))).questions;
|
|
1990
|
+
const named2 = targetsFromAnswers(r.id, parsed.facts, questions);
|
|
1991
|
+
if (Object.keys(named2).length)
|
|
1992
|
+
targets[r.id] = named2;
|
|
1993
|
+
}
|
|
1994
|
+
return { repos, targets, notes };
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// packages/scanner/dist/scan-settings.js
|
|
1998
|
+
var NO_SETTINGS = {
|
|
1999
|
+
exclude: [],
|
|
2000
|
+
prefixes: [],
|
|
2001
|
+
clients: [],
|
|
2002
|
+
problems: []
|
|
2003
|
+
};
|
|
2004
|
+
var HTTP_ENTRY = /* @__PURE__ */ new Set(["rest", "webhook", "websocket", "sse"]);
|
|
2005
|
+
async function citedLine(repo, at3) {
|
|
2006
|
+
const i = at3.lastIndexOf(":");
|
|
2007
|
+
const path = at3.slice(0, i);
|
|
2008
|
+
const n = Number(at3.slice(i + 1));
|
|
2009
|
+
const text = await repo.readText(path).catch(() => void 0);
|
|
2010
|
+
if (text === void 0)
|
|
2011
|
+
return void 0;
|
|
2012
|
+
return text.replace(/\r\n?/g, "\n").split("\n")[n - 1];
|
|
2013
|
+
}
|
|
2014
|
+
var CODE_EXT = new Set("java kt scala groovy ts tsx js jsx mjs cjs py go rb cs fs vb php rs swift m mm c cc cpp h hpp dart ex exs erl clj lua pl".split(" "));
|
|
2015
|
+
var NOT_OWN = /(^|\/)(node_modules|\.git|target|bin|obj|dist|build|\.atticus|vendor)\//;
|
|
2016
|
+
var CONFIG_EXT = new Set("xml json jsonc yaml yml toml properties gradle cfg ini conf csproj sln props targets".split(" "));
|
|
2017
|
+
var CONFIG_NAME = /^(makefile|dockerfile|build\.gradle\.kts|settings\.gradle\.kts|\.[a-z]*ignore|\.editorconfig)$/i;
|
|
2018
|
+
var ext = (f) => /\.([^./]+)$/.exec(f)?.[1]?.toLowerCase() ?? "";
|
|
2019
|
+
var base = (f) => f.slice(f.lastIndexOf("/") + 1);
|
|
2020
|
+
var isCodeFile = (f) => !NOT_OWN.test(f) && CODE_EXT.has(ext(f));
|
|
2021
|
+
var isConfigFile = (f) => CONFIG_EXT.has(ext(f)) || CONFIG_NAME.test(base(f));
|
|
2022
|
+
function globName(glob) {
|
|
2023
|
+
return glob.split("/").map((s) => s.replace(/[*?[\]{}!]/g, "")).reduce((a, b) => b.length > a.length ? b : a, "");
|
|
2024
|
+
}
|
|
2025
|
+
function prefixOf(value) {
|
|
2026
|
+
let v = value.trim().replace(/^["'`]|["'`]$/g, "");
|
|
2027
|
+
const dflt = /^\$\{[^:}]+:([^}]*)\}$/.exec(v);
|
|
2028
|
+
if (dflt)
|
|
2029
|
+
v = dflt[1];
|
|
2030
|
+
const url = /^https?:\/\/[^/?#\s]+(\/[^?#\s]*)?/.exec(v);
|
|
2031
|
+
if (url)
|
|
2032
|
+
v = url[1] ?? "";
|
|
2033
|
+
if (/[${}\s?#]/.test(v))
|
|
2034
|
+
return void 0;
|
|
2035
|
+
const p = `/${v.replace(/^\/+/, "").replace(/\/+$/, "")}`;
|
|
2036
|
+
return p === "/" ? void 0 : p;
|
|
2037
|
+
}
|
|
2038
|
+
var trimSlash = (p) => p.replace(/\/+$/, "") || "/";
|
|
2039
|
+
async function checkScanSettings(s, repo) {
|
|
2040
|
+
const out = { exclude: [], prefixes: [], clients: [], problems: [] };
|
|
2041
|
+
const code = [];
|
|
2042
|
+
if (s.exclude.length) {
|
|
2043
|
+
for await (const f of repo.listFiles(["**/*"]))
|
|
2044
|
+
if (isCodeFile(f))
|
|
2045
|
+
code.push(f);
|
|
2046
|
+
}
|
|
2047
|
+
const perTop = /* @__PURE__ */ new Map();
|
|
2048
|
+
for (const f of code)
|
|
2049
|
+
if (f.includes("/"))
|
|
2050
|
+
perTop.set(f.split("/")[0], (perTop.get(f.split("/")[0]) ?? 0) + 1);
|
|
2051
|
+
const most = Math.max(0, ...perTop.values());
|
|
2052
|
+
const tops = [...perTop].filter(([, n]) => n === most && n > 0).map(([d]) => d);
|
|
2053
|
+
const mainRoots = tops.length === 1 ? tops : [];
|
|
2054
|
+
for (const [i, e] of s.exclude.entries()) {
|
|
2055
|
+
const line = await citedLine(repo, e.at);
|
|
2056
|
+
if (line === void 0) {
|
|
2057
|
+
out.problems.push(`exclude.${i}: ${e.at} isn't in the repo, so ${e.path} is still read`);
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
const re = globToRegExp(e.path);
|
|
2061
|
+
const file = e.at.slice(0, e.at.lastIndexOf(":"));
|
|
2062
|
+
const name = globName(e.path);
|
|
2063
|
+
const names = name !== "" && line.includes(name);
|
|
2064
|
+
if (!re.test(file) && !names) {
|
|
2065
|
+
out.problems.push(`exclude.${i}: ${e.at} isn't in ${e.path} and doesn't name it, so ${e.path} is still read`);
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
if (!(isConfigFile(file) && names)) {
|
|
2069
|
+
const dropped = code.filter((f) => re.test(f));
|
|
2070
|
+
const root = mainRoots.find((d) => {
|
|
2071
|
+
const under = code.filter((f) => f.startsWith(`${d}/`));
|
|
2072
|
+
return under.length && under.every((f) => re.test(f));
|
|
2073
|
+
});
|
|
2074
|
+
const why = " so it is still read; only a build or config file that leaves it out itself can say so";
|
|
2075
|
+
if (dropped.length * 2 > code.length) {
|
|
2076
|
+
out.problems.push(`exclude.${i}: ${e.path} leaves out ${dropped.length} of the repo's ${code.length} code files,${why}`);
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
if (root) {
|
|
2080
|
+
out.problems.push(`exclude.${i}: ${e.path} leaves out ${root}/, where most of the repo's code is (${most} of ${code.length} code files),${why}`);
|
|
2081
|
+
continue;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
out.exclude.push(re);
|
|
2085
|
+
}
|
|
2086
|
+
for (const [i, p] of s.prefixes.entries()) {
|
|
2087
|
+
const line = await citedLine(repo, p.at);
|
|
2088
|
+
if (line === void 0)
|
|
2089
|
+
out.problems.push(`prefixes.${i}: ${p.at} isn't in the repo, so the prefix ${p.prefix} isn't applied`);
|
|
2090
|
+
else if (!line.includes(p.value))
|
|
2091
|
+
out.problems.push(`prefixes.${i}: ${p.at} doesn't say ${p.value}, so the prefix ${p.prefix} isn't applied`);
|
|
2092
|
+
else if (prefixOf(p.value) !== trimSlash(p.prefix))
|
|
2093
|
+
out.problems.push(`prefixes.${i}: the prefix ${p.prefix} isn't the value ${p.value} written at ${p.at}, so it isn't applied`);
|
|
2094
|
+
else
|
|
2095
|
+
out.prefixes.push({ paths: p.paths.map(globToRegExp), prefix: p.prefix });
|
|
2096
|
+
}
|
|
2097
|
+
for (const [i, c] of s.clients.entries()) {
|
|
2098
|
+
const line = await citedLine(repo, c.at);
|
|
2099
|
+
const trim = (x) => x.replace(/\/+$/, "");
|
|
2100
|
+
const paths = c.paths.map(globToRegExp);
|
|
2101
|
+
const missing = line === void 0 ? [] : await namesNotIn(repo, paths, c.names);
|
|
2102
|
+
if (line === void 0)
|
|
2103
|
+
out.problems.push(`clients.${i}: ${c.at} isn't in the repo, so the base isn't applied`);
|
|
2104
|
+
else if (!line.includes(c.value))
|
|
2105
|
+
out.problems.push(`clients.${i}: ${c.at} doesn't say ${c.value}, so the base isn't applied`);
|
|
2106
|
+
else if (!trim(c.base).endsWith(trim(c.value)))
|
|
2107
|
+
out.problems.push(`clients.${i}: the base ${c.base} isn't the value ${c.value} written at ${c.at}, so it isn't applied`);
|
|
2108
|
+
else if (missing.length)
|
|
2109
|
+
out.problems.push(`clients.${i}: ${missing.join(", ")} isn't in any file under ${c.paths.join(", ")}, so the base isn't applied`);
|
|
2110
|
+
else {
|
|
2111
|
+
const k = c.at.lastIndexOf(":");
|
|
2112
|
+
out.clients.push({
|
|
2113
|
+
paths,
|
|
2114
|
+
base: c.base,
|
|
2115
|
+
names: c.names,
|
|
2116
|
+
at: { path: c.at.slice(0, k), line: Number(c.at.slice(k + 1)) }
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return out;
|
|
2121
|
+
}
|
|
2122
|
+
async function namesNotIn(repo, paths, names) {
|
|
2123
|
+
const left = new Set(names);
|
|
2124
|
+
if (left.size === 0)
|
|
2125
|
+
return [];
|
|
2126
|
+
for await (const f of repo.listFiles(["**/*"])) {
|
|
2127
|
+
if (!paths.some((x) => x.test(f)))
|
|
2128
|
+
continue;
|
|
2129
|
+
const text = await repo.readText(f).catch(() => "");
|
|
2130
|
+
for (const n of [...left])
|
|
2131
|
+
if (text.includes(n))
|
|
2132
|
+
left.delete(n);
|
|
2133
|
+
if (left.size === 0)
|
|
2134
|
+
break;
|
|
2135
|
+
}
|
|
2136
|
+
return [...left];
|
|
2137
|
+
}
|
|
2138
|
+
function joinBase(base2, rest) {
|
|
2139
|
+
if (rest === "")
|
|
2140
|
+
return base2;
|
|
2141
|
+
if (/^[?#]/.test(rest))
|
|
2142
|
+
return `${base2.replace(/\/+$/, "")}${rest}`;
|
|
2143
|
+
return `${base2.replace(/\/+$/, "")}/${rest.replace(/^\/+/, "")}`;
|
|
2144
|
+
}
|
|
2145
|
+
function applyClients(records, clients) {
|
|
2146
|
+
if (clients.length === 0)
|
|
2147
|
+
return records;
|
|
2148
|
+
return records.map((r) => {
|
|
2149
|
+
const x = r;
|
|
2150
|
+
if (r.kind !== "Unresolved" || x.unresolvedKind !== "http-call" || !x.raw)
|
|
2151
|
+
return r;
|
|
2152
|
+
const site = r.evidence[0];
|
|
2153
|
+
const file = site?.path;
|
|
2154
|
+
if (!file)
|
|
2155
|
+
return r;
|
|
2156
|
+
const sp = x.raw.indexOf(" ");
|
|
2157
|
+
const method = x.raw.slice(0, sp);
|
|
2158
|
+
const url = x.raw.slice(sp + 1);
|
|
2159
|
+
for (const c of clients) {
|
|
2160
|
+
if (!c.paths.some((p) => p.test(file)))
|
|
2161
|
+
continue;
|
|
2162
|
+
const name = c.names.find((n) => url.startsWith(`\${${n}}`));
|
|
2163
|
+
const resolved = name !== void 0 ? joinBase(c.base, url.slice(name.length + 3)) : url.startsWith("/") && !url.startsWith("//") ? joinBase(c.base, url) : void 0;
|
|
2164
|
+
if (resolved === void 0)
|
|
2165
|
+
continue;
|
|
2166
|
+
return {
|
|
2167
|
+
...r,
|
|
2168
|
+
raw: `${method} ${resolved}`,
|
|
2169
|
+
evidence: [
|
|
2170
|
+
...r.evidence,
|
|
2171
|
+
{
|
|
2172
|
+
source: "config",
|
|
2173
|
+
...site.repo ? { repo: site.repo } : {},
|
|
2174
|
+
path: c.at.path,
|
|
2175
|
+
line: c.at.line,
|
|
2176
|
+
...site.commit ? { commit: site.commit } : {},
|
|
2177
|
+
...site.scannedAt ? { scannedAt: site.scannedAt } : {},
|
|
2178
|
+
side: "caller"
|
|
2179
|
+
}
|
|
2180
|
+
]
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
return r;
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
function withExcludes(repo, exclude) {
|
|
2187
|
+
if (exclude.length === 0)
|
|
2188
|
+
return repo;
|
|
2189
|
+
const wrapped = Object.create(repo);
|
|
2190
|
+
Object.defineProperty(wrapped, "listFiles", {
|
|
2191
|
+
value: async function* (globs) {
|
|
2192
|
+
for await (const f of repo.listFiles(globs))
|
|
2193
|
+
if (!exclude.some((x) => x.test(f)))
|
|
2194
|
+
yield f;
|
|
2195
|
+
}
|
|
2196
|
+
});
|
|
2197
|
+
return wrapped;
|
|
2198
|
+
}
|
|
2199
|
+
var join7 = (prefix, path) => `${prefix.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`.replace(/\/+$/, "") || "/";
|
|
2200
|
+
function applyPrefixes(records, prefixes) {
|
|
2201
|
+
if (prefixes.length === 0)
|
|
2202
|
+
return records;
|
|
2203
|
+
return records.map((r) => {
|
|
2204
|
+
if (r.kind !== "Endpoint")
|
|
2205
|
+
return r;
|
|
2206
|
+
const e = r;
|
|
2207
|
+
if (!HTTP_ENTRY.has(e.endpointType))
|
|
2208
|
+
return r;
|
|
2209
|
+
const file = r.evidence[0]?.path;
|
|
2210
|
+
if (!file)
|
|
2211
|
+
return r;
|
|
2212
|
+
const apply = prefixes.filter((p) => p.paths.some((x) => x.test(file))).reverse();
|
|
2213
|
+
if (apply.length === 0)
|
|
2214
|
+
return r;
|
|
2215
|
+
let path = e.pathOrTopic;
|
|
2216
|
+
let normalized = e.normalizedPath;
|
|
2217
|
+
for (const p of apply) {
|
|
2218
|
+
path = join7(p.prefix, path);
|
|
2219
|
+
if (normalized !== void 0)
|
|
2220
|
+
normalized = join7(p.prefix, normalized);
|
|
2221
|
+
}
|
|
2222
|
+
return {
|
|
2223
|
+
...r,
|
|
2224
|
+
pathOrTopic: path,
|
|
2225
|
+
...normalized !== void 0 ? { normalizedPath: normalized } : {}
|
|
2226
|
+
};
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// packages/scanner/dist/plugins.js
|
|
2231
|
+
var import_yaml2 = __toESM(require_dist(), 1);
|
|
2232
|
+
import { readdir as readdir4, readFile as readFile5 } from "node:fs/promises";
|
|
2233
|
+
import { join as join8 } from "node:path";
|
|
2234
|
+
var TEST_COMMIT = "0".repeat(40);
|
|
2235
|
+
var TEST_AT = "2026-01-01T00:00:00Z";
|
|
2236
|
+
function canonical2(v) {
|
|
2237
|
+
return JSON.stringify(v, (_k, x) => x && typeof x === "object" && !Array.isArray(x) ? Object.fromEntries(Object.entries(x).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : x);
|
|
2238
|
+
}
|
|
2239
|
+
function pluginExtractor(manifest, source) {
|
|
2240
|
+
return sandboxedExtractor({
|
|
2241
|
+
id: manifest.id,
|
|
2242
|
+
version: manifest.version,
|
|
2243
|
+
emits: manifest.emits,
|
|
2244
|
+
limits: manifest.limits,
|
|
2245
|
+
source,
|
|
2246
|
+
entry: manifest.entry
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
async function runPluginTests(dir, manifest, source) {
|
|
2250
|
+
const cases = (await readdir4(join8(dir, "test"), { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
2251
|
+
const extractor = pluginExtractor(manifest, source);
|
|
2252
|
+
const results = [];
|
|
2253
|
+
for (const name of cases) {
|
|
2254
|
+
const failures = [];
|
|
2255
|
+
const caseDir = join8(dir, "test", name);
|
|
2256
|
+
let want;
|
|
2257
|
+
try {
|
|
2258
|
+
const parsed = PluginExpected.safeParse((0, import_yaml2.parse)(await readFile5(join8(caseDir, "expected.yaml"), "utf8")) ?? {});
|
|
2259
|
+
if (!parsed.success) {
|
|
2260
|
+
results.push({
|
|
2261
|
+
name,
|
|
2262
|
+
failures: parsed.error.issues.map((i) => `expected.yaml ${i.path.map(String).join(".")}: ${i.message}`)
|
|
2263
|
+
});
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
want = parsed.data;
|
|
2267
|
+
} catch (e) {
|
|
2268
|
+
results.push({ name, failures: [`can't read expected.yaml: ${e.message}`] });
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
const c = want.context;
|
|
2272
|
+
const scan = await scanRepo({
|
|
2273
|
+
repo: fsRepo(name, join8(caseDir, "repo"), TEST_COMMIT),
|
|
2274
|
+
extractors: [extractor],
|
|
2275
|
+
dialects: c?.dialects ?? [],
|
|
2276
|
+
scannedAt: TEST_AT,
|
|
2277
|
+
repoInfo: {
|
|
2278
|
+
...c?.component ? { component: c.component } : {},
|
|
2279
|
+
...c?.system ? { system: c.system } : {},
|
|
2280
|
+
...c?.repoType ? { repoType: c.repoType } : {}
|
|
2281
|
+
}
|
|
2282
|
+
});
|
|
2283
|
+
for (const run of scan.runs.filter((r) => r.status === "failed"))
|
|
2284
|
+
failures.push(run.error ?? "failed");
|
|
2285
|
+
for (const r of scan.rejected)
|
|
2286
|
+
failures.push(`produced an invalid record: ${r.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
|
|
2287
|
+
const byId = new Map(scan.records.map((r) => [`${r.kind}/${r.id}`, r]));
|
|
2288
|
+
for (const e of want.expect) {
|
|
2289
|
+
const got = byId.get(`${e.kind}/${e.id}`);
|
|
2290
|
+
if (!got) {
|
|
2291
|
+
failures.push(`expected ${e.kind} ${e.id}, got ${[...byId.keys()].join(", ") || "nothing"}`);
|
|
2292
|
+
continue;
|
|
2293
|
+
}
|
|
2294
|
+
for (const [k, v] of Object.entries(e))
|
|
2295
|
+
if (canonical2(got[k]) !== canonical2(v))
|
|
2296
|
+
failures.push(`${e.id}.${k}: expected ${canonical2(v)}, got ${canonical2(got[k])}`);
|
|
2297
|
+
}
|
|
2298
|
+
if (want.exact) {
|
|
2299
|
+
const listed = new Set(want.expect.map((e) => `${e.kind}/${e.id}`));
|
|
2300
|
+
for (const k of byId.keys())
|
|
2301
|
+
if (!listed.has(k))
|
|
2302
|
+
failures.push(`${k} isn't expected`);
|
|
2303
|
+
}
|
|
2304
|
+
for (const id of want.absent)
|
|
2305
|
+
if (scan.records.some((r) => r.id === id))
|
|
2306
|
+
failures.push(`${id} should not be produced`);
|
|
2307
|
+
results.push({ name, failures });
|
|
2308
|
+
}
|
|
2309
|
+
return results;
|
|
2310
|
+
}
|
|
2311
|
+
function pluginApplies(manifest, repo) {
|
|
2312
|
+
const a = manifest.appliesTo;
|
|
2313
|
+
return a.repoTypes.includes("*") || a.repoTypes.includes(repo.type) || a.dialects.some((d) => repo.dialects.includes(d));
|
|
2314
|
+
}
|
|
2315
|
+
async function usablePlugins(op) {
|
|
2316
|
+
const plugins = [];
|
|
2317
|
+
const problems = [];
|
|
2318
|
+
for (const { manifest, dir } of op.plugins) {
|
|
2319
|
+
if (!manifest.enabled)
|
|
2320
|
+
continue;
|
|
2321
|
+
const source = `plugin ${manifest.id}`;
|
|
2322
|
+
const notUsed = (message) => problems.push({ source, message: `${message}; the plugin is not used` });
|
|
2323
|
+
const entry = await readPinnedEntry(dir, manifest);
|
|
2324
|
+
if (!entry.ok) {
|
|
2325
|
+
notUsed(entry.message);
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
const results = await runPluginTests(dir, manifest, entry.source);
|
|
2329
|
+
if (results.length === 0) {
|
|
2330
|
+
notUsed("has no golden tests (test/<case>/repo/ and expected.yaml)");
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
const failed = results.flatMap((r) => r.failures.map((f) => `test "${r.name}": ${f}`));
|
|
2334
|
+
if (failed.length > 0) {
|
|
2335
|
+
for (const f of failed)
|
|
2336
|
+
notUsed(f);
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
plugins.push({
|
|
2340
|
+
manifest,
|
|
2341
|
+
dir,
|
|
2342
|
+
extractor: pluginExtractor(manifest, entry.source),
|
|
2343
|
+
config: configHash({ plugin: `${manifest.sha256}|${JSON.stringify(manifest.limits)}` })
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
return { plugins, problems };
|
|
2347
|
+
}
|
|
2348
|
+
function withPluginScan(scan, extra) {
|
|
2349
|
+
return {
|
|
2350
|
+
...scan,
|
|
2351
|
+
records: [...scan.records, ...extra.records].sort((a, b) => a.kind === b.kind ? a.id < b.id ? -1 : a.id > b.id ? 1 : 0 : a.kind < b.kind ? -1 : 1),
|
|
2352
|
+
runs: [...scan.runs, ...extra.runs],
|
|
2353
|
+
warnings: [...scan.warnings, ...extra.warnings],
|
|
2354
|
+
rejected: [...scan.rejected, ...extra.rejected]
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// packages/scanner/dist/scan.js
|
|
2359
|
+
import { stat } from "node:fs/promises";
|
|
2360
|
+
import { dirname as dirname3, isAbsolute, join as join9, resolve as resolve2 } from "node:path";
|
|
2361
|
+
import { fileURLToPath } from "node:url";
|
|
2362
|
+
|
|
2363
|
+
// packages/scanner/dist/federate-loose.js
|
|
2364
|
+
function resolveFederatedLooseEnds(o) {
|
|
2365
|
+
const originOf = (r) => (r.flags ?? []).find((f) => f.startsWith("federated:"))?.slice("federated:".length);
|
|
2366
|
+
const loose = o.federated.filter((r) => r.kind === "Unresolved");
|
|
2367
|
+
if (!loose.length)
|
|
2368
|
+
return { federated: o.federated, resolved: /* @__PURE__ */ new Map() };
|
|
2369
|
+
const fedComponents = o.federated.filter((r) => r.kind === "Component");
|
|
2370
|
+
const result = o.merge([...o.raw, ...fedComponents, ...loose]);
|
|
2371
|
+
const have = new Set(o.local.map((r) => `${r.kind}/${r.id}`));
|
|
2372
|
+
const known = /* @__PURE__ */ new Set([
|
|
2373
|
+
...o.local.filter((r) => r.kind === "Component" || r.kind === "System").map((r) => r.id),
|
|
2374
|
+
...fedComponents.map((r) => r.id)
|
|
2375
|
+
]);
|
|
2376
|
+
const origin = new Map(fedComponents.map((c) => [c.id, originOf(c)]));
|
|
2377
|
+
const stillLoose = new Set(result.records.filter((r) => r.kind === "Unresolved").map((r) => r.id));
|
|
2378
|
+
const added = [];
|
|
2379
|
+
for (const r of result.records) {
|
|
2380
|
+
if (r.kind !== "Interface" || have.has(`Interface/${r.id}`))
|
|
2381
|
+
continue;
|
|
2382
|
+
const i = r;
|
|
2383
|
+
const from = origin.get(i.from) ?? origin.get(i.to);
|
|
2384
|
+
if (!from || !known.has(i.from) || !known.has(i.to))
|
|
2385
|
+
continue;
|
|
2386
|
+
added.push({ ...r, flags: [.../* @__PURE__ */ new Set([...r.flags ?? [], `federated:${from}`])] });
|
|
2387
|
+
}
|
|
2388
|
+
const key = (e) => JSON.stringify(e);
|
|
2389
|
+
const carried = new Set(added.flatMap((i) => i.evidence.map(key)));
|
|
2390
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
2391
|
+
const kept2 = o.federated.filter((r) => {
|
|
2392
|
+
if (r.kind !== "Unresolved" || stillLoose.has(r.id))
|
|
2393
|
+
return true;
|
|
2394
|
+
if (!r.evidence.some((e) => e.source !== "export" && carried.has(key(e))))
|
|
2395
|
+
return true;
|
|
2396
|
+
const id = originOf(r) ?? "";
|
|
2397
|
+
resolved.set(id, (resolved.get(id) ?? 0) + 1);
|
|
2398
|
+
return false;
|
|
2399
|
+
});
|
|
2400
|
+
return {
|
|
2401
|
+
federated: [...kept2, ...added].sort((a, b) => a.kind === b.kind ? a.id.localeCompare(b.id) : a.kind.localeCompare(b.kind)),
|
|
2402
|
+
resolved
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// packages/scanner/dist/scan.js
|
|
2407
|
+
var STRUCTURED = /\.(ya?ml|json|xml|tf|tfvars|properties|toml|conf|hcl)$/i;
|
|
2408
|
+
var TOOLING = /(^|\/)(package(-lock)?\.json|pnpm-lock\.yaml|yarn\.lock|tsconfig[^/]*\.json|\.eslintrc[^/]*|\.prettierrc[^/]*|renovate\.json|\.vscode\/.*|\.idea\/.*|pom\.xml|catalog-info\.yaml|mkdocs\.yml|codecov\.yml)$/i;
|
|
2409
|
+
async function isDir(p) {
|
|
2410
|
+
try {
|
|
2411
|
+
return (await stat(p)).isDirectory();
|
|
2412
|
+
} catch {
|
|
2413
|
+
return false;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
async function orgPackDirFor(root) {
|
|
2417
|
+
if (!await isDir(join9(root, "org-pack")) && await isDir(join9(root, ".atticus", "org-pack")))
|
|
2418
|
+
return { dir: join9(root, ".atticus", "org-pack"), adopted: true };
|
|
2419
|
+
return { dir: join9(root, "org-pack"), adopted: false };
|
|
2420
|
+
}
|
|
2421
|
+
function inPlace(root, url) {
|
|
2422
|
+
if (url === "." || url === "./")
|
|
2423
|
+
return root;
|
|
2424
|
+
if (url.startsWith("./") && !url.split("/").includes(".."))
|
|
2425
|
+
return join9(root, url.slice(2));
|
|
2426
|
+
return void 0;
|
|
2427
|
+
}
|
|
2428
|
+
async function federate(local, sources, problems, root) {
|
|
2429
|
+
const have = new Set(local.map((r) => `${r.kind}/${r.id}`));
|
|
2430
|
+
const records = [];
|
|
2431
|
+
const contributions = [];
|
|
2432
|
+
for (const s of sources) {
|
|
2433
|
+
const theirs = await loadOrgPack((await orgPackDirFor(s.dir)).dir);
|
|
2434
|
+
const catalogRel = `${theirs.sources.layout.catalog}/generated`;
|
|
2435
|
+
const cat = await readCatalog(join9(s.dir, ...catalogRel.split("/")), { cacheRoot: root });
|
|
2436
|
+
for (const p of cat.problems)
|
|
2437
|
+
problems.push({ source: `federated ${s.id}`, message: `${p.file}: ${p.message}` });
|
|
2438
|
+
const c = { id: s.id, added: 0, shadowed: [] };
|
|
2439
|
+
for (const r of cat.records) {
|
|
2440
|
+
const key = `${r.kind}/${r.id}`;
|
|
2441
|
+
if (have.has(key)) {
|
|
2442
|
+
c.shadowed.push(key);
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
have.add(key);
|
|
2446
|
+
records.push({
|
|
2447
|
+
...r,
|
|
2448
|
+
flags: [.../* @__PURE__ */ new Set([...r.flags ?? [], `federated:${s.id}`])],
|
|
2449
|
+
evidence: [
|
|
2450
|
+
...r.evidence,
|
|
2451
|
+
{
|
|
2452
|
+
source: "export",
|
|
2453
|
+
repo: s.id,
|
|
2454
|
+
path: `${catalogRel}/${r.kind.toLowerCase()}/${r.id}.yaml`
|
|
2455
|
+
}
|
|
2456
|
+
]
|
|
2457
|
+
});
|
|
2458
|
+
c.added++;
|
|
2459
|
+
}
|
|
2460
|
+
contributions.push(c);
|
|
2461
|
+
}
|
|
2462
|
+
return { records, contributions };
|
|
2463
|
+
}
|
|
2464
|
+
var REMOTE_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
2465
|
+
var SCP_URL = /^[^/]+@[^:/]+:/;
|
|
2466
|
+
async function federationSources(root, stateDir, op) {
|
|
2467
|
+
const out = [];
|
|
2468
|
+
for (const a of op.sources.absorbed) {
|
|
2469
|
+
if (a.format !== "atticus" || a.mode !== "link")
|
|
2470
|
+
continue;
|
|
2471
|
+
const local = a.url.startsWith("file://") ? fileURLToPath(a.url) : REMOTE_URL.test(a.url) || SCP_URL.test(a.url) ? void 0 : isAbsolute(a.url) ? a.url : resolve2(root, a.url);
|
|
2472
|
+
const copy = join9(stateDir, "absorbed", a.id);
|
|
2473
|
+
if (local && await isDir(local) && !await isDir(join9(local, "objects")))
|
|
2474
|
+
out.push({ id: a.id, dir: local });
|
|
2475
|
+
else if (await isDir(copy))
|
|
2476
|
+
out.push({ id: a.id, dir: copy });
|
|
2477
|
+
}
|
|
2478
|
+
return out;
|
|
2479
|
+
}
|
|
2480
|
+
async function usableDialects(op, orgPackDir, problems, broken = /* @__PURE__ */ new Set()) {
|
|
2481
|
+
const own = new Set(op.dialects.map((d) => d.name));
|
|
2482
|
+
const shipped = (await loadShippedDialects().catch(() => [])).filter((d) => !own.has(d.name));
|
|
2483
|
+
const all = [
|
|
2484
|
+
...shipped.map((d) => ({ d, dir: SHIPPED_DIALECTS_DIR })),
|
|
2485
|
+
...op.dialects.map((d) => ({ d, dir: join9(orgPackDir, "dialects") }))
|
|
2486
|
+
];
|
|
2487
|
+
const byName = new Map(all.map(({ d }) => [d.name, d]));
|
|
2488
|
+
const ok = [];
|
|
2489
|
+
for (const { d, dir } of all) {
|
|
2490
|
+
const failures = await runDialectTests(d, dir, byName).catch((e) => [
|
|
2491
|
+
{ test: "(load)", message: e.message }
|
|
2492
|
+
]);
|
|
2493
|
+
if (failures.length > 0) {
|
|
2494
|
+
for (const f of failures)
|
|
2495
|
+
problems.push({ source: `dialect ${d.name}`, message: `test "${f.test}": ${f.message}` });
|
|
2496
|
+
broken.add(d.name);
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
ok.push(d);
|
|
2500
|
+
}
|
|
2501
|
+
return ok;
|
|
2502
|
+
}
|
|
2503
|
+
async function usableFrameworks(op, problems) {
|
|
2504
|
+
const ok = [];
|
|
2505
|
+
for (const m of op.frameworks) {
|
|
2506
|
+
const failures = await runFrameworkTests(m).catch((e) => [
|
|
2507
|
+
{ test: "(load)", message: e.message }
|
|
2508
|
+
]);
|
|
2509
|
+
if (failures.length > 0) {
|
|
2510
|
+
for (const f of failures)
|
|
2511
|
+
problems.push({
|
|
2512
|
+
source: `framework ${m.name}`,
|
|
2513
|
+
message: `test "${f.test}": ${f.message}; the model is not used`
|
|
2514
|
+
});
|
|
2515
|
+
continue;
|
|
2516
|
+
}
|
|
2517
|
+
ok.push(m);
|
|
2518
|
+
}
|
|
2519
|
+
return ok;
|
|
2520
|
+
}
|
|
2521
|
+
async function modelsReadingNothing(x, models, cache, scannedAt) {
|
|
2522
|
+
const used = models.filter((m) => [...x.imports.modules.keys()].some((mod) => modelCovers(m, mod)));
|
|
2523
|
+
if (used.length === 0)
|
|
2524
|
+
return [];
|
|
2525
|
+
const read = async (with_) => {
|
|
2526
|
+
const s = await scanRepo({
|
|
2527
|
+
repo: x.handle,
|
|
2528
|
+
extractors: [...MODEL_READERS],
|
|
2529
|
+
dialects: x.dialects,
|
|
2530
|
+
scannedAt,
|
|
2531
|
+
dirty: x.dirty,
|
|
2532
|
+
...cache ? { cache } : {},
|
|
2533
|
+
frameworks: with_,
|
|
2534
|
+
repoInfo: x.repoInfo
|
|
2535
|
+
});
|
|
2536
|
+
return applyPipelineModels(s.records, with_).map((r) => JSON.stringify({ ...r, evidence: void 0 })).sort().join("\n");
|
|
2537
|
+
};
|
|
2538
|
+
const all = await read(models);
|
|
2539
|
+
const out = [];
|
|
2540
|
+
for (const m of used)
|
|
2541
|
+
if (await read(models.filter((o) => o !== m)) === all)
|
|
2542
|
+
out.push(m.name);
|
|
2543
|
+
return out;
|
|
2544
|
+
}
|
|
2545
|
+
async function scanEstate(o) {
|
|
2546
|
+
const pack = o.orgPack ? void 0 : await orgPackDirFor(o.root);
|
|
2547
|
+
const orgPackDir = o.orgPack ?? pack.dir;
|
|
2548
|
+
const stateDir = pack?.adopted ? join9(o.root, ".atticus") : o.root;
|
|
2549
|
+
const op = await loadOrgPack(orgPackDir);
|
|
2550
|
+
const problems = op.problems.map((p) => ({
|
|
2551
|
+
source: `org-pack/${p.file}`,
|
|
2552
|
+
message: `${p.path ? `${p.path}: ` : ""}${p.message}`
|
|
2553
|
+
}));
|
|
2554
|
+
const brokenDialects = /* @__PURE__ */ new Set();
|
|
2555
|
+
const dialects = await usableDialects(op, orgPackDir, problems, brokenDialects);
|
|
2556
|
+
const frameworks = await usableFrameworks(op, problems);
|
|
2557
|
+
const pluginSet = await usablePlugins(op);
|
|
2558
|
+
problems.push(...pluginSet.problems);
|
|
2559
|
+
const byName = new Map(dialects.map((d) => [d.name, d]));
|
|
2560
|
+
const dialectExtractors = dialects.map((d) => ({ d, x: dialectExtractor(d, byName) }));
|
|
2561
|
+
const cache = o.cache === null ? void 0 : o.cache ?? fsScanCache(join9(stateDir, ".index", "scan-cache"));
|
|
2562
|
+
const scannedAt = o.scannedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2563
|
+
const repos = productionRepos(op, o.repos);
|
|
2564
|
+
const results = [];
|
|
2565
|
+
const factNotes = prototypeScanNotes(op, o.repos);
|
|
2566
|
+
const factRepos = [];
|
|
2567
|
+
const structuredOf = /* @__PURE__ */ new Map();
|
|
2568
|
+
const modulesOf = /* @__PURE__ */ new Map();
|
|
2569
|
+
const readers = /* @__PURE__ */ new Map();
|
|
2570
|
+
let done = 0;
|
|
2571
|
+
for (const r of repos) {
|
|
2572
|
+
throwIfCancelled();
|
|
2573
|
+
const dir = o.repoDir?.(r.id) ?? inPlace(o.root, r.url) ?? baseClonePath(o.root, r.id);
|
|
2574
|
+
let state;
|
|
2575
|
+
if (!o.repoState && !await stat(dir).catch(() => void 0)) {
|
|
2576
|
+
problems.push({
|
|
2577
|
+
repo: r.id,
|
|
2578
|
+
source: "workspace",
|
|
2579
|
+
message: "isn't cloned here yet; run atticus workspace sync"
|
|
2580
|
+
});
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
try {
|
|
2584
|
+
state = o.repoState ? await o.repoState(r.id, dir) : await checkoutState(dir);
|
|
2585
|
+
} catch (e) {
|
|
2586
|
+
problems.push({
|
|
2587
|
+
repo: r.id,
|
|
2588
|
+
source: "workspace",
|
|
2589
|
+
message: `can't read the base clone (${e.message}); run atticus workspace sync`
|
|
2590
|
+
});
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
const extractors = [
|
|
2594
|
+
...BUILT_IN_EXTRACTORS,
|
|
2595
|
+
...o.extractors ?? [],
|
|
2596
|
+
...dialectExtractors.filter(({ d }) => r.dialects.includes(d.name) || d.appliesTo.repoTypes.includes(r.type) || // "*": any repo where the marker matches (ADR 0062).
|
|
2597
|
+
d.appliesTo.repoTypes.includes("*")).map(({ x }) => x)
|
|
2598
|
+
];
|
|
2599
|
+
const parsed = createParseCache();
|
|
2600
|
+
const files = fsRepo(r.id, dir, state.commit, { ignore: [...DEFAULT_IGNORES, ".atticus"] });
|
|
2601
|
+
const settingsFor = op.scanSettings.find((s) => s.repo === r.id);
|
|
2602
|
+
const settings = settingsFor ? await checkScanSettings(settingsFor, files) : NO_SETTINGS;
|
|
2603
|
+
for (const message of settings.problems)
|
|
2604
|
+
problems.push({ repo: r.id, source: `org-pack/scan/${r.id}.yaml`, message });
|
|
2605
|
+
const handle = withParseCache(withExcludes(files, settings.exclude), parsed);
|
|
2606
|
+
let scan;
|
|
2607
|
+
try {
|
|
2608
|
+
const modules = r.modules.length ? r.modules : (await detectModules(handle)).map((m) => ({ ...m, hosts: [] }));
|
|
2609
|
+
modulesOf.set(r.id, modules);
|
|
2610
|
+
const structured = [];
|
|
2611
|
+
for await (const f of handle.listFiles(["**/*"]))
|
|
2612
|
+
if (STRUCTURED.test(f) && !TOOLING.test(f))
|
|
2613
|
+
structured.push(f);
|
|
2614
|
+
structuredOf.set(r.id, structured.sort());
|
|
2615
|
+
const repoInfo = {
|
|
2616
|
+
...modules.length ? { modules: modules.map(({ path, component }) => ({ path, component })) } : {},
|
|
2617
|
+
...r.component ? { component: r.component } : {},
|
|
2618
|
+
...r.system ? { system: r.system } : {},
|
|
2619
|
+
repoType: r.type
|
|
2620
|
+
};
|
|
2621
|
+
scan = await scanRepo({
|
|
2622
|
+
repo: handle,
|
|
2623
|
+
extractors,
|
|
2624
|
+
dialects: r.dialects,
|
|
2625
|
+
scannedAt,
|
|
2626
|
+
dirty: state.dirty,
|
|
2627
|
+
...cache ? { cache } : {},
|
|
2628
|
+
configFor: (x) => {
|
|
2629
|
+
const d = dialectExtractors.find((de) => de.x === x)?.d;
|
|
2630
|
+
return d ? configHash({ dialect: JSON.stringify(d) }) : void 0;
|
|
2631
|
+
},
|
|
2632
|
+
frameworks,
|
|
2633
|
+
repoInfo
|
|
2634
|
+
});
|
|
2635
|
+
const here = pluginSet.plugins.filter((p) => pluginApplies(p.manifest, r));
|
|
2636
|
+
if (here.length > 0)
|
|
2637
|
+
scan = withPluginScan(scan, await scanRepo({
|
|
2638
|
+
repo: handle,
|
|
2639
|
+
extractors: here.map((p) => p.extractor),
|
|
2640
|
+
dialects: r.dialects,
|
|
2641
|
+
scannedAt,
|
|
2642
|
+
dirty: state.dirty,
|
|
2643
|
+
...cache ? { cache } : {},
|
|
2644
|
+
configFor: (x) => here.find((p) => p.extractor === x)?.config,
|
|
2645
|
+
frameworks,
|
|
2646
|
+
repoInfo
|
|
2647
|
+
}));
|
|
2648
|
+
readers.set(r.id, {
|
|
2649
|
+
handle,
|
|
2650
|
+
repoInfo,
|
|
2651
|
+
dialects: r.dialects,
|
|
2652
|
+
dirty: state.dirty,
|
|
2653
|
+
imports: await repoImports(handle)
|
|
2654
|
+
});
|
|
2655
|
+
} finally {
|
|
2656
|
+
parsed.dispose();
|
|
2657
|
+
}
|
|
2658
|
+
for (const run of scan.runs.filter((x) => x.status === "failed"))
|
|
2659
|
+
problems.push({ repo: r.id, source: run.extractor, message: run.error ?? "failed" });
|
|
2660
|
+
for (const rej of scan.rejected)
|
|
2661
|
+
problems.push({
|
|
2662
|
+
repo: r.id,
|
|
2663
|
+
source: rej.extractor,
|
|
2664
|
+
message: `invalid record: ${rej.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`
|
|
2665
|
+
});
|
|
2666
|
+
const readerRecords = applyClients(applyPrefixes(scan.records, settings.prefixes), settings.clients);
|
|
2667
|
+
const fromFacts = await factsForScan(o.root, op, r.id, state.commit, readerRecords, {
|
|
2668
|
+
dir,
|
|
2669
|
+
...o.git ? { git: o.git } : {}
|
|
2670
|
+
});
|
|
2671
|
+
if (fromFacts.note)
|
|
2672
|
+
factNotes.push(fromFacts.note);
|
|
2673
|
+
if (fromFacts.facts)
|
|
2674
|
+
factRepos.push({ repo: r.id, facts: fromFacts.facts, targets: fromFacts.targets ?? {} });
|
|
2675
|
+
results.push({ ...scan, records: fromFacts.records ?? readerRecords });
|
|
2676
|
+
o.onProgress?.(++done, repos.length, r.id);
|
|
2677
|
+
}
|
|
2678
|
+
const mergeInput = (records) => estateMergeInput(op, repos, records, (id) => modulesOf.get(id) ?? []);
|
|
2679
|
+
let codeRecords = applyPipelineModels(results.flatMap((s) => s.records), frameworks);
|
|
2680
|
+
if (factRepos.length) {
|
|
2681
|
+
const joinRepos = joinReposOf(op, factRepos);
|
|
2682
|
+
const join10 = joinFacts({
|
|
2683
|
+
repos: joinRepos,
|
|
2684
|
+
targets: Object.fromEntries(factRepos.map((x) => [x.repo, x.targets])),
|
|
2685
|
+
outside: outsideSystems(op)
|
|
2686
|
+
});
|
|
2687
|
+
codeRecords = applyFactJoins(codeRecords, join10, joinRepos, {
|
|
2688
|
+
callers: joinCallers({ repos: joinRepos, join: join10, declared: declaredCallsOf(op.profiles) }),
|
|
2689
|
+
join: join10
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
let merged = mergeCatalog(mergeInput(codeRecords));
|
|
2693
|
+
const curatedDir = o.curatedDir ?? curatedDirOf(o.root, op);
|
|
2694
|
+
const curated = await readCatalog(curatedDir, { cacheRoot: o.root });
|
|
2695
|
+
for (const p of curated.problems)
|
|
2696
|
+
problems.push({ source: `catalog/curated/${p.file}`, message: p.message });
|
|
2697
|
+
let inputs;
|
|
2698
|
+
if (o.inputsDir !== null) {
|
|
2699
|
+
inputs = await scanInputs({
|
|
2700
|
+
root: o.root,
|
|
2701
|
+
orgPack: op,
|
|
2702
|
+
inputsDir: o.inputsDir ?? join9(stateDir, "inputs"),
|
|
2703
|
+
records: [...merged.records, ...curated.records],
|
|
2704
|
+
ingestors: listIngestors(),
|
|
2705
|
+
scannedAt
|
|
2706
|
+
});
|
|
2707
|
+
for (const p of inputs.problems)
|
|
2708
|
+
problems.push({ source: "intake", message: `${p.path ? `${p.path}: ` : ""}${p.message}` });
|
|
2709
|
+
if (inputs.records.length > 0)
|
|
2710
|
+
merged = mergeCatalog(mergeInput([...codeRecords, ...inputs.records]));
|
|
2711
|
+
}
|
|
2712
|
+
const found = await featuresForScan({
|
|
2713
|
+
records: merged.records,
|
|
2714
|
+
decisions: curated.records,
|
|
2715
|
+
proposalsDir: join9(dirname3(curatedDir), "proposals")
|
|
2716
|
+
});
|
|
2717
|
+
for (const p of found.problems)
|
|
2718
|
+
problems.push({ source: "features", message: `${p.decision}: ${p.message}` });
|
|
2719
|
+
const fedSources = o.federated ?? await federationSources(o.root, stateDir, op);
|
|
2720
|
+
let federated;
|
|
2721
|
+
let fedRecords = [];
|
|
2722
|
+
if (fedSources.length) {
|
|
2723
|
+
const fed = await federate([...merged.records, ...found.features], fedSources, problems, o.root);
|
|
2724
|
+
const placed = resolveFederatedLooseEnds({
|
|
2725
|
+
local: merged.records,
|
|
2726
|
+
raw: [...codeRecords, ...inputs?.records ?? []],
|
|
2727
|
+
federated: fed.records,
|
|
2728
|
+
merge: (records) => mergeCatalog(mergeInput(records))
|
|
2729
|
+
});
|
|
2730
|
+
fedRecords = placed.federated;
|
|
2731
|
+
federated = fed.contributions.map((c) => placed.resolved.get(c.id) ? { ...c, resolved: placed.resolved.get(c.id) } : c);
|
|
2732
|
+
}
|
|
2733
|
+
const glossary = await glossaryForScan({
|
|
2734
|
+
root: o.root,
|
|
2735
|
+
orgPack: op,
|
|
2736
|
+
records: [...merged.records, ...found.features],
|
|
2737
|
+
decisions: curated.records,
|
|
2738
|
+
proposalsDir: join9(dirname3(curatedDir), "proposals"),
|
|
2739
|
+
orgPackDir
|
|
2740
|
+
});
|
|
2741
|
+
const notes = [...factNotes, ...merged.notes, ...glossary.problems.map((p) => `glossary: ${p}`)];
|
|
2742
|
+
const withFeatures = [...glossary.records, ...fedRecords].sort((a, b) => a.kind === b.kind ? a.id.localeCompare(b.id) : a.kind.localeCompare(b.kind));
|
|
2743
|
+
const applied = applyCurated(withFeatures, curated.records);
|
|
2744
|
+
for (const p of applied.problems)
|
|
2745
|
+
problems.push({ source: "curated", message: JSON.stringify(p) });
|
|
2746
|
+
merged = { ...merged, records: applied.records };
|
|
2747
|
+
for (const i of merged.invalid)
|
|
2748
|
+
problems.push({
|
|
2749
|
+
source: "merge",
|
|
2750
|
+
message: `invalid record: ${i.issues.map((x) => `${x.path}: ${x.message}`).join("; ")}`
|
|
2751
|
+
});
|
|
2752
|
+
const runs = results.flatMap((s) => s.runs);
|
|
2753
|
+
const unmodelled = unmodelledFrameworks([...readers.values()].map((x) => x.imports), op.frameworks);
|
|
2754
|
+
const idleModels = /* @__PURE__ */ new Map();
|
|
2755
|
+
for (const [id, x] of readers)
|
|
2756
|
+
idleModels.set(id, await modelsReadingNothing(x, frameworks, cache, scannedAt));
|
|
2757
|
+
const coverage = repos.filter((r) => results.some((s) => s.repo === r.id)).map((r) => {
|
|
2758
|
+
const own = /* @__PURE__ */ new Set([
|
|
2759
|
+
r.component ?? r.id,
|
|
2760
|
+
...merged.records.filter((x) => x.kind === "Component" && x.repo === r.id).map((x) => x.id)
|
|
2761
|
+
]);
|
|
2762
|
+
const scan = results.find((s) => s.repo === r.id);
|
|
2763
|
+
const cited = new Set(scan.records.flatMap((x) => x.evidence.map((e) => e.path ?? "")));
|
|
2764
|
+
const unreadConfig = (structuredOf.get(r.id) ?? []).filter((f) => !cited.has(f));
|
|
2765
|
+
const readSomething = scan.records.some((x) => x.kind !== "Component");
|
|
2766
|
+
return {
|
|
2767
|
+
repo: r.id,
|
|
2768
|
+
joined: merged.records.filter((x) => x.kind === "Interface" && own.has(String(x.from))).length,
|
|
2769
|
+
open: merged.records.filter((x) => x.kind === "Unresolved" && own.has(String(x.owner))).length,
|
|
2770
|
+
unreadFiles: new Set(scan.warnings.filter((w) => /but no call to it was read here/.test(w.message)).map((w) => w.path)).size,
|
|
2771
|
+
unreadConfig,
|
|
2772
|
+
needsOnboarding: !readSomething && unreadConfig.length > 0,
|
|
2773
|
+
redraft: r.dialects.flatMap((name) => {
|
|
2774
|
+
if (brokenDialects.has(name))
|
|
2775
|
+
return [{ dialect: name, why: "tests-fail" }];
|
|
2776
|
+
const run = scan.runs.find((x) => x.extractor === `dialect-${name}`);
|
|
2777
|
+
return run && run.status !== "failed" && run.records === 0 ? [{ dialect: name, why: "reads-nothing" }] : [];
|
|
2778
|
+
}),
|
|
2779
|
+
redraftFrameworks: (idleModels.get(r.id) ?? []).map((framework) => ({
|
|
2780
|
+
framework,
|
|
2781
|
+
why: "reads-nothing"
|
|
2782
|
+
}))
|
|
2783
|
+
};
|
|
2784
|
+
});
|
|
2785
|
+
return {
|
|
2786
|
+
coverage,
|
|
2787
|
+
unmodelled,
|
|
2788
|
+
records: merged.records,
|
|
2789
|
+
repos: results,
|
|
2790
|
+
problems,
|
|
2791
|
+
stats: {
|
|
2792
|
+
repos: results.length,
|
|
2793
|
+
cachedRuns: runs.filter((r) => r.status === "cached").length,
|
|
2794
|
+
ranRuns: runs.filter((r) => r.status === "ran").length,
|
|
2795
|
+
records: merged.records.length,
|
|
2796
|
+
...merged.stats,
|
|
2797
|
+
// A file several extractors left out is one warning.
|
|
2798
|
+
warnings: new Set(results.flatMap((s) => s.warnings.map((w) => JSON.stringify([s.repo, w.path, w.message])))).size
|
|
2799
|
+
},
|
|
2800
|
+
...inputs ? { inputs } : {},
|
|
2801
|
+
curated: { applied: applied.applied, suppressed: applied.suppressed },
|
|
2802
|
+
notes,
|
|
2803
|
+
...federated ? { federated } : {}
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// packages/scanner/dist/repo-types.js
|
|
2808
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
2809
|
+
var CLEAR_MATCH = 0.7;
|
|
2810
|
+
var PARTIAL_MATCH = 0.4;
|
|
2811
|
+
var TIE_MARGIN = 0.1;
|
|
2812
|
+
var MAX_FILES_PER_SIGNAL = 2e3;
|
|
2813
|
+
var MAX_LINE = 2e3;
|
|
2814
|
+
var MAX_EVIDENCE = 3;
|
|
2815
|
+
var SKIP = /(^|\/)(node_modules|\.git|target|bin|obj|dist|build|\.atticus|vendor)\//;
|
|
2816
|
+
var RepoFiles = class {
|
|
2817
|
+
repo;
|
|
2818
|
+
files;
|
|
2819
|
+
lines = /* @__PURE__ */ new Map();
|
|
2820
|
+
constructor(repo) {
|
|
2821
|
+
this.repo = repo;
|
|
2822
|
+
}
|
|
2823
|
+
async all() {
|
|
2824
|
+
if (!this.files) {
|
|
2825
|
+
const out = [];
|
|
2826
|
+
for await (const f of this.repo.listFiles(["**/*"]))
|
|
2827
|
+
if (!SKIP.test(f))
|
|
2828
|
+
out.push(f);
|
|
2829
|
+
this.files = out.sort();
|
|
2830
|
+
}
|
|
2831
|
+
return this.files;
|
|
2832
|
+
}
|
|
2833
|
+
async matching(globs) {
|
|
2834
|
+
const res = globs.map(globToRegExp);
|
|
2835
|
+
return (await this.all()).filter((f) => res.some((re) => re.test(f)));
|
|
2836
|
+
}
|
|
2837
|
+
async linesOf(path) {
|
|
2838
|
+
if (!this.lines.has(path)) {
|
|
2839
|
+
const text = await this.repo.readText(path).catch(() => void 0);
|
|
2840
|
+
this.lines.set(path, text === void 0 ? null : text.replace(/\r\n?/g, "\n").split("\n"));
|
|
2841
|
+
}
|
|
2842
|
+
return this.lines.get(path);
|
|
2843
|
+
}
|
|
2844
|
+
/** The first lines (path:line) in files matching `globs` that pass `test`, up to `max`. */
|
|
2845
|
+
async lineHits(globs, test, max = MAX_EVIDENCE) {
|
|
2846
|
+
const hits = [];
|
|
2847
|
+
for (const f of (await this.matching(globs)).slice(0, MAX_FILES_PER_SIGNAL)) {
|
|
2848
|
+
const lines = await this.linesOf(f);
|
|
2849
|
+
if (!lines)
|
|
2850
|
+
continue;
|
|
2851
|
+
const i = lines.findIndex((l) => l.length <= MAX_LINE && test(l));
|
|
2852
|
+
if (i >= 0)
|
|
2853
|
+
hits.push(`${f}:${i + 1}`);
|
|
2854
|
+
if (hits.length >= max)
|
|
2855
|
+
break;
|
|
2856
|
+
}
|
|
2857
|
+
return hits;
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2860
|
+
var words = (line) => line.split(/[^A-Za-z0-9_.@/-]+/).filter(Boolean);
|
|
2861
|
+
var nameTest = (names) => {
|
|
2862
|
+
const tests = names.map((n) => n.endsWith("*") ? (w) => w.startsWith(n.slice(0, -1)) : (w) => w === n);
|
|
2863
|
+
return (line) => words(line).some((w) => tests.some((t) => t(w)));
|
|
2864
|
+
};
|
|
2865
|
+
function label(s) {
|
|
2866
|
+
switch (s.kind) {
|
|
2867
|
+
case "build-file":
|
|
2868
|
+
return `build file ${s.globs.join(" or ")}`;
|
|
2869
|
+
case "dependency":
|
|
2870
|
+
return `dependency ${s.names.join(" or ")}`;
|
|
2871
|
+
case "marker":
|
|
2872
|
+
return `marker /${s.pattern}/ in ${s.in.join(", ")}`;
|
|
2873
|
+
case "layout":
|
|
2874
|
+
return `files in ${s.globs.join(" or ")}`;
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
async function evidenceFor(s, files) {
|
|
2878
|
+
switch (s.kind) {
|
|
2879
|
+
case "build-file":
|
|
2880
|
+
case "layout":
|
|
2881
|
+
return (await files.matching(s.globs)).slice(0, MAX_EVIDENCE);
|
|
2882
|
+
case "dependency":
|
|
2883
|
+
return files.lineHits(s.in, nameTest(s.names));
|
|
2884
|
+
case "marker": {
|
|
2885
|
+
const re = new RegExp(s.pattern);
|
|
2886
|
+
return files.lineHits(s.in, (l) => re.test(l));
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
async function scoreType(t, files) {
|
|
2891
|
+
const signals = [];
|
|
2892
|
+
for (const s of t.fingerprint) {
|
|
2893
|
+
const evidence = await evidenceFor(s, files);
|
|
2894
|
+
signals.push({
|
|
2895
|
+
kind: s.kind,
|
|
2896
|
+
label: label(s),
|
|
2897
|
+
weight: s.weight,
|
|
2898
|
+
required: s.required,
|
|
2899
|
+
found: evidence.length > 0,
|
|
2900
|
+
evidence
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
const total = signals.reduce((n, s) => n + s.weight, 0);
|
|
2904
|
+
const got = signals.filter((s) => s.found).reduce((n, s) => n + s.weight, 0);
|
|
2905
|
+
return {
|
|
2906
|
+
type: t.id,
|
|
2907
|
+
name: t.name,
|
|
2908
|
+
score: Math.round(got * 100 / total) / 100,
|
|
2909
|
+
got,
|
|
2910
|
+
total,
|
|
2911
|
+
ruledOut: signals.some((s) => s.required && !s.found),
|
|
2912
|
+
signals
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
function typeFromWords(description, types) {
|
|
2916
|
+
const text = (description ?? "").toLowerCase();
|
|
2917
|
+
if (!text)
|
|
2918
|
+
return void 0;
|
|
2919
|
+
const hits = [];
|
|
2920
|
+
for (const t of types)
|
|
2921
|
+
for (const words2 of [t.name.toLowerCase(), t.id.replace(/[-_.]+/g, " ")])
|
|
2922
|
+
if (words2.length > 2 && new RegExp(`(^|[^a-z0-9])${escapeRe(words2)}([^a-z0-9]|$)`).test(text))
|
|
2923
|
+
hits.push({ type: t.id, words: words2 });
|
|
2924
|
+
const ids = new Set(hits.map((h) => h.type));
|
|
2925
|
+
if (ids.size !== 1)
|
|
2926
|
+
return void 0;
|
|
2927
|
+
return [...hits].sort((a, b) => b.words.length - a.words.length)[0];
|
|
2928
|
+
}
|
|
2929
|
+
async function matchRepoType(repo, types, o = {}) {
|
|
2930
|
+
const files = new RepoFiles(repo);
|
|
2931
|
+
const matches = [];
|
|
2932
|
+
for (const t of types)
|
|
2933
|
+
matches.push(await scoreType(t, files));
|
|
2934
|
+
const pct = (m) => Math.round(m.score * 100);
|
|
2935
|
+
matches.sort((a, b) => Number(a.ruledOut) - Number(b.ruledOut) || pct(b) - pct(a) || a.type.localeCompare(b.type));
|
|
2936
|
+
const said = typeFromWords(o.description, types);
|
|
2937
|
+
const eligible = matches.filter((m) => !m.ruledOut && pct(m) >= PARTIAL_MATCH * 100);
|
|
2938
|
+
const top2 = eligible[0];
|
|
2939
|
+
if (!top2)
|
|
2940
|
+
return { verdict: "none", matches, ...said ? { fromDescription: said } : {} };
|
|
2941
|
+
const tied = eligible.filter((m) => pct(top2) - pct(m) < TIE_MARGIN * 100);
|
|
2942
|
+
if (tied.length > 1) {
|
|
2943
|
+
const ids = tied.map((m) => m.type);
|
|
2944
|
+
return {
|
|
2945
|
+
verdict: "tie",
|
|
2946
|
+
tied: ids,
|
|
2947
|
+
matches,
|
|
2948
|
+
// Only a tie among the types that actually tie is leaned: the description never introduces
|
|
2949
|
+
// a type the fingerprints ruled out or never reached.
|
|
2950
|
+
...said && ids.includes(said.type) ? { fromDescription: said } : {}
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
return {
|
|
2954
|
+
verdict: pct(top2) >= CLEAR_MATCH * 100 ? "clear" : "partial",
|
|
2955
|
+
best: top2.type,
|
|
2956
|
+
matches
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2959
|
+
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
2960
|
+
async function readRule(rule, path, files) {
|
|
2961
|
+
const lines = await files.linesOf(path);
|
|
2962
|
+
if (!lines)
|
|
2963
|
+
return void 0;
|
|
2964
|
+
if (rule.pattern !== void 0) {
|
|
2965
|
+
const re2 = new RegExp(rule.pattern);
|
|
2966
|
+
for (const [i, l] of lines.entries()) {
|
|
2967
|
+
if (l.length > MAX_LINE)
|
|
2968
|
+
continue;
|
|
2969
|
+
const m = re2.exec(l);
|
|
2970
|
+
if (m)
|
|
2971
|
+
return { value: (m[1] ?? m[0]).trim(), line: i + 1 };
|
|
2972
|
+
}
|
|
2973
|
+
return void 0;
|
|
2974
|
+
}
|
|
2975
|
+
const key = rule.key;
|
|
2976
|
+
if (/\.ya?ml$/i.test(path)) {
|
|
2977
|
+
const counter = new import_yaml3.LineCounter();
|
|
2978
|
+
const text = lines.join("\n");
|
|
2979
|
+
for (const doc of (0, import_yaml3.parseAllDocuments)(text, { lineCounter: counter })) {
|
|
2980
|
+
if (!("getIn" in doc))
|
|
2981
|
+
continue;
|
|
2982
|
+
for (const node of [doc.getIn(key.split("."), true), doc.get(key, true)]) {
|
|
2983
|
+
if ((0, import_yaml3.isScalar)(node) && node.range && node.value !== null && node.value !== void 0)
|
|
2984
|
+
return { value: String(node.value), line: counter.linePos(node.range[0]).line };
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
return void 0;
|
|
2988
|
+
}
|
|
2989
|
+
const re = new RegExp(`^\\s*${escapeRe(key)}\\s*[=:]\\s*(.*?)\\s*$`);
|
|
2990
|
+
for (const [i, l] of lines.entries()) {
|
|
2991
|
+
const m = re.exec(l);
|
|
2992
|
+
if (m)
|
|
2993
|
+
return { value: m[1], line: i + 1 };
|
|
2994
|
+
}
|
|
2995
|
+
return void 0;
|
|
2996
|
+
}
|
|
2997
|
+
function asPrefix(value) {
|
|
2998
|
+
if (/[${}]/.test(value))
|
|
2999
|
+
return void 0;
|
|
3000
|
+
const p = `/${value.trim().replace(/^\/+/, "").replace(/\/+$/, "")}`;
|
|
3001
|
+
return p !== "/" && /^\/[^\s?#]*$/.test(p) ? p : void 0;
|
|
3002
|
+
}
|
|
3003
|
+
async function resolveSettingsTemplate(t, repo, repoId) {
|
|
3004
|
+
const files = new RepoFiles(repo);
|
|
3005
|
+
const out = {
|
|
3006
|
+
schemaVersion: SCAN_SETTINGS_SCHEMA_VERSION,
|
|
3007
|
+
repo: repoId,
|
|
3008
|
+
exclude: [],
|
|
3009
|
+
prefixes: [],
|
|
3010
|
+
clients: []
|
|
3011
|
+
};
|
|
3012
|
+
for (const e of t.settings.exclude) {
|
|
3013
|
+
const first = (await files.matching([e.path]))[0];
|
|
3014
|
+
if (first)
|
|
3015
|
+
out.exclude.push({ path: e.path, why: e.why, at: `${first}:1` });
|
|
3016
|
+
}
|
|
3017
|
+
for (const rule of t.settings.prefixes) {
|
|
3018
|
+
const candidates = (await files.matching(rule.files)).sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
3019
|
+
for (const f of candidates) {
|
|
3020
|
+
const hit = await readRule(rule, f, files);
|
|
3021
|
+
if (!hit)
|
|
3022
|
+
continue;
|
|
3023
|
+
const prefix = asPrefix(hit.value);
|
|
3024
|
+
const line = (await files.linesOf(f))?.[hit.line - 1] ?? "";
|
|
3025
|
+
if (prefix && hit.value && line.includes(hit.value))
|
|
3026
|
+
out.prefixes.push({
|
|
3027
|
+
paths: rule.paths,
|
|
3028
|
+
prefix,
|
|
3029
|
+
why: rule.why,
|
|
3030
|
+
at: `${f}:${hit.line}`,
|
|
3031
|
+
value: hit.value
|
|
3032
|
+
});
|
|
3033
|
+
break;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
return out;
|
|
3037
|
+
}
|
|
3038
|
+
var BUILD_FILES = [
|
|
3039
|
+
["pom.xml", "pom.xml"],
|
|
3040
|
+
["build.gradle", "build.gradle"],
|
|
3041
|
+
["build.gradle.kts", "build.gradle.kts"],
|
|
3042
|
+
["package.json", "package.json"],
|
|
3043
|
+
["*.csproj", "*.csproj"],
|
|
3044
|
+
["pyproject.toml", "pyproject.toml"],
|
|
3045
|
+
["requirements.txt", "requirements.txt"],
|
|
3046
|
+
["go.mod", "go.mod"],
|
|
3047
|
+
["Cargo.toml", "Cargo.toml"],
|
|
3048
|
+
["Gemfile", "Gemfile"],
|
|
3049
|
+
["composer.json", "composer.json"]
|
|
3050
|
+
].map(([name, glob]) => ({ name, glob, re: globToRegExp(`**/${glob}`) }));
|
|
3051
|
+
var PACKAGE_NAME = /^[A-Za-z0-9_.@/-]+$/;
|
|
3052
|
+
function declaredDependencies(path, text) {
|
|
3053
|
+
const out = [];
|
|
3054
|
+
const all = (re, g = 1) => {
|
|
3055
|
+
for (const m of text.matchAll(re))
|
|
3056
|
+
if (m[g])
|
|
3057
|
+
out.push(m[g].trim());
|
|
3058
|
+
};
|
|
3059
|
+
if (path.endsWith("pom.xml")) {
|
|
3060
|
+
for (const block2 of text.matchAll(/<(parent|dependency|plugin)>([\s\S]*?)<\/\1>/g)) {
|
|
3061
|
+
const a = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(block2[2]);
|
|
3062
|
+
if (a)
|
|
3063
|
+
out.push(a[1]);
|
|
3064
|
+
}
|
|
3065
|
+
} else if (/build\.gradle(\.kts)?$/.test(path)) {
|
|
3066
|
+
all(/['"]([\w.-]+):([\w.-]+)(?::[^'"]*)?['"]/g, 2);
|
|
3067
|
+
} else if (path.endsWith("package.json")) {
|
|
3068
|
+
try {
|
|
3069
|
+
const j = JSON.parse(text);
|
|
3070
|
+
const deps = Object.keys(j.dependencies ?? {});
|
|
3071
|
+
out.push(...deps.length ? deps : Object.keys(j.devDependencies ?? {}));
|
|
3072
|
+
} catch {
|
|
3073
|
+
}
|
|
3074
|
+
} else if (path.endsWith(".csproj")) {
|
|
3075
|
+
all(/<PackageReference\s+Include="([^"]+)"/g);
|
|
3076
|
+
} else if (path.endsWith("requirements.txt")) {
|
|
3077
|
+
all(/^\s*([A-Za-z0-9_.-]+)/gm);
|
|
3078
|
+
} else if (path.endsWith("go.mod")) {
|
|
3079
|
+
all(/^\s*(?:require\s+)?([\w.-]+\.[\w.-]+\/[\w./-]+)\s+v/gm);
|
|
3080
|
+
}
|
|
3081
|
+
return out;
|
|
3082
|
+
}
|
|
3083
|
+
var PLAIN_ANNOTATIONS = /* @__PURE__ */ new Set([
|
|
3084
|
+
"Override",
|
|
3085
|
+
"Deprecated",
|
|
3086
|
+
"SuppressWarnings",
|
|
3087
|
+
"FunctionalInterface",
|
|
3088
|
+
"SafeVarargs",
|
|
3089
|
+
"Test",
|
|
3090
|
+
"JvmStatic"
|
|
3091
|
+
]);
|
|
3092
|
+
function top(counts, n) {
|
|
3093
|
+
return [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, n).map(([k]) => k);
|
|
3094
|
+
}
|
|
3095
|
+
var TEST_FILE = /(^|\/)(test|tests|__tests__|src\/test)\/|\.(spec|test)\.[jt]sx?$|Tests?\.(java|kt|cs)$/;
|
|
3096
|
+
async function draftMarkers(files) {
|
|
3097
|
+
const out = [];
|
|
3098
|
+
const source = async (globs) => (await files.matching(globs)).filter((f) => !TEST_FILE.test(f)).slice(0, MAX_FILES_PER_SIGNAL);
|
|
3099
|
+
const marker = (pattern, globs) => ({
|
|
3100
|
+
kind: "marker",
|
|
3101
|
+
pattern,
|
|
3102
|
+
in: globs,
|
|
3103
|
+
weight: 2,
|
|
3104
|
+
required: false
|
|
3105
|
+
});
|
|
3106
|
+
for (const [globs, re] of [
|
|
3107
|
+
[["**/*.java"], /^\s*@([A-Z][A-Za-z0-9_]*)/],
|
|
3108
|
+
[["**/*.kt"], /^\s*@([A-Z][A-Za-z0-9_]*)/],
|
|
3109
|
+
[["**/*.cs"], /^\s*\[([A-Z][A-Za-z0-9_]*)[(\]]/]
|
|
3110
|
+
]) {
|
|
3111
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
3112
|
+
for (const f of await source(globs)) {
|
|
3113
|
+
const lines = await files.linesOf(f) ?? [];
|
|
3114
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3115
|
+
lines.forEach((l, i) => {
|
|
3116
|
+
const m = re.exec(l);
|
|
3117
|
+
if (!m || PLAIN_ANNOTATIONS.has(m[1]))
|
|
3118
|
+
return;
|
|
3119
|
+
const next = lines.slice(i + 1, i + 20).find((x) => x.trim() !== "" && !re.test(x)) ?? "";
|
|
3120
|
+
if (/\b(class|interface|record|enum|object)\b/.test(next))
|
|
3121
|
+
seen.add(m[1]);
|
|
3122
|
+
});
|
|
3123
|
+
for (const s of seen)
|
|
3124
|
+
counts2.set(s, (counts2.get(s) ?? 0) + 1);
|
|
3125
|
+
}
|
|
3126
|
+
for (const name of top(counts2, 3))
|
|
3127
|
+
out.push(marker(globs[0] === "**/*.cs" ? `\\[${name}\\b` : `@${name}\\b`, [...globs]));
|
|
3128
|
+
}
|
|
3129
|
+
const js = ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"];
|
|
3130
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3131
|
+
for (const f of await source(js)) {
|
|
3132
|
+
if (f.endsWith(".d.ts"))
|
|
3133
|
+
continue;
|
|
3134
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3135
|
+
for (const l of await files.linesOf(f) ?? []) {
|
|
3136
|
+
const m = /\bfrom\s+["']([^"'./][^"']*)["']/.exec(l);
|
|
3137
|
+
if (!m)
|
|
3138
|
+
continue;
|
|
3139
|
+
const parts = m[1].split("/");
|
|
3140
|
+
const pkg = m[1].startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
3141
|
+
if (!pkg.startsWith("node:"))
|
|
3142
|
+
seen.add(pkg);
|
|
3143
|
+
}
|
|
3144
|
+
for (const s of seen)
|
|
3145
|
+
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
3146
|
+
}
|
|
3147
|
+
for (const pkg of top(counts, 3))
|
|
3148
|
+
out.push(marker(`from ["']${escapeRe(pkg)}(/[^"']*)?["']`, js));
|
|
3149
|
+
return out;
|
|
3150
|
+
}
|
|
3151
|
+
var LAYOUTS = [
|
|
3152
|
+
"src/main/java/**",
|
|
3153
|
+
"src/main/kotlin/**",
|
|
3154
|
+
"src/main/resources/**",
|
|
3155
|
+
"src/app/**",
|
|
3156
|
+
"app/**",
|
|
3157
|
+
"src/**",
|
|
3158
|
+
"lib/**",
|
|
3159
|
+
"cmd/**",
|
|
3160
|
+
"internal/**",
|
|
3161
|
+
"public/**"
|
|
3162
|
+
];
|
|
3163
|
+
async function draftRepoType(repo, id) {
|
|
3164
|
+
const files = new RepoFiles(repo);
|
|
3165
|
+
const all = await files.all();
|
|
3166
|
+
const fingerprint = [];
|
|
3167
|
+
const builds = all.filter((f) => BUILD_FILES.some((b) => b.re.test(f))).slice(0, 20);
|
|
3168
|
+
const buildGlobs = [];
|
|
3169
|
+
for (const b of BUILD_FILES) {
|
|
3170
|
+
const hits = builds.filter((f) => b.re.test(f));
|
|
3171
|
+
if (hits.length === 0)
|
|
3172
|
+
continue;
|
|
3173
|
+
buildGlobs.push(hits.some((f) => !f.includes("/")) ? b.glob : `**/${b.glob}`);
|
|
3174
|
+
}
|
|
3175
|
+
if (buildGlobs.length)
|
|
3176
|
+
fingerprint.push({ kind: "build-file", globs: buildGlobs, weight: 2, required: true });
|
|
3177
|
+
const deps = /* @__PURE__ */ new Map();
|
|
3178
|
+
for (const f of builds) {
|
|
3179
|
+
const text = (await files.linesOf(f) ?? []).join("\n");
|
|
3180
|
+
const glob = buildGlobs.find((g) => globToRegExp(g).test(f));
|
|
3181
|
+
for (const d of declaredDependencies(f, text)) {
|
|
3182
|
+
if (!PACKAGE_NAME.test(d) || d === repo.name)
|
|
3183
|
+
continue;
|
|
3184
|
+
if (!deps.has(d))
|
|
3185
|
+
deps.set(d, /* @__PURE__ */ new Set());
|
|
3186
|
+
if (glob)
|
|
3187
|
+
deps.get(d).add(glob);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
for (const [name, globs] of [...deps].slice(0, 6))
|
|
3191
|
+
fingerprint.push({
|
|
3192
|
+
kind: "dependency",
|
|
3193
|
+
names: [name],
|
|
3194
|
+
in: globs.size ? [...globs] : [...buildGlobs],
|
|
3195
|
+
weight: 2,
|
|
3196
|
+
required: false
|
|
3197
|
+
});
|
|
3198
|
+
fingerprint.push(...await draftMarkers(files));
|
|
3199
|
+
const layouts = [];
|
|
3200
|
+
for (const g of LAYOUTS) {
|
|
3201
|
+
if (layouts.length >= 3)
|
|
3202
|
+
break;
|
|
3203
|
+
if (g === "src/**" && layouts.some((l) => l.startsWith("src/")))
|
|
3204
|
+
continue;
|
|
3205
|
+
if ((await files.matching([g])).length)
|
|
3206
|
+
layouts.push(g);
|
|
3207
|
+
}
|
|
3208
|
+
for (const g of layouts)
|
|
3209
|
+
fingerprint.push({ kind: "layout", globs: [g], weight: 1, required: false });
|
|
3210
|
+
if (fingerprint.length === 0)
|
|
3211
|
+
return void 0;
|
|
3212
|
+
const prefixes = [];
|
|
3213
|
+
for (const c of PREFIX_CONVENTIONS) {
|
|
3214
|
+
const seen = c.lineIncludes ? (await files.lineHits(c.files, (l) => l.includes(c.lineIncludes), 1)).length > 0 : (await files.matching(c.files)).length > 0;
|
|
3215
|
+
if (!seen)
|
|
3216
|
+
continue;
|
|
3217
|
+
const src = c.paths.filter((g) => layouts.includes(g));
|
|
3218
|
+
prefixes.push({
|
|
3219
|
+
...c.rule.key ? { key: c.rule.key } : {},
|
|
3220
|
+
...c.rule.pattern ? { pattern: c.rule.pattern } : {},
|
|
3221
|
+
files: c.files,
|
|
3222
|
+
paths: src.length ? src : ["**"],
|
|
3223
|
+
why: c.rule.why
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
return {
|
|
3227
|
+
schemaVersion: REPO_TYPE_SCHEMA_VERSION,
|
|
3228
|
+
id,
|
|
3229
|
+
name: id,
|
|
3230
|
+
description: `Drafted from ${repo.name} by atticus repo-type new. Say what kind of repo this is, and keep only the signals every repo of the kind has.`,
|
|
3231
|
+
fingerprint,
|
|
3232
|
+
settings: { exclude: [], prefixes },
|
|
3233
|
+
skills: []
|
|
3234
|
+
};
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
// packages/scanner/dist/spec-check.js
|
|
3238
|
+
var at = (e) => e?.path ? `${e.path}${e.line !== void 0 ? `:${e.line}` : ""}` : void 0;
|
|
3239
|
+
function repoComponents(records, repoId) {
|
|
3240
|
+
return new Set(records.filter((r) => r.kind === "Component" && (r.repo === repoId || r.evidence.some((e) => e.repo === repoId))).map((r) => r.id));
|
|
3241
|
+
}
|
|
3242
|
+
var pathSegs = (p) => normalizePath(p).toLowerCase().split("/").filter(Boolean);
|
|
3243
|
+
function pathScore(called, served) {
|
|
3244
|
+
if (!called.includes("/") || !served.includes("/"))
|
|
3245
|
+
return called.trim().toLowerCase() === served.trim().toLowerCase() ? 1e3 : 0;
|
|
3246
|
+
const a = pathSegs(called);
|
|
3247
|
+
const b = pathSegs(served);
|
|
3248
|
+
if (a.join("/") === b.join("/"))
|
|
3249
|
+
return 1e3;
|
|
3250
|
+
const [short, long] = a.length <= b.length ? [a, b] : [b, a];
|
|
3251
|
+
if (short.length === 0 || !short.some((s) => s !== "{}"))
|
|
3252
|
+
return 0;
|
|
3253
|
+
const tail = long.slice(long.length - short.length);
|
|
3254
|
+
return tail.join("/") === short.join("/") ? short.length : 0;
|
|
3255
|
+
}
|
|
3256
|
+
var resourceKey = (path) => {
|
|
3257
|
+
let p = normalizePath(path).toLowerCase();
|
|
3258
|
+
while (p.endsWith("/{}"))
|
|
3259
|
+
p = p.slice(0, -3);
|
|
3260
|
+
return p || "/";
|
|
3261
|
+
};
|
|
3262
|
+
var endpointLabel = (e) => `${e.method ? `${e.method.toUpperCase()} ` : ""}${e.pathOrTopic}`;
|
|
3263
|
+
var READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
3264
|
+
var work = (e) => !e.method || e.method.toUpperCase() === "ANY" ? void 0 : READ_METHODS.has(e.method.toUpperCase()) ? "read" : "write";
|
|
3265
|
+
function familyMismatches(f, byId, template) {
|
|
3266
|
+
const out = [];
|
|
3267
|
+
const trig = f.trigger.ref ? byId.get(f.trigger.ref) : void 0;
|
|
3268
|
+
const key = trig?.kind === "Endpoint" ? resourceKey(trig.pathOrTopic) : void 0;
|
|
3269
|
+
for (const m of f.family) {
|
|
3270
|
+
const r = byId.get(m);
|
|
3271
|
+
if (!r)
|
|
3272
|
+
continue;
|
|
3273
|
+
if (!template.families.length)
|
|
3274
|
+
out.push({
|
|
3275
|
+
member: m,
|
|
3276
|
+
message: `${f.id} stands for ${m}, but the template ${template.repoType}@${template.version} has no families`
|
|
3277
|
+
});
|
|
3278
|
+
else if (r.kind !== "Endpoint" || trig?.kind !== "Endpoint" || r.component !== trig.component || resourceKey(r.pathOrTopic) !== key)
|
|
3279
|
+
out.push({
|
|
3280
|
+
member: m,
|
|
3281
|
+
message: `${f.id} stands for ${m}, which isn't on the same resource (${key ?? "its trigger has no path"}) of the same component`
|
|
3282
|
+
});
|
|
3283
|
+
else if (work(r) && work(trig) && work(r) !== work(trig))
|
|
3284
|
+
out.push({
|
|
3285
|
+
member: m,
|
|
3286
|
+
message: `${f.id} stands for ${m} (${endpointLabel(r)}), a ${work(r)}, but its trigger ${trig.id} (${endpointLabel(trig)}) is a ${work(trig)}`
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
return out;
|
|
3290
|
+
}
|
|
3291
|
+
function specTriggers(records, repoId, template) {
|
|
3292
|
+
const comps = repoComponents(records, repoId);
|
|
3293
|
+
const out = [];
|
|
3294
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3295
|
+
for (const t of template.triggers) {
|
|
3296
|
+
if (!t.from)
|
|
3297
|
+
continue;
|
|
3298
|
+
for (const r of records) {
|
|
3299
|
+
if (seen.has(r.id))
|
|
3300
|
+
continue;
|
|
3301
|
+
const from = t.from;
|
|
3302
|
+
if (from.kind === "Endpoint" && r.kind === "Endpoint" && comps.has(r.component) && from.endpointType.includes(r.endpointType)) {
|
|
3303
|
+
seen.add(r.id);
|
|
3304
|
+
out.push({
|
|
3305
|
+
id: r.id,
|
|
3306
|
+
kind: t.kind,
|
|
3307
|
+
label: endpointLabel(r),
|
|
3308
|
+
...at(r.evidence[0]) ? { evidence: at(r.evidence[0]) } : {}
|
|
3309
|
+
});
|
|
3310
|
+
} else if (from.kind === "Interface" && r.kind === "Interface" && comps.has(r.to) && from.via.includes(r.via) && from.direction.includes(r.direction)) {
|
|
3311
|
+
seen.add(r.id);
|
|
3312
|
+
out.push({
|
|
3313
|
+
id: r.id,
|
|
3314
|
+
kind: t.kind,
|
|
3315
|
+
label: `${r.method ? `${r.method} ` : ""}${r.normalizedPath}`,
|
|
3316
|
+
...at(r.evidence[0]) ? { evidence: at(r.evidence[0]) } : {}
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
3322
|
+
}
|
|
3323
|
+
var methodPath = (s) => {
|
|
3324
|
+
const m = /^([A-Za-z]+)\s+(\S+)$/.exec(s.trim());
|
|
3325
|
+
return m ? { method: m[1].toUpperCase(), path: m[2] } : void 0;
|
|
3326
|
+
};
|
|
3327
|
+
var OUT_STEPS = /* @__PURE__ */ new Set(["call-out", "publish"]);
|
|
3328
|
+
var lineNamesUiWord = (line, words2) => {
|
|
3329
|
+
const l = line.toLowerCase();
|
|
3330
|
+
return words2.some((w) => {
|
|
3331
|
+
const word = w.toLowerCase();
|
|
3332
|
+
for (let k = l.indexOf(word); k >= 0; k = l.indexOf(word, k + 1))
|
|
3333
|
+
if (k === 0 || !/[a-z]/.test(line[k - 1]) || /[A-Z]/.test(line[k]))
|
|
3334
|
+
return true;
|
|
3335
|
+
return false;
|
|
3336
|
+
});
|
|
3337
|
+
};
|
|
3338
|
+
var namesAny = lineNamesUiWord;
|
|
3339
|
+
function uiFindings(f, triggerLines, stepLines, template) {
|
|
3340
|
+
const kind = f.trigger.kind;
|
|
3341
|
+
if (kind !== "action" && kind !== "page")
|
|
3342
|
+
return [];
|
|
3343
|
+
const out = [];
|
|
3344
|
+
const ui = template.ui;
|
|
3345
|
+
const fileOf = (e) => e.map((x) => x.path).filter((p) => !!p);
|
|
3346
|
+
const calls = f.steps.flatMap((s, i) => OUT_STEPS.has(s.kind) ? [i] : []);
|
|
3347
|
+
const callFiles = new Set(calls.flatMap((i) => fileOf(f.steps[i].evidence)));
|
|
3348
|
+
const startWords = kind === "action" ? ui.actionWords : [...ui.pageWords, ...ui.actionWords];
|
|
3349
|
+
const trigAt = at(f.trigger.evidence[0]);
|
|
3350
|
+
const atCall = fileOf(f.trigger.evidence).some((p) => callFiles.has(p));
|
|
3351
|
+
const opensWithEntry = f.steps[0]?.kind === "entry";
|
|
3352
|
+
if (!triggerLines.some((l) => namesAny(l, startWords)) && (atCall || opensWithEntry))
|
|
3353
|
+
out.push({
|
|
3354
|
+
rule: "ui-start-not-action",
|
|
3355
|
+
target: `${f.id} trigger`,
|
|
3356
|
+
...trigAt ? { evidence: trigAt } : {},
|
|
3357
|
+
message: kind === "action" ? `${f.id} starts at ${trigAt ?? "its trigger"}, ${atCall ? "where the call to the server is made" : "with an entry"}, not at the person's action: start it at the button or form in the component or template, with a validate step for the form's own checks and a transform where the form's value becomes what is sent` : `${f.id} starts at ${trigAt ?? "its trigger"}, ${atCall ? "where the call to the server is made" : "with an entry"}, not where the page loads: start it at the page's route or load hook`
|
|
3358
|
+
});
|
|
3359
|
+
const last = calls.at(-1);
|
|
3360
|
+
if (last !== void 0) {
|
|
3361
|
+
const after = f.steps.slice(last + 1);
|
|
3362
|
+
const ends = after.some((s, k) => fileOf(s.evidence).some((p) => !callFiles.has(p)) || (stepLines[last + 1 + k] ?? []).some((l) => namesAny(l, ui.endWords)));
|
|
3363
|
+
if (!ends)
|
|
3364
|
+
out.push({
|
|
3365
|
+
rule: "ui-end-missing",
|
|
3366
|
+
target: `${f.id} end`,
|
|
3367
|
+
...at(f.steps[last].evidence[0]) ? { evidence: at(f.steps[last].evidence[0]) } : {},
|
|
3368
|
+
message: `${f.id} ends at the call in step ${last + 1}${after.length ? " and what goes back from it" : ""}, not at what the screen does after the response: add a last step citing the line that navigates, refreshes, shows the result or shows an error`
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
return out;
|
|
3372
|
+
}
|
|
3373
|
+
async function checkSpec(o) {
|
|
3374
|
+
const byId = new Map(o.records.map((r) => [r.id, r]));
|
|
3375
|
+
const comps = repoComponents(o.records, o.repoId);
|
|
3376
|
+
const endpoints = o.records.filter((r) => r.kind === "Endpoint");
|
|
3377
|
+
const interfaces = o.records.filter((r) => r.kind === "Interface");
|
|
3378
|
+
const columnsOf = /* @__PURE__ */ new Map();
|
|
3379
|
+
for (const r of o.records)
|
|
3380
|
+
if (r.kind === "Column") {
|
|
3381
|
+
const s = columnsOf.get(r.table) ?? /* @__PURE__ */ new Set();
|
|
3382
|
+
s.add(r.name.toLowerCase());
|
|
3383
|
+
columnsOf.set(r.table, s);
|
|
3384
|
+
}
|
|
3385
|
+
const findings = [];
|
|
3386
|
+
const add = (f) => {
|
|
3387
|
+
const fingerprint = specFindingFingerprint(f.rule, f.target, f.evidence ?? "");
|
|
3388
|
+
if (!findings.some((x) => x.fingerprint === fingerprint))
|
|
3389
|
+
findings.push({ ...f, fingerprint });
|
|
3390
|
+
};
|
|
3391
|
+
const texts = /* @__PURE__ */ new Map();
|
|
3392
|
+
const linesOf = async (path) => {
|
|
3393
|
+
if (!texts.has(path)) {
|
|
3394
|
+
const t = await o.repo.readText(path).catch(() => void 0);
|
|
3395
|
+
texts.set(path, t === void 0 ? null : t.replace(/\r\n?/g, "\n").replace(/\n$/, "").split("\n"));
|
|
3396
|
+
}
|
|
3397
|
+
return texts.get(path);
|
|
3398
|
+
};
|
|
3399
|
+
const cited = async (ev, flow, where) => {
|
|
3400
|
+
const out = [];
|
|
3401
|
+
for (const e of ev) {
|
|
3402
|
+
const a = at(e);
|
|
3403
|
+
if (!e.path || e.line === void 0) {
|
|
3404
|
+
add({
|
|
3405
|
+
rule: "line-missing",
|
|
3406
|
+
target: `${flow} ${where}`,
|
|
3407
|
+
...a ? { evidence: a } : {},
|
|
3408
|
+
message: `${flow} ${where} cites ${a ?? "nothing"}, not a file and line`,
|
|
3409
|
+
flow
|
|
3410
|
+
});
|
|
3411
|
+
continue;
|
|
3412
|
+
}
|
|
3413
|
+
const lines = await linesOf(e.path);
|
|
3414
|
+
if (!lines)
|
|
3415
|
+
add({
|
|
3416
|
+
rule: "line-missing",
|
|
3417
|
+
target: `${flow} ${where}`,
|
|
3418
|
+
evidence: a,
|
|
3419
|
+
message: `${flow} ${where} cites ${a}: ${e.path} isn't in the repo`,
|
|
3420
|
+
flow
|
|
3421
|
+
});
|
|
3422
|
+
else if (e.line < 1 || e.line > lines.length)
|
|
3423
|
+
add({
|
|
3424
|
+
rule: "line-missing",
|
|
3425
|
+
target: `${flow} ${where}`,
|
|
3426
|
+
evidence: a,
|
|
3427
|
+
message: `${flow} ${where} cites ${a}: ${e.path} has ${lines.length} lines`,
|
|
3428
|
+
flow
|
|
3429
|
+
});
|
|
3430
|
+
else
|
|
3431
|
+
out.push(lines[e.line - 1]);
|
|
3432
|
+
}
|
|
3433
|
+
return out;
|
|
3434
|
+
};
|
|
3435
|
+
const known = (id, flow, where, ev) => {
|
|
3436
|
+
if (byId.has(id))
|
|
3437
|
+
return true;
|
|
3438
|
+
add({
|
|
3439
|
+
rule: "ref-unknown",
|
|
3440
|
+
target: `${flow} ${where} ${id}`,
|
|
3441
|
+
...ev ? { evidence: ev } : {},
|
|
3442
|
+
message: `${flow} ${where} names ${id}, which isn't in the catalog`,
|
|
3443
|
+
flow
|
|
3444
|
+
});
|
|
3445
|
+
return false;
|
|
3446
|
+
};
|
|
3447
|
+
const requires = new Map(o.template.steps.map((s) => [s.kind, s.requires]));
|
|
3448
|
+
const has = (s, req) => {
|
|
3449
|
+
switch (req) {
|
|
3450
|
+
case "to":
|
|
3451
|
+
return !!s.to;
|
|
3452
|
+
case "store.table":
|
|
3453
|
+
return !!s.store?.table;
|
|
3454
|
+
case "store.columns":
|
|
3455
|
+
return (s.store?.columns.length ?? 0) > 0;
|
|
3456
|
+
case "repeats":
|
|
3457
|
+
return !!s.repeats;
|
|
3458
|
+
default: {
|
|
3459
|
+
const k = req.slice(5);
|
|
3460
|
+
return (s.data?.[k]?.length ?? 0) > 0;
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
};
|
|
3464
|
+
const target = (component, entry) => {
|
|
3465
|
+
const rec = byId.get(entry);
|
|
3466
|
+
if (rec?.kind === "Endpoint")
|
|
3467
|
+
return { path: rec.pathOrTopic, found: true };
|
|
3468
|
+
if (rec?.kind === "Interface")
|
|
3469
|
+
return { path: rec.normalizedPath, found: true };
|
|
3470
|
+
const mp = methodPath(entry);
|
|
3471
|
+
const path = mp?.path ?? entry.trim();
|
|
3472
|
+
const norm = normalizePath(path).toLowerCase();
|
|
3473
|
+
const sameMethod2 = (m) => !mp || !m || m.toUpperCase() === mp.method;
|
|
3474
|
+
const found = endpoints.some((e) => e.component === component && sameMethod2(e.method) && pathScore(path, e.normalizedPath ?? e.pathOrTopic) > 0) || interfaces.some((i) => comps.has(i.from) && i.to === component && sameMethod2(i.method) && normalizePath(i.normalizedPath).toLowerCase() === norm);
|
|
3475
|
+
return { path, found };
|
|
3476
|
+
};
|
|
3477
|
+
let confirmed = 0;
|
|
3478
|
+
let unconfirmable = 0;
|
|
3479
|
+
const flows = [];
|
|
3480
|
+
for (const f of o.flows) {
|
|
3481
|
+
const fid = f.id;
|
|
3482
|
+
const trigLines = await cited(f.trigger.evidence, fid, "trigger");
|
|
3483
|
+
const trig = f.trigger.ref ? byId.get(f.trigger.ref) : void 0;
|
|
3484
|
+
if (f.trigger.ref)
|
|
3485
|
+
known(f.trigger.ref, fid, "trigger", at(f.trigger.evidence[0]));
|
|
3486
|
+
const caller = f.trigger.caller;
|
|
3487
|
+
if (caller) {
|
|
3488
|
+
await cited(caller.evidence, fid, "caller");
|
|
3489
|
+
if (caller.ref)
|
|
3490
|
+
known(caller.ref, fid, "caller", at(caller.evidence[0]));
|
|
3491
|
+
} else if (f.trigger.kind === "endpoint" || f.trigger.kind === "listener") {
|
|
3492
|
+
const cites = f.trigger.evidence.map(at).filter((x) => !!x);
|
|
3493
|
+
const asked = (o.questions ?? []).some((q) => q.evidence.some((e) => cites.includes(e)));
|
|
3494
|
+
const label2 = trig?.kind === "Endpoint" ? endpointLabel(trig) : "its trigger";
|
|
3495
|
+
const inbound = trig?.kind === "Endpoint" ? interfaces.filter((i) => i.endpoint === trig.id || i.to === trig.component && (!i.method || !trig.method || i.method.toUpperCase() === trig.method.toUpperCase()) && pathScore(i.normalizedPath, trig.normalizedPath ?? trig.pathOrTopic) === 1e3) : [];
|
|
3496
|
+
const callers = [...new Set(inbound.map((i) => `${i.from} (${i.id}, ${i.via})`))];
|
|
3497
|
+
if (callers.length)
|
|
3498
|
+
add({
|
|
3499
|
+
rule: "caller-unknown",
|
|
3500
|
+
target: `${fid} trigger caller`,
|
|
3501
|
+
...cites[0] ? { evidence: cites[0] } : {},
|
|
3502
|
+
message: `${fid}: trigger.caller is missing, and the catalog has ${callers.join(", ")} calling ${label2}: name the caller and its route in, in trigger.caller`,
|
|
3503
|
+
flow: fid,
|
|
3504
|
+
callers
|
|
3505
|
+
});
|
|
3506
|
+
else if (!asked)
|
|
3507
|
+
add({
|
|
3508
|
+
rule: "caller-unknown",
|
|
3509
|
+
target: `${fid} trigger caller`,
|
|
3510
|
+
...cites[0] ? { evidence: cites[0] } : {},
|
|
3511
|
+
message: `${fid}: nothing says who calls ${label2} or how the call arrives (through a gateway, or direct); name them in trigger.caller where the code or config says so, else ask a person`,
|
|
3512
|
+
flow: fid
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
if (!comps.has(f.component) && comps.size > 0)
|
|
3516
|
+
known(f.component, fid, "component", at(f.trigger.evidence[0]));
|
|
3517
|
+
for (const m of f.family)
|
|
3518
|
+
known(m, fid, "family", at(f.trigger.evidence[0]));
|
|
3519
|
+
for (const m of familyMismatches(f, byId, o.template))
|
|
3520
|
+
add({
|
|
3521
|
+
rule: "family-mismatch",
|
|
3522
|
+
target: `${fid} family ${m.member}`,
|
|
3523
|
+
message: m.message,
|
|
3524
|
+
flow: fid
|
|
3525
|
+
});
|
|
3526
|
+
for (const [i, e] of f.errors.entries())
|
|
3527
|
+
await cited(e.evidence, fid, `error ${i + 1}`);
|
|
3528
|
+
const seenFields = new Set(f.trigger.what.split(/[^A-Za-z0-9_]+/).filter(Boolean).map(fieldKey));
|
|
3529
|
+
if (trig?.kind === "Endpoint")
|
|
3530
|
+
for (const m of trig.pathOrTopic.matchAll(/[{:<]\$?\{?([A-Za-z_]\w*)/g))
|
|
3531
|
+
seenFields.add(fieldKey(m[1]));
|
|
3532
|
+
const steps = [];
|
|
3533
|
+
const stepLines = [];
|
|
3534
|
+
for (const [i, s] of f.steps.entries()) {
|
|
3535
|
+
const where = `step ${i + 1} (${s.kind})`;
|
|
3536
|
+
const ev = at(s.evidence[0]);
|
|
3537
|
+
const lines = await cited(s.evidence, fid, where);
|
|
3538
|
+
stepLines.push(lines);
|
|
3539
|
+
for (const r of s.refs)
|
|
3540
|
+
known(r, fid, where, ev);
|
|
3541
|
+
for (const req of requires.get(s.kind) ?? [])
|
|
3542
|
+
if (!has(s, req))
|
|
3543
|
+
add({
|
|
3544
|
+
rule: "template-requires",
|
|
3545
|
+
target: `${fid} ${where} ${req}`,
|
|
3546
|
+
...ev ? { evidence: ev } : {},
|
|
3547
|
+
message: `${fid} ${where}: the template ${o.template.repoType}@${o.template.version} asks a ${s.kind} for ${req}`,
|
|
3548
|
+
flow: fid
|
|
3549
|
+
});
|
|
3550
|
+
let route;
|
|
3551
|
+
if (s.to) {
|
|
3552
|
+
const comp = byId.get(s.to.component);
|
|
3553
|
+
if (!comp || comp.kind !== "Component")
|
|
3554
|
+
add({
|
|
3555
|
+
rule: "call-out-target",
|
|
3556
|
+
target: `${fid} ${where} ${s.to.component}`,
|
|
3557
|
+
...ev ? { evidence: ev } : {},
|
|
3558
|
+
message: `${fid} ${where} goes to ${s.to.component}, which isn't a component in the catalog`,
|
|
3559
|
+
flow: fid
|
|
3560
|
+
});
|
|
3561
|
+
const t = target(s.to.component, s.to.entry);
|
|
3562
|
+
route = t.path;
|
|
3563
|
+
if (comp?.kind === "Component" && !t.found)
|
|
3564
|
+
add({
|
|
3565
|
+
rule: "call-out-target",
|
|
3566
|
+
target: `${fid} ${where} ${s.to.entry}`,
|
|
3567
|
+
...ev ? { evidence: ev } : {},
|
|
3568
|
+
message: `${fid} ${where} goes to ${s.to.entry} on ${s.to.component}, which the catalog has no entry point or interface for`,
|
|
3569
|
+
flow: fid
|
|
3570
|
+
});
|
|
3571
|
+
}
|
|
3572
|
+
let table;
|
|
3573
|
+
if (s.store) {
|
|
3574
|
+
if (known(s.store.table, fid, where, ev)) {
|
|
3575
|
+
const r = byId.get(s.store.table);
|
|
3576
|
+
if (r.kind === "Table")
|
|
3577
|
+
table = r;
|
|
3578
|
+
else
|
|
3579
|
+
add({
|
|
3580
|
+
rule: "write-store",
|
|
3581
|
+
target: `${fid} ${where} ${s.store.table}`,
|
|
3582
|
+
...ev ? { evidence: ev } : {},
|
|
3583
|
+
message: `${fid} ${where} stores in ${s.store.table}, which is a ${r.kind}, not a Table`,
|
|
3584
|
+
flow: fid
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
3587
|
+
const cols = table ? columnsOf.get(table.id) : void 0;
|
|
3588
|
+
if (cols) {
|
|
3589
|
+
for (const c of s.store.columns)
|
|
3590
|
+
if (!cols.has(c.toLowerCase()))
|
|
3591
|
+
add({
|
|
3592
|
+
rule: "column-unknown",
|
|
3593
|
+
target: `${fid} ${where} ${s.store.table}.${c}`,
|
|
3594
|
+
...ev ? { evidence: ev } : {},
|
|
3595
|
+
message: `${fid} ${where} names the column ${c}, which ${table.name} doesn't have in the catalog`,
|
|
3596
|
+
flow: fid
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
if (s.kind === "write" && (!s.store || s.store.columns.length === 0))
|
|
3601
|
+
add({
|
|
3602
|
+
rule: "write-store",
|
|
3603
|
+
target: `${fid} ${where}`,
|
|
3604
|
+
...ev ? { evidence: ev } : {},
|
|
3605
|
+
message: `${fid} ${where} is a write that names no ${s.store ? "columns" : "table and columns"}`,
|
|
3606
|
+
flow: fid
|
|
3607
|
+
});
|
|
3608
|
+
if (s.kind === "write" && s.store && s.store.columns.length) {
|
|
3609
|
+
const cols = s.store.columns;
|
|
3610
|
+
const colKeys = new Set(cols.map(fieldKey));
|
|
3611
|
+
const from = s.data?.from ?? [];
|
|
3612
|
+
const to = s.data?.to ?? [];
|
|
3613
|
+
const list = cols.join(", ");
|
|
3614
|
+
from.forEach((x, k) => {
|
|
3615
|
+
const c = to[k];
|
|
3616
|
+
if (c !== void 0 && !cols.some((y) => y.toLowerCase() === c.toLowerCase()))
|
|
3617
|
+
add({
|
|
3618
|
+
rule: "write-unmapped",
|
|
3619
|
+
target: `${fid} ${where} ${x}`,
|
|
3620
|
+
...ev ? { evidence: ev } : {},
|
|
3621
|
+
message: `${fid} ${where} maps ${x} to ${c}, which isn't one of its columns (${list})`,
|
|
3622
|
+
flow: fid
|
|
3623
|
+
});
|
|
3624
|
+
});
|
|
3625
|
+
const mapped = new Set(from.filter((_, k) => to[k] !== void 0).map(fieldKey));
|
|
3626
|
+
for (const x of s.data?.sends ?? [])
|
|
3627
|
+
if (!colKeys.has(fieldKey(x)) && !mapped.has(fieldKey(x)))
|
|
3628
|
+
add({
|
|
3629
|
+
rule: "write-unmapped",
|
|
3630
|
+
target: `${fid} ${where} ${x}`,
|
|
3631
|
+
...ev ? { evidence: ev } : {},
|
|
3632
|
+
message: `${fid} ${where} sends ${x}, which none of its columns (${list}) takes; map it to a column with data.from and data.to, or leave it out`,
|
|
3633
|
+
flow: fid
|
|
3634
|
+
});
|
|
3635
|
+
}
|
|
3636
|
+
if (i > 0) {
|
|
3637
|
+
for (const x of [...s.data?.sends ?? [], ...s.data?.from ?? []])
|
|
3638
|
+
if (!seenFields.has(fieldKey(x)))
|
|
3639
|
+
add({
|
|
3640
|
+
rule: "field-from-nowhere",
|
|
3641
|
+
target: `${fid} ${where} ${x}`,
|
|
3642
|
+
...ev ? { evidence: ev } : {},
|
|
3643
|
+
message: `${fid} ${where} uses ${x}, which no earlier step or the trigger names; where it comes from another field (a form value, a request, a DTO, an entity), add a transform step before it that maps one to the other in data.from and data.to`,
|
|
3644
|
+
flow: fid
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
for (const x of [
|
|
3648
|
+
...Object.values(s.data ?? {}).flat(),
|
|
3649
|
+
...s.store?.columns ?? []
|
|
3650
|
+
])
|
|
3651
|
+
seenFields.add(fieldKey(x));
|
|
3652
|
+
const claims = [];
|
|
3653
|
+
if (!NEVER_CHECKED_STEPS.includes(s.kind)) {
|
|
3654
|
+
if (s.kind === "entry") {
|
|
3655
|
+
const e = [trig, ...s.refs.map((r) => byId.get(r))].find((r) => r?.kind === "Endpoint");
|
|
3656
|
+
if (e?.kind === "Endpoint")
|
|
3657
|
+
claims.push(e.pathOrTopic.includes("/") ? { kind: "route", value: e.pathOrTopic } : { kind: "word", value: e.pathOrTopic });
|
|
3658
|
+
} else if (s.to && route)
|
|
3659
|
+
claims.push(route.includes("/") ? { kind: "route", value: route } : { kind: "word", value: route });
|
|
3660
|
+
else if (table)
|
|
3661
|
+
claims.push({ kind: "word", value: table.name.split(".").pop() });
|
|
3662
|
+
else
|
|
3663
|
+
for (const x of [
|
|
3664
|
+
...s.data?.sends ?? [],
|
|
3665
|
+
...s.data?.returns ?? [],
|
|
3666
|
+
...s.data?.reads ?? []
|
|
3667
|
+
])
|
|
3668
|
+
claims.push({ kind: "field", value: x });
|
|
3669
|
+
}
|
|
3670
|
+
const holds = claims.length > 0 && lines.length === s.evidence.length && claims.every((c) => lines.some((l) => c.kind === "route" ? lineNamesRoute(l, c.value) : c.kind === "word" ? lineNamesWord(l, c.value) : lineNamesField(l, c.value)));
|
|
3671
|
+
if (claims.length === 0)
|
|
3672
|
+
unconfirmable++;
|
|
3673
|
+
let confirmedBy = s.confirmedBy;
|
|
3674
|
+
if (holds) {
|
|
3675
|
+
confirmed++;
|
|
3676
|
+
if (confirmedBy !== "person")
|
|
3677
|
+
confirmedBy = "check";
|
|
3678
|
+
} else if (s.confirmedBy === "check")
|
|
3679
|
+
add({
|
|
3680
|
+
rule: "check-fails",
|
|
3681
|
+
target: `${fid} ${where}`,
|
|
3682
|
+
...ev ? { evidence: ev } : {},
|
|
3683
|
+
message: claims.length ? `${fid} ${where} says the check confirmed it, but its cited lines don't name ${claims.map((c) => c.value).join(", ")}` : `${fid} ${where} says the check confirmed it, but it names nothing the check can look for`,
|
|
3684
|
+
flow: fid
|
|
3685
|
+
});
|
|
3686
|
+
steps.push({ ...s, ...confirmedBy ? { confirmedBy } : {} });
|
|
3687
|
+
}
|
|
3688
|
+
for (const u of uiFindings(f, trigLines, stepLines, o.template))
|
|
3689
|
+
add({ ...u, flow: fid });
|
|
3690
|
+
flows.push({ ...f, steps });
|
|
3691
|
+
}
|
|
3692
|
+
const triggers = specTriggers(o.records, o.repoId, o.template).map((t) => {
|
|
3693
|
+
const flow = o.flows.find((f) => f.trigger.ref === t.id || f.family.includes(t.id));
|
|
3694
|
+
return { ...t, ...flow ? { flow: flow.id } : {} };
|
|
3695
|
+
});
|
|
3696
|
+
if (!o.perFlowOnly)
|
|
3697
|
+
for (const t of triggers.filter((x) => !x.flow))
|
|
3698
|
+
add({
|
|
3699
|
+
rule: "no-flow",
|
|
3700
|
+
target: `trigger ${t.id}`,
|
|
3701
|
+
...t.evidence ? { evidence: t.evidence } : {},
|
|
3702
|
+
message: `The ${t.kind} ${t.label} (${t.id}) has no flow`
|
|
3703
|
+
});
|
|
3704
|
+
return { findings, flows, triggers, confirmed, unconfirmable };
|
|
3705
|
+
}
|
|
3706
|
+
async function staleFlows(repo, flows, hashes) {
|
|
3707
|
+
const now2 = /* @__PURE__ */ new Map();
|
|
3708
|
+
const hashOf = async (p) => {
|
|
3709
|
+
if (!now2.has(p)) {
|
|
3710
|
+
const t = await repo.readText(p).catch(() => void 0);
|
|
3711
|
+
now2.set(p, t === void 0 ? null : specFileHash(t));
|
|
3712
|
+
}
|
|
3713
|
+
return now2.get(p);
|
|
3714
|
+
};
|
|
3715
|
+
const out = [];
|
|
3716
|
+
for (const f of flows) {
|
|
3717
|
+
const changed = [];
|
|
3718
|
+
for (const p of flowFiles(f))
|
|
3719
|
+
if (hashes[p] === void 0 || await hashOf(p) !== hashes[p])
|
|
3720
|
+
changed.push(p);
|
|
3721
|
+
if (changed.length)
|
|
3722
|
+
out.push({ flow: f.id, files: changed });
|
|
3723
|
+
}
|
|
3724
|
+
return out;
|
|
3725
|
+
}
|
|
3726
|
+
async function citedFileHashes(repo, flows) {
|
|
3727
|
+
const out = {};
|
|
3728
|
+
for (const p of [...new Set(flows.flatMap(flowFiles))].sort()) {
|
|
3729
|
+
const t = await repo.readText(p).catch(() => void 0);
|
|
3730
|
+
if (t !== void 0)
|
|
3731
|
+
out[p] = specFileHash(t);
|
|
3732
|
+
}
|
|
3733
|
+
return out;
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
// packages/scanner/dist/estate.js
|
|
3737
|
+
var estateStepKey = (flow, step) => `${flow}#${step}`;
|
|
3738
|
+
var at2 = (e) => e?.path && e.line !== void 0 ? `${e.path}:${e.line}` : void 0;
|
|
3739
|
+
function parseEntry(s) {
|
|
3740
|
+
const m = /^([A-Za-z]+)\s+(\S+)$/.exec(s.trim());
|
|
3741
|
+
return m ? { method: m[1].toUpperCase(), path: m[2] } : { path: s.trim() };
|
|
3742
|
+
}
|
|
3743
|
+
var segs = (p) => normalizePath(p).toLowerCase().split("/").filter(Boolean);
|
|
3744
|
+
var sameMethod = (a, b) => !a || !b || a.toUpperCase() === b.toUpperCase();
|
|
3745
|
+
async function joinEstate(o) {
|
|
3746
|
+
const byId = new Map(o.records.map((r) => [r.id, r]));
|
|
3747
|
+
const endpoints = o.records.filter((r) => r.kind === "Endpoint");
|
|
3748
|
+
const interfaces = o.records.filter((r) => r.kind === "Interface");
|
|
3749
|
+
const flows = o.specs.flatMap((s) => s.flows.map((f) => ({ repo: s.repo, flow: f })));
|
|
3750
|
+
const cited = async (repo, ev) => !!ev?.path && ev.line !== void 0 && (!o.cited || await o.cited(ev.repo ?? repo, ev.path, ev.line));
|
|
3751
|
+
const arrivals = (i) => {
|
|
3752
|
+
const out = [i.normalizedPath];
|
|
3753
|
+
const route = i.route ? byId.get(i.route) : void 0;
|
|
3754
|
+
if (route?.kind === "GatewayRoute") {
|
|
3755
|
+
const base2 = segs(route.basepath);
|
|
3756
|
+
const p = segs(i.normalizedPath);
|
|
3757
|
+
if (base2.length && base2.every((s, k) => p[k] === s))
|
|
3758
|
+
out.push(`/${p.slice(base2.length).join("/")}`);
|
|
3759
|
+
}
|
|
3760
|
+
return out;
|
|
3761
|
+
};
|
|
3762
|
+
const bestEndpoint = (component, method, paths) => {
|
|
3763
|
+
let best;
|
|
3764
|
+
for (const e of endpoints) {
|
|
3765
|
+
if (e.component !== component || !sameMethod(method, e.method))
|
|
3766
|
+
continue;
|
|
3767
|
+
const score = Math.max(...paths.map((p) => pathScore(p, e.normalizedPath ?? e.pathOrTopic)));
|
|
3768
|
+
if (score > 0 && (!best || score > best.score))
|
|
3769
|
+
best = { e, score };
|
|
3770
|
+
}
|
|
3771
|
+
return best?.e;
|
|
3772
|
+
};
|
|
3773
|
+
const templateOf = (repo) => o.templates?.[repo] ?? DEFAULT_SPEC_TEMPLATE;
|
|
3774
|
+
const flowAt = (endpoint) => {
|
|
3775
|
+
const own = flows.find((x) => x.flow.trigger.ref === endpoint);
|
|
3776
|
+
if (own)
|
|
3777
|
+
return { found: own };
|
|
3778
|
+
let wrong;
|
|
3779
|
+
for (const x of flows.filter((y) => y.flow.family.includes(endpoint))) {
|
|
3780
|
+
const bad = familyMismatches(x.flow, byId, templateOf(x.repo)).find((m) => m.member === endpoint);
|
|
3781
|
+
if (!bad)
|
|
3782
|
+
return { found: x };
|
|
3783
|
+
wrong ??= { ...x, finding: bad.message };
|
|
3784
|
+
}
|
|
3785
|
+
return wrong ? { wrong } : void 0;
|
|
3786
|
+
};
|
|
3787
|
+
const isComponent = (id) => byId.get(id)?.kind === "Component";
|
|
3788
|
+
const repoOfComponent = (id) => {
|
|
3789
|
+
const c = byId.get(id);
|
|
3790
|
+
return c?.kind === "Component" ? c.repo ?? c.evidence.find((e) => e.repo)?.repo : void 0;
|
|
3791
|
+
};
|
|
3792
|
+
const servingExactly = (caller, want, scope) => endpoints.filter((e) => e.component !== caller && (!scope || scope.has(e.component)) && sameMethod(want.method, e.method) && pathScore(want.path, e.normalizedPath ?? e.pathOrTopic) === 1e3).sort((a, b) => a.id.localeCompare(b.id));
|
|
3793
|
+
const joins = [];
|
|
3794
|
+
const unmatched = [];
|
|
3795
|
+
const inferred = [];
|
|
3796
|
+
const asks = [];
|
|
3797
|
+
for (const { repo, flow } of flows) {
|
|
3798
|
+
const callers = repoComponents(o.records, repo);
|
|
3799
|
+
callers.add(flow.component);
|
|
3800
|
+
for (const [i, s] of flow.steps.entries()) {
|
|
3801
|
+
if (!s.to)
|
|
3802
|
+
continue;
|
|
3803
|
+
const step = i + 1;
|
|
3804
|
+
const fromAt = at2(s.evidence[0]);
|
|
3805
|
+
const from = { repo, flow: flow.id, step, ...fromAt ? { evidence: fromAt } : {} };
|
|
3806
|
+
const entry = `${s.to.component} ${s.to.entry}`;
|
|
3807
|
+
const miss = (u) => unmatched.push({ from, entry, ...u });
|
|
3808
|
+
const answer = o.targets?.[estateStepKey(flow.id, step)];
|
|
3809
|
+
const name = answer ?? s.to.component;
|
|
3810
|
+
const comp = byId.get(name);
|
|
3811
|
+
const ofRepo = repoComponents(o.records, name);
|
|
3812
|
+
const unknown = comp?.kind !== "Component" && ofRepo.size === 0;
|
|
3813
|
+
if (unknown && (answer || comp?.kind === "System")) {
|
|
3814
|
+
miss({
|
|
3815
|
+
reason: "no-target",
|
|
3816
|
+
message: answer ? `${answer}, which a person named for it, isn't a component in the catalog` : `${name} isn't a component in the catalog`
|
|
3817
|
+
});
|
|
3818
|
+
continue;
|
|
3819
|
+
}
|
|
3820
|
+
const reach = comp?.kind === "Component" ? /* @__PURE__ */ new Set([name]) : unknown ? void 0 : ofRepo;
|
|
3821
|
+
const reaches = (to) => reach ? reach.has(to) : isComponent(to);
|
|
3822
|
+
const named2 = byId.get(s.to.entry);
|
|
3823
|
+
const want = named2?.kind === "Endpoint" ? { method: named2.method, path: named2.normalizedPath ?? named2.pathOrTopic } : named2?.kind === "Interface" ? { method: named2.method, path: named2.normalizedPath } : parseEntry(s.to.entry);
|
|
3824
|
+
let iface = [named2, ...s.refs.map((r) => byId.get(r))].find((r) => r?.kind === "Interface" && reaches(r.to));
|
|
3825
|
+
if (!iface) {
|
|
3826
|
+
let best = 0;
|
|
3827
|
+
for (const x of interfaces) {
|
|
3828
|
+
if (!callers.has(x.from) || !reaches(x.to) || !sameMethod(want.method, x.method))
|
|
3829
|
+
continue;
|
|
3830
|
+
const score = Math.max(...arrivals(x).map((p) => pathScore(want.path, p)));
|
|
3831
|
+
if (score > best) {
|
|
3832
|
+
best = score;
|
|
3833
|
+
iface = x;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
let target;
|
|
3838
|
+
let how;
|
|
3839
|
+
let guess;
|
|
3840
|
+
const stepName = `${flow.id} step ${step}`;
|
|
3841
|
+
const unnamed = `${stepName} names ${s.to.component}, which isn't a component in the catalog`;
|
|
3842
|
+
if (iface) {
|
|
3843
|
+
how = "interface";
|
|
3844
|
+
if (unknown)
|
|
3845
|
+
guess = {
|
|
3846
|
+
within: "interface",
|
|
3847
|
+
message: `${unnamed}: joined through ${iface.id}, the Interface from ${flow.component} that reaches ${s.to.entry}`
|
|
3848
|
+
};
|
|
3849
|
+
const e = iface.endpoint ? byId.get(iface.endpoint) : void 0;
|
|
3850
|
+
target = e?.kind === "Endpoint" ? e : bestEndpoint(iface.to, iface.method ?? want.method, arrivals(iface));
|
|
3851
|
+
if (!target) {
|
|
3852
|
+
miss({
|
|
3853
|
+
reason: "no-target",
|
|
3854
|
+
interface: iface.id,
|
|
3855
|
+
message: `${iface.id} reaches ${iface.to}, but none of its entry points is ${iface.method ? `${iface.method} ` : ""}${iface.normalizedPath}`
|
|
3856
|
+
});
|
|
3857
|
+
continue;
|
|
3858
|
+
}
|
|
3859
|
+
} else if (named2?.kind === "Endpoint") {
|
|
3860
|
+
how = "endpoint";
|
|
3861
|
+
target = named2;
|
|
3862
|
+
} else if (unknown) {
|
|
3863
|
+
how = "path";
|
|
3864
|
+
const own = repoComponents(o.records, repo);
|
|
3865
|
+
let within = "repo";
|
|
3866
|
+
let hits = servingExactly(flow.component, want, own);
|
|
3867
|
+
if (hits.length === 0) {
|
|
3868
|
+
within = "estate";
|
|
3869
|
+
hits = servingExactly(flow.component, want);
|
|
3870
|
+
}
|
|
3871
|
+
const candidates = [...new Set(hits.map((e) => e.component))].sort();
|
|
3872
|
+
if (candidates.length === 0) {
|
|
3873
|
+
miss({
|
|
3874
|
+
reason: "no-target",
|
|
3875
|
+
message: `${s.to.component} isn't a component in the catalog, and neither an Interface from ${flow.component} nor an entry point of the estate serves ${s.to.entry}`
|
|
3876
|
+
});
|
|
3877
|
+
continue;
|
|
3878
|
+
}
|
|
3879
|
+
if (candidates.length > 1) {
|
|
3880
|
+
const repos = [...new Set(candidates.map(repoOfComponent))];
|
|
3881
|
+
const of = repos.length === 1 && repos[0] ? repos[0] : "the estate";
|
|
3882
|
+
miss({
|
|
3883
|
+
reason: "no-target",
|
|
3884
|
+
message: `${s.to.component} isn't a component in the catalog, and ${candidates.length} components of ${of} serve ${s.to.entry} (${candidates.join(", ")}): a person says which`
|
|
3885
|
+
});
|
|
3886
|
+
asks.push({
|
|
3887
|
+
from,
|
|
3888
|
+
caller: flow.component,
|
|
3889
|
+
named: s.to.component,
|
|
3890
|
+
what: s.to.entry,
|
|
3891
|
+
candidates
|
|
3892
|
+
});
|
|
3893
|
+
continue;
|
|
3894
|
+
}
|
|
3895
|
+
target = hits[0];
|
|
3896
|
+
guess = {
|
|
3897
|
+
within,
|
|
3898
|
+
message: `${unnamed}: joined by method and path to ${target.id}, the only entry point of ${within === "repo" ? `its own repo (${repo})` : "the estate"} that serves ${s.to.entry}`
|
|
3899
|
+
};
|
|
3900
|
+
} else if (comp?.kind !== "Component") {
|
|
3901
|
+
miss({
|
|
3902
|
+
reason: "no-target",
|
|
3903
|
+
message: `${name} isn't a component in the catalog, and no Interface from ${flow.component} reaches ${s.to.entry}`
|
|
3904
|
+
});
|
|
3905
|
+
continue;
|
|
3906
|
+
} else {
|
|
3907
|
+
how = "path";
|
|
3908
|
+
target = bestEndpoint(name, want.method, [want.path]);
|
|
3909
|
+
if (!target) {
|
|
3910
|
+
miss({
|
|
3911
|
+
reason: "no-target",
|
|
3912
|
+
message: `${name} has no entry point for ${s.to.entry}, and no Interface from ${flow.component} reaches it`
|
|
3913
|
+
});
|
|
3914
|
+
continue;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
const reached = flowAt(target.id);
|
|
3918
|
+
const ifaceId = iface ? { interface: iface.id } : {};
|
|
3919
|
+
if (!reached) {
|
|
3920
|
+
miss({
|
|
3921
|
+
reason: "no-flow",
|
|
3922
|
+
trigger: target.id,
|
|
3923
|
+
...ifaceId,
|
|
3924
|
+
message: `${s.to.entry} reaches ${target.id}, but no flow of ${target.component} starts there`
|
|
3925
|
+
});
|
|
3926
|
+
continue;
|
|
3927
|
+
}
|
|
3928
|
+
if ("wrong" in reached) {
|
|
3929
|
+
const w = reached.wrong;
|
|
3930
|
+
miss({
|
|
3931
|
+
reason: "family-mismatch",
|
|
3932
|
+
trigger: target.id,
|
|
3933
|
+
...ifaceId,
|
|
3934
|
+
message: `${s.to.entry === target.id ? "The call" : s.to.entry} reaches ${target.id}, which only ${w.flow.id} stands for, in its family, and atticus spec check finds that wrong: ${w.finding}. Redraft it: atticus spec draft ${w.repo}`
|
|
3935
|
+
});
|
|
3936
|
+
continue;
|
|
3937
|
+
}
|
|
3938
|
+
const callee = reached.found;
|
|
3939
|
+
const toEv = callee.flow.trigger.evidence[0];
|
|
3940
|
+
const ends = [
|
|
3941
|
+
await cited(repo, s.evidence[0]) ? void 0 : `${flow.id} step ${step}`,
|
|
3942
|
+
await cited(callee.repo, toEv) ? void 0 : `the trigger of ${callee.flow.id}`
|
|
3943
|
+
].filter((x) => !!x);
|
|
3944
|
+
if (ends.length) {
|
|
3945
|
+
miss({
|
|
3946
|
+
reason: "no-citation",
|
|
3947
|
+
trigger: target.id,
|
|
3948
|
+
...ifaceId,
|
|
3949
|
+
message: `${ends.join(" and ")} ${ends.length === 1 ? "cites" : "cite"} no file and line that exists, so the join has no citation at both ends`
|
|
3950
|
+
});
|
|
3951
|
+
continue;
|
|
3952
|
+
}
|
|
3953
|
+
joins.push({
|
|
3954
|
+
from: { repo, flow: flow.id, step, evidence: fromAt },
|
|
3955
|
+
to: { repo: callee.repo, flow: callee.flow.id, trigger: target.id, evidence: at2(toEv) },
|
|
3956
|
+
entry,
|
|
3957
|
+
...ifaceId,
|
|
3958
|
+
how
|
|
3959
|
+
});
|
|
3960
|
+
if (guess)
|
|
3961
|
+
inferred.push({ from: { repo, flow: flow.id, step }, named: s.to.component, ...guess });
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
const order = (a, b) => a.from.flow.localeCompare(b.from.flow) || a.from.step - b.from.step;
|
|
3965
|
+
return {
|
|
3966
|
+
joins: joins.sort(order),
|
|
3967
|
+
unmatched: unmatched.sort(order),
|
|
3968
|
+
inferred: inferred.sort(order),
|
|
3969
|
+
asks: asks.sort(order)
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
var stepFields = (s) => [
|
|
3973
|
+
...Object.values(s.data ?? {}).flatMap((v) => v ?? []),
|
|
3974
|
+
...s.store?.columns ?? []
|
|
3975
|
+
];
|
|
3976
|
+
function crossing(caller, callee) {
|
|
3977
|
+
const sends = caller.data?.sends ?? [];
|
|
3978
|
+
const first = callee.steps[0];
|
|
3979
|
+
const receives = first ? [...first.data?.reads ?? [], ...first.data?.sends ?? [], ...first.data?.from ?? []] : [];
|
|
3980
|
+
const returned = callee.steps.filter((s) => s.kind === "respond").flatMap((s) => s.data?.returns ?? []);
|
|
3981
|
+
const named2 = new Set(callee.steps.flatMap(stepFields).map(fieldKey));
|
|
3982
|
+
const taken = new Set(receives.map(fieldKey));
|
|
3983
|
+
const sent = new Set(sends.map(fieldKey));
|
|
3984
|
+
const loose = sends.filter((x) => !taken.has(fieldKey(x)));
|
|
3985
|
+
const owner = (r) => {
|
|
3986
|
+
const parts = r.split(".");
|
|
3987
|
+
return parts.length > 1 ? fieldKey(parts[parts.length - 2]) : void 0;
|
|
3988
|
+
};
|
|
3989
|
+
const objects = [];
|
|
3990
|
+
for (const x of loose) {
|
|
3991
|
+
const on = receives.filter((r) => owner(r) === fieldKey(x));
|
|
3992
|
+
if (on.length)
|
|
3993
|
+
objects.push({ sent: x, fields: on });
|
|
3994
|
+
}
|
|
3995
|
+
if (objects.length === 0 && loose.length === 1) {
|
|
3996
|
+
const fields = receives.filter((r) => !sent.has(fieldKey(r)) && owner(r) === void 0);
|
|
3997
|
+
if (fields.length)
|
|
3998
|
+
objects.push({ sent: loose[0], fields });
|
|
3999
|
+
}
|
|
4000
|
+
const mapped = new Set(objects.map((m) => m.sent));
|
|
4001
|
+
return {
|
|
4002
|
+
sends,
|
|
4003
|
+
receives,
|
|
4004
|
+
returns: returned.length ? returned : caller.data?.returns ?? [],
|
|
4005
|
+
unused: sends.filter((f) => !named2.has(fieldKey(f)) && !mapped.has(f)),
|
|
4006
|
+
objects
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
4009
|
+
function estateFlow(o) {
|
|
4010
|
+
const flows = new Map(o.specs.flatMap((s) => s.flows.map((f) => [f.id, { repo: s.repo, flow: f }])));
|
|
4011
|
+
const limit = o.depth ?? 5;
|
|
4012
|
+
const key = (flow, step) => `${flow}#${step}`;
|
|
4013
|
+
const joins = new Map(o.joins.joins.map((j) => [key(j.from.flow, j.from.step), j]));
|
|
4014
|
+
const misses = new Map(o.joins.unmatched.map((u) => [key(u.from.flow, u.from.step), u]));
|
|
4015
|
+
const walk = (id, prefix, path) => {
|
|
4016
|
+
const f = flows.get(id);
|
|
4017
|
+
if (!f)
|
|
4018
|
+
return void 0;
|
|
4019
|
+
const steps = f.flow.steps.map((s, i) => {
|
|
4020
|
+
const n = `${prefix}${i + 1}`;
|
|
4021
|
+
const out = { n, repo: f.repo, flow: id, step: s };
|
|
4022
|
+
if (!s.to)
|
|
4023
|
+
return out;
|
|
4024
|
+
const j = joins.get(key(id, i + 1));
|
|
4025
|
+
const u = misses.get(key(id, i + 1));
|
|
4026
|
+
if (j && j.from.evidence === at2(s.evidence[0]) && flows.has(j.to.flow)) {
|
|
4027
|
+
const callee = flows.get(j.to.flow).flow;
|
|
4028
|
+
const call = { join: j, crossing: crossing(s, callee) };
|
|
4029
|
+
if (path.includes(j.to.flow))
|
|
4030
|
+
call.stopped = "cycle";
|
|
4031
|
+
else if (path.length > limit)
|
|
4032
|
+
call.stopped = "depth";
|
|
4033
|
+
else
|
|
4034
|
+
call.callee = walk(j.to.flow, `${n}.`, [...path, j.to.flow]);
|
|
4035
|
+
out.call = call;
|
|
4036
|
+
} else if (u && !j)
|
|
4037
|
+
out.call = { unmatched: u };
|
|
4038
|
+
else
|
|
4039
|
+
out.call = { stopped: "stale" };
|
|
4040
|
+
return out;
|
|
4041
|
+
});
|
|
4042
|
+
return { repo: f.repo, flow: f.flow, steps };
|
|
4043
|
+
};
|
|
4044
|
+
return walk(o.flow, "", [o.flow]);
|
|
4045
|
+
}
|
|
4046
|
+
function flattenEstateFlow(f) {
|
|
4047
|
+
return f.steps.flatMap((s) => [s, ...s.call?.callee ? flattenEstateFlow(s.call.callee) : []]);
|
|
4048
|
+
}
|
|
4049
|
+
function fieldLineage(f, field) {
|
|
4050
|
+
const steps = flattenEstateFlow(f);
|
|
4051
|
+
const want = fieldKey(field);
|
|
4052
|
+
const out = { flow: f.flow.id, field, links: [] };
|
|
4053
|
+
const link = (s, as, name) => {
|
|
4054
|
+
const e = s.step.evidence[0];
|
|
4055
|
+
const ev = at2(e);
|
|
4056
|
+
return {
|
|
4057
|
+
n: s.n,
|
|
4058
|
+
repo: s.repo,
|
|
4059
|
+
flow: s.flow,
|
|
4060
|
+
kind: s.step.kind,
|
|
4061
|
+
what: s.step.what,
|
|
4062
|
+
as,
|
|
4063
|
+
field: name,
|
|
4064
|
+
...ev ? { evidence: `${e.repo ?? s.repo}:${ev}` } : {}
|
|
4065
|
+
};
|
|
4066
|
+
};
|
|
4067
|
+
const inObject = (s) => s.call?.crossing?.objects.find((m) => m.fields.some((x) => fieldKey(x) === want));
|
|
4068
|
+
const start = steps.findIndex((s) => (s.step.data?.sends ?? []).some((x) => fieldKey(x) === want) || !!inObject(s));
|
|
4069
|
+
if (start < 0) {
|
|
4070
|
+
out.breaks = { message: `No step of ${f.flow.id}, or of the flows it calls, sends ${field}.` };
|
|
4071
|
+
return out;
|
|
4072
|
+
}
|
|
4073
|
+
const s0 = steps[start];
|
|
4074
|
+
const direct = (s0.step.data?.sends ?? []).find((x) => fieldKey(x) === want);
|
|
4075
|
+
const obj = inObject(s0);
|
|
4076
|
+
out.links.push(link(s0, "sent", direct ?? `${obj.sent}.${obj.fields.find((x) => fieldKey(x) === want).split(".").pop()}`));
|
|
4077
|
+
const tracked = /* @__PURE__ */ new Set([want]);
|
|
4078
|
+
const names = /* @__PURE__ */ new Map([[want, field]]);
|
|
4079
|
+
let last = s0;
|
|
4080
|
+
for (const s of steps.slice(start + 1)) {
|
|
4081
|
+
const d = s.step.data ?? {};
|
|
4082
|
+
const mappedTo = s.step.kind === "write" && s.step.store ? d.to?.[d.from?.findIndex((x) => tracked.has(fieldKey(x))) ?? -1] : void 0;
|
|
4083
|
+
const col = s.step.kind === "write" && (mappedTo ?? s.step.store?.columns.find((c) => tracked.has(fieldKey(c))));
|
|
4084
|
+
if (col) {
|
|
4085
|
+
out.links.push(link(s, "stored", `${s.step.store.table}.${col}`));
|
|
4086
|
+
out.stored = { table: s.step.store.table, column: col };
|
|
4087
|
+
return out;
|
|
4088
|
+
}
|
|
4089
|
+
if (s.step.kind === "transform" && d.from?.length && d.to?.length) {
|
|
4090
|
+
const renamed = [];
|
|
4091
|
+
d.from.forEach((x, k) => {
|
|
4092
|
+
const to = d.to[k];
|
|
4093
|
+
if (to === void 0 || !tracked.has(fieldKey(x)))
|
|
4094
|
+
return;
|
|
4095
|
+
renamed.push(`${x} \u2192 ${to}`);
|
|
4096
|
+
tracked.add(fieldKey(to));
|
|
4097
|
+
names.set(fieldKey(to), to);
|
|
4098
|
+
});
|
|
4099
|
+
if (renamed.length) {
|
|
4100
|
+
out.links.push(link(s, "renamed", renamed.join(", ")));
|
|
4101
|
+
last = s;
|
|
4102
|
+
continue;
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
const hit = stepFields(s.step).find((x) => tracked.has(fieldKey(x)));
|
|
4106
|
+
if (hit) {
|
|
4107
|
+
out.links.push(link(s, "named", hit));
|
|
4108
|
+
last = s;
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
const now2 = [...names.values()].join(" or ");
|
|
4112
|
+
const call = last.call;
|
|
4113
|
+
const message = call?.unmatched && last.step.data?.sends?.some((x) => tracked.has(fieldKey(x))) ? `${last.n} sends ${now2} to ${last.step.to.component}, but that call-out isn't joined to a flow: ${call.unmatched.message}.` : call?.stopped && call.stopped !== "stale" ? `${last.n} sends ${now2} on, but the callee isn't followed (${call.stopped === "cycle" ? "it is already on the path" : "the depth limit"}).` : `After ${last.n} no step names ${now2}, and no write stores it.`;
|
|
4114
|
+
out.breaks = { after: last.n, message };
|
|
4115
|
+
return out;
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
// packages/scanner/dist/hosts-asked.js
|
|
4119
|
+
var HTTP_TYPES = /* @__PURE__ */ new Set(["rest", "graphql", "soap", "websocket", "sse", "webhook"]);
|
|
4120
|
+
var CONFIG_FILES = [
|
|
4121
|
+
"**/application*.properties",
|
|
4122
|
+
"**/application*.yml",
|
|
4123
|
+
"**/application*.yaml",
|
|
4124
|
+
"**/bootstrap*.properties",
|
|
4125
|
+
"**/bootstrap*.yml",
|
|
4126
|
+
"**/bootstrap*.yaml",
|
|
4127
|
+
"**/.env",
|
|
4128
|
+
"**/.env.*",
|
|
4129
|
+
"**/Dockerfile",
|
|
4130
|
+
"**/docker-compose*.yml",
|
|
4131
|
+
"**/docker-compose*.yaml",
|
|
4132
|
+
"**/compose*.yml",
|
|
4133
|
+
"**/compose*.yaml",
|
|
4134
|
+
"**/package.json"
|
|
4135
|
+
];
|
|
4136
|
+
function lineSetsPort(line, port) {
|
|
4137
|
+
const p = String(port);
|
|
4138
|
+
const end = "(?![0-9])";
|
|
4139
|
+
return [
|
|
4140
|
+
new RegExp(`port["']?\\s*[:=]\\s*["']?${p}${end}`, "i"),
|
|
4141
|
+
new RegExp(`^\\s*EXPOSE\\s+(?:[0-9]+(?:/[a-z]+)?\\s+)*${p}${end}`, "i"),
|
|
4142
|
+
new RegExp(`--port[=\\s]+${p}${end}`, "i"),
|
|
4143
|
+
new RegExp(`(?:^|[\\s"'-])${p}:[0-9]+`)
|
|
4144
|
+
].some((re) => re.test(line));
|
|
4145
|
+
}
|
|
4146
|
+
function callHost(url) {
|
|
4147
|
+
if (url.includes("${"))
|
|
4148
|
+
return void 0;
|
|
4149
|
+
const hp = urlHostPort(url);
|
|
4150
|
+
if (!hp || !hp.host)
|
|
4151
|
+
return void 0;
|
|
4152
|
+
const explicit = /^[a-z][a-z0-9+.-]*:\/\/(?:[^/@?#]*@)?[^/:?#]+:(\d+)/i.exec(url);
|
|
4153
|
+
return {
|
|
4154
|
+
label: explicit ? `${hp.host}:${explicit[1]}` : hp.host,
|
|
4155
|
+
host: hp.host,
|
|
4156
|
+
...hp.port !== void 0 ? { port: hp.port } : {}
|
|
4157
|
+
};
|
|
4158
|
+
}
|
|
4159
|
+
async function unknownHosts(o) {
|
|
4160
|
+
const patterns = o.repos.flatMap((r) => [
|
|
4161
|
+
...r.hosts,
|
|
4162
|
+
...(r.modules ?? []).flatMap((m) => m.hosts)
|
|
4163
|
+
]);
|
|
4164
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
4165
|
+
for (const r of o.records) {
|
|
4166
|
+
if (r.kind !== "Unresolved" || r.unresolvedKind !== "http-call")
|
|
4167
|
+
continue;
|
|
4168
|
+
const url = r.raw.slice(r.raw.indexOf(" ") + 1);
|
|
4169
|
+
const h = callHost(url);
|
|
4170
|
+
if (!h)
|
|
4171
|
+
continue;
|
|
4172
|
+
if (patterns.some((p) => hostMatches(p, h.host, h.port)))
|
|
4173
|
+
continue;
|
|
4174
|
+
const e = r.evidence[0];
|
|
4175
|
+
const repo = e?.repo ?? r.owner;
|
|
4176
|
+
if (!e?.path || e.line === void 0 || !repo)
|
|
4177
|
+
continue;
|
|
4178
|
+
const entry = byHost.get(h.label) ?? {
|
|
4179
|
+
...h.port !== void 0 ? { port: h.port } : {},
|
|
4180
|
+
explicit: h.label !== h.host,
|
|
4181
|
+
calls: []
|
|
4182
|
+
};
|
|
4183
|
+
const at3 = `${e.path}:${e.line}`;
|
|
4184
|
+
if (!entry.calls.some((c) => c.repo === repo && c.at === at3))
|
|
4185
|
+
entry.calls.push({ repo, at: at3 });
|
|
4186
|
+
byHost.set(h.label, entry);
|
|
4187
|
+
}
|
|
4188
|
+
if (byHost.size === 0)
|
|
4189
|
+
return [];
|
|
4190
|
+
const compRepo = /* @__PURE__ */ new Map();
|
|
4191
|
+
for (const r of o.repos) {
|
|
4192
|
+
compRepo.set(r.component ?? r.id, r.id);
|
|
4193
|
+
for (const m of r.modules ?? [])
|
|
4194
|
+
compRepo.set(m.component, r.id);
|
|
4195
|
+
}
|
|
4196
|
+
const serving = /* @__PURE__ */ new Set();
|
|
4197
|
+
for (const r of o.records)
|
|
4198
|
+
if (r.kind === "Endpoint" && HTTP_TYPES.has(r.endpointType)) {
|
|
4199
|
+
const repo = compRepo.get(r.component);
|
|
4200
|
+
if (repo)
|
|
4201
|
+
serving.add(repo);
|
|
4202
|
+
}
|
|
4203
|
+
const servers = [...serving].sort();
|
|
4204
|
+
const configs = /* @__PURE__ */ new Map();
|
|
4205
|
+
const configOf = async (repo) => {
|
|
4206
|
+
if (configs.has(repo))
|
|
4207
|
+
return configs.get(repo);
|
|
4208
|
+
const h = o.handle(repo);
|
|
4209
|
+
const out = [];
|
|
4210
|
+
if (h)
|
|
4211
|
+
for await (const path of h.listFiles(CONFIG_FILES)) {
|
|
4212
|
+
const t = await h.readText(path).catch(() => void 0);
|
|
4213
|
+
if (t !== void 0)
|
|
4214
|
+
out.push({ path, lines: t.replace(/\r\n?/g, "\n").split("\n") });
|
|
4215
|
+
}
|
|
4216
|
+
configs.set(repo, out);
|
|
4217
|
+
return out;
|
|
4218
|
+
};
|
|
4219
|
+
const asks = [];
|
|
4220
|
+
for (const [host, entry] of [...byHost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
4221
|
+
if (servers.length === 0)
|
|
4222
|
+
continue;
|
|
4223
|
+
const hits = [];
|
|
4224
|
+
if (entry.explicit && entry.port !== void 0)
|
|
4225
|
+
for (const repo of servers) {
|
|
4226
|
+
hit: for (const f of await configOf(repo))
|
|
4227
|
+
for (const [i, l] of f.lines.entries())
|
|
4228
|
+
if (lineSetsPort(l, entry.port)) {
|
|
4229
|
+
hits.push({ repo, at: `${f.path}:${i + 1}`, line: l.trim() });
|
|
4230
|
+
break hit;
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
const suggest = hits.length === 1 ? hits[0] : void 0;
|
|
4234
|
+
const calls = entry.calls.sort((a, b) => a.repo.localeCompare(b.repo) || a.at.localeCompare(b.at, "en", { numeric: true }));
|
|
4235
|
+
const about = Object.fromEntries(servers.flatMap((s) => {
|
|
4236
|
+
const d = o.repos.find((r) => r.id === s)?.description;
|
|
4237
|
+
return d ? [[s, { text: d.text, trust: d.trust }]] : [];
|
|
4238
|
+
}));
|
|
4239
|
+
asks.push({
|
|
4240
|
+
host,
|
|
4241
|
+
calls,
|
|
4242
|
+
servers: suggest ? [suggest.repo, ...servers.filter((s) => s !== suggest.repo)] : servers,
|
|
4243
|
+
...suggest ? { suggest } : {},
|
|
4244
|
+
...Object.keys(about).length ? { about } : {}
|
|
4245
|
+
});
|
|
4246
|
+
}
|
|
4247
|
+
return asks;
|
|
4248
|
+
}
|
|
4249
|
+
function aboutOf(a, repo) {
|
|
4250
|
+
const d = a.about?.[repo];
|
|
4251
|
+
if (!d)
|
|
4252
|
+
return "";
|
|
4253
|
+
return d.trust === "suggested" ? `. What it is, as your assistant drafted it (unconfirmed): ${d.text}` : `. What it is: ${d.text}`;
|
|
4254
|
+
}
|
|
4255
|
+
function hostQuestionDrafts(asks) {
|
|
4256
|
+
return asks.map((a) => {
|
|
4257
|
+
const repo = a.calls[0].repo;
|
|
4258
|
+
return {
|
|
4259
|
+
repo,
|
|
4260
|
+
draft: {
|
|
4261
|
+
question: hostQuestionText(a.host),
|
|
4262
|
+
options: [
|
|
4263
|
+
...a.servers.map((s) => ({
|
|
4264
|
+
label: s,
|
|
4265
|
+
means: (a.suggest?.repo === s ? `${s} serves HTTP, and its ${a.suggest.at} sets the port ${a.host.split(":").pop()} (${a.suggest.line})` : `${s} serves HTTP`) + aboutOf(a, s),
|
|
4266
|
+
changes: `${a.host} goes under hosts for ${s} in repos.manifest.yaml, and the calls to it join ${s} on the next scan`
|
|
4267
|
+
})),
|
|
4268
|
+
{
|
|
4269
|
+
label: HOSTS_NONE,
|
|
4270
|
+
means: `An outside system answers on ${a.host}, not a repo of this estate`,
|
|
4271
|
+
changes: "Nothing is added to the manifest; the calls stay loose ends, and this isn't asked again"
|
|
4272
|
+
}
|
|
4273
|
+
],
|
|
4274
|
+
evidence: a.calls.filter((c) => c.repo === repo).slice(0, 3).map((c) => c.at),
|
|
4275
|
+
scope: "repo"
|
|
4276
|
+
}
|
|
4277
|
+
};
|
|
4278
|
+
});
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
export {
|
|
4282
|
+
buildGlossary,
|
|
4283
|
+
GLOSSARY_CHANGE,
|
|
4284
|
+
GLOSSARY_OUTPUT,
|
|
4285
|
+
undefinedCodeTerms,
|
|
4286
|
+
proposeDefinitions,
|
|
4287
|
+
GlossaryError,
|
|
4288
|
+
resolveTermId,
|
|
4289
|
+
defineDecision,
|
|
4290
|
+
aliasDecision,
|
|
4291
|
+
linkDecision,
|
|
4292
|
+
writeDecisions,
|
|
4293
|
+
loadGlossary,
|
|
4294
|
+
scanGlossary,
|
|
4295
|
+
completeGlossaryDefinitions,
|
|
4296
|
+
keepCatalogsInMemory,
|
|
4297
|
+
forgetCatalogs,
|
|
4298
|
+
readCatalog,
|
|
4299
|
+
writeCatalog,
|
|
4300
|
+
diffCatalogs,
|
|
4301
|
+
diffIsEmpty,
|
|
4302
|
+
estateRepos,
|
|
4303
|
+
isPrototypeRepo,
|
|
4304
|
+
TARGETS_STEP,
|
|
4305
|
+
targetQuestionDrafts,
|
|
4306
|
+
targetsFromAnswers,
|
|
4307
|
+
outsideSystems,
|
|
4308
|
+
joinReposOf,
|
|
4309
|
+
catalogWithFacts,
|
|
4310
|
+
prototypeLeftOut,
|
|
4311
|
+
estateFacts,
|
|
4312
|
+
citedLine,
|
|
4313
|
+
checkScanSettings,
|
|
4314
|
+
runPluginTests,
|
|
4315
|
+
scanEstate,
|
|
4316
|
+
matchRepoType,
|
|
4317
|
+
resolveSettingsTemplate,
|
|
4318
|
+
draftRepoType,
|
|
4319
|
+
repoComponents,
|
|
4320
|
+
specTriggers,
|
|
4321
|
+
checkSpec,
|
|
4322
|
+
staleFlows,
|
|
4323
|
+
citedFileHashes,
|
|
4324
|
+
estateStepKey,
|
|
4325
|
+
joinEstate,
|
|
4326
|
+
estateFlow,
|
|
4327
|
+
fieldLineage,
|
|
4328
|
+
unknownHosts,
|
|
4329
|
+
hostQuestionDrafts
|
|
4330
|
+
};
|