@fangorn-network/westmarch 0.0.20-dev → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -5
- package/docs/app-to-agent.md +113 -620
- package/package.json +18 -18
- package/site/dist/assets/__vite-browser-external-D2U8RZpp.js +1 -0
- package/site/dist/assets/embed.worker-CmS57Zti.js +1 -0
- package/site/dist/assets/index-CkP1WhkV.js +8 -0
- package/site/dist/assets/roles-BDPDWrwo.js +1 -0
- package/site/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
- package/site/dist/assets/transformers.web-CUZUFjAK.js +49 -0
- package/site/dist/assets/transformers.web-CwR_IYQV.js +40 -0
- package/site/dist/index.html +4 -5
- package/site/feed.js +16 -1
- package/site/no-model.js +4 -0
- package/site/worker.js +382 -59
- package/src/agent/agent-card.js +51 -37
- package/src/agent/mcp.js +173 -500
- package/src/agent/tools.js +3 -2
- package/src/agent/ui.js +1 -1
- package/src/agent/view-tools.js +11 -30
- package/src/agent/x402.js +194 -85
- package/src/core/embed.js +56 -53
- package/src/core/embed.worker.js +1 -1
- package/src/core/rank.js +1 -1
- package/src/core/roles.js +7 -7
- package/src/core/shard.js +12 -8
- package/src/discover/apps.js +103 -67
- package/src/discover/corpora.js +1 -1
- package/src/discover/directory.js +3 -3
- package/src/market/cohort.js +1 -1
- package/src/market/demand.js +1 -1
- package/src/market/reactions.js +24 -18
- package/src/publish/devnet.js +146 -0
- package/src/publish/graph.js +1 -1
- package/src/publish/init.js +74 -0
- package/src/publish/lint.js +3 -2
- package/src/publish/ship.js +433 -287
- package/src/publish/view-cli.js +14 -10
- package/src/publish/view.js +485 -255
- package/src/taste/steam.js +1 -1
- package/src/taste/taste-doc.js +4 -3
- package/src/taste/taste.js +3 -3
- package/src/taste/vault.js +4 -2
- package/template/README.md +48 -0
- package/template/app.json +27 -0
- package/template/eval/goals.md +7 -0
- package/template/eval/golden.jsonl +0 -0
- package/template/gitignore +3 -0
- package/template/package.json +13 -0
- package/template/wrangler.json +12 -0
- package/plugin/scripts/fx.mjs +0 -268
- package/site/dist/assets/AccountNotFoundScreen-C4njXc69-BOfmiSH1.js +0 -1
- package/site/dist/assets/AddFundsSelectionScreen-RAoCn7ej-D_EsIeJu.js +0 -31
- package/site/dist/assets/Address-H8kD1d9O-C85sJ79B.js +0 -17
- package/site/dist/assets/AffirmativeConsentScreen-BG0MJxku-DdEPFOww.js +0 -6
- package/site/dist/assets/AllowlistRejectionScreen-Cae6e1Od-DhVy9iGN.js +0 -1
- package/site/dist/assets/ApiController-CYdiyu9B.js +0 -5
- package/site/dist/assets/ArrowsRightLeftIcon-cp60yobq.js +0 -1
- package/site/dist/assets/AuthenticateWithWalletScreen-CU6hIcFT-8rbYiYYE.js +0 -1
- package/site/dist/assets/AwaitingEvmToSolBridgingScreen-D0e_sZMS-2pkNfCno.js +0 -1
- package/site/dist/assets/AwaitingExternalEthereumTransferScreen-ur5HL4R7-7AQt25_k.js +0 -1
- package/site/dist/assets/AwaitingPasswordlessCodeScreen-DRHz76mM-BZY30TGs.js +0 -103
- package/site/dist/assets/AwaitingSolToEvmBridgingScreen-CdLRrGD2-BILOOLi_.js +0 -1
- package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-CvsbQcFv.js +0 -68
- package/site/dist/assets/CaptchaScreen-DbpvkIGx-D8eDcAkc.js +0 -1
- package/site/dist/assets/CheckCircleIcon-BM409mGI.js +0 -1
- package/site/dist/assets/CheckCircleIcon-NCOQc0Gs.js +0 -1
- package/site/dist/assets/Checkbox-D1EDeo41-rHlvsZzP.js +0 -52
- package/site/dist/assets/ChevronDownIcon-CW-4MSpE.js +0 -1
- package/site/dist/assets/Chip-CZKIKt9K-CutJG010.js +0 -26
- package/site/dist/assets/CoinbaseOnrampStatusScreen-BbyCJDXL-DLs9zX-Q.js +0 -16
- package/site/dist/assets/ConnectEmailForm-B-VRE0MN-N9J6T_YL.js +0 -3
- package/site/dist/assets/ConnectLedgerScreen-DWHABGot-h0uYIUi6.js +0 -10
- package/site/dist/assets/ConnectOnlyLandingScreen-CZYx9Bmp-LeuDMN1E.js +0 -1
- package/site/dist/assets/ConnectOnlyStatusScreen-DCRzjxHx-DaRa59tZ.js +0 -1
- package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-crHzh-TL.js +0 -1
- package/site/dist/assets/ConnectPhoneForm-Dek5xn4C-BEA4FmhI.js +0 -95
- package/site/dist/assets/ConnectWalletView-DqI-PTvX-CMQIad63.js +0 -74
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-BeWLchcf.js +0 -7
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-wu3a3kw4.js +0 -1
- package/site/dist/assets/ConnectorUtil-Cq4lWnfe.js +0 -21
- package/site/dist/assets/ConstantsUtil-DmLsRHAl.js +0 -198
- package/site/dist/assets/CopyToClipboard-i_OQSBJr-DujUHNzW.js +0 -39
- package/site/dist/assets/CopyableText-CQapvaMr-DMIdpDaU.js +0 -28
- package/site/dist/assets/CrossAppAuthScreen-CLw7ymZq-Csju14vF.js +0 -1
- package/site/dist/assets/CustomLandingScreenView-Cm_MBSyX-CBOrbNLo.js +0 -22
- package/site/dist/assets/DelegatedActionsConsentScreen-DYF2U4y5-BL5hzNUI.js +0 -1
- package/site/dist/assets/DelegatedActionsRevokeScreen-bPp6Uwyh-fcO51q5H.js +0 -1
- package/site/dist/assets/DepositAddressAddressStepView-4vdkb55L-B3vY888G.js +0 -187
- package/site/dist/assets/DepositAddressScreen-DBLRs7aI-DmdQaFI2.js +0 -8
- package/site/dist/assets/DepositAddressV3Screen-CDqIEMQw-BoupRyfV.js +0 -1
- package/site/dist/assets/EmailInputForm-Cqo1mda8-B2NG3K5V.js +0 -124
- package/site/dist/assets/EmbeddedWalletConnectingScreen-D4T7DUzs-D_AVgO4s.js +0 -1
- package/site/dist/assets/EmbeddedWalletCreatedScreen-D7WhW5ve-CLsJbPYQ.js +0 -1
- package/site/dist/assets/EmbeddedWalletKeyExportScreen-CqV4NYQ3-C9sAExzE.js +0 -23
- package/site/dist/assets/EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-DxaWAAKa.js +0 -1
- package/site/dist/assets/EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-CxrfLr5q.js +0 -1
- package/site/dist/assets/EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-DHZRGoTN.js +0 -1
- package/site/dist/assets/EnrollTotp-Dibew4dp-SA2pvNEc.js +0 -66
- package/site/dist/assets/EnvelopeIcon-Dh2vSVj4.js +0 -1
- package/site/dist/assets/ErrorBanner-BcpGRt0h-BKOYTyCi.js +0 -21
- package/site/dist/assets/ErrorMessage-D8VaAP5m-CrZOnUyg.js +0 -8
- package/site/dist/assets/ErrorScreen-DipdTWzE-DcxVjMO4.js +0 -1
- package/site/dist/assets/ErrorScreen-DipdTWzE-c8g8En21.js +0 -4
- package/site/dist/assets/ExclamationCircleIcon-DUA2wMfO.js +0 -1
- package/site/dist/assets/ExclamationTriangleIcon-eZfaqcFq.js +0 -1
- package/site/dist/assets/FarcasterConnectStatusScreen-51tjUxqq-s_s5M2CZ.js +0 -75
- package/site/dist/assets/FarcasterSignerStatusScreen-6x-Uh95G-BHBR4k8H.js +0 -28
- package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-BH_sU6EV.js +0 -491
- package/site/dist/assets/FingerPrintIcon-Dm6kxmg7.js +0 -1
- package/site/dist/assets/FundSolWalletWithExternalSolanaWallet-WtLwYaOl-B8TRV36j.js +0 -1
- package/site/dist/assets/FundWalletMethodHeader-BWNkokCT-BQdbJ5Bb.js +0 -1
- package/site/dist/assets/FundWithBankDepositScreen-DajkHxBD-9rPtyA3d.js +0 -58
- package/site/dist/assets/FundingEditAmountScreen-CpvpiO4M-GaXigGI9.js +0 -1
- package/site/dist/assets/FundingMethodSelectionScreen-Db7vhRlE-UsmtTqv4.js +0 -1
- package/site/dist/assets/GlobeAltIcon-vqrVV5ho.js +0 -1
- package/site/dist/assets/GooglePay-B53WnudL-BX5_WtCx.js +0 -1
- package/site/dist/assets/HCaptchaWrapper-CUi9LJsp-D-I8j7NO.js +0 -4
- package/site/dist/assets/InAppBrowserLoginNotPossible-BMR9br1T-BY3j50El.js +0 -1
- package/site/dist/assets/InfoBanner-Cb3p1z12-DLBl_Ywy.js +0 -22
- package/site/dist/assets/InjectedWalletIcon-DLcYOGDj-DLSV_NjP.js +0 -1
- package/site/dist/assets/InstallWalletScreen-DTA8dwLv-CxreyXoD.js +0 -1
- package/site/dist/assets/JsonTree-BHzNC-ic-DcNcbSoM.js +0 -34
- package/site/dist/assets/LabelXs-oqZNqbm_-iafrj25N.js +0 -6
- package/site/dist/assets/LandingScreen-CGWUoSUG-DK1EY-6v.js +0 -1
- package/site/dist/assets/Layouts-BMRfo5hw-CeCOyCLK.js +0 -77
- package/site/dist/assets/Link-BdDilT2T-BEDEdHuS.js +0 -40
- package/site/dist/assets/LinkConflictScreen-C3ZEPsgl-ClWoqvEL.js +0 -77
- package/site/dist/assets/LinkEmailScreen-dKo9frwc-C3v7glH0.js +0 -1
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-D72Gb0LW.js +0 -117
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-DYIl5Kr2.js +0 -1
- package/site/dist/assets/LinkPhoneScreen-oIUpfj0w-C0DuD14j.js +0 -1
- package/site/dist/assets/Loader-UAZqxsnw-Cy7hEE2f.js +0 -62
- package/site/dist/assets/LoadingSkeleton-BMsgO5PV-CiEyS5PZ.js +0 -17
- package/site/dist/assets/LockClosedIcon-BgY_5v_Z.js +0 -1
- package/site/dist/assets/LoginFailedScreen-CN0dJV6U-DhUMiJZJ.js +0 -1
- package/site/dist/assets/ManualTransferScreen-CHqcuqbs-tXiZDHmN.js +0 -1
- package/site/dist/assets/MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CaW7xeCC.js +0 -1
- package/site/dist/assets/MfaAuthVerifyFlowScreen-DbFpyWMy-CeRqvx8k.js +0 -1
- package/site/dist/assets/MfaEnrollmentFlowScreen-DhLnGgnr-BkN1k4lh.js +0 -1
- package/site/dist/assets/MfaVerifyFlowScreen-C_XPjMRP-CXSqLVbE.js +0 -1
- package/site/dist/assets/ModalFooter-BCuEK_nz-c-khvG4Y.js +0 -403
- package/site/dist/assets/MoonpayStatusScreen-CzTUtokc-BRFN2eXr.js +0 -16
- package/site/dist/assets/OAuthStatusScreen-ufbOa-da-CeiGcSia.js +0 -1
- package/site/dist/assets/OpenLink-CUpJ1mOr-DfAvJRyK.js +0 -39
- package/site/dist/assets/PasskeySelectSignupOrLogin-xh2ScC_l-CIf5I7wU.js +0 -1
- package/site/dist/assets/PasskeyStatusScreen-Bla8MaOk-CLoL7QC-.js +0 -6
- package/site/dist/assets/PasswordRecoveryScreen-awGXZkQk-B9F67IYV.js +0 -20
- package/site/dist/assets/PhArrowCircleDown-D97G-BOA.js +0 -14
- package/site/dist/assets/PhArrowClockwise-DVgyFuez.js +0 -14
- package/site/dist/assets/PhArrowDown-87b1BizM.js +0 -14
- package/site/dist/assets/PhArrowLeft-Da0MfnbK.js +0 -14
- package/site/dist/assets/PhArrowRight-AdrIL4DW.js +0 -14
- package/site/dist/assets/PhArrowSquareOut-Bk2CMG3I.js +0 -14
- package/site/dist/assets/PhArrowUp-4sgyKPGx.js +0 -14
- package/site/dist/assets/PhArrowUpRight-BNghD2zq.js +0 -14
- package/site/dist/assets/PhArrowsClockwise-BXRn1GEl.js +0 -14
- package/site/dist/assets/PhArrowsDownUp-DM09Jo_3.js +0 -14
- package/site/dist/assets/PhArrowsLeftRight-MuKBLUoF.js +0 -14
- package/site/dist/assets/PhBank-fHbgCqqZ.js +0 -14
- package/site/dist/assets/PhBrowser-2-9m-AkM.js +0 -14
- package/site/dist/assets/PhCaretDown-BDNabFuL.js +0 -14
- package/site/dist/assets/PhCaretLeft-CI460R-A.js +0 -14
- package/site/dist/assets/PhCaretRight-D7ZS3Ls8.js +0 -14
- package/site/dist/assets/PhCaretUp-C-iIfyvO.js +0 -14
- package/site/dist/assets/PhCheck-DxQXn57r.js +0 -14
- package/site/dist/assets/PhCircleHalf-MJZeRBGF.js +0 -14
- package/site/dist/assets/PhClock-x3MS3Mwi.js +0 -14
- package/site/dist/assets/PhCompass-hYn3HhGN.js +0 -14
- package/site/dist/assets/PhCopy-CWNYcABI.js +0 -14
- package/site/dist/assets/PhCreditCard-Fy4pzwtt.js +0 -14
- package/site/dist/assets/PhCurrencyDollar-BgiaenCi.js +0 -14
- package/site/dist/assets/PhDesktop-CxrzCSv5.js +0 -14
- package/site/dist/assets/PhDeviceMobile-CsyA5Goi.js +0 -14
- package/site/dist/assets/PhDotsThree-D-k1BiYg.js +0 -14
- package/site/dist/assets/PhEnvelope-D8kzeNox.js +0 -14
- package/site/dist/assets/PhFunnelSimple-CfQs5DAW.js +0 -14
- package/site/dist/assets/PhGlobe-DvmSc9_A.js +0 -14
- package/site/dist/assets/PhIdentificationCard-CfkZm-5K.js +0 -14
- package/site/dist/assets/PhImage-Cf0J2ZfJ.js +0 -14
- package/site/dist/assets/PhInfo-Bwi51-2X.js +0 -14
- package/site/dist/assets/PhLightbulb-C4FZV7Es.js +0 -14
- package/site/dist/assets/PhMagnifyingGlass-C9u8jSCJ.js +0 -14
- package/site/dist/assets/PhPaperPlaneRight-zxQVvPLX.js +0 -14
- package/site/dist/assets/PhPlus-C4UoW-2n.js +0 -14
- package/site/dist/assets/PhPower-CFyWDmgD.js +0 -14
- package/site/dist/assets/PhPuzzlePiece-BjoNVfST.js +0 -14
- package/site/dist/assets/PhQrCode-CP3tFauY.js +0 -14
- package/site/dist/assets/PhQuestion-wOWJBd3G.js +0 -14
- package/site/dist/assets/PhQuestionMark-CV0k9C0Y.js +0 -14
- package/site/dist/assets/PhSealCheck-vIKzREuN.js +0 -14
- package/site/dist/assets/PhSignOut-CxELV-i0.js +0 -14
- package/site/dist/assets/PhSpinner-CGDmbzUd.js +0 -14
- package/site/dist/assets/PhTrash-C9PD0SAt.js +0 -14
- package/site/dist/assets/PhUser-0sWNXQeZ.js +0 -14
- package/site/dist/assets/PhVault-6JTskUcC.js +0 -14
- package/site/dist/assets/PhWarning-CoKs3gz6.js +0 -14
- package/site/dist/assets/PhWarningCircle-BXHw1fCt.js +0 -14
- package/site/dist/assets/PhX-DX0SFIqC.js +0 -14
- package/site/dist/assets/PhoneIcon-CvIiPwYQ.js +0 -1
- package/site/dist/assets/PinInput-DbZ0b1i1-CDXZBdQx.js +0 -247
- package/site/dist/assets/PrivyPluginContext-DA-yS5vO-CtQWBiV2.js +0 -1
- package/site/dist/assets/QrCode-DbnZwlkH-h6Bhoe5U.js +0 -22
- package/site/dist/assets/RecoveryOAuthStatusScreen-CsgtTI2x-D-A5P3PX.js +0 -1
- package/site/dist/assets/RecoveryPasswordCreateScreen-Be-qJ-LU-C8lTSeSr.js +0 -1
- package/site/dist/assets/RecoverySelectionScreen-D1iGtpmi-P5P3plkH.js +0 -7
- package/site/dist/assets/SIWXUtil-TUCxa7aA.js +0 -1
- package/site/dist/assets/Screen-D30meWsY-CCeVNuWs.js +0 -175
- package/site/dist/assets/ScreenHeader-CHmc4-Lu-DjyrH7IK.js +0 -31
- package/site/dist/assets/ScreenLayout-OwaQQcxH-CrQM6kvA.js +0 -1
- package/site/dist/assets/SelectSourceAsset-CWFog6gm-D7yTw87h.js +0 -140
- package/site/dist/assets/SetAutomaticRecoveryScreen-BO_OEqAZ-f0iDIGmw.js +0 -14
- package/site/dist/assets/SetWalletPasswordForm-DwCTVtAh-kq1zvA7d.js +0 -11
- package/site/dist/assets/ShieldCheckIcon-o6RHYev1.js +0 -1
- package/site/dist/assets/SignRequestScreen-BHKv32KR-DDpMYyJ1.js +0 -26
- package/site/dist/assets/SolanaWalletConnector-C7Lqk-Wz-DPwhuzcI.js +0 -1
- package/site/dist/assets/StackedContainer-B2vaEl56-eQCM2cmv.js +0 -24
- package/site/dist/assets/StandardSignAndSendTransactionScreen-CfhIXVtd-BJ-onCjv.js +0 -20
- package/site/dist/assets/Subtitle-CV-2yKE4-zxWw2m1Q.js +0 -13
- package/site/dist/assets/TelegramAuthScreen-UL9zLfO8-NWlMgNRX.js +0 -1
- package/site/dist/assets/Title-BnzYV3Is-B_JpTatT.js +0 -7
- package/site/dist/assets/TodoList-DnyULl18-C8Eyy28X.js +0 -36
- package/site/dist/assets/TransactionDetails-e9nesl30-C4MpIHR-.js +0 -109
- package/site/dist/assets/TransactionErrorView-BDBNqA_f-p1T9WVMx.js +0 -178
- package/site/dist/assets/TransferFromWalletScreen-Qvn1Yuno-88IQcRnY.js +0 -31
- package/site/dist/assets/TransferOrBridgeLoadingScreen-Dl1I_iIz-1fDAIxAf.js +0 -6
- package/site/dist/assets/TurnstileWrapper-B7sNddys-Jwy1tJtm.js +0 -1
- package/site/dist/assets/UpdateEmailScreen-DVOhPV0H-Jj2DQW8D.js +0 -1
- package/site/dist/assets/UpdatePhoneScreen-CXRswTNZ-DwYIUBqi.js +0 -1
- package/site/dist/assets/UserLimitReachedScreen-5we2DHn8-D7m70C4b.js +0 -1
- package/site/dist/assets/Value-DTgR824E-BRuIGiec.js +0 -27
- package/site/dist/assets/WalletCards-DH1rqayz-Bgp9Fsgs.js +0 -1
- package/site/dist/assets/WalletIcon-gKf0gTw6.js +0 -1
- package/site/dist/assets/WalletInfoCard-sq_2wgFg-DpNSAGte.js +0 -29
- package/site/dist/assets/WalletInterstitialScreen-Sh7LfsAh-DG-RuO-F.js +0 -1
- package/site/dist/assets/WalletLink-DK5LB5Ha-DHaL350n.js +0 -20
- package/site/dist/assets/WalletOverflowButton-bfCnp-9T-CUyIJFzt.js +0 -1
- package/site/dist/assets/WarningBanner-ZZqCEtZK-2eU4Qo74.js +0 -21
- package/site/dist/assets/__vite-browser-external-CitKSmYk.js +0 -1
- package/site/dist/assets/_esm-B26FZg5Y.js +0 -7
- package/site/dist/assets/_esm-BdeM6pxJ.js +0 -1
- package/site/dist/assets/accounts-D96UKWkC.js +0 -1
- package/site/dist/assets/accounts-DiZ-0VmN.js +0 -1
- package/site/dist/assets/analytics-mkkvFRju-BRBcadka.js +0 -1
- package/site/dist/assets/baseSepolia-DX-qGsik.js +0 -1
- package/site/dist/assets/basic-oIwwSyu4.js +0 -1575
- package/site/dist/assets/capitalizeFirstLetter-DmLYqXsO-DtOXx-2n.js +0 -1
- package/site/dist/assets/ccip-65sIfSPR.js +0 -1
- package/site/dist/assets/ccip-BZvRTL6g.js +0 -7
- package/site/dist/assets/ccip-DA_zG341.js +0 -1
- package/site/dist/assets/chain-DM3oH6Px.js +0 -8
- package/site/dist/assets/check-BPgcwEPP.js +0 -1
- package/site/dist/assets/chevron-down-BSsbXkK-.js +0 -1
- package/site/dist/assets/circle-alert-DWaeLZNj.js +0 -1
- package/site/dist/assets/circle-check-big-Chb1ivw9.js +0 -1
- package/site/dist/assets/circle-x-AV9Cet0R.js +0 -1
- package/site/dist/assets/client-CZG0ndth.js +0 -2
- package/site/dist/assets/client-Dsa7_3V3.js +0 -9
- package/site/dist/assets/coinbase-DCPC2L_R-D3aIgpyd.js +0 -3
- package/site/dist/assets/copy-Ckr0AkL6.js +0 -1
- package/site/dist/assets/core-snws5xwv.js +0 -12
- package/site/dist/assets/createLucideIcon-IPW50ckY.js +0 -1
- package/site/dist/assets/defineChain-CiuCXUZW.js +0 -1
- package/site/dist/assets/dijkstra-Drfl93V4.js +0 -1
- package/site/dist/assets/dist-Cm7pNAtL.js +0 -1
- package/site/dist/assets/dist-CoS6E-3x.js +0 -1
- package/site/dist/assets/dist-Dl07uGq1.js +0 -1
- package/site/dist/assets/dist-DnGANz51.js +0 -16
- package/site/dist/assets/embed.worker-ugGhEqCV.js +0 -1
- package/site/dist/assets/errors-BQ3N-RVt-BLqjTPNR.js +0 -1
- package/site/dist/assets/esm-B5cbIQaz.js +0 -37
- package/site/dist/assets/esm-ByALO6mA.js +0 -1
- package/site/dist/assets/esm-C3DslVJx.js +0 -1
- package/site/dist/assets/esm-Ci1ogKuK.js +0 -1
- package/site/dist/assets/esm-n2lWo7kz.js +0 -1
- package/site/dist/assets/ethers-zTjbodk5-wHhL_BQ1.js +0 -1
- package/site/dist/assets/eventemitter3-CL76jPsW.js +0 -1
- package/site/dist/assets/external-link-D-uehwM2.js +0 -1
- package/site/dist/assets/farcaster-DPlSjvF5-BqTvDXBR.js +0 -1
- package/site/dist/assets/features-DQ0UYy6K.js +0 -2
- package/site/dist/assets/fingerprint-pattern-D8ytA16Q.js +0 -1
- package/site/dist/assets/floating-ui.react-dom-DKWOL9UG.js +0 -1
- package/site/dist/assets/floating-ui.react-xRkl9O3u.js +0 -1
- package/site/dist/assets/formatErc20TokenAmount-BuPk9xcy-BVqOKQif.js +0 -1
- package/site/dist/assets/formatter-CVbp4SlJ.js +0 -1
- package/site/dist/assets/get-entropy-details-for-user-dHRH5Gpi-Dukf7_kJ.js +0 -1
- package/site/dist/assets/getChainName-DjpPdUSc-DUyKDrnp.js +0 -1
- package/site/dist/assets/getErc20Balance-jXNpFf0N-Bb9WM31M.js +0 -1
- package/site/dist/assets/getErc20TokenInfo-BNTrzLMP-KPrdn04K.js +0 -1
- package/site/dist/assets/getFormattedUsdFromLamports-De3U9GlO-Gx4_wxON.js +0 -1
- package/site/dist/assets/getUsdcMintAddress-BVgm9GRo-r2UnDeEy.js +0 -1
- package/site/dist/assets/guards-BIW-GiMJ-CHfXo8XI.js +0 -1
- package/site/dist/assets/hooks.module-BYZGfwDl.js +0 -1
- package/site/dist/assets/hourglass-CPG1HZmf.js +0 -1
- package/site/dist/assets/id-UqEwL2BI-BkKm533U.js +0 -1
- package/site/dist/assets/index-1HYFYA-W.js +0 -8
- package/site/dist/assets/index-BdEw5wyl-ChSJvPZk.js +0 -1
- package/site/dist/assets/index-CWARkn2w-Cezl_uBr.js +0 -61
- package/site/dist/assets/index-DvMddJcd-BatvgC-S.js +0 -12
- package/site/dist/assets/index-YiCw0Y_x-CX5i3Z87.js +0 -437
- package/site/dist/assets/index.browser-B-i-Lj3J.js +0 -1
- package/site/dist/assets/index.browser-BMmUcykv.js +0 -1
- package/site/dist/assets/index.es-BmqyoBEM.js +0 -2
- package/site/dist/assets/index.es-C1v1or--.js +0 -3
- package/site/dist/assets/isPaymentRequestAvailable-Bq1cemEn-By6r0dVB.js +0 -1
- package/site/dist/assets/jsx-runtime-BbDfbRii.js +0 -1
- package/site/dist/assets/landmark-CH6LV6F0.js +0 -1
- package/site/dist/assets/lib-BsS8z8Lj.js +0 -1
- package/site/dist/assets/localBatchGatewayRequest-3kLrX_c7.js +0 -32
- package/site/dist/assets/lock-DcskI4Bb.js +0 -1
- package/site/dist/assets/logger-CsXm3oIv-B0UYpNFI.js +0 -1
- package/site/dist/assets/mail-Bs0r5yOi.js +0 -1
- package/site/dist/assets/modal-context-CwmSeFcv-DruX9J1E.js +0 -1
- package/site/dist/assets/native-BKj27wfc.js +0 -1
- package/site/dist/assets/ofetch.CWycOUEr-CHp-crOQ.js +0 -1
- package/site/dist/assets/parseAbi-Csg-8t59.js +0 -2
- package/site/dist/assets/parseAbiParameters-BWbIMYe4.js +0 -1
- package/site/dist/assets/parseEther-VjhVo3dd.js +0 -1
- package/site/dist/assets/pkce-DXF0yKjP-ChQhE0st.js +0 -1
- package/site/dist/assets/preload-helper-uBIymjUX.js +0 -1
- package/site/dist/assets/privy-context-81KmsBw2-CjWGzyg7.js +0 -2
- package/site/dist/assets/property-DuZ9DLP4.js +0 -3
- package/site/dist/assets/react-DCXkXSrv.js +0 -1
- package/site/dist/assets/react-DQnMl2Xv.js +0 -1
- package/site/dist/assets/react-DrJ0MmMO.js +0 -1
- package/site/dist/assets/react-auth-sHnipzbS.js +0 -1
- package/site/dist/assets/react-dom-5dW7aKoP.js +0 -1
- package/site/dist/assets/reservoir-x-nGuZkT-BMX127i6.js +0 -1
- package/site/dist/assets/roles-32M1l9jI.js +0 -1
- package/site/dist/assets/rolldown-runtime-BcKkbAw3.js +0 -1
- package/site/dist/assets/safe-url-D7SRPu33-CkMedGro.js +0 -1
- package/site/dist/assets/secp256k1-C4nzcPo-.js +0 -1
- package/site/dist/assets/secp256k1-D92aGNnU.js +0 -1
- package/site/dist/assets/secp256k1-DmbwyFau.js +0 -1
- package/site/dist/assets/secp256k1-_MleGpqL.js +0 -1
- package/site/dist/assets/secp256k1-xl2og-wO.js +0 -1
- package/site/dist/assets/serializeAuthorizationList-CX8hGL56.js +0 -1
- package/site/dist/assets/serializeSignature-DfxkPnu-.js +0 -2
- package/site/dist/assets/sha2-BU_QyRTx.js +0 -1
- package/site/dist/assets/shared-DRZUmnL5-xqAUARWo.js +0 -193
- package/site/dist/assets/shared-FM0rljBt--Avg6q2R.js +0 -21
- package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BK_FBkaE.js +0 -1
- package/site/dist/assets/simulateTransaction-CfuOrae4-64lWYJAL.js +0 -1
- package/site/dist/assets/smartphone-CcEZ-XAy.js +0 -1
- package/site/dist/assets/src-BBjkfi-Q.js +0 -1
- package/site/dist/assets/storage-Ufl800Ne-D0bZBw-H.js +0 -9
- package/site/dist/assets/stringify-BlDm7tuc.js +0 -21
- package/site/dist/assets/stripe.esm-CZRDpc-N.js +0 -2
- package/site/dist/assets/styles-BsotlekN-CXEB4Ud9.js +0 -9
- package/site/dist/assets/styles-Cm9H63WO-Br4QtzUL.js +0 -92
- package/site/dist/assets/styles-DVyDvTdj-BTABn2K8.js +0 -74
- package/site/dist/assets/styles-hslXy6xG-D-49b1rk.js +0 -164
- package/site/dist/assets/telegram-B-JqnkqZ-RBoY3PLx.js +0 -1
- package/site/dist/assets/to-ui-error-DWe-qwS3-Cx2bk1kZ.js +0 -4
- package/site/dist/assets/toViemAccount-B5iDGq1b-Ba8L7zed.js +0 -2
- package/site/dist/assets/transaction-BNTP-bFm-M5tJx8vY.js +0 -1
- package/site/dist/assets/transfer-6YztDh-t-BVQkTvG-.js +0 -1
- package/site/dist/assets/transformers.web-CVP3pJOS.js +0 -40
- package/site/dist/assets/transformers.web-D8W1u_I2.js +0 -49
- package/site/dist/assets/triangle-alert-CWjYH75H.js +0 -1
- package/site/dist/assets/twitch-5IOe4sIQ-lDtgk0os.js +0 -1
- package/site/dist/assets/types-4CuOD23i.js +0 -1
- package/site/dist/assets/use-headless-crypto-deposit-Yn-QRLB3-LUIVFBSd.js +0 -1
- package/site/dist/assets/use-unlink-wallet-BKK4_5Ur-CC2pq27z.js +0 -1
- package/site/dist/assets/useActiveWallet-B-yZYGc6-D7Ik-LxJ.js +0 -123
- package/site/dist/assets/useGetSolPrice-x7gfUIHJ-D1hmQfpA.js +0 -1
- package/site/dist/assets/useGetTokenPrice-CaoUkb79-ZnbuJ-vP.js +0 -1
- package/site/dist/assets/useI18n-C-5n_noF-BcqC76yj.js +0 -1
- package/site/dist/assets/useWalletBalance-CZnP5f_f-Dw2GIJ4g.js +0 -1
- package/site/dist/assets/useWallets-X1ndvJ_U-DRjz7uYD.js +0 -1
- package/site/dist/assets/utils-BkR7EQjg.js +0 -1
- package/site/dist/assets/utils-DJhBNOZA.js +0 -1
- package/site/dist/assets/utils-DdckVfQr.js +0 -1
- package/site/dist/assets/w3m-modal-BeVrBOIf.js +0 -1174
- package/site/dist/assets/wallet-Bn85Sg2W.js +0 -1
- package/site/dist/assets/wallet-Bnzeiqed.js +0 -2
- package/site/dist/assets/wallet-connect-BvJtUsDt-Y03hhqFN.js +0 -1
- package/site/dist/assets/walletconnect-registry-xxG8XLCb-D1SSFvMp.js +0 -1
- package/site/dist/assets/walletconnect-v2-3frQFwbg-CrtLXofr.js +0 -12
- package/site/dist/assets/wui-ux-by-reown-D34ILNLS.js +0 -1147
- package/site/dist/assets/x-rlSp9U7W.js +0 -1
- package/src/discover/explore.js +0 -271
- package/src/market/settle.js +0 -165
- package/src/market/terms.js +0 -200
- package/src/publish/cli.js +0 -336
- package/src/publish/envelope.js +0 -185
- package/src/publish/manifest.js +0 -142
package/src/publish/view.js
CHANGED
|
@@ -1,58 +1,60 @@
|
|
|
1
|
-
// An app's
|
|
1
|
+
// An app's presented data → a view any Fangorn reader loads: the embedding lens
|
|
2
|
+
// (WHITEPAPER_1 §3.4), computed by whoever runs this.
|
|
2
3
|
//
|
|
3
|
-
// westmarch-view --app my-app --
|
|
4
|
+
// westmarch-view --app my-app [--repo catalog …] --out site/view (a laptop, CI)
|
|
5
|
+
// site/worker.js, on a cron (the free tier)
|
|
4
6
|
//
|
|
5
|
-
// Reads the
|
|
6
|
-
//
|
|
7
|
-
//
|
|
7
|
+
// Reads the registry and storage and nothing else: every repo the app presents (any
|
|
8
|
+
// publisher's repo of a name it lists), at their latest heads; the schema
|
|
9
|
+
// (app.json's `types` + `relations`) comes from the caller. Embeds only records the view does not hold
|
|
10
|
+
// yet, drops removed ones, and writes, through a store (a directory or an R2 bucket):
|
|
8
11
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
+
// cdn/catalog
|
|
13
|
+
// cdn/domains/<domain>/manifest
|
|
14
|
+
// cdn/domains/<domain>/shards/shard-NNNN-<sha12>.ndjson.gz
|
|
12
15
|
//
|
|
13
|
-
// Stateless: the view's own files are its state,
|
|
14
|
-
//
|
|
16
|
+
// Stateless: the view's own files are its state, and each domain's manifest names the head
|
|
17
|
+
// it was built from, so a run where no head moved reads one manifest per repo and nothing
|
|
18
|
+
// else. A repo the app stops presenting loses its domain on the next run.
|
|
15
19
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// manifest and catalog keys;
|
|
21
|
-
// - a record is a vertex: track_id = its CID, fields = payload + entityType (its tag);
|
|
20
|
+
// The layout is fixed, so any reader (shard.js, a tab, an agent) reads any app's view:
|
|
21
|
+
// - URL layout, domain names (`domainFor`: the publisher's address prefix and the repo),
|
|
22
|
+
// row shape {track_id, owner, fields, v}, manifest and catalog keys;
|
|
23
|
+
// - a record is a row: track_id = its CID, fields = payload + entityType (its type);
|
|
22
24
|
// - the schema: a declared type is embedded and shipped with exactly its role map;
|
|
23
|
-
// - the embedded text (`composeText`
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
25
|
+
// - the embedded text (`composeText`).
|
|
26
|
+
// The encoder is EmbeddingGemma (embed.js). Two runtimes of it agree closely but not bit for
|
|
27
|
+
// bit, so the manifest names its `embedder` and this refuses to append to a view another
|
|
28
|
+
// encoder built.
|
|
27
29
|
//
|
|
28
|
-
// ponytail: flat records
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
30
|
+
// ponytail: flat records: a record's neighbours aren't folded into its text, and publishers
|
|
31
|
+
// aren't fused by shared identity. A reference the schema
|
|
32
|
+
// declares is written onto its source row instead (`linkRelations`), which is all a
|
|
33
|
+
// serverless reader needs.
|
|
32
34
|
|
|
33
35
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
34
36
|
import { createHash } from "node:crypto";
|
|
35
37
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { dirname } from "node:path";
|
|
36
39
|
import { EMBED_DIM, EMBED_DTYPE, EMBED_MODEL, embedDocumentDirect, packVec, unpackVec } from "../core/embed.js";
|
|
37
40
|
import { coverage as fitCoverage } from "../market/reactions.js";
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
// The origin the agent card is built with; the Worker swaps in wherever it is served from.
|
|
43
|
+
// Here, not in site/worker.js: workerd refuses a Worker module exporting anything but handlers.
|
|
44
|
+
export const ORIGIN = "https://origin.westmarch.invalid";
|
|
41
45
|
export const EMBEDDER = { runtime: "transformers.js", model: EMBED_MODEL, dtype: EMBED_DTYPE };
|
|
42
46
|
const SINGULAR = ["identity", "title", "subtitle", "temporal", "spatial", "media", "thread"];
|
|
43
47
|
const MULTI = ["tags", "measures", "relations", "text"];
|
|
44
|
-
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
45
48
|
// Refit coverage once a domain has grown this many times over what it was fit on.
|
|
46
49
|
const COVERAGE_REFIT = 2;
|
|
47
50
|
|
|
48
|
-
// ── the contract
|
|
51
|
+
// ── the view's contract ────────────────────────────────────────
|
|
49
52
|
|
|
50
53
|
const slug = (s) => (String(s ?? "").replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "x");
|
|
51
|
-
/**
|
|
52
|
-
export const domainFor = (
|
|
53
|
-
`${slug(appId.slice(2, 10))}-${slug(owner.slice(2, 10))}-${slug(namespace)}`;
|
|
54
|
+
/** A domain's name in the view: the publisher's address prefix and the repo. */
|
|
55
|
+
export const domainFor = (owner, namespace) => `${slug(owner.slice(2, 10))}-${slug(namespace)}`;
|
|
54
56
|
|
|
55
|
-
/**
|
|
57
|
+
/** The text a row is embedded from, minus the `search_document: ` prefix that
|
|
56
58
|
* `embedDocumentDirect` adds itself. */
|
|
57
59
|
export function composeText(fields, rm) {
|
|
58
60
|
const tags = (rm.tags ?? []).map((t) => (typeof fields[t] === "string" ? fields[t] : "")).join(" ");
|
|
@@ -68,18 +70,13 @@ export function composeText(fields, rm) {
|
|
|
68
70
|
return s.slice(0, 1000 - "search_document: ".length);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
/**
|
|
72
|
-
export function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
tagOf[v.cid] = p.tag;
|
|
79
|
-
}
|
|
80
|
-
const relations = (contents?.edges ?? []).filter((e) => tagOf[e.sourceCid] && tagOf[e.targetCid])
|
|
81
|
-
.map((e) => ({ rel: e.relation, from: tagOf[e.sourceCid], to: tagOf[e.targetCid] }));
|
|
82
|
-
return { types, relations };
|
|
73
|
+
/** app.json's `types` + `relations` → { types: {tag: {description, role_map, presentation}}, relations }. */
|
|
74
|
+
export function schemaOf({ types = {}, relations = [] } = {}) {
|
|
75
|
+
return {
|
|
76
|
+
types: Object.fromEntries(Object.entries(types).map(([tag, t]) =>
|
|
77
|
+
[tag, { description: String(t.description ?? ""), role_map: t.role_map ?? {}, presentation: t.presentation ?? {} }])),
|
|
78
|
+
relations: relations.filter((r) => r.rel && r.from && r.to).map((r) => ({ rel: String(r.rel), from: String(r.from), to: String(r.to) })),
|
|
79
|
+
};
|
|
83
80
|
}
|
|
84
81
|
|
|
85
82
|
/** Declared means declared: every role present, left-out ones empty. */
|
|
@@ -91,8 +88,8 @@ export function completeRoles(rm = {}) {
|
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
// ponytail: for a type the schema does not declare, a guess from field names and
|
|
94
|
-
// value lengths — enough to show and search something, and loud about it.
|
|
95
|
-
//
|
|
91
|
+
// value lengths — enough to show and search something, and loud about it. The fix is
|
|
92
|
+
// declaring the type.
|
|
96
93
|
export function guessRoles(rows) {
|
|
97
94
|
const keys = [...new Set(rows.flatMap((r) => Object.keys(r)))].filter((k) => k !== "entityType");
|
|
98
95
|
const find = (names) => keys.find((k) => names.includes(k.toLowerCase())) ?? null;
|
|
@@ -105,7 +102,7 @@ export function guessRoles(rows) {
|
|
|
105
102
|
});
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
/** Merge the declared types a domain holds (most common first)
|
|
105
|
+
/** Merge the declared types a domain holds (most common first). */
|
|
109
106
|
export function domainSpec(schema, tags) {
|
|
110
107
|
const decls = tags.map((t) => schema?.types?.[t]).filter(Boolean);
|
|
111
108
|
if (!decls.length) return null;
|
|
@@ -122,49 +119,68 @@ export function domainSpec(schema, tags) {
|
|
|
122
119
|
}
|
|
123
120
|
|
|
124
121
|
/**
|
|
125
|
-
* Each declared relation, onto the row it starts from: an agenda item
|
|
126
|
-
* to a matter carries `part_of: <the matter's identity>`,
|
|
127
|
-
*
|
|
128
|
-
* `
|
|
129
|
-
*
|
|
130
|
-
* needs many.
|
|
122
|
+
* Each declared relation, onto the row it starts from: an agenda item whose `part_of` is a
|
|
123
|
+
* reference ({local: <record id>}) to a matter carries `part_of: <the matter's identity>`,
|
|
124
|
+
* so a reader groups rows by it without resolving references. References the schema does not
|
|
125
|
+
* declare are left as they are (the schema is the contract). `rows` maps CID → {id, fields}
|
|
126
|
+
* and is changed in place.
|
|
131
127
|
*/
|
|
132
|
-
export function linkRelations(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
export function linkRelations(rows, schema) {
|
|
129
|
+
const byId = new Map([...rows.values()].map((r) => [r.id, r.fields]));
|
|
130
|
+
for (const { fields } of rows.values()) for (const rel of schema.relations) {
|
|
131
|
+
const ref = fields[rel.rel];
|
|
132
|
+
if (rel.from !== fields.entityType || typeof ref?.local !== "string") continue;
|
|
133
|
+
const to = byId.get(ref.local);
|
|
134
|
+
if (!to || to.entityType !== rel.to) continue;
|
|
135
|
+
const id = schema.types[rel.to]?.role_map?.identity;
|
|
136
|
+
fields[rel.rel] = String((id && to[id]) ?? ref.local);
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
// ── a client that only reads ──────────────────────────────────────────────────
|
|
142
141
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
/** The SDK's deployment for a network name ("baseSepolia"): FANGORN_NETWORK, else its default. */
|
|
143
|
+
export async function network(name = process.env.FANGORN_NETWORK || "baseSepolia") {
|
|
144
|
+
const { SupportedNetworks } = await import("@fangorn-network/sdk");
|
|
145
|
+
if (!SupportedNetworks[name]) throw new Error(`network ${name}: the SDK knows ${Object.keys(SupportedNetworks).join(", ")}`);
|
|
146
|
+
return SupportedNetworks[name];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The IPFS gateway to read contents through: FANGORN_IPFS_GATEWAY, else none (the SDK
|
|
150
|
+
* default, Fangorn's gateway). Not the deployer's CLI config: that would ship into the Worker. */
|
|
151
|
+
export function ipfsGateway() {
|
|
152
|
+
const gateway = process.env.FANGORN_IPFS_GATEWAY || process.env.IPFS_GATEWAY;
|
|
153
|
+
return gateway && !/^https?:\/\//.test(gateway) ? `https://${gateway}` : gateway;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** An SDK client for reading: a throwaway key (nothing signs) and no upload credentials,
|
|
157
|
+
* reading contents through `ipfsGateway()`. */
|
|
148
158
|
export async function readOnlyFangorn() {
|
|
149
|
-
const [{ Fangorn
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
try { gateway ??= JSON.parse(readFileSync(`${homedir()}/.fangorn/config.json`, "utf8")).pinataGateway || undefined; } catch { /* no CLI config */ }
|
|
153
|
-
if (gateway && !/^https?:\/\//.test(gateway)) gateway = `https://${gateway}`;
|
|
159
|
+
const [{ Fangorn }, { generatePrivateKey }] = await Promise.all([import("@fangorn-network/sdk"), import("viem/accounts")]);
|
|
160
|
+
const FangornConfig = await network();
|
|
161
|
+
const gateway = ipfsGateway();
|
|
154
162
|
return Fangorn.create({ privateKey: generatePrivateKey(), config: FangornConfig,
|
|
155
163
|
storage: { signedUrl: gateway ? { gateway } : {} } });
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
/** What publishView reads, served from memory instead of the chain: `chain` maps
|
|
159
|
-
* `<owner>/<
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
export function localFangorn(chain
|
|
167
|
+
* `<owner>/<repo>` to `{ records: [{id, cid, type, payload}] }`, every one presented.
|
|
168
|
+
* A build that must not touch the chain — a candidate recipe graded before it ships —
|
|
169
|
+
* runs the exact code a real one does. */
|
|
170
|
+
export function localFangorn(chain) {
|
|
171
|
+
// Every version handed out is kept, so repoDiff can diff any two, as the SDK diffs two roots.
|
|
172
|
+
const versions = new Map();
|
|
173
|
+
const root = (k) => { const h = `0x${createHash("sha256").update(JSON.stringify(chain[k])).digest("hex")}`; versions.set(h, structuredClone(chain[k]?.records ?? [])); return h; };
|
|
174
|
+
const repoDiff = async (from, to) => {
|
|
175
|
+
if (!versions.has(from) || !versions.has(to)) throw new Error("unknown root");
|
|
176
|
+
const [a, b] = [versions.get(from), versions.get(to)], cidOf = new Map(a.map((r) => [r.id, r.cid])), ids = new Set(b.map((r) => r.id));
|
|
177
|
+
return { puts: b.filter((r) => cidOf.get(r.id) !== r.cid), deletes: a.filter((r) => !ids.has(r.id)).map((r) => r.id),
|
|
178
|
+
superseded: a.filter((r) => !ids.has(r.id) || b.some((x) => x.id === r.id && x.cid !== r.cid)).map((r) => r.cid) };
|
|
179
|
+
};
|
|
163
180
|
return {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
readNamespace: async (o, ns) => { const c = chain[`${o}/${ns}`]; if (!c) throw new Error("no such namespace"); return { contents: c }; },
|
|
181
|
+
engine: { repoDiff }, reads: 0,
|
|
182
|
+
repos: async () => Object.keys(chain).map((k) => ({ owner: k.split("/")[0], repo: k.split("/").slice(1).join("/"), root: root(k) })),
|
|
183
|
+
readRepo: async function (o, repo) { this.reads++; const c = chain[`${o}/${repo}`]; return { tip: c ? root(`${o}/${repo}`) : null, contents: { records: c?.records ?? [] } }; },
|
|
168
184
|
};
|
|
169
185
|
}
|
|
170
186
|
|
|
@@ -187,83 +203,137 @@ export function cachedEmbed(path, embed = embedDocumentDirect) {
|
|
|
187
203
|
return fn;
|
|
188
204
|
}
|
|
189
205
|
|
|
190
|
-
// ── the view
|
|
206
|
+
// ── where the view lives ──────────────────────────────────────────────────────
|
|
207
|
+
//
|
|
208
|
+
// A store is four calls on paths relative to the view's root: get → bytes or null, put,
|
|
209
|
+
// del (a file, or everything under a folder), and list (a folder's immediate children).
|
|
210
|
+
|
|
211
|
+
/** A directory on disk. */
|
|
212
|
+
export function fsStore(out) {
|
|
213
|
+
const at = (p) => `${out}/${p}`;
|
|
214
|
+
return {
|
|
215
|
+
get: async (p) => (existsSync(at(p)) ? readFileSync(at(p)) : null),
|
|
216
|
+
put: async (p, b) => { mkdirSync(dirname(at(p)), { recursive: true }); writeFileSync(`${at(p)}.tmp`, b); renameSync(`${at(p)}.tmp`, at(p)); },
|
|
217
|
+
del: async (p) => rmSync(at(p), { recursive: true, force: true }),
|
|
218
|
+
list: async (p) => (existsSync(at(p)) ? readdirSync(at(p)) : []),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** An R2 bucket (a Worker's binding), under `prefix`. */
|
|
223
|
+
export function r2Store(bucket, prefix = "view/") {
|
|
224
|
+
const keys = async (p, delimiter) => {
|
|
225
|
+
const out = { objects: [], prefixes: [] };
|
|
226
|
+
let cursor;
|
|
227
|
+
do {
|
|
228
|
+
const l = await bucket.list({ prefix: prefix + p, delimiter, cursor });
|
|
229
|
+
out.objects.push(...l.objects.map((o) => o.key));
|
|
230
|
+
out.prefixes.push(...(l.delimitedPrefixes ?? []));
|
|
231
|
+
cursor = l.truncated ? l.cursor : undefined;
|
|
232
|
+
} while (cursor);
|
|
233
|
+
return out;
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
get: async (p) => { const o = await bucket.get(prefix + p); return o ? new Uint8Array(await o.arrayBuffer()) : null; },
|
|
237
|
+
put: async (p, b) => { await bucket.put(prefix + p, b); },
|
|
238
|
+
del: async (p) => {
|
|
239
|
+
const all = [...(await keys(`${p}/`)).objects, ...((await bucket.head(prefix + p)) ? [prefix + p] : [])];
|
|
240
|
+
for (let i = 0; i < all.length; i += 1000) await bucket.delete(all.slice(i, i + 1000));
|
|
241
|
+
},
|
|
242
|
+
list: async (p) => {
|
|
243
|
+
const { objects, prefixes } = await keys(`${p}/`, "/");
|
|
244
|
+
const base = prefix.length + p.length + 1;
|
|
245
|
+
return [...objects.map((k) => k.slice(base)), ...prefixes.map((k) => k.slice(base, -1))];
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
191
249
|
|
|
192
|
-
const readJson = (p, dflt) => (
|
|
193
|
-
const writeJson = (p, o) =>
|
|
250
|
+
const readJson = async (store, p, dflt) => { const b = await store.get(p); return b ? JSON.parse(new TextDecoder().decode(b)) : dflt; };
|
|
251
|
+
const writeJson = (store, p, o) => store.put(p, JSON.stringify(o, null, 1));
|
|
194
252
|
|
|
195
|
-
/** What a domain already serves: its manifest and every row (for tombstones and coverage)
|
|
196
|
-
|
|
197
|
-
|
|
253
|
+
/** What a domain already serves: its manifest and every row (for tombstones and coverage), or
|
|
254
|
+
* with `only`, the rows of those shard files alone. */
|
|
255
|
+
async function loadDomain(store, dir, only = null) {
|
|
256
|
+
const manifest = await readJson(store, `${dir}/manifest`, null);
|
|
198
257
|
const rows = new Map();
|
|
199
|
-
for (const s of manifest?.shards ?? []) {
|
|
200
|
-
const
|
|
201
|
-
|
|
258
|
+
for (const s of (manifest?.shards ?? []).filter((x) => !only || only.includes(x.file))) {
|
|
259
|
+
const b = await store.get(`${dir}/shards/${s.file}`);
|
|
260
|
+
if (!b) throw new Error(`${dir}: shard ${s.file} is named by its manifest and missing`);
|
|
261
|
+
for (const line of gunzipSync(b).toString("utf8").split("\n")) if (line.trim()) { const r = JSON.parse(line); rows.set(r.track_id, r); }
|
|
202
262
|
}
|
|
203
263
|
return { manifest, rows };
|
|
204
264
|
}
|
|
205
265
|
|
|
206
|
-
/** Every live row's fields, across the view
|
|
207
|
-
export function liveFields(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const { manifest, rows } = loadDomain(
|
|
266
|
+
/** Every live row's fields, across the view in `store` (or the directory `out`). */
|
|
267
|
+
export async function liveFields(store) {
|
|
268
|
+
if (typeof store === "string") store = fsStore(store);
|
|
269
|
+
const out = [];
|
|
270
|
+
for (const d of await store.list("cdn/domains")) {
|
|
271
|
+
const { manifest, rows } = await loadDomain(store, `cdn/domains/${d}`);
|
|
212
272
|
const dead = new Set(manifest?.tombstones ?? []);
|
|
213
|
-
|
|
214
|
-
}
|
|
273
|
+
for (const r of rows.values()) if (!dead.has(r.track_id)) out.push(r.fields ?? {});
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
215
276
|
}
|
|
216
277
|
|
|
278
|
+
const sha256 = (s) => createHash("sha256").update(s).digest("hex");
|
|
279
|
+
|
|
217
280
|
/**
|
|
218
|
-
* Bring
|
|
281
|
+
* Bring the view up to date with what the app presents. `fangorn` is an SDK client (reads
|
|
282
|
+
* only); `schema` is app.json's { types, relations } (a type it leaves out is guessed); the view goes to `store`, or the directory `out`. `repos` narrows it to those repo
|
|
283
|
+
* names (every publisher's); without it, every repo. `fromBlock` must be
|
|
284
|
+
* at or before the first update to a repo the app presents, or a repo the scan misses is left out;
|
|
285
|
+
* left out, the SDK's deployment start. `embedBatch(texts)` embeds many at once where the
|
|
286
|
+
* encoder batches (a Worker's AI binding); `embedder` is what the manifest records.
|
|
287
|
+
* `presented` replaces the `repos` scan with a list the caller keeps itself
|
|
288
|
+
* ([{owner, repo, root}]), as a Worker does across runs.
|
|
219
289
|
* Returns per-domain counts of what changed.
|
|
220
290
|
*/
|
|
221
|
-
export async function publishView({ fangorn, app,
|
|
222
|
-
embed = embedDocumentDirect,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const appId = fangorn.getAppId();
|
|
226
|
-
const owner = (await fangorn.getAppRegistry().getAppOwner()).toLowerCase();
|
|
227
|
-
let schema = { types: {}, relations: [] };
|
|
228
|
-
if (owner === ZERO) log(`app ${app} is unclaimed, so it has no schema; every type will be guessed`);
|
|
229
|
-
else {
|
|
230
|
-
try { schema = parseSchema((await fangorn.readNamespace(owner, SCHEMA_NAMESPACE)).contents); }
|
|
231
|
-
catch (e) { log(`schema: none read (${e.message}); every type will be guessed`); }
|
|
232
|
-
}
|
|
291
|
+
export async function publishView({ fangorn, app, schema, repos, out, store = fsStore(out), fromBlock, presented, rebake = false,
|
|
292
|
+
embed = embedDocumentDirect, embedBatch = (ts) => Promise.all(ts.map(embed)),
|
|
293
|
+
embedder = EMBEDDER, log = console.log, shardRows = 20000, concurrency = 4 }) {
|
|
294
|
+
schema = schemaOf(schema);
|
|
233
295
|
const declared = Object.keys(schema.types);
|
|
234
296
|
log(`schema: ${declared.length ? declared.join(", ") : "no declared types"}`);
|
|
297
|
+
const schemaHash = sha256(JSON.stringify(schema));
|
|
298
|
+
|
|
299
|
+
// Every repo the app presents, at its latest head: one log scan.
|
|
300
|
+
// ponytail: open: any publisher's repo of a listed name, and with no names, every repo.
|
|
301
|
+
// Read the app's repo of citations instead once the SDK reads citations.
|
|
302
|
+
const listed = (presented ?? await retry(() => fangorn.repos(fromBlock == null ? {} : { fromBlock: BigInt(fromBlock) })))
|
|
303
|
+
// ponytail: "fangorn.schema" is where apps shipped before 2026-10-22 committed their schema; never data.
|
|
304
|
+
.filter((t) => t.repo !== "fangorn.schema" && (!repos?.length || repos.includes(t.repo)));
|
|
305
|
+
log(`${listed.length} repo(s) presented${repos?.length ? ` of ${repos.join(", ")}` : ""}`);
|
|
235
306
|
|
|
236
307
|
const report = {};
|
|
237
|
-
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// them by app only, so a scan per namespace re-reads every log of the app each time: 376 s a
|
|
242
|
-
// namespace through a public RPC, against 3 s to read the namespace itself.
|
|
243
|
-
const reg = fangorn.getDataRegistry?.();
|
|
244
|
-
let committed = null;
|
|
245
|
-
if (reg?.getStateCommittedLogs && reg.namespaceKey) {
|
|
246
|
-
const logs = await retry(() => reg.getStateCommittedLogs({}, BigInt(fromBlock)));
|
|
247
|
-
committed = { keys: new Set(logs.map((l) => l.namespaceKey)), publishers: [...new Set(logs.map((l) => l.publisher))] };
|
|
248
|
-
log(`commit logs since block ${fromBlock}: ${logs.length} from ${committed.publishers.length} publisher(s)`);
|
|
249
|
-
}
|
|
250
|
-
const publishersOf = (ns) => (committed
|
|
251
|
-
? committed.publishers.filter((p) => committed.keys.has(reg.namespaceKey(p, ns))).map((owner) => ({ owner }))
|
|
252
|
-
: retry(() => fangorn.appNamespaces({ namespace: ns, fromBlock: BigInt(fromBlock) })));
|
|
308
|
+
const live = new Set();
|
|
309
|
+
// Several repos at once: reading one through an IPFS gateway is block by block and takes
|
|
310
|
+
// far longer than embedding it. Each domain writes only its own folder.
|
|
311
|
+
const queue = [...listed];
|
|
253
312
|
const worker = async () => {
|
|
254
|
-
for (let
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
});
|
|
262
|
-
|
|
313
|
+
for (let t; (t = queue.shift()) !== undefined;) {
|
|
314
|
+
const domain = domainFor(t.owner, t.repo);
|
|
315
|
+
live.add(domain);
|
|
316
|
+
// One repo failing (unreadable, the encoder out for the day) keeps its last view and
|
|
317
|
+
// does not stop the others; the next run tries it again.
|
|
318
|
+
const r = await publishDomain({ fangorn, store, schema, schemaHash, publisher: t.owner, repo: t.repo, head: t.root, domain,
|
|
319
|
+
dir: `cdn/domains/${domain}`, rebake, embedBatch, embedder, log, shardRows })
|
|
320
|
+
.catch((e) => { log(`${domain}: ✗ ${e.message}; left as it was`); return { added: 0, removed: 0, error: e.message }; });
|
|
321
|
+
if (r) report[domain] = r;
|
|
263
322
|
}
|
|
264
323
|
};
|
|
265
324
|
await Promise.all(Array.from({ length: concurrency }, worker));
|
|
266
|
-
|
|
325
|
+
|
|
326
|
+
// What the app no longer presents is not served.
|
|
327
|
+
for (const d of await store.list("cdn/domains")) {
|
|
328
|
+
if (live.has(d)) continue;
|
|
329
|
+
const m = await readJson(store, `cdn/domains/${d}/manifest`, null);
|
|
330
|
+
const repo = m?.filter?.repo?.[0] ?? m?.filter?.namespace?.[0];
|
|
331
|
+
if (repos?.length && !repos.includes(repo)) continue;
|
|
332
|
+
await store.del(`cdn/domains/${d}`);
|
|
333
|
+
report[d] = { added: 0, removed: m?.count ?? 0, dropped: true };
|
|
334
|
+
log(`${d}: no longer presented by the app; dropped`);
|
|
335
|
+
}
|
|
336
|
+
await writeCatalog(store);
|
|
267
337
|
return report;
|
|
268
338
|
}
|
|
269
339
|
|
|
@@ -277,76 +347,135 @@ async function retry(f, tries = 6) {
|
|
|
277
347
|
}
|
|
278
348
|
}
|
|
279
349
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
350
|
+
const CHECKPOINT = 2048; // rows embedded between saves; a killed run redoes at most this many
|
|
351
|
+
async function publishDomain({ fangorn, store, schema, schemaHash, publisher, repo, head, domain, dir, rebake, embedBatch, embedder, log, shardRows }) {
|
|
352
|
+
// The head the domain was built from, and the schema it was built under: neither moved,
|
|
353
|
+
// nothing to read. This is what keeps a cron run with nothing new to one read per repo.
|
|
354
|
+
const was = rebake ? null : await readJson(store, `${dir}/manifest`, null);
|
|
355
|
+
if (was && was.head === head && was.schema_hash === schemaHash) return null;
|
|
356
|
+
|
|
357
|
+
// The head moved from one this domain was built from: read only what changed between them (the
|
|
358
|
+
// SDK's repoDiff opens only the changed paths of the record tree), not every record again. A
|
|
359
|
+
// schema with relations links each row against all the others, so it still reads the whole repo;
|
|
360
|
+
// so does an SDK whose diff can't name the replaced versions, and a diff that fails (a force-push
|
|
361
|
+
// to a head whose blocks are gone).
|
|
362
|
+
// ponytail: relations read whole; diff their targets too when a big corpus declares one.
|
|
363
|
+
const diff = was?.head && !schema.relations?.length && fangorn.engine?.repoDiff
|
|
364
|
+
? await retry(() => fangorn.engine.repoDiff(was.head, head)).catch((e) => { log(`${domain}: diff from ${was.head} failed (${e.message}); reading it whole`); return null; })
|
|
365
|
+
: null;
|
|
366
|
+
const got = diff?.superseded ? diff.puts : (await retry(() => fangorn.readRepo(publisher, repo))).contents.records;
|
|
367
|
+
// `@`-ids (`@card`, `@deposit/<generation>`) are the protocol's records, not the publisher's data.
|
|
368
|
+
const records = new Map(got.filter((r) => r.payload && typeof r.payload === "object" && !String(r.id).startsWith("@"))
|
|
369
|
+
.map((r) => [r.cid, { id: r.id, fields: { ...r.payload, entityType: r.type,
|
|
370
|
+
// A record its publisher sells (`fangorn commit --paid`): what a buyer quotes it by, and its price.
|
|
371
|
+
...(r.payload["@terms"]?.terms ? { sale: { publisher, repo, id: r.id, price: String(r.payload["@terms"].terms.price) } } : {}) } }]));
|
|
372
|
+
linkRelations(records, schema);
|
|
373
|
+
const fieldsOf = (cid) => records.get(cid).fields;
|
|
374
|
+
|
|
375
|
+
if (rebake) await store.del(dir); // old shards would stay reachable
|
|
376
|
+
// Append only: the diff replaced nothing, so no row already served changes, and the 50k of them
|
|
377
|
+
// are left unread. Only the shards saved since the last head (`ahead`, a killed run's checkpoints)
|
|
378
|
+
// are read, to skip what they hold. A removal, an edit, a rebake or a coverage refit reads all.
|
|
379
|
+
// Rows in `ahead` are already counted, and come back among the puts of a diff from the old head.
|
|
380
|
+
const aheadRows = (was?.shards ?? []).filter((x) => was.ahead?.includes(x.file)).reduce((n, x) => n + x.count, 0);
|
|
381
|
+
const append = !!was && !!diff?.superseded && !diff.superseded.length && !was.tombstones?.length
|
|
382
|
+
&& (was.count ?? 0) + diff.puts.length - aheadRows < COVERAGE_REFIT * (was.coverage?.fit_at ?? 0);
|
|
383
|
+
const had = await loadDomain(store, dir, append ? was.ahead ?? [] : null);
|
|
288
384
|
const made = had.manifest?.embedder ?? (had.manifest ? { runtime: "fastembed", dtype: "fp32" } : null);
|
|
289
385
|
// Same model and dtype is the same encoder, whatever ran it: q8 on CPU, CUDA and WASM agree to
|
|
290
|
-
// ~0.98-0.99 cosine, fp32 against q8
|
|
291
|
-
if (made && (made.model ??
|
|
292
|
-
throw new Error(`${domain} was embedded by ${made.runtime} ${made.dtype}, not ${
|
|
386
|
+
// ~0.98-0.99 cosine, fp32 against q8 noticeably less.
|
|
387
|
+
if (made && (made.model ?? "") + made.dtype !== embedder.model + embedder.dtype)
|
|
388
|
+
throw new Error(`${domain} was embedded by ${made.runtime} ${made.model ?? ""} ${made.dtype}, not ${embedder.runtime} ${embedder.model} ${embedder.dtype}; ` +
|
|
293
389
|
"vectors from two encoders are not comparable. Rebake it with --rebake, or keep publishing it with the tool that made it.");
|
|
294
390
|
|
|
295
391
|
const dead = new Set(had.manifest?.tombstones ?? []);
|
|
296
392
|
const live = (id) => had.rows.has(id) && !dead.has(id);
|
|
297
393
|
const fresh = [...records.keys()].filter((id) => !live(id));
|
|
298
|
-
|
|
394
|
+
// From a diff, `records` holds only what changed: what is gone is what it superseded.
|
|
395
|
+
const gone = diff?.superseded ? diff.superseded.filter(live) : [...had.rows.keys()].filter((id) => live(id) && !records.has(id));
|
|
299
396
|
// A row already served whose links changed (its matter came later, or moved): new fields,
|
|
300
|
-
// the same vector, so no embedding, only a rewrite.
|
|
301
|
-
const relinked = [...had.rows.keys()].filter((id) => live(id) && records.has(id)
|
|
302
|
-
&& JSON.stringify(had.rows.get(id).fields) !== JSON.stringify(
|
|
303
|
-
for (const id of relinked) had.rows.get(id).fields =
|
|
397
|
+
// the same vector, so no embedding, only a rewrite. Only relations relink, and they read whole.
|
|
398
|
+
const relinked = diff?.superseded ? [] : [...had.rows.keys()].filter((id) => live(id) && records.has(id)
|
|
399
|
+
&& JSON.stringify(had.rows.get(id).fields) !== JSON.stringify(fieldsOf(id)));
|
|
400
|
+
for (const id of relinked) had.rows.get(id).fields = fieldsOf(id);
|
|
304
401
|
|
|
305
402
|
// Roles per type: declared exactly, else guessed (and said so).
|
|
306
403
|
const byType = new Map();
|
|
307
|
-
for (const id of fresh) { const t =
|
|
404
|
+
for (const id of fresh) { const t = fieldsOf(id).entityType; byType.set(t, [...(byType.get(t) ?? []), id]); }
|
|
308
405
|
const rolesOf = new Map();
|
|
309
406
|
for (const [t, ids] of byType) {
|
|
310
407
|
if (schema.types[t]) rolesOf.set(t, completeRoles(schema.types[t].role_map));
|
|
311
408
|
else {
|
|
312
|
-
rolesOf.set(t, guessRoles(ids.slice(0, 200).map(
|
|
409
|
+
rolesOf.set(t, guessRoles(ids.slice(0, 200).map(fieldsOf)));
|
|
313
410
|
log(`${domain}: type ${t} is not in the schema; guessed title=${rolesOf.get(t).title} text=${rolesOf.get(t).text}`);
|
|
314
411
|
}
|
|
315
412
|
}
|
|
316
413
|
|
|
317
|
-
|
|
414
|
+
const manifest = had.manifest ?? { name: domain, description: "", count: 0, dim: EMBED_DIM, model: EMBED_MODEL,
|
|
415
|
+
distance: "Cosine", embedder, filter: { owner: [publisher], repo: [repo] }, shards: [], tombstones: [] };
|
|
416
|
+
// Pages and R2 serve files of any size, but a reader holds a shard whole; 52k Steam games
|
|
417
|
+
// gzipped to 28 MiB in one. ponytail: split by row count (~550 B a row gzipped).
|
|
418
|
+
const writeShard = async (all) => {
|
|
419
|
+
const files = [];
|
|
420
|
+
for (let i = 0; i < all.length; i += shardRows) {
|
|
421
|
+
const ls = all.slice(i, i + shardRows);
|
|
422
|
+
const gz = gzipSync(`${ls.join("\n")}\n`, { level: 9 });
|
|
423
|
+
const digest = sha256(gz);
|
|
424
|
+
const file = `shard-${String(manifest.shards.length).padStart(4, "0")}-${digest.slice(0, 12)}.ndjson.gz`;
|
|
425
|
+
await store.put(`${dir}/shards/${file}`, gz);
|
|
426
|
+
manifest.shards.push({ file, count: ls.length, bytes: gz.length, sha256: digest });
|
|
427
|
+
files.push(file);
|
|
428
|
+
}
|
|
429
|
+
return files;
|
|
430
|
+
};
|
|
431
|
+
// What is embedded so far, served now under the old head: a run killed later (CPU, wall time:
|
|
432
|
+
// no exception, no `finally`) loses at most CHECKPOINT rows, and the next run embeds only the rest.
|
|
433
|
+
const save = async () => {
|
|
434
|
+
manifest.ahead = [...(manifest.ahead ?? []), ...await writeShard(lines)];
|
|
435
|
+
const rows = lines.map((l) => JSON.parse(l));
|
|
436
|
+
for (const r of rows) { had.rows.set(r.track_id, r); dead.delete(r.track_id); }
|
|
437
|
+
lines.length = 0;
|
|
438
|
+
manifest.head = had.manifest?.head ?? null;
|
|
439
|
+
manifest.schema_hash = schemaHash;
|
|
440
|
+
await applySpec(store, dir, manifest, schema, had.rows, dead, rolesOf, append ? rows : null);
|
|
441
|
+
};
|
|
442
|
+
let n = 0, stopped = null;
|
|
318
443
|
const lines = [];
|
|
319
|
-
// 32 at a time:
|
|
444
|
+
// 32 at a time: what an encoder batches (embed.js, a GPU, the Worker's AI binding).
|
|
320
445
|
for (let i = 0; i < fresh.length; i += 32) {
|
|
321
446
|
const ids = fresh.slice(i, i + 32);
|
|
322
|
-
|
|
323
|
-
|
|
447
|
+
try {
|
|
448
|
+
const vs = await embedBatch(ids.map((id) => composeText(fieldsOf(id), rolesOf.get(fieldsOf(id).entityType))));
|
|
449
|
+
ids.forEach((id, j) => lines.push(JSON.stringify({ track_id: id, owner: publisher, fields: fieldsOf(id), v: packVec(vs[j]) })));
|
|
450
|
+
} catch (e) {
|
|
451
|
+
// The encoder gave out (Workers AI's free daily allowance, say): keep what is embedded
|
|
452
|
+
// and come back for the rest. Nothing served yet, nothing to save: fail as before.
|
|
453
|
+
if (!lines.length) throw e;
|
|
454
|
+
stopped = e;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
324
457
|
if (Math.floor((n + ids.length) / 1000) > Math.floor(n / 1000)) log(`${domain}: embedded ${n + ids.length}/${fresh.length}`);
|
|
325
458
|
n += ids.length;
|
|
459
|
+
if (lines.length >= CHECKPOINT && n < fresh.length) await save();
|
|
326
460
|
}
|
|
461
|
+
// Unfinished: the old head stays, so the next run sees a head to catch up to and embeds only what is missing.
|
|
462
|
+
manifest.head = stopped ? (had.manifest?.head ?? null) : head;
|
|
463
|
+
manifest.schema_hash = schemaHash;
|
|
464
|
+
if (!stopped) delete manifest.ahead; // the head is taken: everything is behind it
|
|
327
465
|
if (!fresh.length && !gone.length && !relinked.length && had.manifest && !rebake) {
|
|
328
|
-
applySpec(dir,
|
|
466
|
+
await applySpec(store, dir, manifest, schema, had.rows, dead, new Map(), append ? [] : null); // a schema change alone still lands
|
|
329
467
|
return { added: 0, removed: 0 };
|
|
330
468
|
}
|
|
331
469
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
for (let i = 0; i < all.length; i += shardRows) {
|
|
339
|
-
const ls = all.slice(i, i + shardRows);
|
|
340
|
-
const gz = gzipSync(`${ls.join("\n")}\n`, { level: 9 });
|
|
341
|
-
const sha256 = createHash("sha256").update(gz).digest("hex");
|
|
342
|
-
const file = `shard-${String(manifest.shards.length).padStart(4, "0")}-${sha256.slice(0, 12)}.ndjson.gz`;
|
|
343
|
-
writeFileSync(`${dir}/shards/${file}`, gz);
|
|
344
|
-
manifest.shards.push({ file, count: ls.length, bytes: gz.length, sha256 });
|
|
345
|
-
}
|
|
346
|
-
};
|
|
470
|
+
if (stopped) {
|
|
471
|
+
await save();
|
|
472
|
+
log(`${domain}: stopped at ${n}/${fresh.length} (${stopped.message}); the rest on the next run`);
|
|
473
|
+
return { added: n, removed: 0, partial: true };
|
|
474
|
+
}
|
|
475
|
+
const last = lines.map((l) => JSON.parse(l));
|
|
347
476
|
if (lines.length) {
|
|
348
|
-
writeShard(lines);
|
|
349
|
-
for (const
|
|
477
|
+
await writeShard(lines);
|
|
478
|
+
for (const r of last) { had.rows.set(r.track_id, r); dead.delete(r.track_id); }
|
|
350
479
|
}
|
|
351
480
|
for (const id of gone) dead.add(id);
|
|
352
481
|
// A tombstone hides a row from readers, but its shard still serves the bytes. A record
|
|
@@ -355,91 +484,102 @@ async function publishDomain({ fangorn, schema, publisher, ns, domain, dir, reba
|
|
|
355
484
|
// files are deleted. No re-embedding: the vectors are already in the rows.
|
|
356
485
|
// ponytail: rewrites the whole domain on every removal; compact past a threshold if domains get big.
|
|
357
486
|
if (dead.size || relinked.length) {
|
|
358
|
-
|
|
487
|
+
// A compaction rewrites `had.rows` as the whole domain, which append mode never loaded.
|
|
488
|
+
if (append) throw new Error(`${domain}: append mode reached a compaction; nothing written past the new shard`);
|
|
489
|
+
for (const s of manifest.shards) await store.del(`${dir}/shards/${s.file}`);
|
|
359
490
|
manifest.shards = [];
|
|
360
491
|
for (const id of dead) had.rows.delete(id);
|
|
361
492
|
dead.clear();
|
|
362
|
-
writeShard([...had.rows.values()].map((r) => JSON.stringify(r)));
|
|
493
|
+
await writeShard([...had.rows.values()].map((r) => JSON.stringify(r)));
|
|
363
494
|
}
|
|
364
495
|
manifest.tombstones = [...dead].sort();
|
|
365
496
|
manifest.created_at = Math.floor(Date.now() / 1000);
|
|
366
|
-
applySpec(dir, manifest, schema, had.rows, dead, rolesOf);
|
|
497
|
+
await applySpec(store, dir, manifest, schema, had.rows, dead, rolesOf, append ? last : null);
|
|
367
498
|
log(`${domain}: +${fresh.length} embedded, -${gone.length} removed, ${relinked.length} relinked, ${manifest.count} live`);
|
|
368
499
|
return { added: fresh.length, removed: gone.length, ...(relinked.length ? { relinked: relinked.length } : {}) };
|
|
369
500
|
}
|
|
370
501
|
|
|
371
|
-
/** Counts, types, roles, description, presentation
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
502
|
+
/** Counts, types, roles, description, presentation, coverage and the hosts its media role
|
|
503
|
+
* links to (what a Worker may show inline, see site/worker.js) from what the domain holds.
|
|
504
|
+
* `added`: append mode, where `rows` is not the whole domain: fold these new rows into the
|
|
505
|
+
* manifest's totals instead of recounting. Coverage is left as fitted (publishDomain appends only
|
|
506
|
+
* while no refit is due). ponytail: hosts only grow on append; a removal recounts them. */
|
|
507
|
+
async function applySpec(store, dir, manifest, schema, rows, dead, guessed = new Map(), added = null) {
|
|
508
|
+
const liveRows = added ?? [...rows.values()].filter((r) => !dead.has(r.track_id));
|
|
509
|
+
const types = added ? Object.fromEntries((manifest.entity_types ?? []).map((e) => [e.type, e.count])) : {};
|
|
375
510
|
for (const r of liveRows) types[r.fields?.entityType] = (types[r.fields?.entityType] ?? 0) + 1;
|
|
376
511
|
const tags = Object.entries(types).sort((a, b) => b[1] - a[1]).map(([t]) => t);
|
|
377
|
-
// Compare with what is
|
|
512
|
+
// Compare with what is stored, not with the manifest as it arrived: publishDomain has already
|
|
378
513
|
// swapped shards and tombstones, and a one-for-one replacement changes nothing here, which left
|
|
379
514
|
// the old manifest naming deleted shards.
|
|
380
|
-
const before = JSON.stringify(readJson(`${dir}/manifest`, null));
|
|
515
|
+
const before = JSON.stringify(await readJson(store, `${dir}/manifest`, null));
|
|
381
516
|
|
|
382
|
-
manifest.count = liveRows.length;
|
|
517
|
+
manifest.count = added ? (manifest.count ?? 0) + added.length : liveRows.length;
|
|
383
518
|
manifest.entity_types = tags.map((t) => ({ type: t, count: types[t] }));
|
|
384
519
|
const spec = domainSpec(schema, tags);
|
|
385
|
-
const fields = [...new Set(liveRows.slice(0, 500).flatMap((r) => Object.keys(r.fields ?? {})))].sort();
|
|
520
|
+
const fields = [...new Set([...(added ? manifest.role_map?.fields ?? [] : []), ...liveRows.slice(0, 500).flatMap((r) => Object.keys(r.fields ?? {}))])].sort();
|
|
386
521
|
manifest.role_map = { ...(spec?.role_map ?? manifest.role_map ?? guessed.values().next().value ?? completeRoles()), fields };
|
|
387
522
|
if (spec?.description) manifest.description = spec.description;
|
|
388
523
|
if (spec && Object.keys(spec.presentation).length) manifest.presentation = spec.presentation;
|
|
524
|
+
const media = [manifest.role_map.media ?? []].flat();
|
|
525
|
+
manifest.link_hosts = [...new Set([...(added ? manifest.link_hosts ?? [] : []), ...liveRows.flatMap((r) => media.map((m) => r.fields?.[m]))
|
|
526
|
+
.filter((u) => typeof u === "string" && u.startsWith("https://"))
|
|
527
|
+
.map((u) => { try { return new URL(u).hostname; } catch { return null; } })].filter(Boolean))].sort();
|
|
389
528
|
|
|
390
529
|
const cov = manifest.coverage;
|
|
391
|
-
if (liveRows.length && (!cov || liveRows.length >= COVERAGE_REFIT * (cov.fit_at ?? 0))) {
|
|
530
|
+
if (!added && liveRows.length && (!cov || liveRows.length >= COVERAGE_REFIT * (cov.fit_at ?? 0))) {
|
|
392
531
|
manifest.coverage = { ...fitCoverage(liveRows.map((r) => unpackVec(r.v))), fit_at: liveRows.length };
|
|
393
532
|
}
|
|
394
|
-
if (JSON.stringify(manifest) !== before) {
|
|
395
|
-
mkdirSync(dir, { recursive: true });
|
|
396
|
-
writeJson(`${dir}/manifest`, manifest);
|
|
397
|
-
}
|
|
533
|
+
if (JSON.stringify(manifest) !== before) await writeJson(store, `${dir}/manifest`, manifest);
|
|
398
534
|
}
|
|
399
535
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const domains =
|
|
404
|
-
|
|
536
|
+
/** The catalog: every domain's summary and coverage, the one file a reader routes on. */
|
|
537
|
+
export async function writeCatalog(store) {
|
|
538
|
+
if (typeof store === "string") store = fsStore(store);
|
|
539
|
+
const domains = (await Promise.all((await store.list("cdn/domains")).sort()
|
|
540
|
+
.map((d) => readJson(store, `cdn/domains/${d}/manifest`, null)))).filter(Boolean);
|
|
541
|
+
await writeJson(store, "cdn/catalog", {
|
|
405
542
|
generated_at: Math.floor(Date.now() / 1000), embedding_model: EMBED_MODEL,
|
|
406
543
|
domains: domains.map((m) => ({
|
|
407
544
|
name: m.name, description: m.description, count: m.count, dim: m.dim,
|
|
545
|
+
// Whose it is: a reader can list publishers without opening a manifest.
|
|
546
|
+
...(m.filter?.owner?.length === 1 ? { publisher: m.filter.owner[0], repo: m.filter.repo?.[0] } : {}),
|
|
408
547
|
bytes: m.shards.reduce((s, x) => s + x.bytes, 0), shard_count: m.shards.length,
|
|
409
548
|
entity_types: m.entity_types.map((e) => e.type), manifest: `${m.name}/manifest.json`,
|
|
549
|
+
...(m.link_hosts?.length ? { link_hosts: m.link_hosts } : {}),
|
|
410
550
|
...(m.coverage ? { coverage: m.coverage } : {}),
|
|
411
551
|
})),
|
|
412
552
|
});
|
|
413
553
|
}
|
|
414
554
|
|
|
415
555
|
// ── self-check: `node src/publish/view.js` — a fake chain, a fake embedder, the real reader ──
|
|
416
|
-
if (
|
|
556
|
+
if (import.meta.url === `file://${globalThis.process?.argv?.[1]}` && process.argv[1].endsWith("/view.js")) {
|
|
417
557
|
const { createServer } = await import("node:http");
|
|
418
558
|
const { mkdtempSync } = await import("node:fs");
|
|
419
559
|
const { tmpdir } = await import("node:os");
|
|
420
560
|
const { loadShard, configure } = await import("../core/shard.js");
|
|
421
561
|
const eq = (a, b, m) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${m}: ${JSON.stringify(a)} != ${JSON.stringify(b)}`); };
|
|
422
|
-
const
|
|
562
|
+
const manifestOf = (dir, d) => JSON.parse(readFileSync(`${dir}/cdn/domains/${d}/manifest`, "utf8"));
|
|
563
|
+
const rec = (cid, type, payload, id = cid) => ({ id, cid, type, payload });
|
|
423
564
|
|
|
424
|
-
//
|
|
425
|
-
eq(domainFor(
|
|
565
|
+
// The contract, pinned: domain names and embedded text never drift.
|
|
566
|
+
eq(domainFor("0x55cf6bd2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "media"), "55cf6bd2-media", "domain name: owner prefix and repo");
|
|
426
567
|
eq(composeText({ name: "Oak", family: "Fagaceae", description: "big", entityType: "t" },
|
|
427
568
|
completeRoles({ title: "name", tags: ["family"], text: ["description"] })),
|
|
428
|
-
"Title: Oak. Tags: Fagaceae. big", "embedded text
|
|
569
|
+
"Title: Oak. Tags: Fagaceae. big", "embedded text is pinned");
|
|
429
570
|
|
|
430
571
|
const APP = `0x${"ab".repeat(32)}`, OWNER = `0x${"11".repeat(20)}`, PUB = `0x${"22".repeat(20)}`;
|
|
431
572
|
const TAG = "shop.item.v1";
|
|
573
|
+
const schema = { types: { [TAG]: { description: "Things for a room.", role_map: { identity: "sku", title: "name", text: "about" },
|
|
574
|
+
presentation: { externalUrl: { [TAG]: "https://shop.example/{sku}" } } } } };
|
|
432
575
|
const chain = {
|
|
433
|
-
[`${
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
{ cid: "c2", schemaId: TAG, payload: { sku: "b2", name: "Lamp", about: "brass reading lamp", price: 1 } },
|
|
439
|
-
{ cid: "c3", schemaId: "shop.note.v1", payload: { title: "Note", body: "an undeclared type, with a body long enough to count as its text field" } },
|
|
440
|
-
], edges: [] },
|
|
576
|
+
[`${PUB}/shop`]: { records: [
|
|
577
|
+
rec("c1", TAG, { sku: "a1", name: "Oak table", about: "a solid oak dining table", price: 3 }),
|
|
578
|
+
rec("c2", TAG, { sku: "b2", name: "Lamp", about: "brass reading lamp", price: 1 }),
|
|
579
|
+
rec("c3", "shop.note.v1", { title: "Note", body: "an undeclared type, with a body long enough to count as its text field" }),
|
|
580
|
+
] },
|
|
441
581
|
};
|
|
442
|
-
const fangorn = localFangorn(chain
|
|
582
|
+
const fangorn = localFangorn(chain);
|
|
443
583
|
const embedded = [];
|
|
444
584
|
const embed = async (t) => { embedded.push(t); const v = new Array(EMBED_DIM).fill(0); for (const w of t.split(/\W+/)) if (w) v[w.length % EMBED_DIM] += 1; const n = Math.hypot(...v) || 1; return v.map((x) => x / n); };
|
|
445
585
|
const out = mkdtempSync(`${tmpdir()}/view-`);
|
|
@@ -451,45 +591,42 @@ if (typeof process !== "undefined" && import.meta.url === `file://${process.argv
|
|
|
451
591
|
appendFileSync(`${out}/vectors.ndjson`, '["cut sho');
|
|
452
592
|
eq(packVec(await cachedEmbed(`${out}/vectors.ndjson`, embed)("Title: Oak")), packVec(v1), "a line cut short by a kill is skipped, not fatal");
|
|
453
593
|
embedded.length = 0; rmSync(`${out}/vectors.ndjson`);
|
|
454
|
-
const run = () => publishView({ fangorn, app: APP,
|
|
594
|
+
const run = () => publishView({ fangorn, app: APP, schema, out, embed, log: () => {} });
|
|
455
595
|
|
|
456
|
-
const d = domainFor(
|
|
596
|
+
const d = domainFor(PUB, "shop");
|
|
457
597
|
const split = mkdtempSync(`${tmpdir()}/view-`);
|
|
458
|
-
await publishView({ fangorn, app: APP,
|
|
459
|
-
|
|
460
|
-
eq([m.count, m.shards.map((s) => s.count)], [3, [2, 1]], "rows past shardRows go to the next shard (Pages caps a file at 25 MiB)"); }
|
|
598
|
+
await publishView({ fangorn, app: APP, schema, out: split, embed, log: () => {}, shardRows: 2 });
|
|
599
|
+
eq(manifestOf(split, d).shards.map((s) => s.count), [2, 1], "rows past shardRows go to the next shard");
|
|
461
600
|
embedded.length = 0;
|
|
462
601
|
eq((await run())[d], { added: 3, removed: 0 }, "first run embeds everything");
|
|
463
602
|
if (!embedded.includes("Title: Oak table. Tags: . a solid oak dining table")) throw new Error(`declared roles compose the text: ${embedded}`);
|
|
464
|
-
let m =
|
|
603
|
+
let m = manifestOf(out, d);
|
|
465
604
|
eq([m.role_map.identity, m.role_map.title, m.role_map.text, m.description], ["sku", "name", ["about"], "Things for a room."], "the schema is applied");
|
|
466
|
-
eq(m.embedder, EMBEDDER, "the manifest names its encoder");
|
|
605
|
+
eq([m.embedder, m.filter], [EMBEDDER, { owner: [PUB], repo: ["shop"] }], "the manifest names its encoder and its repo");
|
|
467
606
|
|
|
468
|
-
eq(
|
|
607
|
+
eq(await run(), {}, "no head moved → nothing read, nothing embedded");
|
|
469
608
|
eq(embedded.length, 3, "and the embedder was not called");
|
|
470
609
|
|
|
471
|
-
chain[`${PUB}/shop`].
|
|
472
|
-
|
|
610
|
+
chain[`${PUB}/shop`].records = [chain[`${PUB}/shop`].records[0],
|
|
611
|
+
rec("c2b", TAG, { sku: "b2", name: "Lamp", about: "brass reading lamp", price: 2 }, "c2")];
|
|
473
612
|
eq((await run())[d], { added: 1, removed: 2 }, "a changed record is a new CID; removed ones are dropped");
|
|
474
|
-
m =
|
|
613
|
+
m = manifestOf(out, d);
|
|
475
614
|
eq([m.count, m.shards.length, m.tombstones, readdirSync(`${out}/cdn/domains/${d}/shards`).length], [2, 1, [], 1],
|
|
476
615
|
"a removal compacts: the retracted rows' bytes are no longer served anywhere");
|
|
477
616
|
if (embedded.length !== 4) throw new Error("compaction must not re-embed");
|
|
478
|
-
chain[`${PUB}/shop`].
|
|
617
|
+
chain[`${PUB}/shop`].records[1] = rec("c2c", TAG, { sku: "b3", name: "Lamp", about: "steel reading lamp", price: 2 }, "c2");
|
|
479
618
|
eq((await run())[d], { added: 1, removed: 1 }, "a one-for-one replacement: same count, same types");
|
|
480
|
-
m =
|
|
619
|
+
m = manifestOf(out, d);
|
|
481
620
|
eq(m.shards.map((x) => x.file).sort(), readdirSync(`${out}/cdn/domains/${d}/shards`).sort(),
|
|
482
|
-
"and the manifest names the shards
|
|
483
|
-
chain[`${PUB}/shop`].
|
|
621
|
+
"and the manifest names the shards stored, not the ones compaction deleted");
|
|
622
|
+
chain[`${PUB}/shop`].records[1] = rec("c2b", TAG, { sku: "b2", name: "Lamp", about: "brass reading lamp", price: 2 }, "c2");
|
|
484
623
|
await run(); // back to the state the checks below expect
|
|
485
|
-
|
|
486
|
-
chain[`${PUB}/shop`].vertices.push({ cid: "c4", schemaId: TAG, payload: { sku: "d4", name: "Rug", about: "wool rug" } });
|
|
624
|
+
chain[`${PUB}/shop`].records.push(rec("c4", TAG, { sku: "d4", name: "Rug", about: "wool rug" }));
|
|
487
625
|
await run();
|
|
488
|
-
chain[`${PUB}/shop`].
|
|
489
|
-
|
|
490
|
-
eq([m.count, m.shards.length], [3, 2], "an addition alone is a delta shard");
|
|
626
|
+
chain[`${PUB}/shop`].records.pop();
|
|
627
|
+
eq([manifestOf(out, d).count, manifestOf(out, d).shards.length], [3, 2], "an addition alone is a delta shard");
|
|
491
628
|
await run();
|
|
492
|
-
m =
|
|
629
|
+
m = manifestOf(out, d);
|
|
493
630
|
|
|
494
631
|
// The real reader over HTTP: digests checked, tombstones honoured, the publisher's id resolves.
|
|
495
632
|
const srv = createServer((q, s) => { try { s.end(readFileSync(`${out}${decodeURIComponent(q.url)}`)); } catch { s.statusCode = 404; s.end(); } });
|
|
@@ -502,39 +639,132 @@ if (typeof process !== "undefined" && import.meta.url === `file://${process.argv
|
|
|
502
639
|
const { rolesFrom } = await import("../core/roles.js");
|
|
503
640
|
eq(getRow(got, "b2", rolesFrom([m]))?.id, "c2b", "get by the publisher's own id, not the CID");
|
|
504
641
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
await
|
|
509
|
-
eq((
|
|
642
|
+
// The app stops presenting the repo, and its domain stops being served.
|
|
643
|
+
const shop = chain[`${PUB}/shop`];
|
|
644
|
+
delete chain[`${PUB}/shop`];
|
|
645
|
+
eq((await run())[d], { added: 0, removed: 2, dropped: true }, "a repo the app no longer presents is dropped");
|
|
646
|
+
eq([existsSync(`${out}/cdn/domains/${d}`), JSON.parse(readFileSync(`${out}/cdn/catalog`, "utf8")).domains], [false, []], "with its files and its catalog entry");
|
|
647
|
+
chain[`${PUB}/shop`] = shop;
|
|
648
|
+
eq((await run())[d], { added: 2, removed: 0 }, "and presenting it again brings it back");
|
|
649
|
+
|
|
650
|
+
const setEmbedder = (e) => { const x = manifestOf(out, d); x.embedder = e; x.head = "moved"; writeFileSync(`${out}/cdn/domains/${d}/manifest`, JSON.stringify(x)); };
|
|
651
|
+
setEmbedder({ ...EMBEDDER, runtime: "transformers.js cuda" });
|
|
652
|
+
if ((await run())[d]?.error) throw new Error("another runtime of the same q8 model must be accepted");
|
|
653
|
+
setEmbedder({ runtime: "fastembed", dtype: "fp32" });
|
|
654
|
+
if (!/not comparable/.test((await run())[d]?.error)) throw new Error("appending to another encoder's view must be refused");
|
|
655
|
+
|
|
656
|
+
// The encoder gives out partway (a free daily allowance): what it embedded is served, and
|
|
657
|
+
// the next run embeds only the rest, then takes the head.
|
|
658
|
+
const big = mkdtempSync(`${tmpdir()}/view-`), BIG = `${PUB}/big`;
|
|
659
|
+
chain[BIG] = { records: Array.from({ length: 80 }, (_, i) => ({ id: `r${i}`, cid: `big${i}`, type: "shop.item.v1", payload: { sku: `r${i}`, name: `Thing ${i}` } })) };
|
|
660
|
+
let budget = 40;
|
|
661
|
+
const capped = (ts) => (budget -= ts.length) < 0 ? Promise.reject(new Error("daily allowance used")) : Promise.all(ts.map(embed));
|
|
662
|
+
const bd = domainFor(PUB, "big");
|
|
663
|
+
eq((await publishView({ fangorn, app: APP, schema, out: big, repos: ["big"], embedBatch: capped, log: () => {} }))[bd],
|
|
664
|
+
{ added: 32, removed: 0, partial: true }, "an encoder that gives out keeps what it embedded");
|
|
665
|
+
eq([manifestOf(big, bd).count, manifestOf(big, bd).head], [32, null], "served, and the head not taken");
|
|
666
|
+
budget = 1e9;
|
|
667
|
+
eq((await publishView({ fangorn, app: APP, schema, out: big, repos: ["big"], embedBatch: capped, log: () => {} }))[bd],
|
|
668
|
+
{ added: 48, removed: 0 }, "the next run embeds only the rest");
|
|
669
|
+
eq(manifestOf(big, bd).count, 80, "and serves all of it");
|
|
670
|
+
// Killed partway (a Worker over its CPU or wall time: no exception, no return): the last
|
|
671
|
+
// checkpoint is served, and the next run embeds only what came after it.
|
|
672
|
+
chain[BIG] = { records: Array.from({ length: CHECKPOINT + 100 }, (_, i) => ({ id: `k${i}`, cid: `kill${i}`, type: "shop.item.v1", payload: { sku: `k${i}`, name: `Kept ${i}` } })) };
|
|
673
|
+
let left = CHECKPOINT + 64;
|
|
674
|
+
const dies = (ts) => (left -= ts.length) < 0 ? new Promise(() => {}) : Promise.all(ts.map(embed));
|
|
675
|
+
publishView({ fangorn, app: APP, schema, out: big, repos: ["big"], embedBatch: dies, log: () => {} });
|
|
676
|
+
for (let i = 0; i < 200 && manifestOf(big, bd).count < CHECKPOINT; i++) await new Promise((ok) => setTimeout(ok, 10));
|
|
677
|
+
eq((await publishView({ fangorn, app: APP, schema, out: big, repos: ["big"], embedBatch: capped, log: () => {} }))[bd],
|
|
678
|
+
{ added: 100, removed: 80 }, "a killed run resumes from its last checkpoint");
|
|
679
|
+
// A built domain whose head moves reads only the diff: 10 new tracks on top of thousands
|
|
680
|
+
// never read the thousands again, and a removal or an edit drops the version it replaced.
|
|
681
|
+
const whole = fangorn.reads;
|
|
682
|
+
chain[BIG].records.push(...Array.from({ length: 10 }, (_, i) => ({ id: `n${i}`, cid: `next${i}`, type: "shop.item.v1", payload: { sku: `n${i}`, name: `New ${i}` } })));
|
|
683
|
+
chain[BIG].records.splice(0, 1, { ...chain[BIG].records[0], cid: "kill0-v2", payload: { sku: "k0", name: "Kept 0, renamed" } });
|
|
684
|
+
chain[BIG].records.splice(1, 1);
|
|
685
|
+
eq([(await publishView({ fangorn, app: APP, schema, out: big, repos: ["big"], embedBatch: capped, log: () => {} }))[bd], fangorn.reads - whole],
|
|
686
|
+
[{ added: 11, removed: 2 }, 0], "a moved head reads only what changed");
|
|
687
|
+
eq(manifestOf(big, bd).count, CHECKPOINT + 100 + 10 - 1, "and serves the repo as it now stands");
|
|
688
|
+
// Only additions: the rows already served are not read at all, only a delta shard is written.
|
|
689
|
+
const disk = fsStore(big), opened = [];
|
|
690
|
+
const watched = { ...disk, get: (p) => { opened.push(p); return disk.get(p); } };
|
|
691
|
+
const n0 = manifestOf(big, bd).count, shards0 = manifestOf(big, bd).shards.length;
|
|
692
|
+
chain[BIG].records.push(...Array.from({ length: 10 }, (_, i) => ({ id: `m${i}`, cid: `more${i}`, type: "shop.item.v1", payload: { sku: `m${i}`, name: `More ${i}` } })));
|
|
693
|
+
eq((await publishView({ fangorn, app: APP, schema, store: watched, repos: ["big"], embedBatch: capped, log: () => {} }))[bd],
|
|
694
|
+
{ added: 10, removed: 0 }, "an append embeds only the new records");
|
|
695
|
+
eq(opened.filter((p) => p.includes("/shards/")), [], "and reads no shard it already served");
|
|
696
|
+
const after = manifestOf(big, bd);
|
|
697
|
+
eq([after.count, after.shards.length, after.entity_types[0].count, after.ahead], [n0 + 10, shards0 + 1, n0 + 10, undefined], "counts, types and one delta shard, with the head taken");
|
|
698
|
+
eq((await liveFields(big)).length, n0 + 10, "and every row is still served");
|
|
699
|
+
// An append killed after a checkpoint: the next run reads only that checkpoint's shard, and
|
|
700
|
+
// embeds only what came after it, never the checkpointed rows a second time.
|
|
701
|
+
const n1 = manifestOf(big, bd).count, fit = manifestOf(big, bd).coverage.fit_at, extra = CHECKPOINT + 2;
|
|
702
|
+
if (n1 + extra >= COVERAGE_REFIT * fit) throw new Error(`test setup: ${n1} + ${extra} rows would refit coverage (fit at ${fit}), not append`);
|
|
703
|
+
chain[BIG].records.push(...Array.from({ length: extra }, (_, i) => ({ id: `a${i}`, cid: `app${i}`, type: "shop.item.v1", payload: { sku: `a${i}`, name: `Appended ${i}` } })));
|
|
704
|
+
left = CHECKPOINT + 1;
|
|
705
|
+
publishView({ fangorn, app: APP, schema, out: big, repos: ["big"], embedBatch: dies, log: () => {} });
|
|
706
|
+
for (let i = 0; i < 300 && !manifestOf(big, bd).ahead; i++) await new Promise((ok) => setTimeout(ok, 10));
|
|
707
|
+
eq([manifestOf(big, bd).count, manifestOf(big, bd).ahead?.length], [n1 + CHECKPOINT, 1], "a killed append serves its checkpoint, named as ahead of the head");
|
|
708
|
+
opened.length = 0;
|
|
709
|
+
eq((await publishView({ fangorn, app: APP, schema, store: watched, repos: ["big"], embedBatch: capped, log: () => {} }))[bd],
|
|
710
|
+
{ added: 2, removed: 0 }, "and the next run embeds only what came after it");
|
|
711
|
+
eq([opened.filter((p) => p.includes("/shards/")).length, manifestOf(big, bd).count, (await liveFields(big)).length],
|
|
712
|
+
[1, n1 + extra, n1 + extra], "reading only the checkpoint's shard, and serving each row once");
|
|
713
|
+
delete chain[BIG];
|
|
714
|
+
rmSync(big, { recursive: true, force: true });
|
|
715
|
+
eq((await publishView({ fangorn, app: APP, schema, out, embed, rebake: true, log: () => {} }))[d],
|
|
510
716
|
{ added: 2, removed: 0 }, "--rebake starts the domain over");
|
|
511
717
|
eq(readdirSync(`${out}/cdn/domains/${d}/shards`).length, 1, "and leaves no orphaned shards");
|
|
512
718
|
rmSync(out, { recursive: true, force: true });
|
|
513
719
|
|
|
514
|
-
// Relations: a declared
|
|
515
|
-
// undeclared one
|
|
720
|
+
// Relations: a declared reference lands on its source row as the target's identity; an
|
|
721
|
+
// undeclared one is left alone; a target whose identity moves relinks the row without embedding it.
|
|
516
722
|
{
|
|
517
|
-
const T =
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
[`${PUB}/civic`]: {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
|
|
723
|
+
const T = "civic.item.v1", M = "civic.matter.v1";
|
|
724
|
+
const relSchema = { types: { [T]: { role_map: { identity: "item_id", title: "heading", thread: "part_of" } },
|
|
725
|
+
[M]: { role_map: { identity: "matter_id", title: "title" } } },
|
|
726
|
+
relations: [{ rel: "part_of", from: T, to: M }] };
|
|
727
|
+
const rel = {
|
|
728
|
+
[`${PUB}/civic`]: { records: [
|
|
729
|
+
rec("i1", T, { item_id: "a#1", heading: "Rezone, committee", part_of: { local: "m" } }),
|
|
730
|
+
rec("i2", T, { item_id: "b#4", heading: "Rezone, council", cites: { local: "m" } }),
|
|
731
|
+
rec("m1", M, { matter_id: "plover:7", title: "Rezone" }, "m")] } };
|
|
732
|
+
const o = mkdtempSync(`${tmpdir()}/view-`), dr = domainFor(PUB, "civic");
|
|
733
|
+
const go = () => publishView({ fangorn: localFangorn(rel), app: APP, schema: relSchema, out: o, embed, log: () => {} });
|
|
734
|
+
const rows = async () => Object.fromEntries([...(await loadDomain(fsStore(o), `cdn/domains/${dr}`)).rows.values()]
|
|
735
|
+
.map((r) => [r.fields.heading ?? r.fields.title, r.fields.part_of ?? r.fields.cites ?? null]));
|
|
530
736
|
await go();
|
|
531
|
-
eq(
|
|
532
|
-
eq(
|
|
533
|
-
rel[`${PUB}/civic`].
|
|
737
|
+
eq(await rows(), { "Rezone, committee": "plover:7", "Rezone, council": { local: "m" }, Rezone: null }, "a declared reference carries the target's identity; an undeclared one is left alone");
|
|
738
|
+
eq(manifestOf(o, dr).role_map.thread, "part_of", "the thread role reaches the manifest");
|
|
739
|
+
rel[`${PUB}/civic`].records[2] = rec("m1b", M, { matter_id: "plover:8", title: "Rezone" }, "m");
|
|
534
740
|
const before = embedded.length;
|
|
535
|
-
eq((await go())[dr], { added:
|
|
536
|
-
eq([
|
|
741
|
+
eq((await go())[dr], { added: 1, removed: 1, relinked: 1 }, "a target whose identity moves relinks the row that points at it");
|
|
742
|
+
eq([(await rows())["Rezone, committee"], embedded.length - before], ["plover:8", 1], "without embedding it again");
|
|
537
743
|
rmSync(o, { recursive: true, force: true });
|
|
538
744
|
}
|
|
539
|
-
|
|
745
|
+
|
|
746
|
+
// The R2 store: the same four calls over a bucket's flat keys.
|
|
747
|
+
{
|
|
748
|
+
const objs = new Map();
|
|
749
|
+
const bucket = {
|
|
750
|
+
get: async (k) => objs.has(k) ? { arrayBuffer: async () => objs.get(k) } : null,
|
|
751
|
+
head: async (k) => objs.has(k) ? {} : null,
|
|
752
|
+
put: async (k, b) => { objs.set(k, typeof b === "string" ? new TextEncoder().encode(b) : b); },
|
|
753
|
+
delete: async (ks) => { for (const k of [ks].flat()) objs.delete(k); },
|
|
754
|
+
list: async ({ prefix, delimiter }) => {
|
|
755
|
+
const ks = [...objs.keys()].filter((k) => k.startsWith(prefix)).sort();
|
|
756
|
+
if (!delimiter) return { objects: ks.map((key) => ({ key })), truncated: false };
|
|
757
|
+
const deep = new Set(ks.filter((k) => k.slice(prefix.length).includes("/")).map((k) => prefix + k.slice(prefix.length).split("/")[0] + "/"));
|
|
758
|
+
return { objects: ks.filter((k) => !k.slice(prefix.length).includes("/")).map((key) => ({ key })), delimitedPrefixes: [...deep], truncated: false };
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
const r2 = r2Store(bucket);
|
|
762
|
+
const via = await publishView({ fangorn, app: APP, schema, store: r2, embed, log: () => {} });
|
|
763
|
+
eq(Object.keys(via), [d], "a view builds into a bucket");
|
|
764
|
+
eq(await r2.list("cdn/domains"), [d], "list names a folder's children");
|
|
765
|
+
eq((await liveFields(r2)).length, 2, "and reads back");
|
|
766
|
+
await r2.del(`cdn/domains/${d}`);
|
|
767
|
+
eq([...objs.keys()].filter((k) => k.includes(d)), [], "del removes a folder and everything under it");
|
|
768
|
+
}
|
|
769
|
+
console.log("view.js self-check ok — domain names and embedded text are stable, the schema is applied, an unmoved head reads nothing, additions ship as delta shards, removals compact the domain, an excluded repo is dropped and a restored one returns, the real reader sees exactly the live records by the publisher's id, another encoder's view is refused, declared references land on their source rows, and the view builds the same into R2");
|
|
540
770
|
}
|