Changeset d56d471 in Klonkt for src/services/guardianship
- Timestamp:
- 07/29/2026 12:36:50 PM (6 weeks ago)
- Branches:
- main
- Children:
- 7a93fcf
- Parents:
- 6100ce9
- Location:
- src/services/guardianship
- Files:
-
- 2 edited
-
gated.js (modified) (3 diffs)
-
handshake.js (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/guardianship/gated.js
r6100ce9 rd56d471 182 182 export function clearGatedReviews(id) { rstmts().delAll.run(id); } 183 183 184 export function rememberGatedOffer(offerId, slug, feature, value ) {184 export function rememberGatedOffer(offerId, slug, feature, value, proposer) { 185 185 try { 186 db.prepare('INSERT OR REPLACE INTO ap_gated_offers (offer_id, slug, feature, value ) VALUES (?,?,?,?)')187 .run(offerId, slug, feature, value ? 1 : 0 );186 db.prepare('INSERT OR REPLACE INTO ap_gated_offers (offer_id, slug, feature, value, proposer) VALUES (?,?,?,?,?)') 187 .run(offerId, slug, feature, value ? 1 : 0, proposer || null); 188 188 } catch { /* non-fatal */ } 189 189 } … … 192 192 try { return db.prepare('SELECT * FROM ap_gated_offers WHERE offer_id = ?').get(offerId) || null; } 193 193 catch { return null; } 194 } 195 196 // ── The proposer's own record (5.6) ─────────────────────────────── 197 // "Where did my proposal go?" had no answer: the status was a button caption 198 // that did not survive a refresh. The ward's server tallies elsewhere, so the 199 // proposer keeps its own row and the ward's server ANSWERS the Offer when the 200 // decision settles: Accept when it settled on the proposed value, Reject when 201 // it settled on the opposite. An open row past the window renders as expired, 202 // because an expired decision settles on nothing and nobody writes home. 203 204 export function recordSent(offerId, guardianSlug, wardUri, feature, value) { 205 try { 206 db.prepare(`INSERT OR REPLACE INTO ap_gated_sent (offer_id, guardian_slug, ward_uri, feature, value) 207 VALUES (?,?,?,?,?)`).run(offerId, guardianSlug, wardUri, feature, value ? 1 : 0); 208 } catch { /* non-fatal */ } 209 } 210 211 export function recallSent(offerId) { 212 try { return db.prepare('SELECT * FROM ap_gated_sent WHERE offer_id = ?').get(offerId) || null; } 213 catch { return null; } 214 } 215 216 export function settleSent(offerId, outcome) { 217 try { db.prepare('UPDATE ap_gated_sent SET status = ? WHERE offer_id = ?').run(outcome, offerId); } catch { /* non-fatal */ } 218 } 219 220 /** The latest proposal per feature this guardian sent to this ward. */ 221 export function listSent(guardianSlug, wardUri) { 222 try { 223 return db.prepare(`SELECT * FROM ap_gated_sent WHERE guardian_slug = ? AND ward_uri = ? 224 GROUP BY feature HAVING MAX(created_at) ORDER BY created_at DESC`).all(guardianSlug, wardUri); 225 } catch { return []; } 226 } 227 228 /** 229 * What a sent row means on a screen. Pure, so the rule is testable: an answer 230 * wins, and silence past the window is not "still running", it is over. 231 */ 232 export function sentStatus(row, now) { 233 if (!row) return null; 234 if (row.status === 'accepted' || row.status === 'rejected') return row.status; 235 const opened = new Date(String(row.created_at).includes('T') ? row.created_at : `${row.created_at}Z`.replace(' ', 'T')).getTime(); 236 if (Number.isFinite(opened) && now - opened >= GATED_WINDOW_MS) return 'expired'; 237 return 'open'; 194 238 } 195 239 … … 198 242 parseGatedSetting, buildGatedOffer, rememberGatedOffer, recallGatedOffer, 199 243 recordGatedReview, getGatedReview, listGatedReviews, removeGatedReview, clearGatedReviews, 244 recordSent, recallSent, settleSent, listSent, sentStatus, 200 245 }; -
src/services/guardianship/handshake.js
r6100ce9 rd56d471 81 81 } 82 82 return anyDelivered; 83 } 84 85 /** 86 * §5.6, the closing of the loop: a settled gated decision answers the Offer 87 * that opened it. Accept when it settled on the proposed value, Reject when on 88 * the opposite. Without this the proposer's screen can only ever say 89 * "waiting", forever, whatever actually happened: the tally lives on the 90 * ward's server and nobody else may read it, so the ward's server must speak. 91 */ 92 function answerGatedProposer(site, offerId, r) { 93 const o = gated.recallGatedOffer(offerId); 94 if (!o || !o.proposer) return; 95 const me = deps.selfId(site.slug); 96 if (o.proposer === me) return; // the ward proposed to itself: nothing to write home 97 const agreed = r.value === !!o.value; 98 deps.deliverTo(site, o.proposer, { 99 id: `${me}#gatedanswer-${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`, 100 type: agreed ? 'Accept' : 'Reject', 101 actor: me, to: [o.proposer], object: offerId, 102 }).catch(() => { /* the delivery queue retries */ }); 83 103 } 84 104 … … 301 321 // ── I am the WARD: record, tally, and forward to the other guardians. 302 322 if (gs.ward === me) { 303 gated.rememberGatedOffer(offerId, site.slug, gs.feature, gs.value );323 gated.rememberGatedOffer(offerId, site.slug, gs.feature, gs.value, actor); 304 324 // The proposer's Offer carries its own agreement (§3.1's one-step clause). 305 325 const r = gated.recordGatedVote(site.slug, gs.feature, actor, gs.value); … … 326 346 } else { 327 347 gated.clearGatedReviews(offerId); // settled at once: nothing left to ask 348 answerGatedProposer(site, offerId, r); 328 349 } 329 350 notify(site.slug, { kind: 'gated_setting', feature: gs.feature, value: gs.value, state: r.state }); … … 385 406 // Accept / Reject of an offer we (also) track. 386 407 const offerId = idOf(activity.object); 408 // §5.6, the answer coming HOME: the ward's server settled a decision we 409 // proposed and answers our Offer. Accept = it settled on what we proposed, 410 // Reject = on the opposite. Only the ward may say so: the answer must come 411 // from the ward the proposal was about, or anyone could close our books. 412 const sent = gated.recallSent(offerId); 413 if (sent && sent.guardian_slug === site.slug) { 414 if (actor !== sent.ward_uri) return false; // not the ward's voice: not an outcome 415 const outcome = type === 'Accept' ? 'accepted' : 'rejected'; 416 gated.settleSent(offerId, outcome); 417 notify(site.slug, { kind: 'gated_outcome', feature: sent.feature, value: !!sent.value, outcome, ward: sent.ward_uri }); 418 return true; 419 } 387 420 // §5.6: a fellow guardian answering a gated-setting proposal. The Accept only 388 421 // references the offer, so the value comes from the proposal we stored. A … … 392 425 const value = type === 'Accept' ? !!gsOffer.value : !gsOffer.value; 393 426 const r = gated.recordGatedVote(site.slug, gsOffer.feature, actor, value); 427 if (r.state === 'settled') answerGatedProposer(site, offerId, r); 394 428 notify(site.slug, { kind: 'gated_setting', feature: gsOffer.feature, value, state: r.state }); 395 429 return true;
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)