CAIRNET frontend MVP — non-obvious findings
What landed incairnet/ 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 hostcairn.devarno.cloud.cairnet/src/middleware.ts— airlock SSO same as LORE, header names rebadgedx-cairn-user-*,clientSlug: "cairn".cairnet/src/lib/{auth,auth-server,airlock-client,utils}.ts— copied from LORE verbatim.LoreUser/LoreRolesymbol 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.ts—Result<T>client; identical failure semantics to LORE’sknowledge-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;Resultimport re-pointed atcairn-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 aDegradedBannerand never collapse failures into fake-empty UI.cairnet/src/app/api/cairn/**/route.ts— six proxy routes, one per pebble endpoint. PureproxyToBackendcalls, no business logic.cairnet/src/app/{icon,apple-icon,icon-192,icon-512}.png— brand assets propagated fromatlas/assets/brand/per CLAUDE.md §“Propagation rule”.- LORE side:
lore/src/lib/cross-app-links.tsandlore/src/config/ecosystem-apps.tsextended with acairnentry 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
downlevelIterationstrikes again.[...mySet]failed typecheck infeed-filter.tsx— same trap as inlore/src/components/charts/graph-primitive.tsx(TASKSET 3). UseArray.from(set)in cairnet too. Pin a doctrine if this hits a third time.redirect()isnever-typed in newer Next, but TS still demands a return. ThegetSession()-then-redirect pattern in/profile/page.tsxneeded an explicitreturn redirect(...)to satisfy the null-narrowing checker; without it, TS thinkssessioncould be null after the early return.force-dynamic+cookies()is the right combo. Build logs showDYNAMIC_SERVER_USAGEfor proxy routes — that’s the desired behaviour (no static rendering for cookie-forwarding routes), not a regression. Same as LORE.
Verification done
npm installclean (153 packages, 5 vulns at parity with LORE’s lockfile age).npx tsc --noEmitclean (only one cosmeticawaitwarning inproxy.ts:21—await cookies()is the Next.js 14 contract; identical to LORE).npx next buildsucceeded; 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=truein 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 (nonext-themessystem mode) - Pages:
/feed,/stones/:id,/agents/:id,/explore,/feed/archive, plus/profileredirect and/api-reference - Components:
StoneComposer,StoneCard,FeedFilter,ReactionBar(noThreadViewextracted — thread is rendered inline in/stones/[id]because it’s a flat one-level list at v0; extract if nesting deepens) - No graduation surface