Changeset 88d7c8f in Klonkt for src/services/guardianship


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@…>

Location:
src/services/guardianship
Files:
2 edited

Legend:

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

    r439f095 r88d7c8f  
    145145}
    146146
     147// ── The guardian-side copy (the missing leg of §5.6) ──────────────
     148// A proposal addressed to the ward's server reaches only the proposer and the
     149// ward. The other guardians never learn it exists, so a threshold of two can
     150// never be met and every proposal expires unanswered. The ward's server
     151// therefore FORWARDS it, exactly as it forwards a gated follow (§5.3): each
     152// guardian stores a copy it can answer, and the answer travels back to the
     153// ward, which tallies.
     154
     155let _rs = null;
     156function rstmts() {
     157  if (!_rs) {
     158    _rs = {
     159      ins: db.prepare(`INSERT INTO ap_gated_reviews (id, guardian_slug, ward_uri, ward_inbox, proposer, feature, value)
     160                       VALUES (?,?,?,?,?,?,?)
     161                       ON CONFLICT(guardian_slug, id) DO UPDATE SET value = excluded.value, ward_inbox = excluded.ward_inbox`),
     162      get: db.prepare('SELECT * FROM ap_gated_reviews WHERE guardian_slug = ? AND id = ?'),
     163      bySlug: db.prepare('SELECT * FROM ap_gated_reviews WHERE guardian_slug = ? ORDER BY created_at DESC'),
     164      del: db.prepare('DELETE FROM ap_gated_reviews WHERE guardian_slug = ? AND id = ?'),
     165      delAll: db.prepare('DELETE FROM ap_gated_reviews WHERE id = ?'),
     166    };
     167  }
     168  return _rs;
     169}
     170
     171export function recordGatedReview(guardianSlug, r) {
     172  rstmts().ins.run(r.id, guardianSlug, r.wardUri, r.wardInbox || null, r.proposer || null, r.feature, r.value ? 1 : 0);
     173  return rstmts().get.get(guardianSlug, r.id);
     174}
     175export function getGatedReview(guardianSlug, id) { return rstmts().get.get(guardianSlug, id); }
     176export function listGatedReviews(guardianSlug) { return rstmts().bySlug.all(guardianSlug); }
     177export function removeGatedReview(guardianSlug, id) { rstmts().del.run(guardianSlug, id); }
     178/** Drop every guardian's copy once the decision has settled or lapsed. */
     179export function clearGatedReviews(id) { rstmts().delAll.run(id); }
     180
    147181export function rememberGatedOffer(offerId, slug, feature, value) {
    148182  try {
     
    160194  tallyGatedSetting, thresholdFor, featureColumn, recordGatedVote, gatedProgress, GATED_WINDOW_MS,
    161195  parseGatedSetting, buildGatedOffer, rememberGatedOffer, recallGatedOffer,
     196  recordGatedReview, getGatedReview, listGatedReviews, removeGatedReview, clearGatedReviews,
    162197};
  • 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.