| … | … | @@ -1,9 +1,9 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "favela-bridge", |
| 3 | "version": "0.0.7", | |
| 3 | "version": "0.0.12", | |
| 4 | 4 | "runtime": "bare", |
| 5 | "rootTreeHash": "d0caf910b7b70149d2c2a861573d2f2e7e5b7acb223bf717758ff84716482d5e", | |
| 6 | "publishedAt": "2026-09-09T22:23:14.763Z", | |
| 5 | "rootTreeHash": "25ba97434575533c058d481beb79f2327abea0203a678d61c11ea6b931773393", | |
| 6 | "publishedAt": "2026-09-10T00:03:40.772Z", | |
| 7 | 7 | "deps": [ |
| 8 | 8 | { |
| 9 | 9 | "name": "atproto-store", |
| … | … | @@ -1509,9 +1509,9 @@ |
| 1509 | 1509 | }, |
| 1510 | 1510 | { |
| 1511 | 1511 | "name": "favela-markdown", |
| 1512 | "version": "0.1.6", | |
| 1513 | "tree": "08913adf8bf7452a12cc06528c49b4629c880de61fa45f01990e33b000a820d9", | |
| 1514 | "normHash": "853f6fe2c24cea0c956653eaa7d5a13b7feca966cad5c9a6164c2dc1ff324881", | |
| 1512 | "version": "0.2.0", | |
| 1513 | "tree": "e980c91c65708beb2d24964f4f8bce19d01f9783d1d93e4eff8674851df64611", | |
| 1514 | "normHash": "2b6a91ad3e40196bc8a2bed0be0d54af70e365e9159504e3cb86e73f034b8c5f", | |
| 1515 | 1515 | "workspace": true |
| 1516 | 1516 | }, |
| 1517 | 1517 | { |
| … | … | @@ -2985,8 +2985,8 @@ |
| 2985 | 2985 | "tree": "b6396de092d11e559d109b0dea7995f340b7959c4efeb0baa06b091f99a43245" |
| 2986 | 2986 | } |
| 2987 | 2987 | ], |
| 2988 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 2989 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 2988 | "reason": "dep version-splits only; 0 code-differs", | |
| 2989 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 2990 | 2990 | }, |
| 2991 | 2991 | { |
| 2992 | 2992 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3003,15 +3003,10 @@ |
| 3003 | 3003 | "appName": "favela-web", |
| 3004 | 3004 | "version": "0.0.3", |
| 3005 | 3005 | "tree": "38f44af48b2d59b46f1b5db31b81d02f3ca8b8bad6bc0fb9fffff698241f5e37" |
| 3006 | }, | |
| 3007 | { | |
| 3008 | "appName": "hyper-proto-peer", | |
| 3009 | "version": "0.0.3", | |
| 3010 | "tree": "1aa7863120ed4d861c4ba8230a8ba551d94d0a922e691697ff7ac77ec9ec7f9a" | |
| 3011 | 3006 | } |
| 3012 | 3007 | ], |
| 3013 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3014 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3008 | "reason": "dep version-splits only; 0 code-differs", | |
| 3009 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3015 | 3010 | }, |
| 3016 | 3011 | { |
| 3017 | 3012 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3025,8 +3020,8 @@ |
| 3025 | 3020 | "tree": "6b8b5a11a8be2dd0ea1fb34391d0ca0b8f711c52776588382767f305433c68c4" |
| 3026 | 3021 | } |
| 3027 | 3022 | ], |
| 3028 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3029 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3023 | "reason": "dep version-splits only; 0 code-differs", | |
| 3024 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3030 | 3025 | }, |
| 3031 | 3026 | { |
| 3032 | 3027 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3045,8 +3040,8 @@ |
| 3045 | 3040 | "tree": "5b662871145437a17e36c1dbebd64fc6e29be649b168ffb7638d3c5eaa781443" |
| 3046 | 3041 | } |
| 3047 | 3042 | ], |
| 3048 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3049 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3043 | "reason": "dep version-splits only; 0 code-differs", | |
| 3044 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3050 | 3045 | }, |
| 3051 | 3046 | { |
| 3052 | 3047 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3060,8 +3055,8 @@ |
| 3060 | 3055 | "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d" |
| 3061 | 3056 | } |
| 3062 | 3057 | ], |
| 3063 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3064 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3058 | "reason": "dep version-splits only; 0 code-differs", | |
| 3059 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3065 | 3060 | }, |
| 3066 | 3061 | { |
| 3067 | 3062 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3075,8 +3070,8 @@ |
| 3075 | 3070 | "tree": "a49b9c4ace4ade9435e530c82694bf39e10e0176475ae6b4270dec913bf6c5ef" |
| 3076 | 3071 | } |
| 3077 | 3072 | ], |
| 3078 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3079 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3073 | "reason": "dep version-splits only; 0 code-differs", | |
| 3074 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3080 | 3075 | }, |
| 3081 | 3076 | { |
| 3082 | 3077 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3090,8 +3085,8 @@ |
| 3090 | 3085 | "tree": "6260923011112545fcd768b1c2013d6d3467277913acb90b11404fefd4ddab44" |
| 3091 | 3086 | } |
| 3092 | 3087 | ], |
| 3093 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3094 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3088 | "reason": "dep version-splits only; 0 code-differs", | |
| 3089 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3095 | 3090 | }, |
| 3096 | 3091 | { |
| 3097 | 3092 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3105,8 +3100,8 @@ |
| 3105 | 3100 | "tree": "623bbbece48d0415c906980604a2d46515e0a7b1aeb63516eae5ceb981a27bdd" |
| 3106 | 3101 | } |
| 3107 | 3102 | ], |
| 3108 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3109 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3103 | "reason": "dep version-splits only; 0 code-differs", | |
| 3104 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3110 | 3105 | }, |
| 3111 | 3106 | { |
| 3112 | 3107 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3120,8 +3115,8 @@ |
| 3120 | 3115 | "tree": "e6991084a57472974a7f19d371cdfa226c28bd92be2d3b5a7ba38eabba8b3bb5" |
| 3121 | 3116 | } |
| 3122 | 3117 | ], |
| 3123 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3124 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3118 | "reason": "dep version-splits only; 0 code-differs", | |
| 3119 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3125 | 3120 | }, |
| 3126 | 3121 | { |
| 3127 | 3122 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3135,8 +3130,8 @@ |
| 3135 | 3130 | "tree": "ad81cb00dc100cbacbe692462fefc6cd1d676e4b6022e3b9fa2d9294a4461aac" |
| 3136 | 3131 | } |
| 3137 | 3132 | ], |
| 3138 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3139 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3133 | "reason": "dep version-splits only; 0 code-differs", | |
| 3134 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3140 | 3135 | }, |
| 3141 | 3136 | { |
| 3142 | 3137 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3150,8 +3145,8 @@ |
| 3150 | 3145 | "tree": "5e9cf8230c28d6b667d121cb23a5202b5d5d44e12dbeef0a7d6fbcc55621bb3d" |
| 3151 | 3146 | } |
| 3152 | 3147 | ], |
| 3153 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3154 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3148 | "reason": "dep version-splits only; 0 code-differs", | |
| 3149 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3155 | 3150 | }, |
| 3156 | 3151 | { |
| 3157 | 3152 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3165,8 +3160,8 @@ |
| 3165 | 3160 | "tree": "f43421419fb851f1bf8fd6f284e23d9f8b5ff2bcb320ac71dcf420b261dc7264" |
| 3166 | 3161 | } |
| 3167 | 3162 | ], |
| 3168 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3169 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3163 | "reason": "dep version-splits only; 0 code-differs", | |
| 3164 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3170 | 3165 | }, |
| 3171 | 3166 | { |
| 3172 | 3167 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3180,8 +3175,8 @@ |
| 3180 | 3175 | "tree": "dacb5f2689ca922d0431d9958d29e8c070a472355bd9b3fe3ca112be5c159f23" |
| 3181 | 3176 | } |
| 3182 | 3177 | ], |
| 3183 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3184 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3178 | "reason": "dep version-splits only; 0 code-differs", | |
| 3179 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3185 | 3180 | }, |
| 3186 | 3181 | { |
| 3187 | 3182 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3195,8 +3190,8 @@ |
| 3195 | 3190 | "tree": "d678235d9563addbff0dea060f7739123bb6bc239e344b2fec5db536e2340797" |
| 3196 | 3191 | } |
| 3197 | 3192 | ], |
| 3198 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3199 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3193 | "reason": "dep version-splits only; 0 code-differs", | |
| 3194 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3200 | 3195 | }, |
| 3201 | 3196 | { |
| 3202 | 3197 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3215,8 +3210,8 @@ |
| 3215 | 3210 | "tree": "0423999b6587f905fd2e8cb7768e5d0587da0489595e8dd543497e3604f35ff1" |
| 3216 | 3211 | } |
| 3217 | 3212 | ], |
| 3218 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3219 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3213 | "reason": "dep version-splits only; 0 code-differs", | |
| 3214 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3220 | 3215 | }, |
| 3221 | 3216 | { |
| 3222 | 3217 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3230,8 +3225,8 @@ |
| 3230 | 3225 | "tree": "9386fa8b9509fda8b231ae9fd0d7672da1eab57959333e06ee29ba583e60bb5b" |
| 3231 | 3226 | } |
| 3232 | 3227 | ], |
| 3233 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3234 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3228 | "reason": "dep version-splits only; 0 code-differs", | |
| 3229 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3235 | 3230 | }, |
| 3236 | 3231 | { |
| 3237 | 3232 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3245,8 +3240,8 @@ |
| 3245 | 3240 | "tree": "08013f3f76e93f236b04c5a804dfd12debdc44bc2b5a5a5010f190f96ad682ec" |
| 3246 | 3241 | } |
| 3247 | 3242 | ], |
| 3248 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3249 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3243 | "reason": "dep version-splits only; 0 code-differs", | |
| 3244 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3250 | 3245 | }, |
| 3251 | 3246 | { |
| 3252 | 3247 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3260,8 +3255,8 @@ |
| 3260 | 3255 | "tree": "336bf1b134f4534c20b04e224211a4eda1dfb700abd3f8626fd08036d9df7b84" |
| 3261 | 3256 | } |
| 3262 | 3257 | ], |
| 3263 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3264 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3258 | "reason": "dep version-splits only; 0 code-differs", | |
| 3259 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3265 | 3260 | }, |
| 3266 | 3261 | { |
| 3267 | 3262 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3275,8 +3270,8 @@ |
| 3275 | 3270 | "tree": "3dde067fd3ec455f2aafa0afda7cdfab8f96d439bb5cc4e221997909a755bb7d" |
| 3276 | 3271 | } |
| 3277 | 3272 | ], |
| 3278 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3279 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3273 | "reason": "dep version-splits only; 0 code-differs", | |
| 3274 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3280 | 3275 | }, |
| 3281 | 3276 | { |
| 3282 | 3277 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3300,8 +3295,8 @@ |
| 3300 | 3295 | "tree": "bdebba5a532ce4846b49f1e5ffad774becfc44182d0c3737f7e441ab42f4c43d" |
| 3301 | 3296 | } |
| 3302 | 3297 | ], |
| 3303 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3304 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3298 | "reason": "dep version-splits only; 0 code-differs", | |
| 3299 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3305 | 3300 | }, |
| 3306 | 3301 | { |
| 3307 | 3302 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3320,8 +3315,8 @@ |
| 3320 | 3315 | "tree": "d31788ffa334976153023c6de84e5020b2009f51f9d472d0c2f09a13a53fc2b6" |
| 3321 | 3316 | } |
| 3322 | 3317 | ], |
| 3323 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3324 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3318 | "reason": "dep version-splits only; 0 code-differs", | |
| 3319 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3325 | 3320 | }, |
| 3326 | 3321 | { |
| 3327 | 3322 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3335,8 +3330,8 @@ |
| 3335 | 3330 | "tree": "6d6f0aff7bfff8b3693ddd00349d7aa296cfa35adc9abb0865fba66828814db5" |
| 3336 | 3331 | } |
| 3337 | 3332 | ], |
| 3338 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3339 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3333 | "reason": "dep version-splits only; 0 code-differs", | |
| 3334 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3340 | 3335 | }, |
| 3341 | 3336 | { |
| 3342 | 3337 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3350,8 +3345,8 @@ |
| 3350 | 3345 | "tree": "e693499377f146eec5788fa2887c4e42614f7f2efd3d5848d9019deec86c6632" |
| 3351 | 3346 | } |
| 3352 | 3347 | ], |
| 3353 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3354 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3348 | "reason": "dep version-splits only; 0 code-differs", | |
| 3349 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3355 | 3350 | }, |
| 3356 | 3351 | { |
| 3357 | 3352 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3365,8 +3360,8 @@ |
| 3365 | 3360 | "tree": "d99fbe108f5700f5e73d908e2d3d39ed44021b11bc74e1f152ebb493719b9eaa" |
| 3366 | 3361 | } |
| 3367 | 3362 | ], |
| 3368 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3369 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3363 | "reason": "dep version-splits only; 0 code-differs", | |
| 3364 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3370 | 3365 | }, |
| 3371 | 3366 | { |
| 3372 | 3367 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3380,8 +3375,8 @@ |
| 3380 | 3375 | "tree": "4664a97e951dbbfc25e868cbdbec870de2682812c1c19fe7f6d3daf2fe33446a" |
| 3381 | 3376 | } |
| 3382 | 3377 | ], |
| 3383 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3384 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3378 | "reason": "dep version-splits only; 0 code-differs", | |
| 3379 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3385 | 3380 | }, |
| 3386 | 3381 | { |
| 3387 | 3382 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3400,8 +3395,8 @@ |
| 3400 | 3395 | "tree": "4db5ae5fc0ad8552270405cd87877ac18d796bf082a05cb55e03a158af622290" |
| 3401 | 3396 | } |
| 3402 | 3397 | ], |
| 3403 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3404 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3398 | "reason": "dep version-splits only; 0 code-differs", | |
| 3399 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3405 | 3400 | }, |
| 3406 | 3401 | { |
| 3407 | 3402 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3415,8 +3410,8 @@ |
| 3415 | 3410 | "tree": "91b555116e75b7bc6309063652b2de61264901fb1e9a0326886bf355acd0acd8" |
| 3416 | 3411 | } |
| 3417 | 3412 | ], |
| 3418 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3419 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3413 | "reason": "dep version-splits only; 0 code-differs", | |
| 3414 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3420 | 3415 | }, |
| 3421 | 3416 | { |
| 3422 | 3417 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3430,8 +3425,8 @@ |
| 3430 | 3425 | "tree": "9c22db1f4e3137df4e44ba33887b65ce280bfc93454f8b91d2e41b458452e86b" |
| 3431 | 3426 | } |
| 3432 | 3427 | ], |
| 3433 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3434 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3428 | "reason": "dep version-splits only; 0 code-differs", | |
| 3429 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3435 | 3430 | }, |
| 3436 | 3431 | { |
| 3437 | 3432 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3445,8 +3440,8 @@ |
| 3445 | 3440 | "tree": "d7176ab113d79ab81c4a811285dcaca3948b5c822aac38968594d55904c0f243" |
| 3446 | 3441 | } |
| 3447 | 3442 | ], |
| 3448 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3449 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3443 | "reason": "dep version-splits only; 0 code-differs", | |
| 3444 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3450 | 3445 | }, |
| 3451 | 3446 | { |
| 3452 | 3447 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3460,8 +3455,8 @@ |
| 3460 | 3455 | "tree": "4d8d5a7eb968560e887d7c9d097a5326b281b67b04e69a5e6b34514985e861ae" |
| 3461 | 3456 | } |
| 3462 | 3457 | ], |
| 3463 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3464 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3458 | "reason": "dep version-splits only; 0 code-differs", | |
| 3459 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3465 | 3460 | }, |
| 3466 | 3461 | { |
| 3467 | 3462 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3475,8 +3470,8 @@ |
| 3475 | 3470 | "tree": "7236258ca920c2f9853c24ead4286ec92a5f709fdf0117f38257e9702752d3ce" |
| 3476 | 3471 | } |
| 3477 | 3472 | ], |
| 3478 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3479 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3473 | "reason": "dep version-splits only; 0 code-differs", | |
| 3474 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3480 | 3475 | }, |
| 3481 | 3476 | { |
| 3482 | 3477 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3495,8 +3490,8 @@ |
| 3495 | 3490 | "tree": "955c2e26f31af120554761af5ea65e6773648f137083594bb888429432200344" |
| 3496 | 3491 | } |
| 3497 | 3492 | ], |
| 3498 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3499 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3493 | "reason": "dep version-splits only; 0 code-differs", | |
| 3494 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3500 | 3495 | }, |
| 3501 | 3496 | { |
| 3502 | 3497 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3515,8 +3510,8 @@ |
| 3515 | 3510 | "tree": "a8699972843ff0e9e0b134447b71b055bbd8acd4a6ea3187624a7a52931ce10e" |
| 3516 | 3511 | } |
| 3517 | 3512 | ], |
| 3518 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3519 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3513 | "reason": "dep version-splits only; 0 code-differs", | |
| 3514 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3520 | 3515 | }, |
| 3521 | 3516 | { |
| 3522 | 3517 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3535,8 +3530,8 @@ |
| 3535 | 3530 | "tree": "46b1c3d46b4bf7703f0baf537ffbb80489394e28737a5c2e1641ca94ac115c4c" |
| 3536 | 3531 | } |
| 3537 | 3532 | ], |
| 3538 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3539 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3533 | "reason": "dep version-splits only; 0 code-differs", | |
| 3534 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3540 | 3535 | }, |
| 3541 | 3536 | { |
| 3542 | 3537 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3555,8 +3550,23 @@ |
| 3555 | 3550 | "tree": "db9a29b3a3c27cca5ad561f21995e04945465c98d438579beff71e029e662776" |
| 3556 | 3551 | } |
| 3557 | 3552 | ], |
| 3558 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3559 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3553 | "reason": "dep version-splits only; 0 code-differs", | |
| 3554 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3555 | }, | |
| 3556 | { | |
| 3557 | "type": "VERSION_CONFLICT", | |
| 3558 | "dep": "favela-markdown", | |
| 3559 | "newVersion": "0.2.0", | |
| 3560 | "newTree": "e980c91c65708beb2d24964f4f8bce19d01f9783d1d93e4eff8674851df64611", | |
| 3561 | "existing": [ | |
| 3562 | { | |
| 3563 | "appName": "ap-bridge", | |
| 3564 | "version": "0.1.6", | |
| 3565 | "tree": "08913adf8bf7452a12cc06528c49b4629c880de61fa45f01990e33b000a820d9" | |
| 3566 | } | |
| 3567 | ], | |
| 3568 | "reason": "dep version-splits only; 0 code-differs", | |
| 3569 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3560 | 3570 | }, |
| 3561 | 3571 | { |
| 3562 | 3572 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3570,8 +3580,8 @@ |
| 3570 | 3580 | "tree": "e92100a4f821a638ac99c1407e4c7e784f8eb9661e6d9119ecbec4faae77cc7e" |
| 3571 | 3581 | } |
| 3572 | 3582 | ], |
| 3573 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3574 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3583 | "reason": "dep version-splits only; 0 code-differs", | |
| 3584 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3575 | 3585 | }, |
| 3576 | 3586 | { |
| 3577 | 3587 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3595,8 +3605,8 @@ |
| 3595 | 3605 | "tree": "f4e87130e4397b037b121356f2db0161fadd8f2c320f7042fa31f4b5308193ec" |
| 3596 | 3606 | } |
| 3597 | 3607 | ], |
| 3598 | "reason": "dep version-splits only; gate reports 0 code-differs, 0 unclassified", | |
| 3599 | "acceptedAt": "2026-09-09T22:23:14.783Z" | |
| 3608 | "reason": "dep version-splits only; 0 code-differs", | |
| 3609 | "acceptedAt": "2026-09-10T00:03:40.787Z" | |
| 3600 | 3610 | } |
| 3601 | 3611 | ] |
| 3602 | 3612 | } |
| … | … | @@ -0,0 +1,112 @@ |
| 1 | // An edge, projected. Pure — no network, no store, so what gets published is testable on its own. | |
| 2 | // | |
| 3 | // ⛔ THE SIGNED SET IS FROZEN (interaction.md §5.4): like, repost, follow, block, reply. This handles | |
| 4 | // the three whose subject is a RECORD. `follow` and `block` name an ACTOR, and on atproto an actor is a | |
| 5 | // DID — a favela feed key is not one, so those need the cross-network identity edge (§3.3) and are out | |
| 6 | // of scope until a foreign subject can be named. | |
| 7 | // | |
| 8 | // ⛔ OUR OWN RECORDS ONLY, for now. A subject we authored resolves to `at://<did>/app.bsky.feed.post/ | |
| 9 | // <tid>` by pure function — §3.1's "rkey pairing already makes all three names pure functions of each | |
| 10 | // other" — and its CID is computable from the record's own bytes. A FOREIGN subject needs the naming | |
| 11 | // decision interaction.md §3 calls "the one irreversible decision", and nothing here forces it. | |
| 12 | // | |
| 13 | // ⚠ EVERY REFUSAL IS NAMED. §5.5: "Every verb states its loss", and `projectLike` returning null is | |
| 14 | // called out there as a silence the design forbids. | |
| 15 | ||
| 16 | import { planRecord } from './plan.js' | |
| 17 | import { tidFor } from './tid.js' | |
| 18 | ||
| 19 | export const LIKE_COLLECTION = 'app.bsky.feed.like' | |
| 20 | export const REPOST_COLLECTION = 'app.bsky.feed.repost' | |
| 21 | export const POST_COLLECTION = 'app.bsky.feed.post' | |
| 22 | ||
| 23 | const WITHHELD = (collection, ...losses) => ({ project: false, collection, rkey: null, record: null, losses }) | |
| 24 | ||
| 25 | /** | |
| 26 | * @param {object} edge from peer `edges-by`: { name, collection, target, value, createdAt, body } | |
| 27 | * @param {object} ctx | |
| 28 | * @param {string} ctx.ownFeed this identity's feed key — a target on any other feed is foreign | |
| 29 | * @param {function} ctx.subject (target) => { uri, cid } | null — resolved by the caller, which is the | |
| 30 | * only part that needs the repo | |
| 31 | * @param {string} [ctx.roomType] | |
| 32 | */ | |
| 33 | export function planEdge (edge, ctx = {}) { | |
| 34 | const kind = edge && edge.collection | |
| 35 | const target = (edge && edge.target) || null | |
| 36 | const out = kind === 'repost' ? REPOST_COLLECTION : kind === 'reply' ? POST_COLLECTION : LIKE_COLLECTION | |
| 37 | ||
| 38 | if (!target) return WITHHELD(out, 'the edge names no target') | |
| 39 | ||
| 40 | // ⛔ THE SUBJECT MUST BE OURS. Naming somebody else's record means choosing how a foreign subject is | |
| 41 | // stored, permanently — see the header. Said as a loss rather than skipped, so a person can see that | |
| 42 | // their like of a stranger's post did not travel. | |
| 43 | if (ctx.ownFeed && target.feed !== ctx.ownFeed) { | |
| 44 | return WITHHELD(out, 'subject is on another feed — cross-network subjects are not named yet') | |
| 45 | } | |
| 46 | if (target.collection !== 'post') { | |
| 47 | return WITHHELD(out, 'subject is a ' + target.collection + ', and only a post projects to bsky') | |
| 48 | } | |
| 49 | ||
| 50 | const subject = ctx.subject ? ctx.subject(target) : null | |
| 51 | // ⛔ NOT AN ERROR, AND NOT A SILENCE. A subject that is not in the repo yet is the ordinary case when | |
| 52 | // an edge is projected before the post it points at — the next pass finds it. | |
| 53 | if (!subject || !subject.uri || !subject.cid) { | |
| 54 | return WITHHELD(out, 'the subject is not in the atproto repo yet — it projects once the post does') | |
| 55 | } | |
| 56 | ||
| 57 | const createdAt = edge.createdAt || new Date().toISOString() | |
| 58 | // ⛔ A REAL TID, like a post's. The AppView is keyed "tid" and silently drops records whose rkey is | |
| 59 | // not one — measured on the live network for posts, and there is no reason to believe likes differ. | |
| 60 | // Derived from the edge's own name, so a re-run reproduces it and a reversal can find it. | |
| 61 | const rkey = tidFor(edge.name) | |
| 62 | ||
| 63 | if (kind === 'reaction' || kind === 'like') { | |
| 64 | // ⛔ bsky HAS ONE REACTION. A favela reaction carries a value, and anything but `like` has nowhere | |
| 65 | // to go — reported, never silently sent as a like, which would publish a different act. | |
| 66 | const value = edge.value || 'like' | |
| 67 | if (value !== 'like') { | |
| 68 | return WITHHELD(LIKE_COLLECTION, 'bsky has no reaction other than like, and this one is "' + value + '"') | |
| 69 | } | |
| 70 | return { | |
| 71 | project: true, | |
| 72 | collection: LIKE_COLLECTION, | |
| 73 | rkey, | |
| 74 | record: { $type: LIKE_COLLECTION, subject, createdAt }, | |
| 75 | losses: [] | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | if (kind === 'repost') { | |
| 80 | return { | |
| 81 | project: true, | |
| 82 | collection: REPOST_COLLECTION, | |
| 83 | rkey, | |
| 84 | record: { $type: REPOST_COLLECTION, subject, createdAt }, | |
| 85 | losses: [] | |
| 86 | } | |
| 87 | } | |
| 88 | ||
| 89 | if (kind === 'reply') { | |
| 90 | // ⛔ THROUGH THE SAME PLANNER A POST USES, with verb 'reply' so the reach lattice answers for the | |
| 91 | // right act. Writing a second text pipeline here is how the 300-grapheme rule, the facets and the | |
| 92 | // strip would drift between a post and a reply. | |
| 93 | const planned = planRecord({ name: edge.name, body: edge.body, createdAt }, { roomType: ctx.roomType || 'public', verb: 'reply' }) | |
| 94 | if (!planned.bsky.project) return WITHHELD(POST_COLLECTION, ...planned.bsky.losses) | |
| 95 | ||
| 96 | // ⚠ root AND parent. atproto threads need both; the caller resolves the root by walking up, and | |
| 97 | // falls back to the parent when the subject is itself a top-level post — which is the common case | |
| 98 | // and not an error. | |
| 99 | const root = (ctx.root && ctx.root(target)) || subject | |
| 100 | return { | |
| 101 | project: true, | |
| 102 | collection: POST_COLLECTION, | |
| 103 | rkey, | |
| 104 | record: { ...planned.bsky.record, reply: { root, parent: subject } }, | |
| 105 | losses: planned.bsky.losses | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | return WITHHELD(out, 'no bsky projection for a ' + kind) | |
| 110 | } | |
| 111 | ||
| 112 | export default { planEdge, LIKE_COLLECTION, REPOST_COLLECTION, POST_COLLECTION } |
| … | … | @@ -21,6 +21,7 @@ import { planProfile, sameProfile } from './profile.js' |
| 21 | 21 | import { samePost } from './plan.js' |
| 22 | 22 | import { tidFor } from './tid.js' |
| 23 | 23 | import { POST_COLLECTION } from './bsky.js' |
| 24 | import { planEdge } from './plan-edge.js' | |
| 24 | 25 | |
| 25 | 26 | // oid -> blob ref. Content-addressed, so this can never go stale. |
| 26 | 27 | const blobCache = new Map() |
| … | … | @@ -145,7 +146,11 @@ export async function projectCycle ({ |
| 145 | 146 | // Two header details the CLI knows and this function cannot: whether the room type was CONFIGURED |
| 146 | 147 | // or assumed, and which socket the sink is. Reported because "public (assumed)" is the input the |
| 147 | 148 | // reach lattice cannot infer, and if it is ever wrong it is wrong in the direction of a leak. |
| 148 | roomTypeNote = '', sinkLabel = null | |
| 149 | roomTypeNote = '', sinkLabel = null, | |
| 150 | // ⛔ INJECTED, because resolving a subject means computing an atproto record CID and this package | |
| 151 | // does not depend on the atproto stack. favela-host supplies both; the desktop CLI supplies neither | |
| 152 | // and simply projects no edges, exactly as before. | |
| 153 | resolveSubject = null, resolveRoot = null | |
| 149 | 154 | } = {}) { |
| 150 | 155 | const reach = await peer.reach() |
| 151 | 156 | const { feed, rows } = await peer.ownPosts() |
| … | … | @@ -226,11 +231,63 @@ export async function projectCycle ({ |
| 226 | 231 | else log(' fediverse: withheld — ' + plan.fediverse.losses.join('; ')) |
| 227 | 232 | } |
| 228 | 233 | |
| 234 | // ── edges ──────────────────────────────────────────────────────────────────────────────────── | |
| 235 | // | |
| 236 | // ⛔ AFTER THE POSTS, ALWAYS. An edge names a subject, and the subject resolves only once the post | |
| 237 | // it points at is in the repo — projecting edges first would withhold every one of them on the first | |
| 238 | // pass with "not in the atproto repo yet" and fix itself silently on the second. | |
| 239 | // | |
| 240 | // ⚠ Skipped entirely when the caller supplies no resolver: favela-bridge's CLI has no way to compute | |
| 241 | // a subject CID (that needs the atproto stack, which this package deliberately does not depend on), | |
| 242 | // so a desktop run keeps doing exactly what it did before. | |
| 243 | const edgeStats = { sent: 0, skipped: 0, withheld: 0 } | |
| 244 | if (resolveSubject && reach.networks.includes('bsky')) { | |
| 245 | for (const collection of ['reaction', 'repost', 'reply']) { | |
| 246 | let list = [] | |
| 247 | try { list = await peer.call('edges-by', { collection }) } catch { list = [] } | |
| 248 | for (const e of Array.isArray(list) ? list : []) { | |
| 249 | // ⛔ RESOLVED BEFORE PLANNING. planEdge is pure and calls `subject()` synchronously, so handing | |
| 250 | // it an async resolver puts a Promise where the lexicon wants { uri, cid } — the record would | |
| 251 | // be built, pass every check, and publish an object with no subject. | |
| 252 | const subj = await resolveSubject(e.target) | |
| 253 | const rootSubj = resolveRoot ? await resolveRoot(e.target) : null | |
| 254 | const plan = planEdge(e, { ownFeed: feed, roomType, subject: () => subj, root: rootSubj ? () => rootSubj : null }) | |
| 255 | const line = ' ' + collection + '/' + e.name | |
| 256 | if (!plan.project) { | |
| 257 | edgeStats.withheld++ | |
| 258 | log(line + ' ' + plan.collection + ': withheld — ' + plan.losses.join('; ')) | |
| 259 | continue | |
| 260 | } | |
| 261 | // ⛔ READ BEFORE WRITE, per collection. `have()` lists posts only, and an edge written every | |
| 262 | // pass would append a repo version for a like nobody changed. | |
| 263 | const existing = await bsky.getRecord(plan.collection, plan.rkey) | |
| 264 | if (existing && existing.unknown) { | |
| 265 | log(line + ' ' + plan.collection + ': could not read what is there — not touching it') | |
| 266 | continue | |
| 267 | } | |
| 268 | if (existing) { | |
| 269 | edgeStats.skipped++ | |
| 270 | log(line + ' ' + plan.collection + ': already there') | |
| 271 | continue | |
| 272 | } | |
| 273 | if (!send) { | |
| 274 | log(line + ' ' + plan.collection + ': would send → ' + plan.record.subject.uri) | |
| 275 | continue | |
| 276 | } | |
| 277 | const w = await bsky.putRecord(plan.collection, plan.rkey, plan.record) | |
| 278 | if (w.sent) { edgeStats.sent++; log(line + ' ' + plan.collection + ': SENT ' + plan.rkey) } | |
| 279 | else log(line + ' ' + plan.collection + ': FAILED — ' + w.error) | |
| 280 | } | |
| 281 | } | |
| 282 | log(' edges: sent ' + edgeStats.sent + ' · already there ' + edgeStats.skipped + | |
| 283 | ' · withheld ' + edgeStats.withheld) | |
| 284 | } | |
| 285 | ||
| 229 | 286 | log(' ' + (send ? 'sent ' + sent : 'would send ' + (rows.length - withheld - skipped)) + |
| 230 | 287 | ' · already there ' + skipped + ' · withheld ' + withheld + |
| 231 | 288 | (held ? ' · held by limit ' + held : '')) |
| 232 | 289 | |
| 233 | return { refused: null, feed, rows: rows.length, sent, skipped, withheld, held, profile, reach } | |
| 290 | return { refused: null, feed, rows: rows.length, sent, skipped, withheld, held, profile, reach, edges: edgeStats } | |
| 234 | 291 | } |
| 235 | 292 | |
| 236 | 293 | export default { projectCycle, reconcileProfile, buildEmbed } |
| … | … | @@ -0,0 +1 @@ |
| 1 | ../../favela-identity |
| … | … | @@ -0,0 +1,94 @@ |
| 1 | // Projecting an edge — a like, a repost, a reply. | |
| 2 | // | |
| 3 | // ⛔ THE SIGNED SET IS FROZEN (interaction.md §5.4). These are the three whose subject is a RECORD; | |
| 4 | // follow and block name an ACTOR, and on atproto an actor is a DID, which a favela feed key is not. | |
| 5 | // | |
| 6 | // ⛔ EVERY REFUSAL IS NAMED. §5.5 says "every verb states its loss" and calls out `projectLike` | |
| 7 | // returning null as a silence the design forbids — so a withheld edge carries a reason a person can | |
| 8 | // read, not an absence they have to infer. | |
| 9 | ||
| 10 | import test from 'brittle' | |
| 11 | import { planEdge, LIKE_COLLECTION, REPOST_COLLECTION, POST_COLLECTION } from '../lib/plan-edge.js' | |
| 12 | import { tidFor } from '../lib/tid.js' | |
| 13 | ||
| 14 | const OURS = 'a'.repeat(64) | |
| 15 | const THEIRS = 'b'.repeat(64) | |
| 16 | const SUBJECT = { uri: 'at://did:plc:me/app.bsky.feed.post/3mv', cid: 'bafy123' } | |
| 17 | const ctx = (extra = {}) => ({ ownFeed: OURS, subject: () => SUBJECT, ...extra }) | |
| 18 | const edge = (collection, extra = {}) => ({ | |
| 19 | name: '1788822724600', | |
| 20 | collection, | |
| 21 | target: { feed: OURS, collection: 'post', name: '1788800000000' }, | |
| 22 | createdAt: '2026-09-07T23:12:04.600Z', | |
| 23 | ...extra | |
| 24 | }) | |
| 25 | ||
| 26 | test('★★ a like becomes app.bsky.feed.like, under a TID rkey', (t) => { | |
| 27 | const p = planEdge(edge('reaction', { value: 'like' }), ctx()) | |
| 28 | t.ok(p.project) | |
| 29 | t.is(p.collection, LIKE_COLLECTION) | |
| 30 | t.alike(p.record.subject, SUBJECT, 'subject carries uri AND cid — the lexicon requires both') | |
| 31 | t.is(p.record.createdAt, '2026-09-07T23:12:04.600Z', 'the edge\'s own time, not now') | |
| 32 | t.is(p.rkey, tidFor('1788822724600'), 'derived from the edge name, so a re-run reproduces it') | |
| 33 | }) | |
| 34 | ||
| 35 | test('★★★ a non-like reaction is withheld, never sent as a like', (t) => { | |
| 36 | // ⛔ bsky has exactly one reaction. Sending a 🎉 as a like would publish a different act under | |
| 37 | // somebody's name. | |
| 38 | const p = planEdge(edge('reaction', { value: '🎉' }), ctx()) | |
| 39 | t.absent(p.project) | |
| 40 | t.ok(p.losses.some((l) => /no reaction other than like/.test(l)), p.losses.join('; ')) | |
| 41 | }) | |
| 42 | ||
| 43 | test('★★ a repost becomes app.bsky.feed.repost', (t) => { | |
| 44 | const p = planEdge(edge('repost'), ctx()) | |
| 45 | t.ok(p.project) | |
| 46 | t.is(p.collection, REPOST_COLLECTION) | |
| 47 | t.alike(p.record.subject, SUBJECT) | |
| 48 | }) | |
| 49 | ||
| 50 | test('★★★ a reply is a post carrying root and parent', (t) => { | |
| 51 | const p = planEdge(edge('reply', { body: 'well said' }), ctx()) | |
| 52 | t.ok(p.project) | |
| 53 | t.is(p.collection, POST_COLLECTION) | |
| 54 | t.is(p.record.text, 'well said') | |
| 55 | t.alike(p.record.reply.parent, SUBJECT) | |
| 56 | // ⚠ With no deeper chain the root IS the parent — a top-level reply, which is the common case and | |
| 57 | // not an error. | |
| 58 | t.alike(p.record.reply.root, SUBJECT, 'root falls back to the parent') | |
| 59 | }) | |
| 60 | ||
| 61 | test('★★★ a reply to a reply carries the thread ROOT, not the parent twice', (t) => { | |
| 62 | const ROOT = { uri: 'at://did:plc:me/app.bsky.feed.post/3root', cid: 'bafyroot' } | |
| 63 | const p = planEdge(edge('reply', { body: 'and another thing' }), ctx({ root: () => ROOT })) | |
| 64 | t.alike(p.record.reply.root, ROOT, 'the walked-up root') | |
| 65 | t.alike(p.record.reply.parent, SUBJECT, 'and the immediate parent') | |
| 66 | }) | |
| 67 | ||
| 68 | test('★★★ a subject on someone else\'s feed is withheld, and says why', (t) => { | |
| 69 | // ⛔ Naming a foreign subject means choosing how it is stored, permanently (interaction.md §3). | |
| 70 | // Nothing here forces that decision, and a person should see that their like did not travel. | |
| 71 | const p = planEdge(edge('reaction', { value: 'like', target: { feed: THEIRS, collection: 'post', name: 'x' } }), ctx()) | |
| 72 | t.absent(p.project) | |
| 73 | t.ok(p.losses.some((l) => /another feed/.test(l)), p.losses.join('; ')) | |
| 74 | }) | |
| 75 | ||
| 76 | test('★★★ an unresolvable subject is an ordinary wait, not a failure', (t) => { | |
| 77 | // An edge projected before the post it points at — the next pass finds it. | |
| 78 | const p = planEdge(edge('reaction', { value: 'like' }), ctx({ subject: () => null })) | |
| 79 | t.absent(p.project) | |
| 80 | t.ok(p.losses.some((l) => /not in the atproto repo yet/.test(l)), p.losses.join('; ')) | |
| 81 | }) | |
| 82 | ||
| 83 | test('★★★ a reply in a room that reaches nothing is withheld by the lattice', (t) => { | |
| 84 | // ⛔ The reach lattice answers for the VERB. A members room reaches nothing but favela, and an | |
| 85 | // interaction written there must not escape because the identity has bsky switched on. | |
| 86 | const p = planEdge(edge('reply', { body: 'secret' }), ctx({ roomType: 'members' })) | |
| 87 | t.absent(p.project) | |
| 88 | t.ok(p.losses.length > 0, 'and says so: ' + p.losses.join('; ')) | |
| 89 | }) | |
| 90 | ||
| 91 | test('★★ a follow is not projected here — an actor is not a record', (t) => { | |
| 92 | const p = planEdge(edge('follow'), ctx()) | |
| 93 | t.absent(p.project) | |
| 94 | }) |
| … | … | @@ -118,3 +118,59 @@ test('★★ the profile is projected, and only when it differs', async (t) => { |
| 118 | 118 | const second = await projectCycle({ peer, bsky: sink, send: true }) |
| 119 | 119 | t.absent(second.profile.done, 'and NOT rewritten when it already matches — each write is a repo version') |
| 120 | 120 | }) |
| 121 | ||
| 122 | test('★★★ edges are projected after the posts, and only with a resolver', async (t) => { | |
| 123 | // ⛔ AFTER, ALWAYS. A subject resolves only once the post it points at is in the repo, so edges | |
| 124 | // first would withhold every one on the first pass and quietly fix itself on the second. | |
| 125 | const name = '1788822724600' | |
| 126 | const { sink, written } = fakeSink() | |
| 127 | const peer = fakePeer({ rows: [row(name, 'hello')] }) | |
| 128 | peer.call = async (cmd, args = {}) => { | |
| 129 | if (cmd === 'edges-by' && args.collection === 'reaction') { | |
| 130 | return [{ name: 'e1', collection: 'reaction', value: 'like', createdAt: '2026-09-07T00:00:00.000Z', target: { feed: 'f'.repeat(64), collection: 'post', name } }] | |
| 131 | } | |
| 132 | return cmd === 'profile' ? null : [] | |
| 133 | } | |
| 134 | ||
| 135 | // Without a resolver nothing edge-shaped happens — the desktop CLI cannot compute a subject CID. | |
| 136 | const plain = await projectCycle({ peer, bsky: sink, send: true }) | |
| 137 | t.absent(plain.edges && plain.edges.sent, 'no resolver, no edges') | |
| 138 | ||
| 139 | const SUBJ = { uri: 'at://did:plc:me/app.bsky.feed.post/3mv', cid: 'bafy1' } | |
| 140 | const r = await projectCycle({ peer, bsky: sink, send: true, resolveSubject: () => SUBJ }) | |
| 141 | t.is(r.edges.sent, 1, 'the like was projected') | |
| 142 | ||
| 143 | const like = written.find((w) => w.collection === 'app.bsky.feed.like') | |
| 144 | t.ok(like, 'as an app.bsky.feed.like') | |
| 145 | t.alike(like.record.subject, SUBJ, 'with uri and cid') | |
| 146 | ||
| 147 | const post = written.find((w) => w.collection === 'app.bsky.feed.post') | |
| 148 | t.ok(post, 'and the post it points at went too') | |
| 149 | }) | |
| 150 | ||
| 151 | test('★★★ an edge already in the repo is not written again', async (t) => { | |
| 152 | // ⛔ Each write is a repo version. A like nobody changed must not append one every pass. | |
| 153 | const name = '1788822724600' | |
| 154 | const { sink, written, records } = fakeSink() | |
| 155 | records.set('app.bsky.feed.like/' + tidFor('e1'), { $type: 'app.bsky.feed.like' }) | |
| 156 | const peer = fakePeer({ rows: [] }) | |
| 157 | peer.call = async (cmd, args = {}) => (cmd === 'edges-by' && args.collection === 'reaction') | |
| 158 | ? [{ name: 'e1', collection: 'reaction', value: 'like', createdAt: '2026-09-07T00:00:00.000Z', target: { feed: 'f'.repeat(64), collection: 'post', name } }] | |
| 159 | : (cmd === 'profile' ? null : []) | |
| 160 | ||
| 161 | const r = await projectCycle({ peer, bsky: sink, send: true, resolveSubject: () => ({ uri: 'at://x/y/z', cid: 'c' }) }) | |
| 162 | t.is(r.edges.skipped, 1, 'recognised as already there') | |
| 163 | t.is(written.filter((w) => w.collection === 'app.bsky.feed.like').length, 0, 'and nothing was written') | |
| 164 | }) | |
| 165 | ||
| 166 | test('★★★ with bsky switched off, no edge is projected either', async (t) => { | |
| 167 | const { sink, written } = fakeSink() | |
| 168 | const peer = fakePeer({ networks: ['fediverse'], rows: [] }) | |
| 169 | peer.call = async (cmd, args = {}) => (cmd === 'edges-by' && args.collection === 'reaction') | |
| 170 | ? [{ name: 'e1', collection: 'reaction', value: 'like', createdAt: '2026-09-07T00:00:00.000Z', target: { feed: 'f'.repeat(64), collection: 'post', name: 'p' } }] | |
| 171 | : (cmd === 'profile' ? null : []) | |
| 172 | ||
| 173 | const r = await projectCycle({ peer, bsky: sink, send: true, resolveSubject: () => ({ uri: 'at://x/y/z', cid: 'c' }) }) | |
| 174 | t.absent(r.edges.sent, 'consent gates edges exactly as it gates posts') | |
| 175 | t.is(written.length, 0) | |
| 176 | }) |