Index: src/services/guardianship/relations.js
===================================================================
--- src/services/guardianship/relations.js	(revision 6b5d7da762e4f4cedbefb24fb218a1006897f071)
+++ src/services/guardianship/relations.js	(revision 780a7c655af23759b03e276bc56d8be0bc5d44ff)
@@ -1,14 +1,8 @@
 /**
- * Guardianship (FEP-633c) — the ward ↔ guardian relations (ap_guardianships).
- *
- * Every row is one relation seen from a LOCAL site: role 'guardian' means the
- * site guards `other_uri` (a ward, possibly remote); role 'ward' means
- * `other_uri` guards the site. A local ward with a local guardian yields two
- * rows, one per perspective — intentional, each side reads its own.
- *
- * The handshake (spec §3): the guardian-candidate — and only the candidate —
- * Offers a Relationship {subject: ward, relationship: shaer:Guardian,
- * object: candidate}; the ward Accepts (or Rejects). Status walks
- * 'offered' → 'accepted'; a Reject deletes the row.
+ * Guardianship (FEP-633c) — the COMMITTED ward ↔ guardian relations
+ * (ap_guardianships). Pending offers live in offers.js; a row here means the
+ * handshake committed (§3.1.4). Every row is one relation seen from a LOCAL
+ * site: role 'guardian' = the site guards other_uri; role 'ward' = other_uri
+ * guards the site.
  */
 import db from '../../config/database.js';
@@ -18,11 +12,10 @@
   if (!_s) {
     _s = {
-      ins: db.prepare(`INSERT OR IGNORE INTO ap_guardianships (slug, role, other_uri, other_handle, status, offer_id, created_at)
-                       VALUES (?,?,?,?,?,?,CURRENT_TIMESTAMP)`),
-      accept: db.prepare(`UPDATE ap_guardianships SET status='accepted' WHERE slug=? AND role=? AND other_uri=?`),
+      commit: db.prepare(`INSERT INTO ap_guardianships (slug, role, other_uri, other_handle, status, offer_id, created_at)
+                          VALUES (?,?,?,?, 'accepted', ?, CURRENT_TIMESTAMP)
+                          ON CONFLICT(slug, role, other_uri) DO UPDATE SET status='accepted', offer_id=excluded.offer_id`),
       del: db.prepare('DELETE FROM ap_guardianships WHERE slug=? AND role=? AND other_uri=?'),
-      bySlugRole: db.prepare('SELECT * FROM ap_guardianships WHERE slug=? AND role=? ORDER BY created_at DESC'),
+      bySlugRole: db.prepare("SELECT * FROM ap_guardianships WHERE slug=? AND role=? AND status='accepted' ORDER BY created_at DESC"),
       one: db.prepare('SELECT * FROM ap_guardianships WHERE slug=? AND role=? AND other_uri=?'),
-      byOffer: db.prepare('SELECT * FROM ap_guardianships WHERE offer_id=?'),
     };
   }
@@ -33,42 +26,29 @@
 
 /** Accepted guardian URIs of a local ward (feeds shaer:guardians). */
-export function listGuardians(slug) {
-  return stmts().bySlugRole.all(slug, 'ward').filter((r) => r.status === 'accepted');
+export function listGuardians(slug) { return stmts().bySlugRole.all(slug, 'ward'); }
+
+/** Accepted wards of a local guardian (the wards queue). */
+export function listWards(slug) { return stmts().bySlugRole.all(slug, 'guardian'); }
+
+/** A site is a guardian once it stands in any accepted guardian relation. */
+export function isGuardian(slug) { return listWards(slug).length > 0; }
+
+export function getRelation(slug, role, otherUri) { return stmts().one.get(slug, role, otherUri); }
+
+// ── Writes (only the handshake commit lands here) ────────────────────────
+
+/** The local ward gains a guardian (commit, §3.1.4). */
+export function commitGuardianForWard(wardSlug, guardianUri, { handle = null, offerId = null } = {}) {
+  stmts().commit.run(wardSlug, 'ward', guardianUri, handle, offerId);
+  return stmts().one.get(wardSlug, 'ward', guardianUri);
 }
 
-/** All ward relations of a local guardian (accepted + pending offers). */
-export function listWards(slug) {
-  return stmts().bySlugRole.all(slug, 'guardian');
+/** The local guardian gains a ward (commit, §3.1.4). */
+export function commitWardForGuardian(guardianSlug, wardUri, { handle = null, offerId = null } = {}) {
+  stmts().commit.run(guardianSlug, 'guardian', wardUri, handle, offerId);
+  return stmts().one.get(guardianSlug, 'guardian', wardUri);
 }
 
-/** Pending offers where the local site is a party (either side). */
-export function listOffers(slug) {
-  return [...stmts().bySlugRole.all(slug, 'guardian'), ...stmts().bySlugRole.all(slug, 'ward')]
-    .filter((r) => r.status === 'offered');
-}
-
-/** A site is a guardian once it stands in any guardian-side relation. */
-export function isGuardian(slug) {
-  return stmts().bySlugRole.all(slug, 'guardian').length > 0;
-}
-
-export function getRelation(slug, role, otherUri) { return stmts().one.get(slug, role, otherUri); }
-export function findByOfferId(offerId) { return offerId ? stmts().byOffer.all(offerId) : []; }
-
-// ── Writes (the handshake walks through these) ───────────────────────────
-
-/** Record an outgoing/incoming Offer on the local side with `role`. */
-export function recordOffer(slug, role, otherUri, { handle = null, offerId = null } = {}) {
-  stmts().ins.run(slug, role, otherUri, handle, 'offered', offerId);
-  return stmts().one.get(slug, role, otherUri);
-}
-
-/** The ward said yes (or our own offer was accepted): relation becomes real. */
-export function acceptRelation(slug, role, otherUri) {
-  stmts().accept.run(slug, role, otherUri);
-  return stmts().one.get(slug, role, otherUri);
-}
-
-/** Reject / retract / end a relation: the row disappears. */
+/** End a relation locally (Undo, §3.2 — federation of the Undo is Fase 4). */
 export function removeRelation(slug, role, otherUri) {
   stmts().del.run(slug, role, otherUri);
@@ -79,9 +59,12 @@
 
 /**
- * The guardianship properties for a local actor doc. `id` is the actor URI.
- * - shaer:guardians: accepted guardians of this ward (omitted when none)
+ * Guardianship props for a local actor doc. `id` is the actor URI.
+ * - shaer:guardians: accepted guardians of this ward (omitted when none, §2.1)
  * - shaer:isGuardian: true once the site guards anyone
- * - shaer:queues: the owner-only dashboard collections (always advertised,
- *   like `blocked`: clients discover, the routes enforce auth)
+ * - shaer:queues: the owner-only dashboard collections
+ *
+ * §1 mutual exclusion: a ward (has guardians) is never a guardian, so
+ * shaer:isGuardian is suppressed if guardians exist; the offer path already
+ * bars a ward from offering.
  */
 export function actorProps(id, slug) {
@@ -94,11 +77,14 @@
   };
   const guardians = listGuardians(slug).map((r) => r.other_uri);
-  if (guardians.length) props['shaer:guardians'] = guardians;
-  if (isGuardian(slug)) props['shaer:isGuardian'] = true;
+  if (guardians.length) {
+    props['shaer:guardians'] = guardians;   // a ward
+  } else if (isGuardian(slug)) {
+    props['shaer:isGuardian'] = true;        // a guardian (never both, §1)
+  }
   return props;
 }
 
 export default {
-  listGuardians, listWards, listOffers, isGuardian, getRelation, findByOfferId,
-  recordOffer, acceptRelation, removeRelation, actorProps,
+  listGuardians, listWards, isGuardian, getRelation,
+  commitGuardianForWard, commitWardForGuardian, removeRelation, actorProps,
 };
