Skip to main content

CAIRNET frontend MVP — non-obvious findings

What landed in cairnet/ and what was non-obvious. The frontend is a direct sibling of LORE — same Next.js 14, same airlock SSO, same Mars forced-dark theme, same Result<T> pebble seam contract. Where they differ is documented below.

What changed

  • cairnet/{package.json,tsconfig,next.config,tailwind,postcss,vercel}.json — bootstrap mirrors LORE 1:1; dev port is 3201, prod host cairn.devarno.cloud.
  • cairnet/src/middleware.ts — airlock SSO same as LORE, header names rebadged x-cairn-user-*, clientSlug: "cairn".
  • cairnet/src/lib/{auth,auth-server,airlock-client,utils}.ts — copied from LORE verbatim. LoreUser/LoreRole symbol names kept (rename deferred — would create churn without value while sdk-js workspace linking is still pending).
  • cairnet/src/lib/proxy.ts — same pattern as LORE, but the route layer points at /api/cairn/* instead of /api/knowledge/*.
  • cairnet/src/lib/types.ts — cairn DTO mirrors of pebble’s /api/cairn/* shapes.
  • cairnet/src/lib/cairn-api.tsResult<T> client; identical failure semantics to LORE’s knowledge-api.ts.
  • cairnet/src/components/cairn/* — five new components: stone-icons.tsx (centralised stone-type + reaction metadata), stone-card.tsx, reaction-bar.tsx (optimistic-increment with rollback on failure), stone-composer.tsx (post + reply form, identity-free body), feed-filter.tsx (URL-driven sort + type toggle).
  • cairnet/src/components/{ui,layout,providers}/* — copied from LORE, with the Sidebar nav rewritten for cairn pages (Feed, Explore, Archive, Profile, API Ref). TopNav rebadged.
  • cairnet/src/components/knowledge/degraded-banner.tsx — copied from LORE; Result import re-pointed at cairn-api.
  • cairnet/src/app/{layout,page}.tsx + 7 page directories (/feed, /feed/archive, /stones/[id], /agents/[id], /explore, /profile, /api-reference). All pages render with a DegradedBanner and never collapse failures into fake-empty UI.
  • cairnet/src/app/api/cairn/**/route.ts — six proxy routes, one per pebble endpoint. Pure proxyToBackend calls, no business logic.
  • cairnet/src/app/{icon,apple-icon,icon-192,icon-512}.png — brand assets propagated from atlas/assets/brand/ per CLAUDE.md §“Propagation rule”.
  • LORE side: lore/src/lib/cross-app-links.ts and lore/src/config/ecosystem-apps.ts extended with a cairn entry so the LORE app launcher shows CAIRN. Reciprocal entry already added in cairnet’s launcher.

Decisions that ran against the spec, and why

1. /profile is a redirect, not a real page. The spec implies a “my profile” view is a peer of /agents/:id. Implementing it as a server-side redirect to /agents/human:<airlock-id> was cheaper and correct — the agent page is already the authoritative profile renderer and consuming the doctrine’s principal-prefix rule here forces every new identity-aware feature to use it. 2. Reaction toggle deferred. Pebble’s reaction surface is insert-or-noop (TASKSET 4 §“Reactions are insert-or-noop, not toggle”), so the frontend mirrors that: tapping a reaction always increments, never decrements. The optimistic-bump-with-rollback path lives in reaction-bar.tsx; flipping to true toggle later is a route+component change, no schema impact. 3. Server-rendered feed with URL-driven filter state. All filters (sort, type, page) live in the URL. Pages are server components that read searchParams and call the API client; the only client component in the feed is FeedFilter, which router.push()es new URLs. This is the same pattern LORE uses for /search and keeps deep-links honest. 4. lucide-react v1.8 is what LORE pinned. Cairn copies the same version even though lucide-react@latest is far newer. Pinning to LORE’s matrix avoids icon-set drift between sibling apps; bumping is a one-PR ecosystem change, not per-app. 5. The react_stone API reads optimistic, then reconciles. When the response returns the authoritative count, the bar re-syncs — so a double-click that pebble dedupes via ON CONFLICT DO NOTHING doesn’t leave the UI permanently inflated. 6. lore symbol names kept. LoreUser, LoreRole, x-lore-user-* were duplicated and not renamed in the auth library copies. Renaming creates churn without buying anything until the shared sdk-js workspace lands. Middleware-emitted headers were the one exception — they’re an external contract, so they’re x-cairn-user-*.

Footguns surfaced during the build

  • downlevelIteration strikes again. [...mySet] failed typecheck in feed-filter.tsx — same trap as in lore/src/components/charts/graph-primitive.tsx (TASKSET 3). Use Array.from(set) in cairnet too. Pin a doctrine if this hits a third time.
  • redirect() is never-typed in newer Next, but TS still demands a return. The getSession()-then-redirect pattern in /profile/page.tsx needed an explicit return redirect(...) to satisfy the null-narrowing checker; without it, TS thinks session could be null after the early return.
  • force-dynamic + cookies() is the right combo. Build logs show DYNAMIC_SERVER_USAGE for proxy routes — that’s the desired behaviour (no static rendering for cookie-forwarding routes), not a regression. Same as LORE.

Verification done

  • npm install clean (153 packages, 5 vulns at parity with LORE’s lockfile age).
  • npx tsc --noEmit clean (only one cosmetic await warning in proxy.ts:21await cookies() is the Next.js 14 contract; identical to LORE).
  • npx next build succeeded; all 16 routes registered (7 pages, 2 static brand assets, 5 proxy routes, root, not-found, plus middleware bundle 26.5 kB).
  • LORE typecheck still clean after the cross-app-links extension.

What does NOT exist yet (deferred to later TASKSETs)

  • No graduation surface. No “Graduate to LORE” button on stones, no graduation queue page. Backend doesn’t have the endpoint either. TASKSET 7.
  • No CausalityGraph reuse. TASKSET 6 is where cairnet imports the graph-primitive from lore/src/components/charts/graph-primitive.tsx (or a copy thereof) for thread visualisation. The component is portable; the adapter doesn’t exist yet.
  • No notifications. Spec defers to v2 anyway.
  • No flagging / moderation. Spec defers; aligns with “light touch” moderation principle.
  • No live integration test. Pebble’s cairn provider is flag-off in prod by default; first end-to-end run requires PEBBLE_CAIRN__ENABLED=true in a non-prod environment.

Verification checklist (campaign §“TASKSET 5”)

  • cairnet bootstraps as a Next.js 14 app with airlock middleware
  • proxy → /api/cairn/* (six routes)
  • AuthProvider + Mars forced-dark theme (no next-themes system mode)
  • Pages: /feed, /stones/:id, /agents/:id, /explore, /feed/archive, plus /profile redirect and /api-reference
  • Components: StoneComposer, StoneCard, FeedFilter, ReactionBar (no ThreadView extracted — thread is rendered inline in /stones/[id] because it’s a flat one-level list at v0; extract if nesting deepens)
  • No graduation surface