<!-- SOLIDIVISM-STAGING · part 17 -->
PISTEMOLOGY MODULE — Phase 1 Staging
   Read-only observation of 3D world. Never modifies 3D. Never breaks 3D.
   Persona of Articulation governs disposition. PostgreSQL is authoritative.
   ═══════════════════════════════════════════════════════════════════ */
const Epistemology = (function() {
  'use strict';

  const API_BASE = '/api/solidivism/epistemology';
  const CACHE_KEY = 'solidivism.epistemology.pending';
  const SESSION_ID = 'sess-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);

  // ── In-memory runtime index (NOT authoritative) ──
  const registry = new Map();
  const ledgerCache = [];

  // ── Health (separate from 3D, diagnosable) ──
  const Health = {
    observations: { attempted: 0, succeeded: 0, failed: 0 },
    lastError: null,
    lastSuccess: null,
    recordAttempt() { this.observations.attempted++; },
    recordSuccess() {
      this.observations.succeeded++;
      this.lastSuccess = new Date().toISOString();
    },
    recordFailure(err) {
      this.observations.failed++;
      this.lastError = { time: new Date().toISOString(), message: String(err && err.message || err) };
    },
    report() {
      return {
        observations: Object.assign({}, this.observations),
        lastError: this.lastError,
        lastSuccess: this.lastSuccess,
        pendingSync: getPendingCount(),
        session: SESSION_ID
      };
    }
  };

  // ── Local cache (write buffer ONLY, not authoritative) ──
  function getPending() {
    try {
      return JSON.parse(localStorage.getItem(CACHE_KEY) || '[]');
    } catch (e) { return []; }
  }
  function getPendingCount() { return getPending().length; }
  function addToPending(event) {
    try {
      const pending = getPending();
      pending.push(event);
      // Circular buffer: keep most recent 5000
      while (pending.length > 5000) pending.shift();
      localStorage.setItem(CACHE_KEY, JSON.stringify(pending));
    } catch (e) {
      Health.recordFailure(new Error('localStorage full or unavailable'));
    }
  }

  // ── Event recording ──
  function recordEvent(action, data) {
    data = data || {};
    const event = {
      idempotency_key: 'evt-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9),
      event_time: new Date().toISOString(),
      actor_id: data.actor || 'solidivism:agent:solid:v1',
      action: action,
      target_id: data.target || null,
      inquiry: data.inquiry || {},
      confidence: data.confidence || 'medium',
      gaps: data.gaps || [],
      session_id: SESSION_ID,
      source: 'browser',
      checksum: 'pending-server'  // computed server-side
    };
    // Add to local cache immediately
    addToPending(event);
    // Attempt sync (non-blocking)
    syncToServer();
    return event;
  }

  // ── Server sync ──
  let syncInFlight = false;
  function syncToServer() {
    if (syncInFlight) return;
    const pending = getPending();
    if (!pending.length) return;
    syncInFlight = true;
    fetch(API_BASE + '/events', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ events: pending })
    }).then(function(res) {
      if (!res.ok) throw new Error('HTTP ' + res.status);
      return res.json();
    }).then(function(data) {
      // Remove accepted events from cache
      const accepted = new Set(data.accepted || []);
      const remaining = getPending().filter(function(e) {
        return !accepted.has(e.idempotency_key);
      });
      try { localStorage.setItem(CACHE_KEY, JSON.stringify(remaining)); } catch (e) {}
      syncInFlight = false;
    }).catch(function(err) {
      Health.recordFailure(err);
      syncInFlight = false;
      // Events remain in cache for retry
    });
  }

  // ── Observation handler (called by 3D observe()) ──
  let lastCollisionLog = 0;
  function handleObservation(eventType, data) {
    Health.recordAttempt();
    try {
      data = data || {};
      // Throttle high-frequency events
      if (eventType === 'collision.detect') {
        const now = Date.now();
        if (now - lastCollisionLog < 1000) return;  // max 1/sec
        lastCollisionLog = now;
      }
      recordEvent(eventType, {
        target: data.label ? 'solidivism:building:' + data.label.toLowerCase() : null,
        inquiry: {
          what: eventType,
          when: new Date().toISOString(),
          where: 'epistles.xyz/solidivism-staging/',
          how: '3D observation via observe() interface'
        },
        confidence: 'high',  // direct observation
        gaps: []
      });
      // Also record raw data in registry for inquiry
      if (data.label) {
        registry.set('solidivism:building:' + data.label.toLowerCase(), {
          type: 'BUILDING',
          label: data.label,
          position: data.position,
          dimensions: data.dimensions,
          observedAt: new Date().toISOString()
        });
      }
      Health.recordSuccess();
    } catch (err) {
  