@fangorn-network/westmarch 0.0.20-dev → 2026.10.2-2.0-dev

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.
Files changed (327) hide show
  1. package/README.md +33 -3
  2. package/docs/app-to-agent.md +117 -60
  3. package/package.json +10 -5
  4. package/plugin/scripts/fx.mjs +7 -9
  5. package/site/dist/assets/{AccountNotFoundScreen-C4njXc69-BOfmiSH1.js → AccountNotFoundScreen-C4njXc69-DyVH4CEz.js} +1 -1
  6. package/site/dist/assets/{AddFundsSelectionScreen-RAoCn7ej-D_EsIeJu.js → AddFundsSelectionScreen-RAoCn7ej-Dz1--SS9.js} +1 -1
  7. package/site/dist/assets/{Address-H8kD1d9O-C85sJ79B.js → Address-H8kD1d9O-BdFZmRwI.js} +1 -1
  8. package/site/dist/assets/{AffirmativeConsentScreen-BG0MJxku-DdEPFOww.js → AffirmativeConsentScreen-BG0MJxku-CxaCAisL.js} +1 -1
  9. package/site/dist/assets/{AllowlistRejectionScreen-Cae6e1Od-DhVy9iGN.js → AllowlistRejectionScreen-Cae6e1Od-JGk7vndN.js} +1 -1
  10. package/site/dist/assets/{ApiController-CYdiyu9B.js → ApiController-CTLbMCcH.js} +4 -4
  11. package/site/dist/assets/{ArrowsRightLeftIcon-cp60yobq.js → ArrowsRightLeftIcon-B8g9adpz.js} +1 -1
  12. package/site/dist/assets/{AuthenticateWithWalletScreen-CU6hIcFT-8rbYiYYE.js → AuthenticateWithWalletScreen-CU6hIcFT-CXD9_OP8.js} +1 -1
  13. package/site/dist/assets/{AwaitingEvmToSolBridgingScreen-D0e_sZMS-2pkNfCno.js → AwaitingEvmToSolBridgingScreen-D0e_sZMS-BtJCglvU.js} +1 -1
  14. package/site/dist/assets/{AwaitingExternalEthereumTransferScreen-ur5HL4R7-7AQt25_k.js → AwaitingExternalEthereumTransferScreen-ur5HL4R7-BgC2TsqH.js} +1 -1
  15. package/site/dist/assets/{AwaitingPasswordlessCodeScreen-DRHz76mM-BZY30TGs.js → AwaitingPasswordlessCodeScreen-DRHz76mM-B9OzLeEO.js} +1 -1
  16. package/site/dist/assets/{AwaitingSolToEvmBridgingScreen-CdLRrGD2-BILOOLi_.js → AwaitingSolToEvmBridgingScreen-CdLRrGD2-C7r5snUL.js} +1 -1
  17. package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-BTV0NRio.js +68 -0
  18. package/site/dist/assets/{CaptchaScreen-DbpvkIGx-D8eDcAkc.js → CaptchaScreen-DbpvkIGx-7rSZIuM-.js} +1 -1
  19. package/site/dist/assets/{CheckCircleIcon-NCOQc0Gs.js → CheckCircleIcon-B1l17ioc.js} +1 -1
  20. package/site/dist/assets/{CheckCircleIcon-BM409mGI.js → CheckCircleIcon-BGSI8nKf.js} +1 -1
  21. package/site/dist/assets/{Checkbox-D1EDeo41-rHlvsZzP.js → Checkbox-D1EDeo41-BUfq0w99.js} +1 -1
  22. package/site/dist/assets/{ChevronDownIcon-CW-4MSpE.js → ChevronDownIcon-B6uJvPHu.js} +1 -1
  23. package/site/dist/assets/{Chip-CZKIKt9K-CutJG010.js → Chip-CZKIKt9K-r9Nk__0-.js} +1 -1
  24. package/site/dist/assets/{CoinbaseOnrampStatusScreen-BbyCJDXL-DLs9zX-Q.js → CoinbaseOnrampStatusScreen-BbyCJDXL-DWWNHxmR.js} +1 -1
  25. package/site/dist/assets/{ConnectEmailForm-B-VRE0MN-N9J6T_YL.js → ConnectEmailForm-B-VRE0MN-CRQwS0Xu.js} +1 -1
  26. package/site/dist/assets/{ConnectLedgerScreen-DWHABGot-h0uYIUi6.js → ConnectLedgerScreen-DWHABGot-CZcCt31O.js} +1 -1
  27. package/site/dist/assets/{ConnectOnlyLandingScreen-CZYx9Bmp-LeuDMN1E.js → ConnectOnlyLandingScreen-CZYx9Bmp-C5IOf-SQ.js} +1 -1
  28. package/site/dist/assets/{ConnectOnlyStatusScreen-DCRzjxHx-DaRa59tZ.js → ConnectOnlyStatusScreen-DCRzjxHx-cwg74F3A.js} +1 -1
  29. package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-D28oeK6b.js +1 -0
  30. package/site/dist/assets/{ConnectPhoneForm-Dek5xn4C-BEA4FmhI.js → ConnectPhoneForm-Dek5xn4C-EEdkKeTp.js} +1 -1
  31. package/site/dist/assets/ConnectWalletView-DqI-PTvX-C2mEzukj.js +74 -0
  32. package/site/dist/assets/{ConnectionStatusScreen-fUfROm9m-BeWLchcf.js → ConnectionStatusScreen-fUfROm9m-CH_7r8iV.js} +1 -1
  33. package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-Z33kis0B.js +1 -0
  34. package/site/dist/assets/{ConnectorUtil-Cq4lWnfe.js → ConnectorUtil-DH4NePk5.js} +1 -1
  35. package/site/dist/assets/{CopyToClipboard-i_OQSBJr-DujUHNzW.js → CopyToClipboard-i_OQSBJr-_G88JCVR.js} +1 -1
  36. package/site/dist/assets/{CopyableText-CQapvaMr-DMIdpDaU.js → CopyableText-CQapvaMr-CGGfc_zM.js} +1 -1
  37. package/site/dist/assets/{CrossAppAuthScreen-CLw7ymZq-Csju14vF.js → CrossAppAuthScreen-CLw7ymZq-C9Okoc0V.js} +1 -1
  38. package/site/dist/assets/{CustomLandingScreenView-Cm_MBSyX-CBOrbNLo.js → CustomLandingScreenView-Cm_MBSyX-D2l6hdSl.js} +1 -1
  39. package/site/dist/assets/{DelegatedActionsConsentScreen-DYF2U4y5-BL5hzNUI.js → DelegatedActionsConsentScreen-DYF2U4y5-Dda3KKqS.js} +1 -1
  40. package/site/dist/assets/{DelegatedActionsRevokeScreen-bPp6Uwyh-fcO51q5H.js → DelegatedActionsRevokeScreen-bPp6Uwyh-DG8kJPt1.js} +1 -1
  41. package/site/dist/assets/{DepositAddressAddressStepView-4vdkb55L-B3vY888G.js → DepositAddressAddressStepView-4vdkb55L-CcOUfn6f.js} +1 -1
  42. package/site/dist/assets/{DepositAddressScreen-DBLRs7aI-DmdQaFI2.js → DepositAddressScreen-DBLRs7aI-DeituDNh.js} +1 -1
  43. package/site/dist/assets/{DepositAddressV3Screen-CDqIEMQw-BoupRyfV.js → DepositAddressV3Screen-CDqIEMQw-FDg6K14-.js} +1 -1
  44. package/site/dist/assets/{EmailInputForm-Cqo1mda8-B2NG3K5V.js → EmailInputForm-Cqo1mda8-BmbxvPHC.js} +1 -1
  45. package/site/dist/assets/{EmbeddedWalletConnectingScreen-D4T7DUzs-D_AVgO4s.js → EmbeddedWalletConnectingScreen-D4T7DUzs-B05CQ9rx.js} +1 -1
  46. package/site/dist/assets/{EmbeddedWalletCreatedScreen-D7WhW5ve-CLsJbPYQ.js → EmbeddedWalletCreatedScreen-D7WhW5ve-Cxd1b9XH.js} +1 -1
  47. package/site/dist/assets/{EmbeddedWalletKeyExportScreen-CqV4NYQ3-C9sAExzE.js → EmbeddedWalletKeyExportScreen-CqV4NYQ3-CrJy0ViX.js} +1 -1
  48. package/site/dist/assets/{EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-DxaWAAKa.js → EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-Bakaa3zu.js} +1 -1
  49. package/site/dist/assets/{EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-CxrfLr5q.js → EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-j78vKrcj.js} +1 -1
  50. package/site/dist/assets/{EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-DHZRGoTN.js → EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-D3dr7W9_.js} +1 -1
  51. package/site/dist/assets/{EnrollTotp-Dibew4dp-SA2pvNEc.js → EnrollTotp-Dibew4dp-BXbYV8nI.js} +1 -1
  52. package/site/dist/assets/{EnvelopeIcon-Dh2vSVj4.js → EnvelopeIcon-DjPofnRI.js} +1 -1
  53. package/site/dist/assets/{ErrorBanner-BcpGRt0h-BKOYTyCi.js → ErrorBanner-BcpGRt0h-BhH6_V-9.js} +1 -1
  54. package/site/dist/assets/{ErrorMessage-D8VaAP5m-CrZOnUyg.js → ErrorMessage-D8VaAP5m-Bs7uX2a9.js} +1 -1
  55. package/site/dist/assets/{ErrorScreen-DipdTWzE-c8g8En21.js → ErrorScreen-DipdTWzE-BgPRgnOb.js} +1 -1
  56. package/site/dist/assets/ErrorScreen-DipdTWzE-CvxXC6pz.js +1 -0
  57. package/site/dist/assets/{ExclamationCircleIcon-DUA2wMfO.js → ExclamationCircleIcon-BE-WGrut.js} +1 -1
  58. package/site/dist/assets/{ExclamationTriangleIcon-eZfaqcFq.js → ExclamationTriangleIcon-Ca1Xlc_f.js} +1 -1
  59. package/site/dist/assets/{FarcasterConnectStatusScreen-51tjUxqq-s_s5M2CZ.js → FarcasterConnectStatusScreen-51tjUxqq-De2JvZdV.js} +1 -1
  60. package/site/dist/assets/{FarcasterSignerStatusScreen-6x-Uh95G-BHBR4k8H.js → FarcasterSignerStatusScreen-6x-Uh95G-bLp3hXoO.js} +1 -1
  61. package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-Jr6D6JiZ.js +491 -0
  62. package/site/dist/assets/{FingerPrintIcon-Dm6kxmg7.js → FingerPrintIcon-DOCbS93y.js} +1 -1
  63. package/site/dist/assets/{FundSolWalletWithExternalSolanaWallet-WtLwYaOl-B8TRV36j.js → FundSolWalletWithExternalSolanaWallet-WtLwYaOl-A_G5kfa1.js} +1 -1
  64. package/site/dist/assets/{FundWalletMethodHeader-BWNkokCT-BQdbJ5Bb.js → FundWalletMethodHeader-BWNkokCT-LPv35wkD.js} +1 -1
  65. package/site/dist/assets/{FundWithBankDepositScreen-DajkHxBD-9rPtyA3d.js → FundWithBankDepositScreen-DajkHxBD-0hfGrfvf.js} +1 -1
  66. package/site/dist/assets/{FundingEditAmountScreen-CpvpiO4M-GaXigGI9.js → FundingEditAmountScreen-CpvpiO4M-Dz0s_NWR.js} +1 -1
  67. package/site/dist/assets/{FundingMethodSelectionScreen-Db7vhRlE-UsmtTqv4.js → FundingMethodSelectionScreen-Db7vhRlE-ZGm-3-Io.js} +1 -1
  68. package/site/dist/assets/{GlobeAltIcon-vqrVV5ho.js → GlobeAltIcon-BBRl2MMn.js} +1 -1
  69. package/site/dist/assets/{GooglePay-B53WnudL-BX5_WtCx.js → GooglePay-B53WnudL-BGiVpB8y.js} +1 -1
  70. package/site/dist/assets/{HCaptchaWrapper-CUi9LJsp-D-I8j7NO.js → HCaptchaWrapper-CUi9LJsp-CHdfIeD_.js} +3 -3
  71. package/site/dist/assets/{InAppBrowserLoginNotPossible-BMR9br1T-BY3j50El.js → InAppBrowserLoginNotPossible-BMR9br1T-Cgoiow3m.js} +1 -1
  72. package/site/dist/assets/{InfoBanner-Cb3p1z12-DLBl_Ywy.js → InfoBanner-Cb3p1z12-_YqQmRiu.js} +1 -1
  73. package/site/dist/assets/{InjectedWalletIcon-DLcYOGDj-DLSV_NjP.js → InjectedWalletIcon-DLcYOGDj-tuIrKQHH.js} +1 -1
  74. package/site/dist/assets/{InstallWalletScreen-DTA8dwLv-CxreyXoD.js → InstallWalletScreen-DTA8dwLv-BMU_wa61.js} +1 -1
  75. package/site/dist/assets/{JsonTree-BHzNC-ic-DcNcbSoM.js → JsonTree-BHzNC-ic-DcVr0YF0.js} +1 -1
  76. package/site/dist/assets/{LabelXs-oqZNqbm_-iafrj25N.js → LabelXs-oqZNqbm_-CNprGVQM.js} +1 -1
  77. package/site/dist/assets/LandingScreen-CGWUoSUG-fH6rsQen.js +1 -0
  78. package/site/dist/assets/{Layouts-BMRfo5hw-CeCOyCLK.js → Layouts-BMRfo5hw-e-sepi6F.js} +1 -1
  79. package/site/dist/assets/{Link-BdDilT2T-BEDEdHuS.js → Link-BdDilT2T-BdwlRJch.js} +1 -1
  80. package/site/dist/assets/{LinkConflictScreen-C3ZEPsgl-ClWoqvEL.js → LinkConflictScreen-C3ZEPsgl-BXKN2Qm5.js} +1 -1
  81. package/site/dist/assets/LinkEmailScreen-dKo9frwc-Cyn7Q8p1.js +1 -0
  82. package/site/dist/assets/{LinkPasskeyScreen-zbrl_AI_-D72Gb0LW.js → LinkPasskeyScreen-zbrl_AI_-CWiqnwLM.js} +1 -1
  83. package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-YmDevJzf.js +1 -0
  84. package/site/dist/assets/{LinkPhoneScreen-oIUpfj0w-C0DuD14j.js → LinkPhoneScreen-oIUpfj0w-h7shW-Ay.js} +1 -1
  85. package/site/dist/assets/{Loader-UAZqxsnw-Cy7hEE2f.js → Loader-UAZqxsnw-DJEVblm1.js} +1 -1
  86. package/site/dist/assets/{LoadingSkeleton-BMsgO5PV-CiEyS5PZ.js → LoadingSkeleton-BMsgO5PV-D2atjsP5.js} +1 -1
  87. package/site/dist/assets/{LockClosedIcon-BgY_5v_Z.js → LockClosedIcon-9EFzWK1C.js} +1 -1
  88. package/site/dist/assets/{LoginFailedScreen-CN0dJV6U-DhUMiJZJ.js → LoginFailedScreen-CN0dJV6U-By3Si77a.js} +1 -1
  89. package/site/dist/assets/{ManualTransferScreen-CHqcuqbs-tXiZDHmN.js → ManualTransferScreen-CHqcuqbs-BAHPY8rs.js} +1 -1
  90. package/site/dist/assets/{MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CaW7xeCC.js → MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CLjffFG1.js} +1 -1
  91. package/site/dist/assets/{MfaAuthVerifyFlowScreen-DbFpyWMy-CeRqvx8k.js → MfaAuthVerifyFlowScreen-DbFpyWMy-BmQg1Y61.js} +1 -1
  92. package/site/dist/assets/{MfaEnrollmentFlowScreen-DhLnGgnr-BkN1k4lh.js → MfaEnrollmentFlowScreen-DhLnGgnr-DmBtTtlw.js} +1 -1
  93. package/site/dist/assets/{MfaVerifyFlowScreen-C_XPjMRP-CXSqLVbE.js → MfaVerifyFlowScreen-C_XPjMRP-4a683Bmf.js} +1 -1
  94. package/site/dist/assets/{ModalFooter-BCuEK_nz-c-khvG4Y.js → ModalFooter-BCuEK_nz-C_71qsaz.js} +1 -1
  95. package/site/dist/assets/{MoonpayStatusScreen-CzTUtokc-BRFN2eXr.js → MoonpayStatusScreen-CzTUtokc-DEHHcEf8.js} +1 -1
  96. package/site/dist/assets/{OAuthStatusScreen-ufbOa-da-CeiGcSia.js → OAuthStatusScreen-ufbOa-da-BjAb_WsY.js} +1 -1
  97. package/site/dist/assets/{OpenLink-CUpJ1mOr-DfAvJRyK.js → OpenLink-CUpJ1mOr-BLWFsPOW.js} +1 -1
  98. package/site/dist/assets/{PasskeySelectSignupOrLogin-xh2ScC_l-CIf5I7wU.js → PasskeySelectSignupOrLogin-xh2ScC_l-2PJTJxX-.js} +1 -1
  99. package/site/dist/assets/{PasskeyStatusScreen-Bla8MaOk-CLoL7QC-.js → PasskeyStatusScreen-Bla8MaOk-DCEg761l.js} +1 -1
  100. package/site/dist/assets/{PasswordRecoveryScreen-awGXZkQk-B9F67IYV.js → PasswordRecoveryScreen-awGXZkQk-8nVjNc8k.js} +1 -1
  101. package/site/dist/assets/{PhoneIcon-CvIiPwYQ.js → PhoneIcon-CZ47md80.js} +1 -1
  102. package/site/dist/assets/{PinInput-DbZ0b1i1-CDXZBdQx.js → PinInput-DbZ0b1i1-BrTXu3Km.js} +1 -1
  103. package/site/dist/assets/{PrivyPluginContext-DA-yS5vO-CtQWBiV2.js → PrivyPluginContext-DA-yS5vO-CY60yubk.js} +1 -1
  104. package/site/dist/assets/{QrCode-DbnZwlkH-h6Bhoe5U.js → QrCode-DbnZwlkH-CcMrWV5D.js} +2 -2
  105. package/site/dist/assets/{RecoveryOAuthStatusScreen-CsgtTI2x-D-A5P3PX.js → RecoveryOAuthStatusScreen-CsgtTI2x-DgeDnHOK.js} +1 -1
  106. package/site/dist/assets/{RecoveryPasswordCreateScreen-Be-qJ-LU-C8lTSeSr.js → RecoveryPasswordCreateScreen-Be-qJ-LU-CvJJwLhK.js} +1 -1
  107. package/site/dist/assets/{RecoverySelectionScreen-D1iGtpmi-P5P3plkH.js → RecoverySelectionScreen-D1iGtpmi-CADtGtSV.js} +1 -1
  108. package/site/dist/assets/{SIWXUtil-TUCxa7aA.js → SIWXUtil-Cpiv91-M.js} +1 -1
  109. package/site/dist/assets/{Screen-D30meWsY-CCeVNuWs.js → Screen-D30meWsY-vEusJ7KX.js} +1 -1
  110. package/site/dist/assets/{ScreenHeader-CHmc4-Lu-DjyrH7IK.js → ScreenHeader-CHmc4-Lu-BvGTLg53.js} +1 -1
  111. package/site/dist/assets/{ScreenLayout-OwaQQcxH-CrQM6kvA.js → ScreenLayout-OwaQQcxH-cfOTmyl0.js} +1 -1
  112. package/site/dist/assets/{SelectSourceAsset-CWFog6gm-D7yTw87h.js → SelectSourceAsset-CWFog6gm-DxYklZYs.js} +1 -1
  113. package/site/dist/assets/{SetAutomaticRecoveryScreen-BO_OEqAZ-f0iDIGmw.js → SetAutomaticRecoveryScreen-BO_OEqAZ-oeqeo05x.js} +1 -1
  114. package/site/dist/assets/{SetWalletPasswordForm-DwCTVtAh-kq1zvA7d.js → SetWalletPasswordForm-DwCTVtAh-BxFee9UA.js} +1 -1
  115. package/site/dist/assets/{ShieldCheckIcon-o6RHYev1.js → ShieldCheckIcon-eFjsosCA.js} +1 -1
  116. package/site/dist/assets/{SignRequestScreen-BHKv32KR-DDpMYyJ1.js → SignRequestScreen-BHKv32KR-z8ixfIma.js} +1 -1
  117. package/site/dist/assets/{SolanaWalletConnector-C7Lqk-Wz-DPwhuzcI.js → SolanaWalletConnector-C7Lqk-Wz-Bc_lvdiF.js} +1 -1
  118. package/site/dist/assets/{StackedContainer-B2vaEl56-eQCM2cmv.js → StackedContainer-B2vaEl56-Do5dXB8-.js} +1 -1
  119. package/site/dist/assets/{StandardSignAndSendTransactionScreen-CfhIXVtd-BJ-onCjv.js → StandardSignAndSendTransactionScreen-CfhIXVtd-DudetZAc.js} +1 -1
  120. package/site/dist/assets/{Subtitle-CV-2yKE4-zxWw2m1Q.js → Subtitle-CV-2yKE4-DV0o1av3.js} +1 -1
  121. package/site/dist/assets/{TelegramAuthScreen-UL9zLfO8-NWlMgNRX.js → TelegramAuthScreen-UL9zLfO8-Bo3sONvf.js} +1 -1
  122. package/site/dist/assets/{Title-BnzYV3Is-B_JpTatT.js → Title-BnzYV3Is-C3qOPnO_.js} +1 -1
  123. package/site/dist/assets/{TodoList-DnyULl18-C8Eyy28X.js → TodoList-DnyULl18-BvxvubbF.js} +1 -1
  124. package/site/dist/assets/{TransactionDetails-e9nesl30-C4MpIHR-.js → TransactionDetails-e9nesl30-WWflkOIi.js} +1 -1
  125. package/site/dist/assets/{TransactionErrorView-BDBNqA_f-p1T9WVMx.js → TransactionErrorView-BDBNqA_f-CotYKcgf.js} +1 -1
  126. package/site/dist/assets/{TransferFromWalletScreen-Qvn1Yuno-88IQcRnY.js → TransferFromWalletScreen-Qvn1Yuno-sVFlWqa8.js} +1 -1
  127. package/site/dist/assets/{TransferOrBridgeLoadingScreen-Dl1I_iIz-1fDAIxAf.js → TransferOrBridgeLoadingScreen-Dl1I_iIz-CV2-mY19.js} +1 -1
  128. package/site/dist/assets/{TurnstileWrapper-B7sNddys-Jwy1tJtm.js → TurnstileWrapper-B7sNddys-oPbuMTQt.js} +1 -1
  129. package/site/dist/assets/{UpdateEmailScreen-DVOhPV0H-Jj2DQW8D.js → UpdateEmailScreen-DVOhPV0H-D_mjSpAy.js} +1 -1
  130. package/site/dist/assets/{UpdatePhoneScreen-CXRswTNZ-DwYIUBqi.js → UpdatePhoneScreen-CXRswTNZ-6RLG0ahv.js} +1 -1
  131. package/site/dist/assets/{UserLimitReachedScreen-5we2DHn8-D7m70C4b.js → UserLimitReachedScreen-5we2DHn8-CaVN0bti.js} +1 -1
  132. package/site/dist/assets/{Value-DTgR824E-BRuIGiec.js → Value-DTgR824E-Bp6WGZZm.js} +1 -1
  133. package/site/dist/assets/{WalletCards-DH1rqayz-Bgp9Fsgs.js → WalletCards-DH1rqayz-D3cOTb9x.js} +1 -1
  134. package/site/dist/assets/{WalletIcon-gKf0gTw6.js → WalletIcon-DOIZhb2l.js} +1 -1
  135. package/site/dist/assets/{WalletInfoCard-sq_2wgFg-DpNSAGte.js → WalletInfoCard-sq_2wgFg-bOqfl5MA.js} +1 -1
  136. package/site/dist/assets/{WalletInterstitialScreen-Sh7LfsAh-DG-RuO-F.js → WalletInterstitialScreen-Sh7LfsAh-DWaVkMCA.js} +1 -1
  137. package/site/dist/assets/{WalletLink-DK5LB5Ha-DHaL350n.js → WalletLink-DK5LB5Ha-DjUZvqs-.js} +1 -1
  138. package/site/dist/assets/WalletOverflowButton-bfCnp-9T-B6P7DQXA.js +1 -0
  139. package/site/dist/assets/{WarningBanner-ZZqCEtZK-2eU4Qo74.js → WarningBanner-ZZqCEtZK-Bc2YKZb7.js} +1 -1
  140. package/site/dist/assets/__vite-browser-external-NciY4Vu1.js +1 -0
  141. package/site/dist/assets/{_esm-BdeM6pxJ.js → _esm-BZP3cXTv.js} +1 -1
  142. package/site/dist/assets/_esm-ZmzBJpEs.js +6 -0
  143. package/site/dist/assets/{accounts-D96UKWkC.js → accounts-D4AXF17G.js} +1 -1
  144. package/site/dist/assets/accounts-IkdFx9nq.js +1 -0
  145. package/site/dist/assets/{baseSepolia-DX-qGsik.js → baseSepolia-Bn1j1Ysw.js} +1 -1
  146. package/site/dist/assets/{basic-oIwwSyu4.js → basic-Cz1YiCSk.js} +4 -4
  147. package/site/dist/assets/{ccip-BZvRTL6g.js → ccip-B1J3oTZx.js} +2 -2
  148. package/site/dist/assets/ccip-Bd7261zy.js +1 -0
  149. package/site/dist/assets/{ccip-65sIfSPR.js → ccip-D6EHKpjI.js} +1 -1
  150. package/site/dist/assets/{chain-DM3oH6Px.js → chain-scefkYIH.js} +3 -6
  151. package/site/dist/assets/check-DHnllI8N.js +1 -0
  152. package/site/dist/assets/{chevron-down-BSsbXkK-.js → chevron-down-1y-r4bGO.js} +1 -1
  153. package/site/dist/assets/{circle-alert-DWaeLZNj.js → circle-alert-OwxamXUX.js} +1 -1
  154. package/site/dist/assets/{circle-check-big-Chb1ivw9.js → circle-check-big-3sZGVC10.js} +1 -1
  155. package/site/dist/assets/{circle-x-AV9Cet0R.js → circle-x-DQjNUV9J.js} +1 -1
  156. package/site/dist/assets/client-B-BpRtwY.js +1 -0
  157. package/site/dist/assets/client-BfIR1FvH.js +9 -0
  158. package/site/dist/assets/coinbase-DCPC2L_R-YEuIn3jh.js +3 -0
  159. package/site/dist/assets/{copy-Ckr0AkL6.js → copy-DMOX_yn7.js} +1 -1
  160. package/site/dist/assets/core-GIEmxmAa.js +12 -0
  161. package/site/dist/assets/{createLucideIcon-IPW50ckY.js → createLucideIcon-BsERoFTA.js} +1 -1
  162. package/site/dist/assets/{defineChain-CiuCXUZW.js → defineChain-B-SQDLTG.js} +1 -1
  163. package/site/dist/assets/{dijkstra-Drfl93V4.js → dijkstra-CY95afk4.js} +1 -1
  164. package/site/dist/assets/dist-Cp6IXeJd.js +1 -0
  165. package/site/dist/assets/{dist-Cm7pNAtL.js → dist-CrNxmdZj.js} +1 -1
  166. package/site/dist/assets/{dist-DnGANz51.js → dist-DYGtf4F0.js} +4 -4
  167. package/site/dist/assets/embed.worker-Cnl4VGr3.js +1 -0
  168. package/site/dist/assets/{errors-BQ3N-RVt-BLqjTPNR.js → errors-BQ3N-RVt-CZPAWCgC.js} +1 -1
  169. package/site/dist/assets/{esm-B5cbIQaz.js → esm-Co94ENUV.js} +17 -17
  170. package/site/dist/assets/{esm-ByALO6mA.js → esm-bb8SANZh.js} +1 -1
  171. package/site/dist/assets/{ethers-zTjbodk5-wHhL_BQ1.js → ethers-zTjbodk5-Cs648CnD.js} +1 -1
  172. package/site/dist/assets/{eventemitter3-CL76jPsW.js → eventemitter3-Dzc_0Uq2.js} +1 -1
  173. package/site/dist/assets/{external-link-D-uehwM2.js → external-link-BBPG3rG7.js} +1 -1
  174. package/site/dist/assets/{farcaster-DPlSjvF5-BqTvDXBR.js → farcaster-DPlSjvF5-CzBCmgyL.js} +1 -1
  175. package/site/dist/assets/{features-DQ0UYy6K.js → features-CuiHbgEJ.js} +1 -1
  176. package/site/dist/assets/{fingerprint-pattern-D8ytA16Q.js → fingerprint-pattern-CjLtyIxB.js} +1 -1
  177. package/site/dist/assets/floating-ui.react-DmaCfayr.js +1 -0
  178. package/site/dist/assets/{floating-ui.react-dom-DKWOL9UG.js → floating-ui.react-dom-CaoMF8mP.js} +1 -1
  179. package/site/dist/assets/{get-entropy-details-for-user-dHRH5Gpi-Dukf7_kJ.js → get-entropy-details-for-user-dHRH5Gpi-BNiOFXbK.js} +1 -1
  180. package/site/dist/assets/{getErc20Balance-jXNpFf0N-Bb9WM31M.js → getErc20Balance-jXNpFf0N-q4bV8kQq.js} +1 -1
  181. package/site/dist/assets/{getErc20TokenInfo-BNTrzLMP-KPrdn04K.js → getErc20TokenInfo-BNTrzLMP-DcnTFrXN.js} +1 -1
  182. package/site/dist/assets/{guards-BIW-GiMJ-CHfXo8XI.js → guards-BIW-GiMJ-D3tWTQAR.js} +1 -1
  183. package/site/dist/assets/{hourglass-CPG1HZmf.js → hourglass-D_GfQdJr.js} +1 -1
  184. package/site/dist/assets/{id-UqEwL2BI-BkKm533U.js → id-UqEwL2BI-CTlvRMA5.js} +1 -1
  185. package/site/dist/assets/{index-BdEw5wyl-ChSJvPZk.js → index-BdEw5wyl-kLbhqLbv.js} +1 -1
  186. package/site/dist/assets/index-C2h3EneX.js +8 -0
  187. package/site/dist/assets/{index-CWARkn2w-Cezl_uBr.js → index-CWARkn2w-DxuckIFn.js} +1 -1
  188. package/site/dist/assets/{index-DvMddJcd-BatvgC-S.js → index-DvMddJcd-CNBb4VUn.js} +1 -1
  189. package/site/dist/assets/{index-YiCw0Y_x-CX5i3Z87.js → index-YiCw0Y_x-Czl0tWfF.js} +5 -6
  190. package/site/dist/assets/{index.es-BmqyoBEM.js → index.es-Crd0TyUS.js} +1 -1
  191. package/site/dist/assets/index.es-DQbup1WG.js +3 -0
  192. package/site/dist/assets/{isPaymentRequestAvailable-Bq1cemEn-By6r0dVB.js → isPaymentRequestAvailable-Bq1cemEn-Cjf3l05I.js} +1 -1
  193. package/site/dist/assets/{jsx-runtime-BbDfbRii.js → jsx-runtime-CndCZlyt.js} +1 -1
  194. package/site/dist/assets/{landmark-CH6LV6F0.js → landmark-BZPqMFEf.js} +1 -1
  195. package/site/dist/assets/lib-DGtzi2DK.js +1 -0
  196. package/site/dist/assets/{localBatchGatewayRequest-3kLrX_c7.js → localBatchGatewayRequest-B71J8sQ9.js} +7 -10
  197. package/site/dist/assets/{lock-DcskI4Bb.js → lock-DyN_c-ud.js} +1 -1
  198. package/site/dist/assets/{mail-Bs0r5yOi.js → mail-V_mcPb64.js} +1 -1
  199. package/site/dist/assets/{modal-context-CwmSeFcv-DruX9J1E.js → modal-context-CwmSeFcv-DPWNJrfK.js} +1 -1
  200. package/site/dist/assets/{ofetch.CWycOUEr-CHp-crOQ.js → ofetch.CWycOUEr-Duby3iHT.js} +1 -1
  201. package/site/dist/assets/parseEther-CHS0GNkM.js +1 -0
  202. package/site/dist/assets/{pkce-DXF0yKjP-ChQhE0st.js → pkce-DXF0yKjP-7_VY-d83.js} +1 -1
  203. package/site/dist/assets/preload-helper-CRzLMJPS.js +1 -0
  204. package/site/dist/assets/{privy-context-81KmsBw2-CjWGzyg7.js → privy-context-81KmsBw2-DiR6-Yq8.js} +1 -1
  205. package/site/dist/assets/{react-DrJ0MmMO.js → react-BfYA1q0G.js} +1 -1
  206. package/site/dist/assets/react-CK2HRI1t.js +1 -0
  207. package/site/dist/assets/{react-DQnMl2Xv.js → react-DxGeY8B8.js} +1 -1
  208. package/site/dist/assets/{react-dom-5dW7aKoP.js → react-dom-BI2Koe4o.js} +1 -1
  209. package/site/dist/assets/{reservoir-x-nGuZkT-BMX127i6.js → reservoir-x-nGuZkT-BguVFH3D.js} +1 -1
  210. package/site/dist/assets/roles-BDPDWrwo.js +1 -0
  211. package/site/dist/assets/rolldown-runtime-Bjp52BOI.js +1 -0
  212. package/site/dist/assets/secp256k1-BeYL8aZl.js +1 -0
  213. package/site/dist/assets/{secp256k1-_MleGpqL.js → secp256k1-Bh4MSkhz.js} +1 -1
  214. package/site/dist/assets/secp256k1-Ci4cqHl0.js +1 -0
  215. package/site/dist/assets/secp256k1-DsxXqbXv.js +1 -0
  216. package/site/dist/assets/secp256k1-N9RUmafO.js +1 -0
  217. package/site/dist/assets/{serializeAuthorizationList-CX8hGL56.js → serializeAuthorizationList-DGMik9Wx.js} +1 -1
  218. package/site/dist/assets/{serializeSignature-DfxkPnu-.js → serializeSignature-CWKpxoab.js} +1 -1
  219. package/site/dist/assets/sha2-C6s7zZmb.js +1 -0
  220. package/site/dist/assets/{shared-DRZUmnL5-xqAUARWo.js → shared-DRZUmnL5-BewE5wK8.js} +1 -1
  221. package/site/dist/assets/{shared-FM0rljBt--Avg6q2R.js → shared-FM0rljBt-CxLACri_.js} +1 -1
  222. package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BkAKsRNi.js +1 -0
  223. package/site/dist/assets/{simulateTransaction-CfuOrae4-64lWYJAL.js → simulateTransaction-CfuOrae4-PjA_tGjX.js} +1 -1
  224. package/site/dist/assets/{smartphone-CcEZ-XAy.js → smartphone-3p8nFSTE.js} +1 -1
  225. package/site/dist/assets/{storage-Ufl800Ne-D0bZBw-H.js → storage-Ufl800Ne-1qCR2JMO.js} +7 -8
  226. package/site/dist/assets/{stringify-BlDm7tuc.js → stringify-DqUj9VzO.js} +2 -3
  227. package/site/dist/assets/stripe.esm-DEi2D291.js +2 -0
  228. package/site/dist/assets/{styles-BsotlekN-CXEB4Ud9.js → styles-BsotlekN-Aq27nZGc.js} +1 -1
  229. package/site/dist/assets/{styles-Cm9H63WO-Br4QtzUL.js → styles-Cm9H63WO-p_ic_dxE.js} +1 -1
  230. package/site/dist/assets/{styles-DVyDvTdj-BTABn2K8.js → styles-DVyDvTdj-CjsWqAld.js} +1 -1
  231. package/site/dist/assets/{styles-hslXy6xG-D-49b1rk.js → styles-hslXy6xG-D5QHIrJU.js} +1 -1
  232. package/site/dist/assets/{telegram-B-JqnkqZ-RBoY3PLx.js → telegram-B-JqnkqZ-Dqkm4rDn.js} +1 -1
  233. package/site/dist/assets/{to-ui-error-DWe-qwS3-Cx2bk1kZ.js → to-ui-error-DWe-qwS3-Bi1HUQI9.js} +1 -1
  234. package/site/dist/assets/toViemAccount-B5iDGq1b-CdFZwv07.js +2 -0
  235. package/site/dist/assets/transformers.web-BTyOEJ0C.js +40 -0
  236. package/site/dist/assets/transformers.web-CUZUFjAK.js +49 -0
  237. package/site/dist/assets/{triangle-alert-CWjYH75H.js → triangle-alert-BG3Qw-Hq.js} +1 -1
  238. package/site/dist/assets/{twitch-5IOe4sIQ-lDtgk0os.js → twitch-5IOe4sIQ-CaXHFlAu.js} +1 -1
  239. package/site/dist/assets/{use-headless-crypto-deposit-Yn-QRLB3-LUIVFBSd.js → use-headless-crypto-deposit-Yn-QRLB3-CQqeyhGY.js} +1 -1
  240. package/site/dist/assets/{use-unlink-wallet-BKK4_5Ur-CC2pq27z.js → use-unlink-wallet-BKK4_5Ur-C0xQN_Lp.js} +1 -1
  241. package/site/dist/assets/{useActiveWallet-B-yZYGc6-D7Ik-LxJ.js → useActiveWallet-B-yZYGc6-BkK6rFBm.js} +1 -1
  242. package/site/dist/assets/{useGetSolPrice-x7gfUIHJ-D1hmQfpA.js → useGetSolPrice-x7gfUIHJ-CLVXqv6I.js} +1 -1
  243. package/site/dist/assets/{useGetTokenPrice-CaoUkb79-ZnbuJ-vP.js → useGetTokenPrice-CaoUkb79-DAXXs3rs.js} +1 -1
  244. package/site/dist/assets/{useI18n-C-5n_noF-BcqC76yj.js → useI18n-C-5n_noF-CGpkgIVy.js} +1 -1
  245. package/site/dist/assets/{useWalletBalance-CZnP5f_f-Dw2GIJ4g.js → useWalletBalance-CZnP5f_f-Cfd64m4n.js} +1 -1
  246. package/site/dist/assets/{useWallets-X1ndvJ_U-DRjz7uYD.js → useWallets-X1ndvJ_U-CAYqg7Za.js} +1 -1
  247. package/site/dist/assets/{w3m-modal-BeVrBOIf.js → w3m-modal-B4H_uiPH.js} +2 -2
  248. package/site/dist/assets/{wallet-Bn85Sg2W.js → wallet-BNI12IYk.js} +1 -1
  249. package/site/dist/assets/wallet-CITSaPlB.js +2 -0
  250. package/site/dist/assets/{wallet-connect-BvJtUsDt-Y03hhqFN.js → wallet-connect-BvJtUsDt-CVlSNIXP.js} +1 -1
  251. package/site/dist/assets/{walletconnect-registry-xxG8XLCb-D1SSFvMp.js → walletconnect-registry-xxG8XLCb-BpzpBq5s.js} +1 -1
  252. package/site/dist/assets/walletconnect-v2-3frQFwbg-CzZ6oybX.js +12 -0
  253. package/site/dist/assets/{wui-ux-by-reown-D34ILNLS.js → wui-ux-by-reown-XyHdtV1O.js} +1 -1
  254. package/site/dist/assets/x-gjDrY2rk.js +1 -0
  255. package/site/dist/index.html +5 -5
  256. package/site/feed.js +16 -1
  257. package/site/no-model.js +4 -0
  258. package/site/worker.js +586 -59
  259. package/src/agent/agent-card.js +20 -11
  260. package/src/agent/mcp.js +190 -324
  261. package/src/agent/view-tools.js +46 -13
  262. package/src/agent/x402.js +340 -44
  263. package/src/core/embed.js +51 -52
  264. package/src/core/embed.worker.js +1 -1
  265. package/src/core/shard.js +7 -3
  266. package/src/discover/apps.js +41 -5
  267. package/src/discover/explore.js +50 -30
  268. package/src/market/reactions.js +2 -2
  269. package/src/publish/devnet.js +95 -0
  270. package/src/publish/init.js +74 -0
  271. package/src/publish/lint.js +2 -1
  272. package/src/publish/ship.js +559 -267
  273. package/src/publish/view-cli.js +14 -10
  274. package/src/publish/view.js +355 -218
  275. package/src/taste/taste-doc.js +3 -2
  276. package/src/taste/taste.js +2 -2
  277. package/src/taste/vault.js +3 -1
  278. package/template/README.md +49 -0
  279. package/template/app.json +27 -0
  280. package/template/eval/goals.md +7 -0
  281. package/template/eval/golden.jsonl +0 -0
  282. package/template/gitignore +3 -0
  283. package/template/package.json +13 -0
  284. package/template/wrangler.json +12 -0
  285. package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-CvsbQcFv.js +0 -68
  286. package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-crHzh-TL.js +0 -1
  287. package/site/dist/assets/ConnectWalletView-DqI-PTvX-CMQIad63.js +0 -74
  288. package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-wu3a3kw4.js +0 -1
  289. package/site/dist/assets/ErrorScreen-DipdTWzE-DcxVjMO4.js +0 -1
  290. package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-BH_sU6EV.js +0 -491
  291. package/site/dist/assets/LandingScreen-CGWUoSUG-DK1EY-6v.js +0 -1
  292. package/site/dist/assets/LinkEmailScreen-dKo9frwc-C3v7glH0.js +0 -1
  293. package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-DYIl5Kr2.js +0 -1
  294. package/site/dist/assets/WalletOverflowButton-bfCnp-9T-CUyIJFzt.js +0 -1
  295. package/site/dist/assets/__vite-browser-external-CitKSmYk.js +0 -1
  296. package/site/dist/assets/_esm-B26FZg5Y.js +0 -7
  297. package/site/dist/assets/accounts-DiZ-0VmN.js +0 -1
  298. package/site/dist/assets/ccip-DA_zG341.js +0 -1
  299. package/site/dist/assets/check-BPgcwEPP.js +0 -1
  300. package/site/dist/assets/client-CZG0ndth.js +0 -2
  301. package/site/dist/assets/client-Dsa7_3V3.js +0 -9
  302. package/site/dist/assets/coinbase-DCPC2L_R-D3aIgpyd.js +0 -3
  303. package/site/dist/assets/core-snws5xwv.js +0 -12
  304. package/site/dist/assets/dist-Dl07uGq1.js +0 -1
  305. package/site/dist/assets/embed.worker-ugGhEqCV.js +0 -1
  306. package/site/dist/assets/floating-ui.react-xRkl9O3u.js +0 -1
  307. package/site/dist/assets/index-1HYFYA-W.js +0 -8
  308. package/site/dist/assets/index.es-C1v1or--.js +0 -3
  309. package/site/dist/assets/lib-BsS8z8Lj.js +0 -1
  310. package/site/dist/assets/parseEther-VjhVo3dd.js +0 -1
  311. package/site/dist/assets/preload-helper-uBIymjUX.js +0 -1
  312. package/site/dist/assets/react-DCXkXSrv.js +0 -1
  313. package/site/dist/assets/roles-32M1l9jI.js +0 -1
  314. package/site/dist/assets/rolldown-runtime-BcKkbAw3.js +0 -1
  315. package/site/dist/assets/secp256k1-C4nzcPo-.js +0 -1
  316. package/site/dist/assets/secp256k1-D92aGNnU.js +0 -1
  317. package/site/dist/assets/secp256k1-DmbwyFau.js +0 -1
  318. package/site/dist/assets/secp256k1-xl2og-wO.js +0 -1
  319. package/site/dist/assets/sha2-BU_QyRTx.js +0 -1
  320. package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BK_FBkaE.js +0 -1
  321. package/site/dist/assets/stripe.esm-CZRDpc-N.js +0 -2
  322. package/site/dist/assets/toViemAccount-B5iDGq1b-Ba8L7zed.js +0 -2
  323. package/site/dist/assets/transformers.web-CVP3pJOS.js +0 -40
  324. package/site/dist/assets/transformers.web-D8W1u_I2.js +0 -49
  325. package/site/dist/assets/wallet-Bnzeiqed.js +0 -2
  326. package/site/dist/assets/walletconnect-v2-3frQFwbg-CrtLXofr.js +0 -12
  327. package/site/dist/assets/x-rlSp9U7W.js +0 -1
@@ -1,47 +1,48 @@
1
- // An app's committed data → a view any Fangorn reader loads: the free, local tier.
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 --namespace my-app --out site/view
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 chain and nothing else: each publisher's commits to `namespaces`, and
6
- // the app owner's schema (`fangorn.schema`, see below). Embeds only records the view
7
- // does not hold yet, marks removed ones as tombstones, and writes static files:
7
+ // Reads the registry and storage and nothing else: every repo the app presents (its
8
+ // members' repos, less its exclusions, `appRepos`), 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
- // <out>/cdn/catalog
10
- // <out>/cdn/domains/<domain>/manifest
11
- // <out>/cdn/domains/<domain>/shards/shard-NNNN-<sha12>.ndjson.gz
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, so it runs from a laptop, CI or an
14
- // agent, and a second run with nothing new on chain changes nothing.
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 (an exclusion) loses its domain on the next run.
15
19
  //
16
- // THE SAME CONTRACT AS QUICKBEAM (the hosted tier), so an app moves between the two by
17
- // changing one URL in its card:
18
- // - URL layout, domain names (`domainFor`, byte-identical to quickbeam's
19
- // `_domain_for` and the registry's `domainFor`), row shape {track_id, fields, v},
20
- // manifest and catalog keys;
21
- // - a record is a vertex: track_id = its CID, fields = payload + entityType (its tag);
20
+ // The layout is quickbeam's (the hosted tier), so an app moves between the two by changing
21
+ // one URL in its card:
22
+ // - URL layout, domain names (`domainFor`, byte-identical to quickbeam's `_domain_for`),
23
+ // row shape {track_id, fields, v}, manifest and catalog keys;
24
+ // - a record is a row: track_id = its CID, fields = payload + entityType (its type);
22
25
  // - the schema: a declared type is embedded and shipped with exactly its role map;
23
26
  // - the embedded text (`composeText` mirrors quickbeam's `compose_document_text`).
24
- // What differs is the encoder: transformers.js q8 here, fastembed fp32 there. Their
25
- // vectors agree to ~0.95 cosine, close but not interchangeable, so the manifest names
26
- // its `embedder` and this refuses to append to a view another encoder built.
27
+ // The encoder is EmbeddingGemma (embed.js). Two runtimes of it agree closely but not bit for
28
+ // bit, so the manifest names its `embedder` and this refuses to append to a view another
29
+ // encoder built.
27
30
  //
28
- // ponytail: flat records. quickbeam folds a record's graph neighbours into its text
29
- // and can fuse publishers by shared identity; that is the hosted tier's job. Edges are
30
- // not shipped as edges (quickbeam's edges.json): a relation the schema declares is written
31
- // onto its source row instead (`linkRelations`), which is all a serverless reader needs.
31
+ // ponytail: flat records. quickbeam folds a record's neighbours into its text and can fuse
32
+ // publishers by shared identity; that is the hosted tier's job. A reference the schema
33
+ // declares is written onto its source row instead (`linkRelations`), which is all a
34
+ // serverless reader needs.
32
35
 
33
36
  import { gunzipSync, gzipSync } from "node:zlib";
34
37
  import { createHash } from "node:crypto";
35
38
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
39
+ import { dirname } from "node:path";
36
40
  import { EMBED_DIM, EMBED_DTYPE, EMBED_MODEL, embedDocumentDirect, packVec, unpackVec } from "../core/embed.js";
37
41
  import { coverage as fitCoverage } from "../market/reactions.js";
38
42
 
39
- export const SCHEMA_NAMESPACE = "fangorn.schema";
40
- export const TYPE_TAG = "fangorn.type.v1";
41
43
  export const EMBEDDER = { runtime: "transformers.js", model: EMBED_MODEL, dtype: EMBED_DTYPE };
42
44
  const SINGULAR = ["identity", "title", "subtitle", "temporal", "spatial", "media", "thread"];
43
45
  const MULTI = ["tags", "measures", "relations", "text"];
44
- const ZERO = "0x0000000000000000000000000000000000000000";
45
46
  // Refit coverage once a domain has grown this many times over what it was fit on.
46
47
  const COVERAGE_REFIT = 2;
47
48
 
@@ -68,18 +69,13 @@ export function composeText(fields, rm) {
68
69
  return s.slice(0, 1000 - "search_document: ".length);
69
70
  }
70
71
 
71
- /** `fangorn.schema` contents → { types: {tag: {description, role_map, presentation}}, relations }. */
72
- export function parseSchema(contents) {
73
- const types = {}, tagOf = {};
74
- for (const v of contents?.vertices ?? []) {
75
- const p = v.payload ?? {};
76
- if (v.schemaId !== TYPE_TAG || typeof p.tag !== "string") continue;
77
- types[p.tag] = { description: String(p.description ?? ""), role_map: p.role_map ?? {}, presentation: p.presentation ?? {} };
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 };
72
+ /** app.json's `types` + `relations` → { types: {tag: {description, role_map, presentation}}, relations }. */
73
+ export function schemaOf({ types = {}, relations = [] } = {}) {
74
+ return {
75
+ types: Object.fromEntries(Object.entries(types).map(([tag, t]) =>
76
+ [tag, { description: String(t.description ?? ""), role_map: t.role_map ?? {}, presentation: t.presentation ?? {} }])),
77
+ relations: relations.filter((r) => r.rel && r.from && r.to).map((r) => ({ rel: String(r.rel), from: String(r.from), to: String(r.to) })),
78
+ };
83
79
  }
84
80
 
85
81
  /** Declared means declared: every role present, left-out ones empty. */
@@ -122,49 +118,62 @@ export function domainSpec(schema, tags) {
122
118
  }
123
119
 
124
120
  /**
125
- * Each declared relation, onto the row it starts from: an agenda item with a `part_of` edge
126
- * to a matter carries `part_of: <the matter's identity>`, so a reader groups rows by it
127
- * without walking the graph. Undeclared edges are left alone (the schema is the contract).
128
- * `records` maps CID → fields and is changed in place.
129
- * ponytail: one target per relation and row (the last edge wins); a list when a relation
130
- * needs many.
121
+ * Each declared relation, onto the row it starts from: an agenda item whose `part_of` is a
122
+ * reference ({local: <record id>}) to a matter carries `part_of: <the matter's identity>`,
123
+ * so a reader groups rows by it without resolving references. References the schema does not
124
+ * declare are left as they are (the schema is the contract). `rows` maps CID → {id, fields}
125
+ * and is changed in place.
131
126
  */
132
- export function linkRelations(records, edges, schema) {
133
- for (const e of edges ?? []) {
134
- const from = records.get(e.sourceCid), to = records.get(e.targetCid);
135
- if (!from || !to || !schema.relations.some((r) => r.rel === e.relation && r.from === from.entityType && r.to === to.entityType)) continue;
136
- const id = schema.types[to.entityType]?.role_map?.identity;
137
- from[e.relation] = String((id && to[id]) ?? e.targetCid);
127
+ export function linkRelations(rows, schema) {
128
+ const byId = new Map([...rows.values()].map((r) => [r.id, r.fields]));
129
+ for (const { fields } of rows.values()) for (const rel of schema.relations) {
130
+ const ref = fields[rel.rel];
131
+ if (rel.from !== fields.entityType || typeof ref?.local !== "string") continue;
132
+ const to = byId.get(ref.local);
133
+ if (!to || to.entityType !== rel.to) continue;
134
+ const id = schema.types[rel.to]?.role_map?.identity;
135
+ fields[rel.rel] = String((id && to[id]) ?? ref.local);
138
136
  }
139
137
  }
140
138
 
141
139
  // ── a client that only reads ──────────────────────────────────────────────────
142
140
 
143
- /** An SDK client for reading commits: a throwaway key (nothing signs) and no upload
144
- * credentials. Contents come through an IPFS gateway: FANGORN_IPFS_GATEWAY, else the one
145
- * in ~/.fangorn/config.json (what the fangorn CLI reads through), else the SDK default.
146
- * ponytail: the SDK's default is ipfs.io, which no longer serves raw blocks, so on a
147
- * machine with neither setting reads fail; set FANGORN_IPFS_GATEWAY. */
148
- export async function readOnlyFangorn() {
149
- const [{ Fangorn, FangornConfig }, { generatePrivateKey }, { homedir }] = await Promise.all([
150
- import("@fangorn-network/sdk"), import("viem/accounts"), import("node:os")]);
141
+ /** The SDK's deployment for a network name ("baseSepolia"): FANGORN_NETWORK, else its default. */
142
+ export async function network(name = process.env.FANGORN_NETWORK || "baseSepolia") {
143
+ const { SupportedNetworks } = await import("@fangorn-network/sdk");
144
+ if (!SupportedNetworks[name]) throw new Error(`network ${name}: the SDK knows ${Object.keys(SupportedNetworks).join(", ")}`);
145
+ return SupportedNetworks[name];
146
+ }
147
+
148
+ /** The IPFS gateway to read contents through: FANGORN_IPFS_GATEWAY, else the one in
149
+ * ~/.fangorn/config.json (what the fangorn CLI reads through), else none (the SDK default). */
150
+ export function ipfsGateway() {
151
151
  let gateway = process.env.FANGORN_IPFS_GATEWAY ?? process.env.IPFS_GATEWAY;
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}`;
152
+ try { gateway ??= JSON.parse(readFileSync(`${process.env.HOME}/.fangorn/config.json`, "utf8")).pinataGateway || undefined; } catch { /* no CLI config */ }
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()`. */
158
+ export async function readOnlyFangorn() {
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>/<namespace>` to namespace contents ({ vertices: [{cid, schemaId, payload}],
160
- * edges }). A build that must not touch the chain — a candidate recipe graded before it
161
- * ships — runs the exact code a real one does. */
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. */
162
170
  export function localFangorn(chain, { appId, owner }) {
171
+ const root = (k) => `0x${createHash("sha256").update(JSON.stringify(chain[k])).digest("hex")}`;
163
172
  return {
164
173
  setAppId() {}, getAppId: () => appId,
165
174
  getAppRegistry: () => ({ getAppOwner: async () => owner }),
166
- appNamespaces: async ({ namespace }) => Object.keys(chain).filter((k) => k.endsWith(`/${namespace}`)).map((k) => ({ owner: k.split("/")[0] })),
167
- readNamespace: async (o, ns) => { const c = chain[`${o}/${ns}`]; if (!c) throw new Error("no such namespace"); return { contents: c }; },
175
+ appRepos: async () => Object.keys(chain).map((k) => ({ owner: k.split("/")[0], repo: k.split("/").slice(1).join("/"), root: root(k) })),
176
+ readRepo: async (o, repo) => { const c = chain[`${o}/${repo}`]; return { tip: c ? root(`${o}/${repo}`) : null, contents: { records: c?.records ?? [] } }; },
168
177
  };
169
178
  }
170
179
 
@@ -187,83 +196,136 @@ export function cachedEmbed(path, embed = embedDocumentDirect) {
187
196
  return fn;
188
197
  }
189
198
 
190
- // ── the view on disk ──────────────────────────────────────────────────────────
199
+ // ── where the view lives ──────────────────────────────────────────────────────
200
+ //
201
+ // A store is four calls on paths relative to the view's root: get → bytes or null, put,
202
+ // del (a file, or everything under a folder), and list (a folder's immediate children).
203
+
204
+ /** A directory on disk. */
205
+ export function fsStore(out) {
206
+ const at = (p) => `${out}/${p}`;
207
+ return {
208
+ get: async (p) => (existsSync(at(p)) ? readFileSync(at(p)) : null),
209
+ put: async (p, b) => { mkdirSync(dirname(at(p)), { recursive: true }); writeFileSync(`${at(p)}.tmp`, b); renameSync(`${at(p)}.tmp`, at(p)); },
210
+ del: async (p) => rmSync(at(p), { recursive: true, force: true }),
211
+ list: async (p) => (existsSync(at(p)) ? readdirSync(at(p)) : []),
212
+ };
213
+ }
214
+
215
+ /** An R2 bucket (a Worker's binding), under `prefix`. */
216
+ export function r2Store(bucket, prefix = "view/") {
217
+ const keys = async (p, delimiter) => {
218
+ const out = { objects: [], prefixes: [] };
219
+ let cursor;
220
+ do {
221
+ const l = await bucket.list({ prefix: prefix + p, delimiter, cursor });
222
+ out.objects.push(...l.objects.map((o) => o.key));
223
+ out.prefixes.push(...(l.delimitedPrefixes ?? []));
224
+ cursor = l.truncated ? l.cursor : undefined;
225
+ } while (cursor);
226
+ return out;
227
+ };
228
+ return {
229
+ get: async (p) => { const o = await bucket.get(prefix + p); return o ? new Uint8Array(await o.arrayBuffer()) : null; },
230
+ put: async (p, b) => { await bucket.put(prefix + p, b); },
231
+ del: async (p) => {
232
+ const all = [...(await keys(`${p}/`)).objects, ...((await bucket.head(prefix + p)) ? [prefix + p] : [])];
233
+ for (let i = 0; i < all.length; i += 1000) await bucket.delete(all.slice(i, i + 1000));
234
+ },
235
+ list: async (p) => {
236
+ const { objects, prefixes } = await keys(`${p}/`, "/");
237
+ const base = prefix.length + p.length + 1;
238
+ return [...objects.map((k) => k.slice(base)), ...prefixes.map((k) => k.slice(base, -1))];
239
+ },
240
+ };
241
+ }
191
242
 
192
- const readJson = (p, dflt) => (existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : dflt);
193
- const writeJson = (p, o) => { writeFileSync(`${p}.tmp`, JSON.stringify(o, null, 1)); renameSync(`${p}.tmp`, p); };
243
+ const readJson = async (store, p, dflt) => { const b = await store.get(p); return b ? JSON.parse(new TextDecoder().decode(b)) : dflt; };
244
+ const writeJson = (store, p, o) => store.put(p, JSON.stringify(o, null, 1));
194
245
 
195
246
  /** What a domain already serves: its manifest and every row (for tombstones and coverage). */
196
- function loadDomain(dir) {
197
- const manifest = readJson(`${dir}/manifest`, null);
247
+ async function loadDomain(store, dir) {
248
+ const manifest = await readJson(store, `${dir}/manifest`, null);
198
249
  const rows = new Map();
199
250
  for (const s of manifest?.shards ?? []) {
200
- const text = gunzipSync(readFileSync(`${dir}/shards/${s.file}`)).toString("utf8");
201
- for (const line of text.split("\n")) if (line.trim()) { const r = JSON.parse(line); rows.set(r.track_id, r); }
251
+ const b = await store.get(`${dir}/shards/${s.file}`);
252
+ if (!b) throw new Error(`${dir}: shard ${s.file} is named by its manifest and missing`);
253
+ 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
254
  }
203
255
  return { manifest, rows };
204
256
  }
205
257
 
206
- /** Every live row's fields, across the view at `out`. */
207
- export function liveFields(out) {
208
- const root = `${out}/cdn/domains`;
209
- if (!existsSync(root)) return [];
210
- return readdirSync(root).flatMap((d) => {
211
- const { manifest, rows } = loadDomain(`${root}/${d}`);
258
+ /** Every live row's fields, across the view in `store` (or the directory `out`). */
259
+ export async function liveFields(store) {
260
+ if (typeof store === "string") store = fsStore(store);
261
+ const out = [];
262
+ for (const d of await store.list("cdn/domains")) {
263
+ const { manifest, rows } = await loadDomain(store, `cdn/domains/${d}`);
212
264
  const dead = new Set(manifest?.tombstones ?? []);
213
- return [...rows.values()].filter((r) => !dead.has(r.track_id)).map((r) => r.fields ?? {});
214
- });
265
+ for (const r of rows.values()) if (!dead.has(r.track_id)) out.push(r.fields ?? {});
266
+ }
267
+ return out;
215
268
  }
216
269
 
270
+ const sha256 = (s) => createHash("sha256").update(s).digest("hex");
271
+
217
272
  /**
218
- * Bring `out` up to date with the chain. `fangorn` is an SDK client (reads only).
273
+ * Bring the view up to date with what the app presents. `fangorn` is an SDK client (reads
274
+ * 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
275
+ * names (every publisher's); without it, every repo the app presents. `fromBlock` must be
276
+ * at or before the first update to a repo the app presents, or a repo the scan misses is left out;
277
+ * left out, the SDK's deployment start. `embedBatch(texts)` embeds many at once where the
278
+ * encoder batches (a Worker's AI binding); `embedder` is what the manifest records.
279
+ * `presented` replaces the `appRepos` scan with a list the caller keeps itself
280
+ * ([{owner, repo, root}], exclusions already applied), as a Worker does across runs.
219
281
  * Returns per-domain counts of what changed.
220
282
  */
221
- export async function publishView({ fangorn, app, namespaces, out, fromBlock = 0n, rebake = false,
222
- embed = embedDocumentDirect, log = console.log, shardRows = 20000, concurrency = 4 }) {
223
- if (!namespaces?.length) throw new Error("publishView: name the data namespace(s) to publish");
283
+ export async function publishView({ fangorn, app, schema, repos, out, store = fsStore(out), fromBlock, presented, rebake = false,
284
+ embed = embedDocumentDirect, embedBatch = (ts) => Promise.all(ts.map(embed)),
285
+ embedder = EMBEDDER, log = console.log, shardRows = 20000, concurrency = 4, open = (p) => p }) {
224
286
  fangorn.setAppId(app);
225
287
  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
- }
288
+ schema = schemaOf(schema);
233
289
  const declared = Object.keys(schema.types);
234
290
  log(`schema: ${declared.length ? declared.join(", ") : "no declared types"}`);
291
+ const schemaHash = sha256(JSON.stringify(schema));
292
+
293
+ // Every repo the app presents, at its accepted head: one log scan, exclusions applied.
294
+ const listed = (presented ?? await retry(() => fangorn.appRepos(fromBlock == null ? {} : { fromBlock: BigInt(fromBlock) })))
295
+ // ponytail: "fangorn.schema" is where apps shipped before 2026-10-22 committed their schema; never data.
296
+ .filter((t) => t.repo !== "fangorn.schema" && (!repos?.length || repos.includes(t.repo)));
297
+ log(`${listed.length} repo(s) presented${repos?.length ? ` of ${repos.join(", ")}` : ""}`);
235
298
 
236
299
  const report = {};
237
- // Several namespaces at once: reading one back through an IPFS gateway is block by block and
238
- // takes minutes, while its embedding takes seconds. Each domain writes only its own folder.
239
- const queue = namespaces.filter((n) => n !== SCHEMA_NAMESPACE);
240
- // Who published into each namespace, from ONE scan of the app's commit logs. The chain filters
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) })));
300
+ const live = new Set();
301
+ // Several repos at once: reading one through an IPFS gateway is block by block and takes
302
+ // far longer than embedding it. Each domain writes only its own folder.
303
+ const queue = [...listed];
253
304
  const worker = async () => {
254
- for (let ns; (ns = queue.shift()) !== undefined;) {
255
- const timelines = await publishersOf(ns);
256
- log(`${ns}: ${timelines.length} publisher(s) since block ${fromBlock}`);
257
- for (const { owner: publisher } of timelines) {
258
- const domain = domainFor(appId, publisher, ns);
259
- report[domain] = await publishDomain({
260
- fangorn, schema, publisher, ns, domain, dir: `${out}/cdn/domains/${domain}`, rebake, embed, log, shardRows,
261
- });
262
- }
305
+ for (let t; (t = queue.shift()) !== undefined;) {
306
+ const domain = domainFor(appId, t.owner, t.repo);
307
+ live.add(domain);
308
+ // One repo failing (unreadable, the encoder out for the day) keeps its last view and
309
+ // does not stop the others; the next run tries it again.
310
+ const r = await publishDomain({ fangorn, store, schema, schemaHash, publisher: t.owner, repo: t.repo, head: t.root, domain,
311
+ dir: `cdn/domains/${domain}`, rebake, embedBatch, embedder, log, shardRows, open })
312
+ .catch((e) => { log(`${domain}: ✗ ${e.message}; left as it was`); return { added: 0, removed: 0, error: e.message }; });
313
+ if (r) report[domain] = r;
263
314
  }
264
315
  };
265
316
  await Promise.all(Array.from({ length: concurrency }, worker));
266
- writeCatalog(out);
317
+
318
+ // What the app no longer presents (excluded, or its publisher banned) is not served.
319
+ for (const d of await store.list("cdn/domains")) {
320
+ if (live.has(d)) continue;
321
+ const m = await readJson(store, `cdn/domains/${d}/manifest`, null);
322
+ const repo = m?.filter?.repo?.[0] ?? m?.filter?.namespace?.[0];
323
+ if (repos?.length && !repos.includes(repo)) continue;
324
+ await store.del(`cdn/domains/${d}`);
325
+ report[d] = { added: 0, removed: m?.count ?? 0, dropped: true };
326
+ log(`${d}: no longer presented by the app; dropped`);
327
+ }
328
+ await writeCatalog(store);
267
329
  return report;
268
330
  }
269
331
 
@@ -277,19 +339,26 @@ async function retry(f, tries = 6) {
277
339
  }
278
340
  }
279
341
 
280
- async function publishDomain({ fangorn, schema, publisher, ns, domain, dir, rebake, embed, log, shardRows }) {
281
- const { contents } = await retry(() => fangorn.readNamespace(publisher, ns));
282
- const records = new Map((contents.vertices ?? []).filter((v) => v.payload && typeof v.payload === "object")
283
- .map((v) => [v.cid, { ...v.payload, entityType: v.schemaId }]));
284
- linkRelations(records, contents.edges, schema);
285
-
286
- if (rebake) rmSync(dir, { recursive: true, force: true }); // old shards would stay reachable
287
- const had = loadDomain(dir);
342
+ async function publishDomain({ fangorn, store, schema, schemaHash, publisher, repo, head, domain, dir, rebake, embedBatch, embedder, log, shardRows, open }) {
343
+ // The head the domain was built from, and the schema it was built under: neither moved,
344
+ // nothing to read. This is what keeps a cron run with nothing new to one read per repo.
345
+ const was = rebake ? null : await readJson(store, `${dir}/manifest`, null);
346
+ if (was && was.head === head && was.schema_hash === schemaHash) return null;
347
+
348
+ const { contents } = await retry(() => fangorn.readRepo(publisher, repo));
349
+ // `open`: a private app's rows are sealed on chain (x402.js's sealRow); the Worker opens them here.
350
+ const records = new Map(await Promise.all(contents.records.filter((r) => r.payload && typeof r.payload === "object")
351
+ .map(async (r) => [r.cid, { id: r.id, fields: { ...(await open(r.payload)), entityType: r.type } }])));
352
+ linkRelations(records, schema);
353
+ const fieldsOf = (cid) => records.get(cid).fields;
354
+
355
+ if (rebake) await store.del(dir); // old shards would stay reachable
356
+ const had = await loadDomain(store, dir);
288
357
  const made = had.manifest?.embedder ?? (had.manifest ? { runtime: "fastembed", dtype: "fp32" } : null);
289
358
  // 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 only to ~0.95.
291
- if (made && (made.model ?? EMBED_MODEL) + made.dtype !== EMBEDDER.model + EMBEDDER.dtype)
292
- throw new Error(`${domain} was embedded by ${made.runtime} ${made.dtype}, not ${EMBEDDER.runtime} ${EMBEDDER.dtype}; ` +
359
+ // ~0.98-0.99 cosine, fp32 against q8 noticeably less.
360
+ if (made && (made.model ?? "") + made.dtype !== embedder.model + embedder.dtype)
361
+ throw new Error(`${domain} was embedded by ${made.runtime} ${made.model ?? ""} ${made.dtype}, not ${embedder.runtime} ${embedder.model} ${embedder.dtype}; ` +
293
362
  "vectors from two encoders are not comparable. Rebake it with --rebake, or keep publishing it with the tool that made it.");
294
363
 
295
364
  const dead = new Set(had.manifest?.tombstones ?? []);
@@ -299,53 +368,70 @@ async function publishDomain({ fangorn, schema, publisher, ns, domain, dir, reba
299
368
  // A row already served whose links changed (its matter came later, or moved): new fields,
300
369
  // the same vector, so no embedding, only a rewrite.
301
370
  const relinked = [...had.rows.keys()].filter((id) => live(id) && records.has(id)
302
- && JSON.stringify(had.rows.get(id).fields) !== JSON.stringify(records.get(id)));
303
- for (const id of relinked) had.rows.get(id).fields = records.get(id);
371
+ && JSON.stringify(had.rows.get(id).fields) !== JSON.stringify(fieldsOf(id)));
372
+ for (const id of relinked) had.rows.get(id).fields = fieldsOf(id);
304
373
 
305
374
  // Roles per type: declared exactly, else guessed (and said so).
306
375
  const byType = new Map();
307
- for (const id of fresh) { const t = records.get(id).entityType; byType.set(t, [...(byType.get(t) ?? []), id]); }
376
+ for (const id of fresh) { const t = fieldsOf(id).entityType; byType.set(t, [...(byType.get(t) ?? []), id]); }
308
377
  const rolesOf = new Map();
309
378
  for (const [t, ids] of byType) {
310
379
  if (schema.types[t]) rolesOf.set(t, completeRoles(schema.types[t].role_map));
311
380
  else {
312
- rolesOf.set(t, guessRoles(ids.slice(0, 200).map((id) => records.get(id))));
381
+ rolesOf.set(t, guessRoles(ids.slice(0, 200).map(fieldsOf)));
313
382
  log(`${domain}: type ${t} is not in the schema; guessed title=${rolesOf.get(t).title} text=${rolesOf.get(t).text}`);
314
383
  }
315
384
  }
316
385
 
317
- let n = 0;
386
+ let n = 0, stopped = null;
318
387
  const lines = [];
319
- // 32 at a time: the embedder batches calls made together (embed.js), the GPU's speed.
388
+ // 32 at a time: what an encoder batches (embed.js, a GPU, the Worker's AI binding).
320
389
  for (let i = 0; i < fresh.length; i += 32) {
321
390
  const ids = fresh.slice(i, i + 32);
322
- const vs = await Promise.all(ids.map((id) => embed(composeText(records.get(id), rolesOf.get(records.get(id).entityType)))));
323
- ids.forEach((id, j) => lines.push(JSON.stringify({ track_id: id, fields: records.get(id), v: packVec(vs[j]) })));
391
+ try {
392
+ const vs = await embedBatch(ids.map((id) => composeText(fieldsOf(id), rolesOf.get(fieldsOf(id).entityType))));
393
+ ids.forEach((id, j) => lines.push(JSON.stringify({ track_id: id, fields: fieldsOf(id), v: packVec(vs[j]) })));
394
+ } catch (e) {
395
+ // The encoder gave out (Workers AI's free daily allowance, say): keep what is embedded
396
+ // and come back for the rest. Nothing served yet, nothing to save: fail as before.
397
+ if (!lines.length) throw e;
398
+ stopped = e;
399
+ break;
400
+ }
324
401
  if (Math.floor((n + ids.length) / 1000) > Math.floor(n / 1000)) log(`${domain}: embedded ${n + ids.length}/${fresh.length}`);
325
402
  n += ids.length;
326
403
  }
404
+ const manifest = had.manifest ?? { name: domain, description: "", count: 0, dim: EMBED_DIM, model: EMBED_MODEL,
405
+ distance: "Cosine", embedder, filter: { owner: [publisher], repo: [repo] }, shards: [], tombstones: [] };
406
+ // Unfinished: the old head stays, so the next run sees a head to catch up to and embeds only what is missing.
407
+ manifest.head = stopped ? (had.manifest?.head ?? null) : head;
408
+ manifest.schema_hash = schemaHash;
327
409
  if (!fresh.length && !gone.length && !relinked.length && had.manifest && !rebake) {
328
- applySpec(dir, had.manifest, schema, had.rows, dead); // a schema change alone still lands
410
+ await applySpec(store, dir, manifest, schema, had.rows, dead); // a schema change alone still lands
329
411
  return { added: 0, removed: 0 };
330
412
  }
331
413
 
332
- mkdirSync(`${dir}/shards`, { recursive: true });
333
- const manifest = had.manifest ?? { name: domain, description: "", count: 0, dim: EMBED_DIM, model: EMBED_MODEL,
334
- distance: "Cosine", embedder: EMBEDDER, filter: { owner: [publisher], namespace: [ns] }, shards: [], tombstones: [] };
335
- // Pages serves files up to 25 MiB; 52k Steam games gzipped to 28 MiB in one shard.
336
- // ponytail: split by row count (~550 B a row gzipped); split by bytes if rows get much fatter.
337
- const writeShard = (all) => {
414
+ // Pages and R2 serve files of any size, but a reader holds a shard whole; 52k Steam games
415
+ // gzipped to 28 MiB in one. ponytail: split by row count (~550 B a row gzipped).
416
+ const writeShard = async (all) => {
338
417
  for (let i = 0; i < all.length; i += shardRows) {
339
418
  const ls = all.slice(i, i + shardRows);
340
419
  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 });
420
+ const digest = sha256(gz);
421
+ const file = `shard-${String(manifest.shards.length).padStart(4, "0")}-${digest.slice(0, 12)}.ndjson.gz`;
422
+ await store.put(`${dir}/shards/${file}`, gz);
423
+ manifest.shards.push({ file, count: ls.length, bytes: gz.length, sha256: digest });
345
424
  }
346
425
  };
426
+ if (stopped) {
427
+ await writeShard(lines);
428
+ for (const l of lines) { const r = JSON.parse(l); had.rows.set(r.track_id, r); dead.delete(r.track_id); }
429
+ await applySpec(store, dir, manifest, schema, had.rows, dead, rolesOf);
430
+ log(`${domain}: stopped at ${n}/${fresh.length} (${stopped.message}); the rest on the next run`);
431
+ return { added: n, removed: 0, partial: true };
432
+ }
347
433
  if (lines.length) {
348
- writeShard(lines);
434
+ await writeShard(lines);
349
435
  for (const l of lines) { const r = JSON.parse(l); had.rows.set(r.track_id, r); dead.delete(r.track_id); }
350
436
  }
351
437
  for (const id of gone) dead.add(id);
@@ -355,29 +441,30 @@ async function publishDomain({ fangorn, schema, publisher, ns, domain, dir, reba
355
441
  // files are deleted. No re-embedding: the vectors are already in the rows.
356
442
  // ponytail: rewrites the whole domain on every removal; compact past a threshold if domains get big.
357
443
  if (dead.size || relinked.length) {
358
- for (const s of manifest.shards) rmSync(`${dir}/shards/${s.file}`, { force: true });
444
+ for (const s of manifest.shards) await store.del(`${dir}/shards/${s.file}`);
359
445
  manifest.shards = [];
360
446
  for (const id of dead) had.rows.delete(id);
361
447
  dead.clear();
362
- writeShard([...had.rows.values()].map((r) => JSON.stringify(r)));
448
+ await writeShard([...had.rows.values()].map((r) => JSON.stringify(r)));
363
449
  }
364
450
  manifest.tombstones = [...dead].sort();
365
451
  manifest.created_at = Math.floor(Date.now() / 1000);
366
- applySpec(dir, manifest, schema, had.rows, dead, rolesOf);
452
+ await applySpec(store, dir, manifest, schema, had.rows, dead, rolesOf);
367
453
  log(`${domain}: +${fresh.length} embedded, -${gone.length} removed, ${relinked.length} relinked, ${manifest.count} live`);
368
454
  return { added: fresh.length, removed: gone.length, ...(relinked.length ? { relinked: relinked.length } : {}) };
369
455
  }
370
456
 
371
- /** Counts, types, roles, description, presentation and coverage from what the domain now holds. */
372
- function applySpec(dir, manifest, schema, rows, dead, guessed = new Map()) {
457
+ /** Counts, types, roles, description, presentation, coverage and the hosts its media role
458
+ * links to (what a Worker may show inline, see site/worker.js) from what the domain holds. */
459
+ async function applySpec(store, dir, manifest, schema, rows, dead, guessed = new Map()) {
373
460
  const liveRows = [...rows.values()].filter((r) => !dead.has(r.track_id));
374
461
  const types = {};
375
462
  for (const r of liveRows) types[r.fields?.entityType] = (types[r.fields?.entityType] ?? 0) + 1;
376
463
  const tags = Object.entries(types).sort((a, b) => b[1] - a[1]).map(([t]) => t);
377
- // Compare with what is on disk, not with the manifest as it arrived: publishDomain has already
464
+ // Compare with what is stored, not with the manifest as it arrived: publishDomain has already
378
465
  // swapped shards and tombstones, and a one-for-one replacement changes nothing here, which left
379
466
  // the old manifest naming deleted shards.
380
- const before = JSON.stringify(readJson(`${dir}/manifest`, null));
467
+ const before = JSON.stringify(await readJson(store, `${dir}/manifest`, null));
381
468
 
382
469
  manifest.count = liveRows.length;
383
470
  manifest.entity_types = tags.map((t) => ({ type: t, count: types[t] }));
@@ -386,27 +473,30 @@ function applySpec(dir, manifest, schema, rows, dead, guessed = new Map()) {
386
473
  manifest.role_map = { ...(spec?.role_map ?? manifest.role_map ?? guessed.values().next().value ?? completeRoles()), fields };
387
474
  if (spec?.description) manifest.description = spec.description;
388
475
  if (spec && Object.keys(spec.presentation).length) manifest.presentation = spec.presentation;
476
+ const media = [manifest.role_map.media ?? []].flat();
477
+ manifest.link_hosts = [...new Set(liveRows.flatMap((r) => media.map((m) => r.fields?.[m]))
478
+ .filter((u) => typeof u === "string" && u.startsWith("https://"))
479
+ .map((u) => { try { return new URL(u).hostname; } catch { return null; } }).filter(Boolean))].sort();
389
480
 
390
481
  const cov = manifest.coverage;
391
482
  if (liveRows.length && (!cov || liveRows.length >= COVERAGE_REFIT * (cov.fit_at ?? 0))) {
392
483
  manifest.coverage = { ...fitCoverage(liveRows.map((r) => unpackVec(r.v))), fit_at: liveRows.length };
393
484
  }
394
- if (JSON.stringify(manifest) !== before) {
395
- mkdirSync(dir, { recursive: true });
396
- writeJson(`${dir}/manifest`, manifest);
397
- }
485
+ if (JSON.stringify(manifest) !== before) await writeJson(store, `${dir}/manifest`, manifest);
398
486
  }
399
487
 
400
- export function writeCatalog(out) {
401
- const root = `${out}/cdn/domains`;
402
- mkdirSync(`${out}/cdn`, { recursive: true });
403
- const domains = existsSync(root) ? readdirSync(root).sort().map((d) => readJson(`${root}/${d}/manifest`, null)).filter(Boolean) : [];
404
- writeJson(`${out}/cdn/catalog`, {
488
+ /** The catalog: every domain's summary and coverage, the one file a reader routes on. */
489
+ export async function writeCatalog(store) {
490
+ if (typeof store === "string") store = fsStore(store);
491
+ const domains = (await Promise.all((await store.list("cdn/domains")).sort()
492
+ .map((d) => readJson(store, `cdn/domains/${d}/manifest`, null)))).filter(Boolean);
493
+ await writeJson(store, "cdn/catalog", {
405
494
  generated_at: Math.floor(Date.now() / 1000), embedding_model: EMBED_MODEL,
406
495
  domains: domains.map((m) => ({
407
496
  name: m.name, description: m.description, count: m.count, dim: m.dim,
408
497
  bytes: m.shards.reduce((s, x) => s + x.bytes, 0), shard_count: m.shards.length,
409
498
  entity_types: m.entity_types.map((e) => e.type), manifest: `${m.name}/manifest.json`,
499
+ ...(m.link_hosts?.length ? { link_hosts: m.link_hosts } : {}),
410
500
  ...(m.coverage ? { coverage: m.coverage } : {}),
411
501
  })),
412
502
  });
@@ -419,7 +509,8 @@ if (typeof process !== "undefined" && import.meta.url === `file://${process.argv
419
509
  const { tmpdir } = await import("node:os");
420
510
  const { loadShard, configure } = await import("../core/shard.js");
421
511
  const eq = (a, b, m) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${m}: ${JSON.stringify(a)} != ${JSON.stringify(b)}`); };
422
- const throws = async (f, re, m) => { try { await f(); } catch (e) { if (re.test(e.message)) return; throw e; } throw new Error(`must throw: ${m}`); };
512
+ const manifestOf = (dir, d) => JSON.parse(readFileSync(`${dir}/cdn/domains/${d}/manifest`, "utf8"));
513
+ const rec = (cid, type, payload, id = cid) => ({ id, cid, type, payload });
423
514
 
424
515
  // quickbeam's own outputs, pinned: the contract, not an approximation of it.
425
516
  eq(domainFor(`0x${"7e1497af".padEnd(64, "0")}`, "0x55cf6bd2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "media"), "7e1497af-55cf6bd2-media", "domain name = quickbeam's");
@@ -429,15 +520,14 @@ if (typeof process !== "undefined" && import.meta.url === `file://${process.argv
429
520
 
430
521
  const APP = `0x${"ab".repeat(32)}`, OWNER = `0x${"11".repeat(20)}`, PUB = `0x${"22".repeat(20)}`;
431
522
  const TAG = "shop.item.v1";
523
+ const schema = { types: { [TAG]: { description: "Things for a room.", role_map: { identity: "sku", title: "name", text: "about" },
524
+ presentation: { externalUrl: { [TAG]: "https://shop.example/{sku}" } } } } };
432
525
  const chain = {
433
- [`${OWNER}/${SCHEMA_NAMESPACE}`]: { vertices: [{ cid: "t1", schemaId: TYPE_TAG, payload: {
434
- tag: TAG, description: "Things for a room.", role_map: { identity: "sku", title: "name", text: "about" },
435
- presentation: { externalUrl: { [TAG]: "https://shop.example/{sku}" } } } }], edges: [] },
436
- [`${PUB}/shop`]: { vertices: [
437
- { cid: "c1", schemaId: TAG, payload: { sku: "a1", name: "Oak table", about: "a solid oak dining table", price: 3 } },
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: [] },
526
+ [`${PUB}/shop`]: { records: [
527
+ rec("c1", TAG, { sku: "a1", name: "Oak table", about: "a solid oak dining table", price: 3 }),
528
+ rec("c2", TAG, { sku: "b2", name: "Lamp", about: "brass reading lamp", price: 1 }),
529
+ rec("c3", "shop.note.v1", { title: "Note", body: "an undeclared type, with a body long enough to count as its text field" }),
530
+ ] },
441
531
  };
442
532
  const fangorn = localFangorn(chain, { appId: APP, owner: OWNER });
443
533
  const embedded = [];
@@ -451,45 +541,42 @@ if (typeof process !== "undefined" && import.meta.url === `file://${process.argv
451
541
  appendFileSync(`${out}/vectors.ndjson`, '["cut sho');
452
542
  eq(packVec(await cachedEmbed(`${out}/vectors.ndjson`, embed)("Title: Oak")), packVec(v1), "a line cut short by a kill is skipped, not fatal");
453
543
  embedded.length = 0; rmSync(`${out}/vectors.ndjson`);
454
- const run = () => publishView({ fangorn, app: APP, namespaces: ["shop"], out, embed, log: () => {} });
544
+ const run = () => publishView({ fangorn, app: APP, schema, out, embed, log: () => {} });
455
545
 
456
546
  const d = domainFor(APP, PUB, "shop");
457
547
  const split = mkdtempSync(`${tmpdir()}/view-`);
458
- await publishView({ fangorn, app: APP, namespaces: ["shop"], out: split, embed, log: () => {}, shardRows: 2 });
459
- m0: { const m = readJson(`${split}/cdn/domains/${d}/manifest`);
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)"); }
548
+ await publishView({ fangorn, app: APP, schema, out: split, embed, log: () => {}, shardRows: 2 });
549
+ eq(manifestOf(split, d).shards.map((s) => s.count), [2, 1], "rows past shardRows go to the next shard");
461
550
  embedded.length = 0;
462
551
  eq((await run())[d], { added: 3, removed: 0 }, "first run embeds everything");
463
552
  if (!embedded.includes("Title: Oak table. Tags: . a solid oak dining table")) throw new Error(`declared roles compose the text: ${embedded}`);
464
- let m = readJson(`${out}/cdn/domains/${d}/manifest`);
553
+ let m = manifestOf(out, d);
465
554
  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");
555
+ eq([m.embedder, m.filter], [EMBEDDER, { owner: [PUB], repo: ["shop"] }], "the manifest names its encoder and its repo");
467
556
 
468
- eq((await run())[d], { added: 0, removed: 0 }, "nothing new on chain → nothing embedded");
557
+ eq(await run(), {}, "no head moved → nothing read, nothing embedded");
469
558
  eq(embedded.length, 3, "and the embedder was not called");
470
559
 
471
- chain[`${PUB}/shop`].vertices = [chain[`${PUB}/shop`].vertices[0],
472
- { cid: "c2b", schemaId: TAG, payload: { sku: "b2", name: "Lamp", about: "brass reading lamp", price: 2 } }];
560
+ chain[`${PUB}/shop`].records = [chain[`${PUB}/shop`].records[0],
561
+ rec("c2b", TAG, { sku: "b2", name: "Lamp", about: "brass reading lamp", price: 2 }, "c2")];
473
562
  eq((await run())[d], { added: 1, removed: 2 }, "a changed record is a new CID; removed ones are dropped");
474
- m = readJson(`${out}/cdn/domains/${d}/manifest`);
563
+ m = manifestOf(out, d);
475
564
  eq([m.count, m.shards.length, m.tombstones, readdirSync(`${out}/cdn/domains/${d}/shards`).length], [2, 1, [], 1],
476
565
  "a removal compacts: the retracted rows' bytes are no longer served anywhere");
477
566
  if (embedded.length !== 4) throw new Error("compaction must not re-embed");
478
- chain[`${PUB}/shop`].vertices[1] = { cid: "c2c", schemaId: TAG, payload: { sku: "b3", name: "Lamp", about: "steel reading lamp", price: 2 } };
567
+ chain[`${PUB}/shop`].records[1] = rec("c2c", TAG, { sku: "b3", name: "Lamp", about: "steel reading lamp", price: 2 }, "c2");
479
568
  eq((await run())[d], { added: 1, removed: 1 }, "a one-for-one replacement: same count, same types");
480
- m = readJson(`${out}/cdn/domains/${d}/manifest`);
569
+ m = manifestOf(out, d);
481
570
  eq(m.shards.map((x) => x.file).sort(), readdirSync(`${out}/cdn/domains/${d}/shards`).sort(),
482
- "and the manifest names the shards on disk, not the ones compaction deleted");
483
- chain[`${PUB}/shop`].vertices[1] = { cid: "c2b", schemaId: TAG, payload: { sku: "b2", name: "Lamp", about: "brass reading lamp", price: 2 } };
571
+ "and the manifest names the shards stored, not the ones compaction deleted");
572
+ chain[`${PUB}/shop`].records[1] = rec("c2b", TAG, { sku: "b2", name: "Lamp", about: "brass reading lamp", price: 2 }, "c2");
484
573
  await run(); // back to the state the checks below expect
485
- embedded.length = 4; // the replacement's embedding is not what the checks below count
486
- chain[`${PUB}/shop`].vertices.push({ cid: "c4", schemaId: TAG, payload: { sku: "d4", name: "Rug", about: "wool rug" } });
574
+ chain[`${PUB}/shop`].records.push(rec("c4", TAG, { sku: "d4", name: "Rug", about: "wool rug" }));
487
575
  await run();
488
- chain[`${PUB}/shop`].vertices.pop();
489
- m = readJson(`${out}/cdn/domains/${d}/manifest`);
490
- eq([m.count, m.shards.length], [3, 2], "an addition alone is a delta shard");
576
+ chain[`${PUB}/shop`].records.pop();
577
+ eq([manifestOf(out, d).count, manifestOf(out, d).shards.length], [3, 2], "an addition alone is a delta shard");
491
578
  await run();
492
- m = readJson(`${out}/cdn/domains/${d}/manifest`);
579
+ m = manifestOf(out, d);
493
580
 
494
581
  // The real reader over HTTP: digests checked, tombstones honoured, the publisher's id resolves.
495
582
  const srv = createServer((q, s) => { try { s.end(readFileSync(`${out}${decodeURIComponent(q.url)}`)); } catch { s.statusCode = 404; s.end(); } });
@@ -502,39 +589,89 @@ if (typeof process !== "undefined" && import.meta.url === `file://${process.argv
502
589
  const { rolesFrom } = await import("../core/roles.js");
503
590
  eq(getRow(got, "b2", rolesFrom([m]))?.id, "c2b", "get by the publisher's own id, not the CID");
504
591
 
505
- m.embedder = { ...EMBEDDER, runtime: "transformers.js cuda" }; writeJson(`${out}/cdn/domains/${d}/manifest`, m);
506
- await run(); // throws if another runtime of the same q8 model is refused
507
- m.embedder = { runtime: "fastembed", dtype: "fp32" }; writeJson(`${out}/cdn/domains/${d}/manifest`, m);
508
- await throws(run, /not comparable/, "appending to another encoder's view");
509
- eq((await publishView({ fangorn, app: APP, namespaces: ["shop"], out, embed, rebake: true, log: () => {} }))[d],
592
+ // An exclusion: the app stops presenting the repo, and its domain stops being served.
593
+ const shop = chain[`${PUB}/shop`];
594
+ delete chain[`${PUB}/shop`];
595
+ eq((await run())[d], { added: 0, removed: 2, dropped: true }, "a repo the app no longer presents is dropped");
596
+ eq([existsSync(`${out}/cdn/domains/${d}`), JSON.parse(readFileSync(`${out}/cdn/catalog`, "utf8")).domains], [false, []], "with its files and its catalog entry");
597
+ chain[`${PUB}/shop`] = shop;
598
+ eq((await run())[d], { added: 2, removed: 0 }, "and a restore brings it back");
599
+
600
+ const setEmbedder = (e) => { const x = manifestOf(out, d); x.embedder = e; x.head = "moved"; writeFileSync(`${out}/cdn/domains/${d}/manifest`, JSON.stringify(x)); };
601
+ setEmbedder({ ...EMBEDDER, runtime: "transformers.js cuda" });
602
+ if ((await run())[d]?.error) throw new Error("another runtime of the same q8 model must be accepted");
603
+ setEmbedder({ runtime: "fastembed", dtype: "fp32" });
604
+ if (!/not comparable/.test((await run())[d]?.error)) throw new Error("appending to another encoder's view must be refused");
605
+
606
+ // The encoder gives out partway (a free daily allowance): what it embedded is served, and
607
+ // the next run embeds only the rest, then takes the head.
608
+ const big = mkdtempSync(`${tmpdir()}/view-`), BIG = `${PUB}/big`;
609
+ 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}` } })) };
610
+ let budget = 40;
611
+ const capped = (ts) => (budget -= ts.length) < 0 ? Promise.reject(new Error("daily allowance used")) : Promise.all(ts.map(embed));
612
+ const bd = domainFor(APP, PUB, "big");
613
+ eq((await publishView({ fangorn, app: APP, schema, out: big, repos: ["big"], embedBatch: capped, log: () => {} }))[bd],
614
+ { added: 32, removed: 0, partial: true }, "an encoder that gives out keeps what it embedded");
615
+ eq([manifestOf(big, bd).count, manifestOf(big, bd).head], [32, null], "served, and the head not taken");
616
+ budget = 1e9;
617
+ eq((await publishView({ fangorn, app: APP, schema, out: big, repos: ["big"], embedBatch: capped, log: () => {} }))[bd],
618
+ { added: 48, removed: 0 }, "the next run embeds only the rest");
619
+ eq(manifestOf(big, bd).count, 80, "and serves all of it");
620
+ delete chain[BIG];
621
+ rmSync(big, { recursive: true, force: true });
622
+ eq((await publishView({ fangorn, app: APP, schema, out, embed, rebake: true, log: () => {} }))[d],
510
623
  { added: 2, removed: 0 }, "--rebake starts the domain over");
511
624
  eq(readdirSync(`${out}/cdn/domains/${d}/shards`).length, 1, "and leaves no orphaned shards");
512
625
  rmSync(out, { recursive: true, force: true });
513
626
 
514
- // Relations: a declared edge lands on its source row as the target's identity; an
515
- // undeclared one does not; a link that arrives later rewrites the row without embedding it.
627
+ // Relations: a declared reference lands on its source row as the target's identity; an
628
+ // undeclared one is left alone; a target whose identity moves relinks the row without embedding it.
516
629
  {
517
- const T = { tag: "civic.item.v1" }, M = { tag: "civic.matter.v1" };
518
- const rel = { [`${OWNER}/${SCHEMA_NAMESPACE}`]: { vertices: [
519
- { cid: "ti", schemaId: TYPE_TAG, payload: { ...T, role_map: { identity: "item_id", title: "heading", thread: "part_of" } } },
520
- { cid: "tm", schemaId: TYPE_TAG, payload: { ...M, role_map: { identity: "matter_id", title: "title" } } }],
521
- edges: [{ relation: "part_of", sourceCid: "ti", targetCid: "tm" }] },
522
- [`${PUB}/civic`]: { vertices: [
523
- { cid: "i1", schemaId: T.tag, payload: { item_id: "a#1", heading: "Rezone, committee" } },
524
- { cid: "i2", schemaId: T.tag, payload: { item_id: "b#4", heading: "Rezone, council" } },
525
- { cid: "m1", schemaId: M.tag, payload: { matter_id: "plover:7", title: "Rezone" } }],
526
- edges: [{ relation: "part_of", sourceCid: "i1", targetCid: "m1" }, { relation: "cites", sourceCid: "i2", targetCid: "m1" }] } };
630
+ const T = "civic.item.v1", M = "civic.matter.v1";
631
+ const relSchema = { types: { [T]: { role_map: { identity: "item_id", title: "heading", thread: "part_of" } },
632
+ [M]: { role_map: { identity: "matter_id", title: "title" } } },
633
+ relations: [{ rel: "part_of", from: T, to: M }] };
634
+ const rel = {
635
+ [`${PUB}/civic`]: { records: [
636
+ rec("i1", T, { item_id: "a#1", heading: "Rezone, committee", part_of: { local: "m" } }),
637
+ rec("i2", T, { item_id: "b#4", heading: "Rezone, council", cites: { local: "m" } }),
638
+ rec("m1", M, { matter_id: "plover:7", title: "Rezone" }, "m")] } };
527
639
  const o = mkdtempSync(`${tmpdir()}/view-`), dr = domainFor(APP, PUB, "civic");
528
- const go = () => publishView({ fangorn: localFangorn(rel, { appId: APP, owner: OWNER }), app: APP, namespaces: ["civic"], out: o, embed, log: () => {} });
529
- const parts = () => Object.fromEntries([...loadDomain(`${o}/cdn/domains/${dr}`).rows.values()].map((r) => [r.fields.heading ?? r.fields.title, r.fields.part_of ?? r.fields.cites ?? null]));
640
+ const go = () => publishView({ fangorn: localFangorn(rel, { appId: APP, owner: OWNER }), app: APP, schema: relSchema, out: o, embed, log: () => {} });
641
+ const rows = async () => Object.fromEntries([...(await loadDomain(fsStore(o), `cdn/domains/${dr}`)).rows.values()]
642
+ .map((r) => [r.fields.heading ?? r.fields.title, r.fields.part_of ?? r.fields.cites ?? null]));
530
643
  await go();
531
- eq(parts(), { "Rezone, committee": "plover:7", "Rezone, council": null, Rezone: null }, "a declared relation carries the target's identity; an undeclared one is not written");
532
- eq(readJson(`${o}/cdn/domains/${dr}/manifest`).role_map.thread, "part_of", "the thread role reaches the manifest");
533
- rel[`${PUB}/civic`].edges.push({ relation: "part_of", sourceCid: "i2", targetCid: "m1" });
644
+ 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");
645
+ eq(manifestOf(o, dr).role_map.thread, "part_of", "the thread role reaches the manifest");
646
+ rel[`${PUB}/civic`].records[2] = rec("m1b", M, { matter_id: "plover:8", title: "Rezone" }, "m");
534
647
  const before = embedded.length;
535
- eq((await go())[dr], { added: 0, removed: 0, relinked: 1 }, "a link that comes later relinks the row");
536
- eq([parts()["Rezone, council"], embedded.length - before], ["plover:7", 0], "without embedding it again");
648
+ eq((await go())[dr], { added: 1, removed: 1, relinked: 1 }, "a target whose identity moves relinks the row that points at it");
649
+ eq([(await rows())["Rezone, committee"], embedded.length - before], ["plover:8", 1], "without embedding it again");
537
650
  rmSync(o, { recursive: true, force: true });
538
651
  }
539
- console.log("view.js self-check ok — domain names and embedded text match quickbeam's, the schema is applied, a second run embeds nothing, additions ship as delta shards, removals compact the domain, the real reader sees exactly the live records by the publisher's id, and another encoder's view is refused, and declared relations land on their source rows");
652
+
653
+ // The R2 store: the same four calls over a bucket's flat keys.
654
+ {
655
+ const objs = new Map();
656
+ const bucket = {
657
+ get: async (k) => objs.has(k) ? { arrayBuffer: async () => objs.get(k) } : null,
658
+ head: async (k) => objs.has(k) ? {} : null,
659
+ put: async (k, b) => { objs.set(k, typeof b === "string" ? new TextEncoder().encode(b) : b); },
660
+ delete: async (ks) => { for (const k of [ks].flat()) objs.delete(k); },
661
+ list: async ({ prefix, delimiter }) => {
662
+ const ks = [...objs.keys()].filter((k) => k.startsWith(prefix)).sort();
663
+ if (!delimiter) return { objects: ks.map((key) => ({ key })), truncated: false };
664
+ const deep = new Set(ks.filter((k) => k.slice(prefix.length).includes("/")).map((k) => prefix + k.slice(prefix.length).split("/")[0] + "/"));
665
+ return { objects: ks.filter((k) => !k.slice(prefix.length).includes("/")).map((key) => ({ key })), delimitedPrefixes: [...deep], truncated: false };
666
+ },
667
+ };
668
+ const r2 = r2Store(bucket);
669
+ const via = await publishView({ fangorn, app: APP, schema, store: r2, embed, log: () => {} });
670
+ eq(Object.keys(via), [d], "a view builds into a bucket");
671
+ eq(await r2.list("cdn/domains"), [d], "list names a folder's children");
672
+ eq((await liveFields(r2)).length, 2, "and reads back");
673
+ await r2.del(`cdn/domains/${d}`);
674
+ eq([...objs.keys()].filter((k) => k.includes(d)), [], "del removes a folder and everything under it");
675
+ }
676
+ console.log("view.js self-check ok — domain names and embedded text match quickbeam's, 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
677
  }