OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer commits / 508640c

0.0.57: an interaction rings the doorbell, and the doorbell's wire is proven
⛔ OBSERVED 2026-09-15: a like never reached its target. An interaction lives in its AUTHOR's feed, so the target learns of it by replication and ONLY if they hold that feed. From anyone else it is invisible and nothing rings. #61 §11.2 wrote that down in advance — "A never learns ⇒ the doorbell is mandatory, not optional." It is now mandatory: react, repost and reply each ring. The edge still lives in the liker's feed. The bell carries no content and grants nothing — one row in a namespace that can never become a record. Writing the interaction into the target's feed would be authoring as them, which #61 §4 removed the room primitive to avoid. ⛔ AND THE WIRE WAS NEVER PROVEN. feed-base.js said so on knock(), and doorbell-peer.test.js appended with its OWN writer "because there is no transport to carry a stranger's optimistic append". So every part of the doorbell was proven except the part that makes it one. doorbell-wire.test.js now runs it across a replication pipe: a reader who is not a writer appends, the row lands, her writer set is untouched and a record from him is refused. ⛔ `base.writable` GOES TRUE ON THE APPENDER after an optimistic append — "I have a local writer core", never "my appends are admitted". My first assertion used it as an authorisation check and was wrong; the test now asserts the property that matters. ⚠ A KNOCK FROM A FEED WE ALREADY HOLD IS NO LONGER LISTED. Someone we follow now rings on every reaction, and we can already read what they did — so it would be a second notification for one act. Skipped for RENDERING; the row stays, so consent is untouched. The suite caught this. ⚠ And the test that caught it was itself wrong: "the one thing a STRANGER can do" knocked from a feed we already hold. It was testing a friend and passing because nothing told the two apart. 392/392, 932/932. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
508640c98ba8e0eab1afd092230b7a062ceab4b7
by plankatron <fleeky@gmail.com> · 2026-09-15T18:04:56.000Z

Changed files (8) +1074 -150

.pgh-lock.json +750 -142
……@@ -1,9 +1,9 @@
11{
22 "name": "favela-peer",
3 "version": "0.0.50",
3 "version": "0.0.56",
44 "runtime": "bare",
5 "rootTreeHash": "d6422e37ce071e93589035c85bf6aea0db341b785fc763f6b6c77c3a3860586f",
6 "publishedAt": "2026-09-10T00:03:29.133Z",
5 "rootTreeHash": "d671f1d149cab9418c05acb06158ebeb24cd1daa984d57e428f31a716732f590",
6 "publishedAt": "2026-09-15T16:44:35.825Z",
77 "deps": [
88 {
99 "name": "@hyperswarm/secret-stream",
……@@ -50,7 +50,7 @@
5050 {
5151 "name": "anproto",
5252 "version": "0.1.0",
53 "tree": "ef3c43a7858ae5cbd82b7102dc5f330d38fc556d995d099c89c40c90fb0e3684",
53 "tree": "35bb93f536c3a8089ee2dfc238c17dddfb9208cd11a059c0886262ab242a5a0b",
5454 "normHash": "8d313660cbe4657820bede16f5e167b28e4f57e57a027fd4c00daf7b62fd63cd",
5555 "workspace": true
5656 },
……@@ -1571,9 +1571,9 @@
15711571 },
15721572 {
15731573 "name": "favela-identity",
1574 "version": "0.0.2",
1575 "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765",
1576 "normHash": "764cabc80c7ebc250bf01507b94d7e0960fa26958305cae299a326913d716d5f",
1574 "version": "0.0.3",
1575 "tree": "f5a680144af3a0d302a8a7af1e4009d1d5bf9960a65d31f11595cd303a04498e",
1576 "normHash": "b2e516b78dd9288fb8986981eaae3b6904732c1ce927b2263f43af34d09691c6",
15771577 "workspace": true
15781578 },
15791579 {
……@@ -2966,6 +2966,163 @@
29662966 "normHash": "2c833f38bcd7ee1f2208d8965be025d5f9cd7a117b121ee74133552f25294484",
29672967 "workspace": false
29682968 },
2969 {
2970 "name": "hyperident",
2971 "version": "0.2.6",
2972 "tree": "a0bdbfd60e82d2f521ccf99c9df4a919a384bab87e5c74f008c17dc09bc7ba2f",
2973 "normHash": "7e74dbbe38fbc48c7c7f24c3f3f796ff0b529718d01feefb79201e9650895b55",
2974 "workspace": true
2975 },
2976 {
2977 "name": "bare-os",
2978 "version": "2.4.4",
2979 "tree": "cb2f018b4019fd85e32b7008f157f55e41a977b7210dd51bec7b96e20384b28e",
2980 "normHash": "993d50d943f557a641970768cc4fe277d3b420bbabd0a2f33f30838674ecbf82",
2981 "workspace": false,
2982 "prebuilds": {
2983 "android-arm": "76cae1adbd819fd9ead5fbc728941303f4251c3f3f01111abf54554bf2001f9e",
2984 "android-arm64": "4b6607c9e10b108232be86e87ffc2eaf3f14e86965c9ead1e5fc9666a52922bc",
2985 "android-ia32": "05c734cbfa3c9d1e7ad6d7daf8e3373eb3dda16ccd3543ef5e96a77d230ee3f4",
2986 "android-x64": "f0444a6a31b4710c4710e6feb4fe2eed4cdc10f652439f053adb1c835157fb9b",
2987 "darwin-arm64": "ff054689e98b5f1be2e0f788ca5a673c56471bfcdea2d0b067f5bfee6f2e6dc7",
2988 "darwin-x64": "b3124e601d32980eb0219de5cee4b29363ea595f2aa57d671dc8f73af816fc80",
2989 "ios-arm64": "e643c7f59b981d4fb1c0478087249c734f29039baf3cda31223f9e58b9d70013",
2990 "ios-arm64-simulator": "4a9da039ad26bba49b8f234bd4a0f2153ca376ea6e9a5f6595ee046218840475",
2991 "ios-x64-simulator": "ca6eb709d86b5f63e6ba8616a6dae99e3834a64481937162173fda08e235b14f",
2992 "linux-arm64": "5d80405b7444bf7776d3a54fa95e4b5621aaf4566172276b4d1abf25b0379e83",
2993 "linux-x64": "eafbf19219e1fb13b298735563690280b9d479526b237a8164e83cc669848e9b",
2994 "win32-arm64": "f2de213e7a97077da9b51c2694894805f0e45fdb99c414e0e84b495dac7d98db",
2995 "win32-x64": "9677d035bbd03882511d7c58585b301e719c18252c72404e071e01aa58597d81"
2996 }
2997 },
2998 {
2999 "name": "bare-path",
3000 "version": "2.1.3",
3001 "tree": "277fa04b3d4e0a88695daa41addb494ad849d5a7321aae48fa50c2ff3575273c",
3002 "normHash": "ca9ad156584ecbe83abacd1a85bd6b861a90a42acc7ce9ce7dc7e46e3559830d",
3003 "workspace": false
3004 },
3005 {
3006 "name": "bare-subprocess",
3007 "version": "3.1.4",
3008 "tree": "c5f18b3620514f54714aeb351f074adface216a335242e323e33d28d261376fe",
3009 "normHash": "43a6cb895749f501c58a66837659c58c13b79248d198b11f8b1b00b33917c54a",
3010 "workspace": false,
3011 "prebuilds": {
3012 "android-arm": "f85ffa30c774472dbe0b22bf71311984dc379f099ab6b72257ce053015516f8f",
3013 "android-arm64": "a2efa038594daf8d76bb3bb05710fd958b8bb735e01c394993014e59e0f41364",
3014 "android-ia32": "3bb5eb9e8454a60b808f99b020d809a27516a25befc7a17f53b2022c6cdd5034",
3015 "android-x64": "a07b6643e4668a4f3e84b0a1b4eef7ee44c39cff02ff7d2d1831b4f667981a77",
3016 "darwin-arm64": "0c33409f3da9fd965a8ee0e2b0c555978fad4cb2f2ba1ae1111576ec7ce0e65d",
3017 "darwin-x64": "04999d3ce6c3cd93629fbb62f850428cbb1ff01d6da5f718d2de1888fb8e668b",
3018 "ios-arm64": "2f1c463a9e1d593d37ef3eff5b20bc85e6af974cb6e8f6d179cd880c9075fc4d",
3019 "ios-arm64-simulator": "513362f4d98a7a04e44f2400c21135321762679a20f9f25f9f0fb6d0d9506bc5",
3020 "ios-x64-simulator": "a9d91e9fb44b21c3c65d300d66fb5023cc29fd84a57eccd01037c0b73396fa3c",
3021 "linux-arm64": "89d285437ae5bfce40fa9721f79772f67f2e6e9a8469a938e63ead8b9834a352",
3022 "linux-x64": "9062c9eb6a12ace70192924a2ae2ccd41455c5949c687ca59505c8307b7d1688",
3023 "win32-arm64": "9d3f1ece4b42895c11b78417582010dd1e1ca87ea78f7126a2acd59625222cc3",
3024 "win32-x64": "6cb729616d573cb25b047a11f7713c1bf90ce58199ad26518f19fa9c706fdbcc"
3025 }
3026 },
3027 {
3028 "name": "bare-env",
3029 "version": "2.1.0",
3030 "tree": "d63f99cd758e899132ab23ff18b4ef341152b3dadd51cbd8584c1213b431d487",
3031 "normHash": "c8e0def456fd18d6844fc790dccb538c973c40d7fb723e184341aa00841bbc5a",
3032 "workspace": false
3033 },
3034 {
3035 "name": "bare-pipe",
3036 "version": "3.3.8",
3037 "tree": "5953a1f81079276ab95313c9b4133345558adb232125c486777aa6158954f5b9",
3038 "normHash": "01d2f0c1ee319f117626e553ee4746f57e9126f59e2c2c98966c51266302b3cb",
3039 "workspace": false,
3040 "prebuilds": {
3041 "android-arm": "46c0942b854a04849603bb4376fd59424b6024d4bf241975da8de3a126d0e797",
3042 "android-arm64": "ea0c10b6523aacd462a0754ef988ce0ce43a089aff3d852b465c773e4b036efc",
3043 "android-ia32": "490948ca1d3a767bd841a3a710d18630f8d26d3d0d885470587088f9e70e4355",
3044 "android-x64": "a33347347574ea9395474cb3948818d1d5f97da317d66456086370639640c29f",
3045 "darwin-arm64": "a3a922f6ca2c5266ebe557d60dc42cc791f3bec60406dc7b2b43cc9d893ed336",
3046 "darwin-x64": "ef422e27fb752432df1d11c7338f150b1f3adfb4fc35a1fc1f2e4438cbbd9d4b",
3047 "ios-arm64": "4c901f0acf189c06ab7fdf69347de79fdd2e2d06cd43e0964714e43966d7886a",
3048 "ios-arm64-simulator": "0c2861c14d70e664ee8acb54a69d83af7f11e5ca4df29226140430aab1c5aaca",
3049 "ios-x64-simulator": "c2cf7fad90a9da61f527fcc448d9427b973b47d5decca2fc5c17c0062afb0512",
3050 "linux-arm64": "2a4bd1012ac7fde0f4c8a073b678dda750b4dcc6859fd06b11f29b619cad38b4",
3051 "linux-x64": "718e0eb16d3ac31b7c3112de804c1d935ffb28f87c406b80b730fede917c503e",
3052 "win32-arm64": "bff3ef77e16d010ce988b423b1665865a2e40b9649c6013a4efb32299ff1cd40",
3053 "win32-x64": "38070c20b01420ef25df118efd293fd56152a8514bb38a484392f34ec8496916"
3054 }
3055 },
3056 {
3057 "name": "blind-relay",
3058 "version": "1.4.0",
3059 "tree": "e92100a4f821a638ac99c1407e4c7e784f8eb9661e6d9119ecbec4faae77cc7e",
3060 "normHash": "030126024d051810159e501686120c5759ba2483289fbeaa8093bb259e008c76",
3061 "workspace": false
3062 },
3063 {
3064 "name": "bogon",
3065 "version": "1.2.0",
3066 "tree": "1f5de2daa08643a5cb8bce2153ca4d55d4504fb366f1f1ef80bca1700ec085f7",
3067 "normHash": "87c8dca1d81792afc4686524aa041812a737709e9fb7f32824ef37e03fe10572",
3068 "workspace": false
3069 },
3070 {
3071 "name": "compact-encoding-bitfield",
3072 "version": "1.0.0",
3073 "tree": "579bdc1eaabbca726d3916f1fc74cfd4986653f6748632db586817a2a8caabea",
3074 "normHash": "f434c400769425fb1ca995f4096c83036f881eb9506d70b776ecb94a3cf39f52",
3075 "workspace": false
3076 },
3077 {
3078 "name": "compact-encoding-net",
3079 "version": "1.2.0",
3080 "tree": "967cf214778dbacd7cc7cc3850ffdff6a6f145b4bd73de741d0938d7d84d33d5",
3081 "normHash": "c87929fef6210618cdeb9ea6e19e11c77a77c8ca1274aa32e231eb082b753092",
3082 "workspace": false
3083 },
3084 {
3085 "name": "dht-rpc",
3086 "version": "6.26.4",
3087 "tree": "67c4c05f69061f65a2548595c43282be2da8e0934e6ce347afa034b29cc40c81",
3088 "normHash": "54d4bc92a385c266f0225919234c5eac168c7614704b13f59dd1e75139a067a5",
3089 "workspace": false
3090 },
3091 {
3092 "name": "hyperdht",
3093 "version": "6.30.0",
3094 "tree": "fc36977bda7dd2c7467653aa2ef520890b3a0aae16870a9269f0f71691673dce",
3095 "normHash": "429b22b6f4cce71778bf115219863d8c775ef749f4d360148d72f03df62431ec",
3096 "workspace": false
3097 },
3098 {
3099 "name": "hyperdht-address",
3100 "version": "1.0.1",
3101 "tree": "bba869f5c85b11bce5671d57a87cd2793b490588867cd07fbe94cad796114970",
3102 "normHash": "df578f3052c3a2b94031bdfa7b3953492623e8635140ce9d807fbc9dd135f5c7",
3103 "workspace": false
3104 },
3105 {
3106 "name": "hyperschema",
3107 "version": "1.20.1",
3108 "tree": "d0099e1743480f28a08c7374d20ad5048088475180ab606626b63acd59e365a5",
3109 "normHash": "46977f73b99138138d4a68833c760cc70397e6e99a94f99221b9e0987e24035b",
3110 "workspace": false
3111 },
3112 {
3113 "name": "pear-interface",
3114 "version": "1.1.0",
3115 "tree": "34d0bdd07463a6e2405f13875a5e2aed437ed0becfb3924542ac2062b7a5cbaf",
3116 "normHash": "e1aa4a09de7991fc2b4e07bf67cbbdf88ed1f4d853fc1cb582820e39f7326628",
3117 "workspace": false
3118 },
3119 {
3120 "name": "protomux",
3121 "version": "3.10.1",
3122 "tree": "60429c7756f039ec2b3e5240f702dee450c1da19ad6de06900a3877c6c72bb52",
3123 "normHash": "7011cea58a13650cebd82908f26ec8c724892686fce37829f66aedfa9b4934aa",
3124 "workspace": false
3125 },
29693126 {
29703127 "name": "sodium-hmac",
29713128 "version": "2.1.0",
……@@ -2973,6 +3130,42 @@
29733130 "normHash": "f4385c7a2896581aac1ac1ace2d510fcfa5ac07b3f6f42f7a1a6c5105e2dd8d1",
29743131 "workspace": false
29753132 },
3133 {
3134 "name": "udx-native",
3135 "version": "1.19.2",
3136 "tree": "2ecc91a8c3595dda5dfc86ace62a70875520adc4e588a2ac14945dbc4313c450",
3137 "normHash": "e0f1d39ff27047c2a137b8e66d2c9a5a2fe03552557a8a42856d2846e0d1f3d5",
3138 "workspace": false,
3139 "prebuilds": {
3140 "android-arm": "9777bdb34468acf089b5715a57b366ad18b06bb63641848652205014cc154872",
3141 "android-arm64": "a390555806c20b12f2a5aac3b8bf3a8d1dcf8d8d695c24eec35e7c213a56fc08",
3142 "android-ia32": "d41e14dccc86c8f37727bd44dca98c4cc86bd788dcbd579d8cd76c7bda6b9f61",
3143 "android-x64": "b9e82a968a8b87b115420631b81e2a7cd170c1a94e8f860a67a85f070810e50e",
3144 "darwin-arm64": "cab4b8c29a1c0fad1abd99a23d993554e6acf03760d5f64f4023426de998cb27",
3145 "darwin-x64": "2e1d33eed937120353b51b7b00136fa1ca81377f87d20193db6574630c36dbb4",
3146 "ios-arm64": "50db6c9694a3885ed9f81928a426c43372989b6063ab3828ea5917b4d272e010",
3147 "ios-arm64-simulator": "8073d1bd65623e59bf5c1da5b3185c276b37701bac2226ace67e5f47c4bcc154",
3148 "ios-x64-simulator": "6e682d4fd2d969d45543cbb68c572d767347eae7d347b16a5c9227f8f10beac5",
3149 "linux-arm64": "04ca107b93195b7620c84bcf885451066736c569295e5e338ea34c2b7bfa9c48",
3150 "linux-x64": "7daf2e3a8ff336614c06d741a6c9b93966ef3ed11bca0b366ac5b8a209e510b4",
3151 "win32-arm64": "2bbf9b4222f03ee18b14bd6467b9f6c79500d700925a225c9be37be52dbea5d8",
3152 "win32-x64": "abe49e5b7258d53037d967fc691a4ef2f2e4fa99b58c74fcaa14446c0194f5a7"
3153 }
3154 },
3155 {
3156 "name": "protomux-rpc",
3157 "version": "1.10.0",
3158 "tree": "b62444872f047f97d5cb1a22fbb99554846af2f8347dc2ac44f32f13940ef0f4",
3159 "normHash": "77db2864ca197ce57ec7ff47e8c8bf4fc62f66ae7a41af6dac616c783fd1a448",
3160 "workspace": false
3161 },
3162 {
3163 "name": "compact-encoding",
3164 "version": "3.3.0",
3165 "tree": "754944ca8ff184c8bdc55a8db26d1c1fcaf8f0fa855fd6779b3eb8c3ca3ec9e4",
3166 "normHash": "7bcc444da9f2a16ed2eeb780167cd5cebf42cdb33a396c98724489da6e8695b8",
3167 "workspace": false
3168 },
29763169 {
29773170 "name": "tiny-buffer-map",
29783171 "version": "1.1.1",
……@@ -3004,6 +3197,201 @@
30043197 }
30053198 ],
30063199 "acceptedConflicts": [
3200 {
3201 "type": "VERSION_CONFLICT",
3202 "dep": "bare-crypto",
3203 "newVersion": "1.15.3",
3204 "newTree": "c82ab3af512e791d52d721ee8c40199926fc649f34164d340d019b89b5c5a2bc",
3205 "existing": [
3206 {
3207 "appName": "hypernote",
3208 "version": "1.13.2",
3209 "tree": "da4c6b2654e9d66f10b0faa989311cf182878c24ab51b1c41458cfbf37ad29cd"
3210 }
3211 ],
3212 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3213 "acceptedAt": "2026-09-15T16:44:35.844Z"
3214 },
3215 {
3216 "type": "VERSION_CONFLICT",
3217 "dep": "bare-format",
3218 "newVersion": "1.0.2",
3219 "newTree": "a5309948aded2b30875eee7d9eb2c997f3fa01d43eb0c1fd90484cf68cd8b5d5",
3220 "existing": [
3221 {
3222 "appName": "hypernote",
3223 "version": "1.0.1",
3224 "tree": "40be8f559a13b54ebf0689e63e6c70f0d218035359e17c497ed7bc8a85c3cc95"
3225 }
3226 ],
3227 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3228 "acceptedAt": "2026-09-15T16:44:35.844Z"
3229 },
3230 {
3231 "type": "VERSION_CONFLICT",
3232 "dep": "bare-fs",
3233 "newVersion": "4.8.1",
3234 "newTree": "3358c77ab49da8137602bca8d21e56bb0d90916a237250446d6ee7724d8b949e",
3235 "existing": [
3236 {
3237 "appName": "hypernote",
3238 "version": "4.5.4",
3239 "tree": "3eae85efce82299769898c8151b5ed0a94e5a5ebdbcdee2105b815ca733a61cb"
3240 }
3241 ],
3242 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3243 "acceptedAt": "2026-09-15T16:44:35.844Z"
3244 },
3245 {
3246 "type": "VERSION_CONFLICT",
3247 "dep": "bare-http-parser",
3248 "newVersion": "2.1.4",
3249 "newTree": "e6991084a57472974a7f19d371cdfa226c28bd92be2d3b5a7ba38eabba8b3bb5",
3250 "existing": [
3251 {
3252 "appName": "hypernote",
3253 "version": "1.0.1",
3254 "tree": "8d9e01d443a127781773598d2e77c3f372334fad09e20623a819dabe49b22e1a"
3255 }
3256 ],
3257 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3258 "acceptedAt": "2026-09-15T16:44:35.844Z"
3259 },
3260 {
3261 "type": "VERSION_CONFLICT",
3262 "dep": "bare-http1",
3263 "newVersion": "4.6.1",
3264 "newTree": "ad81cb00dc100cbacbe692462fefc6cd1d676e4b6022e3b9fa2d9294a4461aac",
3265 "existing": [
3266 {
3267 "appName": "hypernote",
3268 "version": "4.2.3",
3269 "tree": "dc1ad6c999fe90d946ba0ef34698ea9af451b639be2ad1c5b61eabb08337a6ca"
3270 }
3271 ],
3272 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3273 "acceptedAt": "2026-09-15T16:44:35.844Z"
3274 },
3275 {
3276 "type": "VERSION_CONFLICT",
3277 "dep": "bare-https",
3278 "newVersion": "3.1.0",
3279 "newTree": "5e9cf8230c28d6b667d121cb23a5202b5d5d44e12dbeef0a7d6fbcc55621bb3d",
3280 "existing": [
3281 {
3282 "appName": "hypernote",
3283 "version": "2.1.2",
3284 "tree": "88b4779570e5113aa21b204989ac018d1628b387681f7e53428a7c99114fb701"
3285 }
3286 ],
3287 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3288 "acceptedAt": "2026-09-15T16:44:35.844Z"
3289 },
3290 {
3291 "type": "VERSION_CONFLICT",
3292 "dep": "bare-module",
3293 "newVersion": "6.4.0",
3294 "newTree": "bec68c2a03a0d3e53b5f5b5ad94a912c3404f870d1bcc033e9d667d0d3530d03",
3295 "existing": [
3296 {
3297 "appName": "hypernote",
3298 "version": "6.1.3",
3299 "tree": "f81b0df4731379088009176f2b02a3af0b32c33df73049d78f00c282e9a48203"
3300 }
3301 ],
3302 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3303 "acceptedAt": "2026-09-15T16:44:35.844Z"
3304 },
3305 {
3306 "type": "VERSION_CONFLICT",
3307 "dep": "bare-net",
3308 "newVersion": "2.3.3",
3309 "newTree": "4e55af5c11c37b80f1d5a1f3bf2a0f9b8f8857d7688719dffe00cff16736ccf6",
3310 "existing": [
3311 {
3312 "appName": "hypernote",
3313 "version": "2.2.0",
3314 "tree": "720b867ec4300a3caa2325eb1449499f9940bfb88341ecceb5992fa0fdf5f37d"
3315 }
3316 ],
3317 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3318 "acceptedAt": "2026-09-15T16:44:35.844Z"
3319 },
3320 {
3321 "type": "VERSION_CONFLICT",
3322 "dep": "bare-os",
3323 "newVersion": "3.9.3",
3324 "newTree": "e811e3d7014f67fba132cd31556892864227232560ae0fda3f3123f4a75c2992",
3325 "existing": [
3326 {
3327 "appName": "hypernote",
3328 "version": "3.6.2",
3329 "tree": "b697fe174482a708b49d0764cd4786291845f0034a31e0699381ca7e7b269d04"
3330 }
3331 ],
3332 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3333 "acceptedAt": "2026-09-15T16:44:35.844Z"
3334 },
3335 {
3336 "type": "VERSION_CONFLICT",
3337 "dep": "bare-pipe",
3338 "newVersion": "4.3.1",
3339 "newTree": "08013f3f76e93f236b04c5a804dfd12debdc44bc2b5a5a5010f190f96ad682ec",
3340 "existing": [
3341 {
3342 "appName": "hypernote",
3343 "version": "4.1.3",
3344 "tree": "767f07242277ae80bd395de5a4969fcdbf956c216158ae517fb2b562558ac1a4"
3345 }
3346 ],
3347 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3348 "acceptedAt": "2026-09-15T16:44:35.844Z"
3349 },
3350 {
3351 "type": "VERSION_CONFLICT",
3352 "dep": "bare-process",
3353 "newVersion": "4.5.1",
3354 "newTree": "6d77305da357bc79789ae5f37818f9ca5f41c71ec27edacc74ce527c88b7e8c1",
3355 "existing": [
3356 {
3357 "appName": "hypernote",
3358 "version": "4.2.2",
3359 "tree": "de7deed21f587ffc8bde4971abe00fca77bb6c71c4e7effb31ad34ee330815df"
3360 }
3361 ],
3362 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3363 "acceptedAt": "2026-09-15T16:44:35.844Z"
3364 },
3365 {
3366 "type": "VERSION_CONFLICT",
3367 "dep": "bare-semver",
3368 "newVersion": "1.1.0",
3369 "newTree": "a823aa1d9b306b2af0c80660fb5a63a5a5adbc5dfa003f887fd482112c906025",
3370 "existing": [
3371 {
3372 "appName": "hypernote",
3373 "version": "1.0.2",
3374 "tree": "2df42517ebf73cd9c11f2f652f75e87401fd4ed7b856c0adc15c33baf1d4f1ad"
3375 }
3376 ],
3377 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3378 "acceptedAt": "2026-09-15T16:44:35.844Z"
3379 },
3380 {
3381 "type": "VERSION_CONFLICT",
3382 "dep": "bare-stream",
3383 "newVersion": "2.13.4",
3384 "newTree": "c61803ebf372fc64d3549eb1043087f3a531e95c2217768226320e09eb373077",
3385 "existing": [
3386 {
3387 "appName": "hypernote",
3388 "version": "2.8.0",
3389 "tree": "325aca171cbff560487177584078c8066133e91176639ca47c98d70e195aa92e"
3390 }
3391 ],
3392 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3393 "acceptedAt": "2026-09-15T16:44:35.844Z"
3394 },
30073395 {
30083396 "type": "VERSION_CONFLICT",
30093397 "dep": "bare-subprocess",
……@@ -3011,13 +3399,178 @@
30113399 "newTree": "d31788ffa334976153023c6de84e5020b2009f51f9d472d0c2f09a13a53fc2b6",
30123400 "existing": [
30133401 {
3014 "appName": "hyperident",
3015 "version": "3.1.4",
3016 "tree": "c5f18b3620514f54714aeb351f074adface216a335242e323e33d28d261376fe"
3402 "appName": "hypernote",
3403 "version": "5.2.2",
3404 "tree": "1bd0eb382789c62cd4622d47bffb60984fdd766260c79f73a2fc7a136f3edbe9"
3405 }
3406 ],
3407 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3408 "acceptedAt": "2026-09-15T16:44:35.844Z"
3409 },
3410 {
3411 "type": "VERSION_CONFLICT",
3412 "dep": "bare-tls",
3413 "newVersion": "3.1.10",
3414 "newTree": "e693499377f146eec5788fa2887c4e42614f7f2efd3d5848d9019deec86c6632",
3415 "existing": [
3416 {
3417 "appName": "hypernote",
3418 "version": "2.1.7",
3419 "tree": "4526fe9dc5468264c7171ec16e9303726674b7b44ca0b3e09cb8b9a6a1be6169"
3420 }
3421 ],
3422 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3423 "acceptedAt": "2026-09-15T16:44:35.844Z"
3424 },
3425 {
3426 "type": "VERSION_CONFLICT",
3427 "dep": "bare-tty",
3428 "newVersion": "5.2.1",
3429 "newTree": "d99fbe108f5700f5e73d908e2d3d39ed44021b11bc74e1f152ebb493719b9eaa",
3430 "existing": [
3431 {
3432 "appName": "hypernote",
3433 "version": "5.0.3",
3434 "tree": "32b291cd756e142da18a6ed01c351e024fcedc3e2b1ecfbfb85f3106ada4419b"
3435 }
3436 ],
3437 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3438 "acceptedAt": "2026-09-15T16:44:35.844Z"
3439 },
3440 {
3441 "type": "VERSION_CONFLICT",
3442 "dep": "bare-url",
3443 "newVersion": "2.5.4",
3444 "newTree": "99519af1452440489f560cb045aba0ad314bbae19632f1184cdf91d5946571f5",
3445 "existing": [
3446 {
3447 "appName": "hypernote",
3448 "version": "2.3.2",
3449 "tree": "4a8cd3b0093092b33088617dc0415e8179882d7f2f13b7649962ca20b08beeed"
3450 }
3451 ],
3452 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3453 "acceptedAt": "2026-09-15T16:44:35.844Z"
3454 },
3455 {
3456 "type": "VERSION_CONFLICT",
3457 "dep": "bare-utils",
3458 "newVersion": "1.6.0",
3459 "newTree": "c05399d286dc85281bcaa88572c3d9c54ad631a50ecacfc50c0b73451b2a18a8",
3460 "existing": [
3461 {
3462 "appName": "hypernote",
3463 "version": "1.5.1",
3464 "tree": "4f63f165ee6a581f689b40c4a40df4df01ab6fba17d46bd29e40ef5bbdd9884f"
3465 }
3466 ],
3467 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3468 "acceptedAt": "2026-09-15T16:44:35.844Z"
3469 },
3470 {
3471 "type": "VERSION_CONFLICT",
3472 "dep": "bare-ws",
3473 "newVersion": "3.1.0",
3474 "newTree": "9c22db1f4e3137df4e44ba33887b65ce280bfc93454f8b91d2e41b458452e86b",
3475 "existing": [
3476 {
3477 "appName": "hypernote",
3478 "version": "2.0.4",
3479 "tree": "735109c736c0bd3413ab47414fd8b6ea9a99c401fe8e6d608459c79293806754"
3480 }
3481 ],
3482 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3483 "acceptedAt": "2026-09-15T16:44:35.844Z"
3484 },
3485 {
3486 "type": "VERSION_CONFLICT",
3487 "dep": "compact-encoding",
3488 "newVersion": "3.4.0",
3489 "newTree": "7579a2d3581169529b52ec241bf982d2d0c1b2fe24f8b64873baed5e3595436f",
3490 "existing": [
3491 {
3492 "appName": "hypernote",
3493 "version": "2.19.0",
3494 "tree": "b32b75bf1b2acdd18ed0cc370d01d477fba42ba09fd1f2f78af3ebad925e72d5"
3495 }
3496 ],
3497 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3498 "acceptedAt": "2026-09-15T16:44:35.844Z"
3499 },
3500 {
3501 "type": "VERSION_CONFLICT",
3502 "dep": "paparam",
3503 "newVersion": "1.13.0",
3504 "newTree": "4d8d5a7eb968560e887d7c9d097a5326b281b67b04e69a5e6b34514985e861ae",
3505 "existing": [
3506 {
3507 "appName": "hypernote",
3508 "version": "1.10.0",
3509 "tree": "3bedca5287a9041fc2fd5ec5ace4df1fa86240e6afec0a105ba67398bafc09a5"
3510 }
3511 ],
3512 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3513 "acceptedAt": "2026-09-15T16:44:35.844Z"
3514 },
3515 {
3516 "type": "VERSION_CONFLICT",
3517 "dep": "picomatch",
3518 "newVersion": "4.0.7",
3519 "newTree": "7236258ca920c2f9853c24ead4286ec92a5f709fdf0117f38257e9702752d3ce",
3520 "existing": [
3521 {
3522 "appName": "hypernote",
3523 "version": "4.0.3",
3524 "tree": "b9ca9b272008bd67672a1b252390e8b527095c46b46cf6b6725a7a187eb038b6"
30173525 }
30183526 ],
3019 "reason": "dep version-splits only; 0 code-differs",
3020 "acceptedAt": "2026-09-10T00:03:29.149Z"
3527 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3528 "acceptedAt": "2026-09-15T16:44:35.844Z"
3529 },
3530 {
3531 "type": "VERSION_CONFLICT",
3532 "dep": "require-addon",
3533 "newVersion": "1.2.0",
3534 "newTree": "c51c5fb701d40f980004f1e17c5ae02d0a7657b4731292ec63d5ae7814acf355",
3535 "existing": [
3536 {
3537 "appName": "wirecore",
3538 "version": "1.3.0",
3539 "tree": "7793645d7d4248d6d6533e3cbcd4bf40bed4f38c678b0ca0606c15640ba5e7b9"
3540 }
3541 ],
3542 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3543 "acceptedAt": "2026-09-15T16:44:35.844Z"
3544 },
3545 {
3546 "type": "VERSION_CONFLICT",
3547 "dep": "sodium-native",
3548 "newVersion": "5.1.0",
3549 "newTree": "5552185a3644b46195a7a31c2cc21fca5eced9a552c541736120ed65900feff5",
3550 "existing": [
3551 {
3552 "appName": "hypernote",
3553 "version": "5.0.10",
3554 "tree": "79447513d4ac0357c8d60bdc7b81b3c5143d761b26700340cba266c4d1cb77b0"
3555 }
3556 ],
3557 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3558 "acceptedAt": "2026-09-15T16:44:35.844Z"
3559 },
3560 {
3561 "type": "VERSION_CONFLICT",
3562 "dep": "streamx",
3563 "newVersion": "2.28.1",
3564 "newTree": "2cb964b1c3ebf23b9605c6971c3ff1782b97d48730574458198f630d770f4cbe",
3565 "existing": [
3566 {
3567 "appName": "hypernote",
3568 "version": "2.23.0",
3569 "tree": "279d4e0447eb79fd75816864ba7f41f2df03080c9da69ea4603fe2900c8d9e53"
3570 }
3571 ],
3572 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3573 "acceptedAt": "2026-09-15T16:44:35.844Z"
30213574 },
30223575 {
30233576 "type": "VERSION_CONFLICT",
……@@ -3059,10 +3612,15 @@
30593612 "appName": "favela-bridge",
30603613 "version": "3.6.1",
30613614 "tree": "6bb5233025d1c30e4f181e29b6ab5af549d8909c7070c7b4438b30f1a4e76ce2"
3615 },
3616 {
3617 "appName": "hypernote",
3618 "version": "3.4.4",
3619 "tree": "3bb78d8f06263a549bd1077f37a7df257b1d557e44608631435af625f05a9c11"
30623620 }
30633621 ],
3064 "reason": "dep version-splits only; 0 code-differs",
3065 "acceptedAt": "2026-09-10T00:03:29.149Z"
3622 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3623 "acceptedAt": "2026-09-15T16:44:35.844Z"
30663624 },
30673625 {
30683626 "type": "VERSION_CONFLICT",
……@@ -3074,10 +3632,15 @@
30743632 "appName": "pear-git-v3",
30753633 "version": "5.2.3",
30763634 "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d"
3635 },
3636 {
3637 "appName": "hypernote",
3638 "version": "5.2.3",
3639 "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d"
30773640 }
30783641 ],
3079 "reason": "dep version-splits only; 0 code-differs",
3080 "acceptedAt": "2026-09-10T00:03:29.149Z"
3642 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3643 "acceptedAt": "2026-09-15T16:44:35.844Z"
30813644 },
30823645 {
30833646 "type": "VERSION_CONFLICT",
……@@ -3099,10 +3662,15 @@
30993662 "appName": "pgh-sidecar",
31003663 "version": "1.5.5",
31013664 "tree": "bdebba5a532ce4846b49f1e5ffad774becfc44182d0c3737f7e441ab42f4c43d"
3665 },
3666 {
3667 "appName": "hypernote",
3668 "version": "1.5.2",
3669 "tree": "89a40400106bf4fef95fb5c44a66ee29aada6f76c7740bfba09d8aeeb841f39c"
31023670 }
31033671 ],
3104 "reason": "dep version-splits only; 0 code-differs",
3105 "acceptedAt": "2026-09-10T00:03:29.149Z"
3672 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3673 "acceptedAt": "2026-09-15T16:44:35.844Z"
31063674 },
31073675 {
31083676 "type": "VERSION_CONFLICT",
……@@ -3131,68 +3699,23 @@
31313699 "tree": "b799e6ba8726aa951b45f12cc7ecffe5f4e9ee1e8adf09a34d026d9d5658828a"
31323700 }
31333701 ],
3134 "reason": "dep version-splits only; 0 code-differs",
3135 "acceptedAt": "2026-09-10T00:03:29.149Z"
3136 },
3137 {
3138 "type": "VERSION_CONFLICT",
3139 "dep": "blind-relay",
3140 "newVersion": "1.6.1",
3141 "newTree": "bfbfcab6c72c2aba5d6d9d8394454d69ef55adb58001b0b232d8d8a3bde6aeea",
3142 "existing": [
3143 {
3144 "appName": "hyperident",
3145 "version": "1.4.0",
3146 "tree": "e92100a4f821a638ac99c1407e4c7e784f8eb9661e6d9119ecbec4faae77cc7e"
3147 }
3148 ],
3149 "reason": "dep version-splits only; 0 code-differs",
3150 "acceptedAt": "2026-09-10T00:03:29.149Z"
3151 },
3152 {
3153 "type": "VERSION_CONFLICT",
3154 "dep": "bogon",
3155 "newVersion": "1.3.0",
3156 "newTree": "4387e12ae7e7adeaa6c77821e4342762d4e0c500761c159a9b1e42cbf57fdbf8",
3157 "existing": [
3158 {
3159 "appName": "hyperident",
3160 "version": "1.2.0",
3161 "tree": "1f5de2daa08643a5cb8bce2153ca4d55d4504fb366f1f1ef80bca1700ec085f7"
3162 }
3163 ],
3164 "reason": "dep version-splits only; 0 code-differs",
3165 "acceptedAt": "2026-09-10T00:03:29.149Z"
3166 },
3167 {
3168 "type": "VERSION_CONFLICT",
3169 "dep": "compact-encoding-bitfield",
3170 "newVersion": "1.1.0",
3171 "newTree": "72d0bc16fc40b93fecca34b548ac02c5c94e91d2a00100f18ce9c3dd9b6eb88d",
3172 "existing": [
3173 {
3174 "appName": "hyperident",
3175 "version": "1.0.0",
3176 "tree": "579bdc1eaabbca726d3916f1fc74cfd4986653f6748632db586817a2a8caabea"
3177 }
3178 ],
3179 "reason": "dep version-splits only; 0 code-differs",
3180 "acceptedAt": "2026-09-10T00:03:29.149Z"
3702 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3703 "acceptedAt": "2026-09-15T16:44:35.844Z"
31813704 },
31823705 {
31833706 "type": "VERSION_CONFLICT",
3184 "dep": "compact-encoding-net",
3185 "newVersion": "1.3.0",
3186 "newTree": "67c6f518d7418df62906199d7c944d3785629d478a6c7ade1aca263bc6188dd0",
3707 "dep": "blind-pairing-core",
3708 "newVersion": "2.10.1",
3709 "newTree": "645ea490be09f10f5bba3dd728574604c63fa40ca61723ec74b9e449605d48d1",
31873710 "existing": [
31883711 {
3189 "appName": "hyperident",
3190 "version": "1.2.0",
3191 "tree": "967cf214778dbacd7cc7cc3850ffdff6a6f145b4bd73de741d0938d7d84d33d5"
3712 "appName": "hypernote",
3713 "version": "2.10.0",
3714 "tree": "c486fe73805bf013e9c5de91b5780d1aa5d2b2295306f411f7863757aaa05e60"
31923715 }
31933716 ],
3194 "reason": "dep version-splits only; 0 code-differs",
3195 "acceptedAt": "2026-09-10T00:03:29.149Z"
3717 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3718 "acceptedAt": "2026-09-15T16:44:35.844Z"
31963719 },
31973720 {
31983721 "type": "VERSION_CONFLICT",
……@@ -3214,10 +3737,20 @@
32143737 "appName": "favela-bridge",
32153738 "version": "7.11.0",
32163739 "tree": "35b214bfbf85f8bbcb9f1926470e77f4e240d9b5045b07f81ce83fec24c1380c"
3740 },
3741 {
3742 "appName": "wirecore",
3743 "version": "6.18.4",
3744 "tree": "ed4e0b480417610e195ff0b59e5b9a453abbf2db6d6ce72d7cd522269f835e9a"
3745 },
3746 {
3747 "appName": "hypernote",
3748 "version": "7.9.2",
3749 "tree": "ceb96c67cbac0fda8612669b90fc6574c408b25c985f0563cbb31136c1fb44e8"
32173750 }
32183751 ],
3219 "reason": "dep version-splits only; 0 code-differs",
3220 "acceptedAt": "2026-09-10T00:03:29.149Z"
3752 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3753 "acceptedAt": "2026-09-15T16:44:35.844Z"
32213754 },
32223755 {
32233756 "type": "VERSION_CONFLICT",
……@@ -3226,38 +3759,63 @@
32263759 "newTree": "b8a0963abe945df0c795a3e086562a4c3e53d569cff054894942e3e772733ec2",
32273760 "existing": [
32283761 {
3229 "appName": "hyperident",
3230 "version": "6.26.4",
3231 "tree": "67c4c05f69061f65a2548595c43282be2da8e0934e6ce347afa034b29cc40c81"
3762 "appName": "hypernote",
3763 "version": "6.26.3",
3764 "tree": "a366a73e74f8d80f106465058083b62bb70b833c721047124e99c89880e5c706"
32323765 }
32333766 ],
3234 "reason": "dep version-splits only; 0 code-differs",
3235 "acceptedAt": "2026-09-10T00:03:29.149Z"
3767 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3768 "acceptedAt": "2026-09-15T16:44:35.844Z"
32363769 },
32373770 {
32383771 "type": "VERSION_CONFLICT",
3239 "dep": "favela-markdown",
3240 "newVersion": "0.2.0",
3241 "newTree": "e980c91c65708beb2d24964f4f8bce19d01f9783d1d93e4eff8674851df64611",
3772 "dep": "favela-identity",
3773 "newVersion": "0.0.3",
3774 "newTree": "f5a680144af3a0d302a8a7af1e4009d1d5bf9960a65d31f11595cd303a04498e",
32423775 "existing": [
32433776 {
3244 "appName": "ap-bridge",
3245 "version": "0.1.6",
3246 "tree": "08913adf8bf7452a12cc06528c49b4629c880de61fa45f01990e33b000a820d9"
3247 },
3777 "appName": "favela-bridge",
3778 "version": "0.0.2",
3779 "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765"
3780 }
3781 ],
3782 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3783 "acceptedAt": "2026-09-15T16:44:35.844Z"
3784 },
3785 {
3786 "type": "VERSION_CONFLICT",
3787 "dep": "keet-identity-key",
3788 "newVersion": "3.2.0",
3789 "newTree": "f49a14a1e514c3b607ba0211e7d993e37a387ad60456831abaff3c5eae917114",
3790 "existing": [
32483791 {
3249 "appName": "favela-surface",
3250 "version": "0.1.6",
3251 "tree": "08913adf8bf7452a12cc06528c49b4629c880de61fa45f01990e33b000a820d9"
3792 "appName": "wirecore",
3793 "version": "3.1.0",
3794 "tree": "2adc29428c08a3a6cf227eba2d06ccdccaf5df83f47404f7f9c1da5f4b05dc80"
32523795 },
32533796 {
3254 "appName": "favela-bridge",
3255 "version": "0.1.6",
3256 "tree": "08913adf8bf7452a12cc06528c49b4629c880de61fa45f01990e33b000a820d9"
3797 "appName": "hypernote",
3798 "version": "3.1.0",
3799 "tree": "189397178121e9a18430943345dab820691648a324229071ceb980bbb77a7393"
3800 }
3801 ],
3802 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3803 "acceptedAt": "2026-09-15T16:44:35.844Z"
3804 },
3805 {
3806 "type": "VERSION_CONFLICT",
3807 "dep": "fs-native-extensions",
3808 "newVersion": "1.5.1",
3809 "newTree": "da7a884ddc1fee07899b74d81b79232dc905d14d043720e551e1f24d8a5fdb98",
3810 "existing": [
3811 {
3812 "appName": "hypernote",
3813 "version": "1.4.5",
3814 "tree": "fc766e83f3b898a1a78e898755509ca8ae59d9ecacbbad6e160787d541aeb655"
32573815 }
32583816 ],
3259 "reason": "dep version-splits only; 0 code-differs",
3260 "acceptedAt": "2026-09-10T00:03:29.149Z"
3817 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3818 "acceptedAt": "2026-09-15T16:44:35.844Z"
32613819 },
32623820 {
32633821 "type": "VERSION_CONFLICT",
……@@ -3279,10 +3837,20 @@
32793837 "appName": "favela-bridge",
32803838 "version": "11.33.5",
32813839 "tree": "4ef8a03ba96b1488cb7ee35dc582b14d096ae4b3fab5481186c94516d434fe18"
3840 },
3841 {
3842 "appName": "wirecore",
3843 "version": "10.38.2",
3844 "tree": "50137bc8c268148b89c11b15cb8918375338c3ea14c6507c3bce89e3cc91ceba"
3845 },
3846 {
3847 "appName": "hypernote",
3848 "version": "11.27.17",
3849 "tree": "a9e07d54e526c838dfe5e27df33936682aefb218f61a45b6c15289243999f648"
32823850 }
32833851 ],
3284 "reason": "dep version-splits only; 0 code-differs",
3285 "acceptedAt": "2026-09-10T00:03:29.149Z"
3852 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3853 "acceptedAt": "2026-09-15T16:44:35.844Z"
32863854 },
32873855 {
32883856 "type": "VERSION_CONFLICT",
……@@ -3304,10 +3872,15 @@
33043872 "appName": "favela-bridge",
33053873 "version": "3.2.0",
33063874 "tree": "5fc49d698f6ddfd653b3cdaa72ec572d6804293e2e9c3e98303f69417115e903"
3875 },
3876 {
3877 "appName": "hypernote",
3878 "version": "2.7.1",
3879 "tree": "9d84c86441fe138b227c4fd01d1e948aa83bf8d13cfb1aa957838feea05fbb57"
33073880 }
33083881 ],
3309 "reason": "dep version-splits only; 0 code-differs",
3310 "acceptedAt": "2026-09-10T00:03:29.149Z"
3882 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3883 "acceptedAt": "2026-09-15T16:44:35.844Z"
33113884 },
33123885 {
33133886 "type": "VERSION_CONFLICT",
……@@ -3316,13 +3889,28 @@
33163889 "newTree": "c352064599040eaa58e03c28aa235e3ce7b1155bfb5b1c422b7ff98180099857",
33173890 "existing": [
33183891 {
3319 "appName": "hyperident",
3320 "version": "1.20.1",
3321 "tree": "d0099e1743480f28a08c7374d20ad5048088475180ab606626b63acd59e365a5"
3892 "appName": "wirecore",
3893 "version": "1.24.0",
3894 "tree": "4a3afbe2fc7a882e24454eb2e7e7cfe6816811ee5d73ae0f92e8b9ea9cfb8c78"
33223895 }
33233896 ],
3324 "reason": "dep version-splits only; 0 code-differs",
3325 "acceptedAt": "2026-09-10T00:03:29.149Z"
3897 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3898 "acceptedAt": "2026-09-15T16:44:35.844Z"
3899 },
3900 {
3901 "type": "VERSION_CONFLICT",
3902 "dep": "index-encoder",
3903 "newVersion": "3.5.0",
3904 "newTree": "9d7780c45bc16e68fb1931323ee89c6ac8413a311f140c3ffbf846035e91693d",
3905 "existing": [
3906 {
3907 "appName": "hypernote",
3908 "version": "3.4.0",
3909 "tree": "1e5c5810d8971a3c27fde7fb0e4d633dcabbe2cf98d8df86b473f8d67eead8b9"
3910 }
3911 ],
3912 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3913 "acceptedAt": "2026-09-15T16:44:35.844Z"
33263914 },
33273915 {
33283916 "type": "VERSION_CONFLICT",
……@@ -3331,13 +3919,13 @@
33313919 "newTree": "9afec1f15a5bf7dd535eeb082e0ec4c60f5fd425a44977bfb1e0f0aa635d342b",
33323920 "existing": [
33333921 {
3334 "appName": "hyperident",
3335 "version": "3.10.1",
3336 "tree": "60429c7756f039ec2b3e5240f702dee450c1da19ad6de06900a3877c6c72bb52"
3922 "appName": "wirecore",
3923 "version": "3.12.0",
3924 "tree": "e3478f50e85ba0ac288efed83e84c992483904897dc39ccf243d20a4ea25d972"
33373925 }
33383926 ],
3339 "reason": "dep version-splits only; 0 code-differs",
3340 "acceptedAt": "2026-09-10T00:03:29.149Z"
3927 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3928 "acceptedAt": "2026-09-15T16:44:35.844Z"
33413929 },
33423930 {
33433931 "type": "VERSION_CONFLICT",
……@@ -3359,10 +3947,30 @@
33593947 "appName": "favela-bridge",
33603948 "version": "3.17.2",
33613949 "tree": "6fc80ad1ccbfb046613f36299032f21a4752e2614ff8d88a7a5bd7ce66fc1293"
3950 },
3951 {
3952 "appName": "hypernote",
3953 "version": "3.13.0",
3954 "tree": "c05d8bf7eab5024ea98140f78c84d12fb494b4b7b10c7eedeb50b2be842fa519"
3955 }
3956 ],
3957 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3958 "acceptedAt": "2026-09-15T16:44:35.844Z"
3959 },
3960 {
3961 "type": "VERSION_CONFLICT",
3962 "dep": "safety-catch",
3963 "newVersion": "1.0.3",
3964 "newTree": "ff3d215ed5100cbae606310a5e795aaed09dc5505bee24e929d64898b78761d8",
3965 "existing": [
3966 {
3967 "appName": "hypernote",
3968 "version": "1.0.2",
3969 "tree": "0697bb338bbcec517353cffcdb2345a8232b9b90cc627a549a306f0323576390"
33623970 }
33633971 ],
3364 "reason": "dep version-splits only; 0 code-differs",
3365 "acceptedAt": "2026-09-10T00:03:29.149Z"
3972 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3973 "acceptedAt": "2026-09-15T16:44:35.844Z"
33663974 },
33673975 {
33683976 "type": "VERSION_CONFLICT",
……@@ -3376,58 +3984,58 @@
33763984 "tree": "10193a86e2c6f80c2d4377379a0aae4a959f8a40fd9e852b85fee081bfe6675a"
33773985 }
33783986 ],
3379 "reason": "dep version-splits only; 0 code-differs",
3380 "acceptedAt": "2026-09-10T00:03:29.149Z"
3987 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
3988 "acceptedAt": "2026-09-15T16:44:35.844Z"
33813989 },
33823990 {
33833991 "type": "VERSION_CONFLICT",
3384 "dep": "hyperdht",
3385 "newVersion": "6.34.0",
3386 "newTree": "f4e87130e4397b037b121356f2db0161fadd8f2c320f7042fa31f4b5308193ec",
3992 "dep": "bare-zlib",
3993 "newVersion": "1.4.0",
3994 "newTree": "79d82655bc02fcfbc7d118ed2d64ad8f5914720192017140dccacea98601652d",
33873995 "existing": [
33883996 {
3389 "appName": "hyperident",
3390 "version": "6.30.0",
3391 "tree": "fc36977bda7dd2c7467653aa2ef520890b3a0aae16870a9269f0f71691673dce"
3392 },
3393 {
3394 "appName": "favela-bridge",
3395 "version": "6.33.0",
3396 "tree": "36d01631ddee74ac2ce62971abd7c0ac30562da8dfd6495480da56c534eb80b7"
3997 "appName": "hypernote",
3998 "version": "1.3.1",
3999 "tree": "20b1ecd2937f3e35102892288234b86a8ec6deb3711016d96ff3509474ac465b"
33974000 }
33984001 ],
3399 "reason": "dep version-splits only; 0 code-differs",
3400 "acceptedAt": "2026-09-10T00:03:29.149Z"
4002 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
4003 "acceptedAt": "2026-09-15T16:44:35.844Z"
34014004 },
34024005 {
34034006 "type": "VERSION_CONFLICT",
3404 "dep": "hyperdht-address",
3405 "newVersion": "1.1.1",
3406 "newTree": "5f549048ecf991023104f0ca66a7cb9fab6fe1a8ec8f8123889c6fa67ecce189",
4007 "dep": "hyperdht",
4008 "newVersion": "6.34.0",
4009 "newTree": "f4e87130e4397b037b121356f2db0161fadd8f2c320f7042fa31f4b5308193ec",
34074010 "existing": [
34084011 {
3409 "appName": "hyperident",
3410 "version": "1.0.1",
3411 "tree": "bba869f5c85b11bce5671d57a87cd2793b490588867cd07fbe94cad796114970"
4012 "appName": "favela-bridge",
4013 "version": "6.33.0",
4014 "tree": "36d01631ddee74ac2ce62971abd7c0ac30562da8dfd6495480da56c534eb80b7"
4015 },
4016 {
4017 "appName": "hypernote",
4018 "version": "6.29.0",
4019 "tree": "7912e790c9e87e9829a6daa037c5f5da0cc4355214f51d334f96e57678dfddeb"
34124020 }
34134021 ],
3414 "reason": "dep version-splits only; 0 code-differs",
3415 "acceptedAt": "2026-09-10T00:03:29.149Z"
4022 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
4023 "acceptedAt": "2026-09-15T16:44:35.844Z"
34164024 },
34174025 {
34184026 "type": "VERSION_CONFLICT",
3419 "dep": "udx-native",
3420 "newVersion": "1.21.1",
3421 "newTree": "e6197dd12fe7f94f5d6a90ff2cc5817762aea83cbfdc765976bec8bb2bdaf421",
4027 "dep": "hyperswarm",
4028 "newVersion": "4.17.0",
4029 "newTree": "b668835d94500d16f2c5fecb5f5842620360c9991e26ec081c49fbf7e42c36c5",
34224030 "existing": [
34234031 {
3424 "appName": "hyperident",
3425 "version": "1.19.2",
3426 "tree": "2ecc91a8c3595dda5dfc86ace62a70875520adc4e588a2ac14945dbc4313c450"
4032 "appName": "wirecore",
4033 "version": "4.17.1",
4034 "tree": "003ac841449fcdda1abc3c87fee291a5d9596d245925a7298bd8ef5f8e423eae"
34274035 }
34284036 ],
3429 "reason": "dep version-splits only; 0 code-differs",
3430 "acceptedAt": "2026-09-10T00:03:29.149Z"
4037 "reason": "favela-peer 0.0.56: 46 conflicts, all version-splits/docs/packaging, 0 code-differs per pgh's classification",
4038 "acceptedAt": "2026-09-15T16:44:35.844Z"
34314039 }
34324040 ]
34334041}
lib/feed-base.js +6 -2
……@@ -222,8 +222,12 @@ export class Feed {
222222 // Ring someone's doorbell: append to THEIR feed, optimistically, naming MY feed. It grants
223223 // nothing and asks for nothing except to be seen.
224224 //
225 // ⚠ Requires holding their base, which requires a transport. Untested end to end for that reason —
226 // the authorisation rules it relies on are proven in knock.test.js, the wire is not.
225 // ⚠ Requires holding their base, which requires a transport. PROVEN END TO END 2026-09-15 in
226 // doorbell-wire.test.js: a reader who is not a writer appends across a replication pipe and the
227 // row lands in the owner's feed, with her writer set untouched and a record from him refused.
228 //
229 // ⛔ `base.writable` GOES TRUE ON THE APPENDER after an optimistic append — it means "I have a
230 // local writer core", never "my appends are admitted". Do not use it as an authorisation check.
227231 async knock (myFeedKey) {
228232 await this.base.append(enc({ t: 'knock', feed: hex(myFeedKey) }), { optimistic: true })
229233 }
lib/peer.js +51 -1
……@@ -504,9 +504,41 @@ export class Peer {
504504 if (!v) throw new Error('Peer.react: a reaction value is 1-' + MAX_VALUE + ' printable characters')
505505 const name = Peer.reactionId(t, v)
506506 await this.feed.put(REACTION, name, { frontmatter: { createdAt, target: t, value: v }, body: '' })
507 await this._ring(t)
507508 return name
508509 }
509510
511 /**
512 * ⛔ RING THE TARGET'S DOORBELL. An interaction lives in its AUTHOR's feed, so the target learns of
513 * it by replication — and only if they hold that feed. Without this, a like from anyone whose feed
514 * they do not hold is invisible and nothing rings. hyper-social #61 §11.2 wrote that down in
515 * advance as the falsification: "A never learns ⇒ the doorbell is mandatory, not optional."
516 *
517 * ⛔ IT CARRIES NOTHING. One row saying a feed would like to be seen, in a namespace that can never
518 * become a record, granting no write and no admission (doorbell-wire.test.js proves both across a
519 * real pipe). The edge itself stays in our feed — writing it into theirs would be authoring as
520 * them, which #61 §4 removed the room primitive to avoid.
521 *
522 * ⚠ Bounded by construction: applyKnock keys on the FEED, so a thousand acts are one row. Inherited
523 * from the knock, not added here.
524 *
525 * ⚠ BEST EFFORT, NEVER FATAL. The edge is ours and is already written; an unreachable target must
526 * not undo it. But it is said out loud rather than swallowed — a doorbell that silently fails is
527 * the exact condition this exists to end.
528 */
529 async _ring (t) {
530 if (!t || t.feed === Peer.nameFor(this.feed.key)) return false // being told about yourself is not a notification
531 const theirs = this.feeds.get(t.feed)
532 if (!theirs || typeof theirs.knock !== 'function') return false // we do not hold it; there is nothing to append to
533 try {
534 await theirs.knock(this.feed.key)
535 return true
536 } catch (err) {
537 try { console.error(JSON.stringify({ msg: 'doorbell: could not ring — they will not learn of this interaction until they read our feed', feed: t.feed, error: (err && err.message) || String(err) })) } catch {}
538 return false
539 }
540 }
541
510542 async unreact (target, value) {
511543 const v = Peer.normalizeValue(value)
512544 if (!v) return
……@@ -528,6 +560,7 @@ export class Peer {
528560 if (!t) throw new Error('Peer.reply: a target needs { feed, collection, name }')
529561 if (typeof name !== 'string' || !name) throw new Error('Peer.reply: a reply needs its own name')
530562 await this.feed.put(REPLY, name, { frontmatter: { createdAt, target: t }, body: String(body || '') })
563 await this._ring(t)
531564 return name
532565 }
533566
……@@ -558,11 +591,17 @@ export class Peer {
558591 }))
559592 }
560593
561 /** Repost a target — no body, nothing written to their feed. */
594 /**
595 * Repost a target — no body, and no RECORD written to their feed.
596 *
597 * ⚠ It does ring their doorbell (`_ring`), which is one row in a reserved namespace and never a
598 * record. The older wording here said "nothing written to their feed" and that is no longer exact.
599 */
562600 async repost (target, createdAt = new Date().toISOString()) {
563601 const t = Peer.normalizeTarget(target)
564602 if (!t) throw new Error('Peer.repost: a target needs { feed, collection, name }')
565603 await this.feed.put(REPOST, Peer.targetId(t), { frontmatter: { createdAt, target: t }, body: '' })
604 await this._ring(t)
566605 return Peer.targetId(t)
567606 }
568607
……@@ -801,7 +840,18 @@ export class Peer {
801840
802841 // ⛔ The doorbell. Everything above needs their feed; this is what somebody you hold NOTHING of
803842 // can still do, and it is the whole answer to the stranger half of the problem.
843 //
844 // ⛔ A KNOCK FROM A FEED WE ALREADY HOLD IS NOT NEWS, and it is skipped. Since an interaction
845 // rings the doorbell (`_ring`), someone we already follow now rings it every time they react —
846 // and we can already READ everything they did, so the knock would be a second notification for
847 // an act that already produced one. The doorbell's own purpose is the stranger; that is exactly
848 // the set left here.
849 //
850 // ⚠ Skipped for RENDERING, not dismissed. The row stays in the feed, so accepting it later still
851 // works and nothing about consent is decided by this filter.
852 const held = new Set([...this.feeds.keys()])
804853 for (const k of await this.feed.knocks()) {
854 if (held.has(k.feed)) continue
805855 out.push({ kind: 'knock', author: k.feed, at: '', target: null, value: null, name: k.feed, body: '' })
806856 }
807857
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-peer",
3 "version": "0.0.56",
3 "version": "0.0.57",
44 "description": "The favela daemon \u2014 feeds and documents of markdown records over Hypercore. No atproto, no ActivityPub.",
55 "type": "module",
66 "scripts": {
test/doorbell-peer.test.js +3 -3
……@@ -1,8 +1,8 @@
11// The receiving half of the doorbell, at the Peer surface.
22//
3// ⚠ The optimistic append itself is NOT exercised here: ringing someone's bell means appending to
4// THEIR base, which needs a transport this repo does not have yet. What is proven is everything the
5// wire would carry into — that a knock lands as a request, that accepting one follows the knocker
3// ⚠ The optimistic append itself is not exercised HERE — that is doorbell-wire.test.js, which since
4// 2026-09-15 proves it across a replication pipe. What this file proves is everything the wire
5// carries into — that a knock lands as a request, that accepting one follows the knocker
66// rather than admitting them, and that nothing about it touches the feed's writer set.
77
88import test from 'brittle'
test/doorbell-wire.test.js +122 -0
……@@ -0,0 +1,122 @@
1// ★ The doorbell, OVER THE WIRE — the half two other files say is unproven.
2//
3// `feed-base.js` on knock(): "⚠ Requires holding their base, which requires a transport. Untested
4// end to end for that reason — the authorisation rules it relies on are proven in knock.test.js,
5// the wire is not."
6//
7// `doorbell-peer.test.js`: "The optimistic append itself is NOT exercised here… Appended by MY OWN
8// writer, because there is no transport to carry a stranger's optimistic append."
9//
10// So every part of the doorbell is proven except the one that makes it a doorbell: that somebody who
11// is NOT a writer on your feed can put a row in it from another machine. That is the claim here, and
12// it runs over the same in-memory duplex pair replicate.test.js uses — the wiring under test is "can
13// a stranger's append cross", and a swarm would add DHT timing to a test that is not about that.
14//
15// ⛔ WHY IT MATTERS NOW. An interaction lives in its author's feed, so if you do not hold that feed
16// you never learn of it, and the doorbell is the only answer the model has for a stranger. If this
17// append does not cross, that answer does not exist — and "the like never showed up" is exactly the
18// symptom, indistinguishable from a dozen other causes.
19
20import test from 'brittle'
21import fs from '#node:fs'
22import os from '#node:os'
23import b4a from 'b4a'
24import { join } from '#node:path'
25import Corestore from 'corestore'
26import { RecordStore } from '../lib/record-store.js'
27import { Feed } from '../lib/feed-base.js'
28import { attest } from './helpers.js'
29
30const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-bell-wire-'))
31const hex = (k) => b4a.toString(k, 'hex')
32
33function pipe (a, b) {
34 const s1 = a.replicate(true)
35 const s2 = b.replicate(false)
36 s1.pipe(s2).pipe(s1)
37 return () => { s1.destroy(); s2.destroy() }
38}
39
40// Wait for the CONDITION, never for a duration — a fixed sleep passes on a fast machine and flakes
41// on a loaded one, which is how a suite starts lying.
42async function until (fn, { timeout = 15000 } = {}) {
43 const deadline = Date.now() + timeout
44 while (Date.now() < deadline) {
45 const v = await fn()
46 if (v) return v
47 await new Promise((r) => setTimeout(r, 25))
48 }
49 return null
50}
51
52async function mkFeed (store, key, records, opts = {}) {
53 const f = new Feed(store, key, { store: new RecordStore(records), ...opts })
54 await f.ready()
55 return f
56}
57
58async function owned (records) {
59 const f = await mkFeed(new Corestore(tmp()), null, records, { optimistic: true })
60 await f.addDevice(await attest(f))
61 await f.update()
62 return f
63}
64
65test('★★★ a stranger who is NOT a writer rings the doorbell from another machine', async (t) => {
66 const records = tmp()
67 const alice = await owned(records)
68 const bob = await owned(tmp())
69
70 // Bob opens ALICE's feed by key. He is a reader of her log and a writer of nothing in it.
71 const aliceOnBob = await mkFeed(new Corestore(tmp()), alice.key, records, { optimistic: true })
72 const unpipe = pipe(alice, aliceOnBob)
73 t.teardown(async () => { unpipe(); await alice.close(); await bob.close(); await aliceOnBob.close() })
74
75 await until(async () => { await aliceOnBob.update(); return (await aliceOnBob.devices()).length > 0 })
76 t.absent(aliceOnBob.writable, '⛔ he is NOT a writer on her feed — that is the whole premise')
77
78 await aliceOnBob.knock(bob.key)
79
80 const knocks = await until(async () => {
81 await alice.update()
82 const k = await alice.knocks()
83 return k.length ? k : null
84 })
85
86 t.ok(knocks, 'the knock crossed the wire and landed in her feed')
87 t.is(knocks && knocks[0].feed, hex(bob.key), 'and it names the feed that rang, not the device')
88})
89
90test('⛔ and it grants nothing — a knock is not admission', async (t) => {
91 const records = tmp()
92 const alice = await owned(records)
93 const bob = await owned(tmp())
94 const before = await alice.devices()
95
96 const aliceOnBob = await mkFeed(new Corestore(tmp()), alice.key, records, { optimistic: true })
97 const unpipe = pipe(alice, aliceOnBob)
98 t.teardown(async () => { unpipe(); await alice.close(); await bob.close(); await aliceOnBob.close() })
99
100 await until(async () => { await aliceOnBob.update(); return (await aliceOnBob.devices()).length > 0 })
101 await aliceOnBob.knock(bob.key)
102 await until(async () => { await alice.update(); return (await alice.knocks()).length })
103
104 t.alike(await alice.devices(), before, "her writer set is untouched — the thing a doorbell must never change")
105
106 // ⛔ `writable` IS TRUE HERE, AND IT DOES NOT MEAN WHAT IT LOOKS LIKE. An optimistic append creates
107 // a local writer core, so autobase reports the base as writable from that moment — "I have a core I
108 // can append to", not "my appends are admitted". Asserting on it would test the wrong thing and
109 // pass for the wrong reason. The property that matters is whether a RECORD lands, so write one.
110 const { POST } = await import('../lib/peer.js')
111 await aliceOnBob.put(POST, 'intruder', { frontmatter: { createdAt: '2026-09-15T00:00:00Z' }, body: 'not mine to write' })
112 await aliceOnBob.update()
113
114 // Give it every chance to arrive before concluding it did not — a race that passes by being fast
115 // is not a refusal.
116 const landed = await until(async () => {
117 await alice.update()
118 return (await alice.get(POST, 'intruder')) ? true : null
119 }, { timeout: 3000 })
120
121 t.absent(landed, '⛔ his record is REFUSED — a doorbell grants no write, however writable the base claims to be')
122})
test/interact-doorbell.test.js +114 -0
……@@ -0,0 +1,114 @@
1// ★ An interaction rings the target's doorbell — hyper-social #61 §11.2 test 11.
2//
3// ⛔ THE FAILURE THIS CLOSES, observed 2026-09-15. An interaction lives in its AUTHOR's feed, so the
4// target learns of it by replication — and only if they hold that feed. A like from anyone else is
5// invisible and NOTHING RINGS. §11.2 wrote that down in advance as the falsification: "A never
6// learns ⇒ the doorbell is mandatory, not optional." It is now mandatory.
7//
8// ⚠ THE EDGE STILL LIVES IN THE LIKER'S FEED. The doorbell carries no content and grants nothing —
9// it is one row saying a feed would like to be seen, in a namespace that can never become a record.
10// Writing the interaction itself into the target's feed would be authoring as them, which #61 §4
11// removed the room primitive to avoid.
12//
13// ⚠ Bounded by construction: applyKnock keys on the FEED, so a thousand likes from one person are
14// one row. That is the same structural bound a knock flood already had, inherited rather than added.
15
16import test from 'brittle'
17import fs from '#node:fs'
18import os from '#node:os'
19import { join } from '#node:path'
20import Corestore from 'corestore'
21import { RecordStore } from '../lib/record-store.js'
22import { Feed } from '../lib/feed-base.js'
23import { Peer, POST } from '../lib/peer.js'
24import { identity } from './helpers.js'
25
26const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-ring-'))
27const rec = (body, createdAt = '2026-09-15T00:00:00Z') => ({ frontmatter: { createdAt }, body })
28
29async function mkPeer () {
30 const id = await identity()
31 const d = await id.device()
32 const f = new Feed(new Corestore(tmp()), null, { store: new RecordStore(tmp()), optimistic: true })
33 await f.ready()
34 await f.addDevice(d.bind(f.deviceKey))
35 await f.update()
36 return new Peer(f).ready()
37}
38
39// Bob holds Alice's feed — he must, to have seen the post he is about to like.
40async function pair () {
41 const alice = await mkPeer()
42 const bob = await mkPeer()
43 await alice.feed.put(POST, 'p1', rec('worth reacting to'))
44 await alice.update()
45 bob.feeds.set(Peer.nameFor(alice.feed.key), alice.feed)
46 await bob.update()
47 return { alice, bob, target: { feed: Peer.nameFor(alice.feed.key), collection: POST, name: 'p1' } }
48}
49
50const rang = async (peer, who) =>
51 (await peer.feed.knocks()).some((k) => k.feed === Peer.nameFor(who.feed.key))
52
53test('★★★ a like rings the target\'s doorbell, so they learn of it holding nothing of ours', async (t) => {
54 const { alice, bob, target } = await pair()
55 t.absent(await rang(alice, bob), 'nothing has rung yet')
56
57 await bob.like(target)
58 await alice.update()
59
60 t.ok(await rang(alice, bob), 'her doorbell names his feed')
61 // ⛔ And the edge is still HIS. Nothing was authored in her feed.
62 t.is((await bob.likesOn(target)).length, 1, 'the like lives in the liker\'s feed, as it always did')
63})
64
65test('a repost and a reply ring it too', async (t) => {
66 for (const act of ['repost', 'reply']) {
67 const { alice, bob, target } = await pair()
68 if (act === 'repost') await bob.repost(target)
69 else await bob.reply(target, 'well said', 'r1')
70 await alice.update()
71 t.ok(await rang(alice, bob), act + ' rings')
72 }
73})
74
75test('⛔ reacting to my OWN post rings nothing — being told about yourself is not a notification', async (t) => {
76 const alice = await mkPeer()
77 await alice.feed.put(POST, 'mine', rec('my own'))
78 await alice.update()
79 await alice.like({ feed: Peer.nameFor(alice.feed.key), collection: POST, name: 'mine' })
80 await alice.update()
81 t.is((await alice.feed.knocks()).length, 0)
82})
83
84test('⚠ it is idempotent — a hundred likes are one row, the bound applyKnock already had', async (t) => {
85 const { alice, bob, target } = await pair()
86 await bob.like(target)
87 await bob.react(target, '🎉')
88 await bob.repost(target)
89 await alice.update()
90 t.is((await alice.feed.knocks()).length, 1, 'one row for the feed, not one per act')
91})
92
93test('⛔ undoing does not ring — a retraction is not an approach', async (t) => {
94 const { alice, bob, target } = await pair()
95 await bob.like(target)
96 await alice.update()
97 await alice.feed.dismissKnock(bob.feed.key)
98 await alice.update()
99 t.is((await alice.feed.knocks()).length, 0, 'she dismissed it')
100
101 await bob.unlike(target)
102 await alice.update()
103 t.is((await alice.feed.knocks()).length, 0, 'un-liking does not ring it again')
104})
105
106test('⛔ a doorbell we cannot reach never fails the interaction', async (t) => {
107 const { bob, target } = await pair()
108 // The feed we hold stops accepting appends — an offline target, a closed base, a broken pipe.
109 bob.feeds.get(target.feed).knock = async () => { throw new Error('unreachable') }
110
111 const name = await bob.like(target)
112 t.ok(name, 'the like was still written — the edge is ours and does not depend on reaching them')
113 t.is((await bob.likesOn(target)).length, 1)
114})
test/notifications.test.js +27 -1
……@@ -102,12 +102,38 @@ test('a notification says WHO, WHAT and WHICH record', async (t) => {
102102test('★ the DOORBELL is in the list — the one thing a stranger can do', async (t) => {
103103 const { me, them } = await two()
104104 t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))
105 await me.peer.feed.knock(them.peer.feed.key)
105
106 // ⛔ A STRANGER, which is what this test always claimed to be about. It used to knock from `them`,
107 // whose feed `me` already holds — so it was really testing a knock from a friend, and it passed
108 // because nothing distinguished the two. Since an interaction now rings the doorbell, that
109 // distinction is load-bearing: a knock from a held feed is a SECOND notification for an act that
110 // already produced one, so it is skipped, and this test would have been the thing that noticed.
111 const stranger = 'ab'.repeat(32)
112 await me.peer.feed.knock(Buffer.from(stranger, 'hex'))
106113 await me.peer.update()
114
107115 const knocks = (await me.peer.notifications()).filter((n) => n.kind === 'knock')
108116 // ⛔ This is the answer to the half #62 could not solve. Everything else here requires holding the
109117 // author's feed; a knock is what somebody you hold NOTHING of can still do.
110118 t.is(knocks.length, 1)
119 t.is(knocks[0].author, stranger, 'and it names the feed that rang')
120})
121
122test('⛔ a knock from a feed we ALREADY HOLD is not a second notification', async (t) => {
123 const { me, them, target } = await two()
124 t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))
125
126 // They react, which now also rings. We can already READ the reaction, so the knock says nothing
127 // the list does not already carry.
128 await them.peer.react(target, '🔥', '2026-09-05T00:00:00Z')
129 await them.peer.update(); await me.peer.update()
130
131 const list = await me.peer.notifications()
132 t.is(list.filter((n) => n.kind === 'reaction').length, 1, 'the reaction is there, by replication')
133 t.is(list.filter((n) => n.kind === 'knock').length, 0, 'and not announced a second time as a knock')
134
135 // ⚠ Skipped for RENDERING only — the row is still in the feed, so consent is untouched.
136 t.is((await me.peer.feed.knocks()).length, 1, 'the knock itself was not dismissed')
111137})
112138
113139test('newest first, and bounded', async (t) => {