<!-- SOLID CITY · part 1 -->
===== FILE: lib/city-builds.js =====
'use strict';
// SOLID CITY 🔨 THE FORGE — world persistence for creations built in /city (2026-10-09).
// Validation (shapes, sizes, no-build zones, height, span) is public/city/forge-core.js, the SAME
// file the browser uses, so both sides agree. The server adds: login, CSRF (site middleware),
// rate limits, per-user caps, and the cross-build overlap check (piece AABBs vs every other
// public/pending build). The King's (admin) builds go public at once; everyone else's are
// 'pending' (visible to the owner and admins) until an admin approves or rejects them.
//   GET    /api/city/builds              public builds + mine (+ all pending for admins)   no-store
//   POST   /api/city/builds {name,pieces}  login, 6/min + 40/h                      -> {ok, build}
//   POST   /api/city/builds/:id/delete   owner or admin, 30/min (POST so the site CSRF check applies)
//   POST   /api/city/builds/:id/review {action:'approve'|'reject'}  admin only
// Table: city_builds (migrations/0006_city_builds.sql; ensured here at startup).
const path = require('path');
const { get: dbGet, all: dbAll, run: dbRun, exec: dbExec, now } = require('./db');
const core = require(path.join(__dirname, '..', 'public', 'city', 'forge-core.js'));
const royal = require('./royal'); // ROYAL RIGHTS GATE (2026-10-12)
const L = core.LIMITS;

const DDL = `
CREATE TABLE IF NOT EXISTS city_builds (
  id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  name TEXT NOT NULL DEFAULT '', pieces TEXT NOT NULL, n_pieces INTEGER NOT NULL,
  x0 REAL NOT NULL, y0 REAL NOT NULL, z0 REAL NOT NULL, x1 REAL NOT NULL, y1 REAL NOT NULL, z1 REAL NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending', reviewed_by INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
CREATE INDEX IF NOT EXISTS city_builds_status_idx ON city_builds(status);
CREATE INDEX IF NOT EXISTS city_builds_user_idx ON city_builds(user_id);`;

module.exports = function mountCityBuilds(app, deps) {
  const { requireLogin, rateLimit } = deps;
  let ready = null;
  const ensure = () => { if (!ready) ready = dbExec(DDL).catch(e => { ready = null; throw e; }); return ready; };
  ensure().catch(e => console.error('[city-builds] ensure:', e.message));

  const isAdmin = (req) => !!(req.user && req.user.is_admin);
  const ownerName = (r) => String(r.display_name || r.username || 'citizen').slice(0, 60);
  const cleanName = (v) => String(v || '').replace(/<[^>]*>/g, '').replace(/[\u0000-\u001f\u007f<>]/g, '').replace(/\s+/g, ' ').trim().slice(0, L.NAME) || 'Creation';
  const out = (r, req) => {
    let pieces = []; try { pieces = JSON.parse(r.pieces); } catch (e) {}
    return { id: r.id, name: r.name, owner: ownerName(r), mine: !!(req.user && req.user.id === r.user_id), status: r.status, created_at: r.created_at, pieces };
  };

  app.get('/api/city/builds', rateLimit(60000, 30), async (req, res) => {
    res.set('Cache-Control', 'no-store');
    try {
      await ensure();
      const uid = req.user ? req.user.id : -1, adm = isAdmin(req);
      const rows = await dbAll(
        `SELECT id, user_id, name, pieces, status, created_at FROM city_builds
          WHERE status = 'public' OR user_id = ? OR (? = 1 AND status = 'pending')
          ORDER BY id ASC LIMIT 600`, uid, adm ? 1 : 0);
      const ids = [...new Set(rows.map(r => r.user_id))];
      const names = new Map();
      if (ids.length) (await dbAll('SELECT id, display_name, username FROM users WHERE id IN (' + ids.map(() => '?').join(',') + ')', ...ids)).forEach(u => names.set(u.id, u));
      rows.forEach(r => { const u = names.get(r.user_id) || {}; r.display_name = u.display_name; r.username = u.username; });
      let budget = 9000; const builds = [];
      for (const r of rows) { const b = out(r, req); if (budget - b.pieces.length < 0) break; budget -= b.pieces.length; builds.push(b); }
      res.json({ ok: true, admin: adm, builds });
    } catch (e) { console.error('[city-builds] list:', e.message); res.status(500).json({ ok: false, error: 'builds unavailable' }); }
  });

  app.post('/api/city/builds', requireLogin, rateLimit(60000, 6), rateLimit(3600000, 40), async (req, res) => {
    try {
      await ensure();
      // ROYAL RIGHTS: building in the King's map is the King's alone —
      // the session must be royal-unlocked (server-decided, logged).
      if (!royal.isUnlocked(req)) { royal.logEvent({ event: 'build_refused', user_id: req.user.id, username: req.user.username }); return res.status(403).json({ ok: false, error: 'Building in this world is reserved to the King.' }); }
      const raw = req.body && req.body.pieces;
      if (JSON.stringify(raw || '').length > L.MAX_JSON * 2) return res.status(413).json({ ok: false, error: 'creation too large' });
      const v = core.validateCreation(raw);
      if (!v.ok) return res.status(400).json({ ok: false, error: v.error });
      const json = JSON.stringify(v.pieces)