Ignore:
Timestamp:
07/29/2026 07:15:16 AM (6 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
e27b8db
Parents:
439f095
Message:

Een gated voorstel bereikte de andere guardians nooit

Op de vloot nagekeken waarom YouTube-voorbeelden bij beta uit blijven: het
voorstel van sound-fabrics staat er, met een ja van een van de drie guardians,
en het is stil verlopen. Niet omdat iemand bezwaar had, maar omdat niemand
anders het ooit gezien heeft.

Het voorstel wordt geadresseerd aan de server van het kind, want die telt en
handhaaft (5.6). Maar daarmee bereikt het alleen de voorsteller en het kind. De
twee guardians op andere servers weten van niets, kunnen dus niet antwoorden, en
een drempel van twee is onhaalbaar. Elk voorstel verloopt na een dag.

De ontbrekende schakel is het doorsturen, precies wat 5.3 al doet voor een
gated follow: de server van het kind kent de gezaghebbende guardian-lijst, dus
die stuurt het voorstel door. Elke guardian bewaart een kopie die hij kan
beantwoorden, en het antwoord reist terug naar het kind, dat telt.

Changed files:
src/config/database.js

  • tabel ap_gated_reviews, de guardian-kopie (zelfde vorm als ap_follow_reviews)

src/services/guardianship/gated.js

  • de kopie-opslag: bewaren, lezen, beantwoorden, opruimen

src/services/guardianship/handshake.js

  • ward-kant: doorsturen naar de andere guardians zodra het voorstel openstaat
  • guardian-kant: de doorgestuurde kopie bewaren om te kunnen antwoorden

src/routes/guardian.js

  • gatedReviews in de dashboardstaat; POST /guardian/api/gated/:id stuurt het antwoord naar de inbox van het kind

src/assets/js/guardian.js, src/assets/css/guardian.css

src/services/i18n.js

  • de teksten in nl, en, de

test/gated-settings.test.js

  • de hele keten: voorstellen, doorsturen naar allebei de anderen, de kopie opslaan, antwoorden, en pas bij twee van drie gaat de gate open

remarks: dit forceert niets; het maakt alleen mogelijk wat de spec al bedoelde.
Twee van de drie guardians moeten nog steeds akkoord gaan, en het venster is
24 uur.

-robo
Co-Authored-By: Claude Opus 5 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/guardianship/handshake.js

    r439f095 r88d7c8f  
    302302    const gs = gated.parseGatedSetting(activity.object);
    303303    if (gs) {
    304       if (gs.ward !== me) return false;                       // not our ward
    305       gated.rememberGatedOffer(idOf(activity), site.slug, gs.feature, gs.value);
    306       // The proposer's Offer carries its own agreement (§3.1's one-step clause).
    307       const r = gated.recordGatedVote(site.slug, gs.feature, actor, gs.value);
    308       notify(site.slug, { kind: 'gated_setting', feature: gs.feature, value: gs.value, state: r.state });
    309       return true;
     304      const offerId = idOf(activity);
     305      // ── I am the WARD: record, tally, and forward to the other guardians.
     306      if (gs.ward === me) {
     307        gated.rememberGatedOffer(offerId, site.slug, gs.feature, gs.value);
     308        // The proposer's Offer carries its own agreement (§3.1's one-step clause).
     309        const r = gated.recordGatedVote(site.slug, gs.feature, actor, gs.value);
     310        // The forward is the leg that was missing. A proposal addressed to the
     311        // ward's server reaches only the proposer and the ward; the other
     312        // guardians never learn it exists, so a threshold of two can never be
     313        // met and every proposal expires unanswered. The ward's server is the
     314        // one that knows the authoritative guardian list, which is exactly why
     315        // §5.3 forwards a gated follow from here too.
     316        if (r.state === 'open') {
     317          for (const g of relations.listGuardians(site.slug).map((x) => x.other_uri)) {
     318            if (g === actor) continue;   // the proposer already answered
     319            deps.deliverTo(site, g, {
     320              id: offerId, type: 'Offer', actor, to: [g], object: activity.object,
     321            }).catch(() => { /* the delivery queue retries */ });
     322          }
     323        } else {
     324          gated.clearGatedReviews(offerId);   // settled at once: nothing left to ask
     325        }
     326        notify(site.slug, { kind: 'gated_setting', feature: gs.feature, value: gs.value, state: r.state });
     327        return true;
     328      }
     329      // ── I am one of the GUARDIANS: the forwarded copy. Store it so this
     330      //    guardian can answer; the answer goes back to the ward, which tallies.
     331      if (relations.getRelation(site.slug, 'guardian', gs.ward)) {
     332        const wardDoc = await deps.fetchActor(gs.ward).catch(() => null);
     333        gated.recordGatedReview(site.slug, {
     334          id: offerId, wardUri: gs.ward, wardInbox: wardDoc && wardDoc.inbox,
     335          proposer: actor, feature: gs.feature, value: gs.value,
     336        });
     337        notify(site.slug, { kind: 'gated_review', feature: gs.feature, value: gs.value, ward: gs.ward });
     338        return true;
     339      }
     340      return false;   // not our ward, and not a ward we guard
    310341    }
    311342    // §3.6.3: a co-guardian proposes releasing a dormant guardian of THIS
Note: See TracChangeset for help on using the changeset viewer.