Skip to main content

LORE pebble seam verification — refactor notes + run punch list

This entry captures what was non-obvious during TASKSET 2 of the CAIRNET–LORE coupling campaign. The doctrine (pebble-knowledge-seam-contract.doctrine.md) defines the contract; this learning records what the contract collided with on first contact.

What changed in LORE

  • lore/src/lib/knowledge-api.ts — every export now returns Result<T> = { ok: true, data } | { ok: false, status, reason, detail } instead of T | null. The safe() wrapper is gone. reason is one of "network" | "5xx" | "4xx" | "shape".
  • lore/src/components/knowledge/degraded-banner.tsx — server component that renders one banner aggregating any failures across a page’s parallel fetches. Three visual variants map to severity (outage for network/5xx, permission for 4xx, shape for parse/predicate failures).
  • Pages updated to consume Result<T>: search, decisions/[id], governance, agents, analytics, graph. Detail pages distinguish 4xx → notFound() from 5xx/network/shape → render chrome + banner, per the doctrine’s UI rendering rules.
  • lore/scripts/verify-pebble-seam.mjs — pure-Node smoke harness with hand-rolled shape predicates mirroring lore/src/lib/types.ts.

Decisions that ran against the spec, and why

No zod. The doctrine said “prefer zod” because pebble already pins zod@^3.24.4 for Astro/Starlight compatibility. LORE doesn’t have zod in its deps, and adding a runtime validator dep just for a smoke harness fails the “don’t add features beyond what the task requires” bar in CLAUDE.md. The script’s hand-rolled predicates trade zod’s diff quality for zero install cost — fine for a smoke test by design. If a CI job later wants higher-fidelity diffs, swap in zod then. Optional fields treated as soft. The pebble side currently returns severity, outcome, kbCommit, kbBranch, decisionFile only on some rows. The predicates list these as optional rather than required, even though lore/src/lib/types.ts types them as required strings. Reasoning: the smoke test should fail loudly on real drift, not on the LORE type file being slightly more strict than reality. The type file is the right thing to tighten if/when pebble starts returning these uniformly — the predicates document the actual ground truth today. Write routes are gated. annotate and archive mutate state, so the script only exercises them when both DECISION_ID and EXERCISE_WRITES=1 are set. The doctrine’s verification checklist asks for “write paths green or punch-list filed” — opt-in is the right posture for a script someone might run against prod by mistake.

Footguns surfaced during the refactor

  1. getDecision was returning null on 404. Pre-refactor pages called notFound() on null, which collapsed permission denials and missing rows into the same UX. Post-refactor, only reason: "4xx" triggers notFound(); 5xx/network/shape render the banner so an operator can tell why the page is empty.
  2. Annotate handler was throwing a generic Error. The replacement includes result.reason and result.detail in the thrown message; without these, a Server Action failure surfaced in the UI with no diagnostic. (Genuine UX issue worth tracking — Server Actions currently propagate errors as opaque toast strings.)
  3. Pre-existing dead imports across pages. TypeScript flagged a wave of unused imports (notFound, CardHeader, CardTitle, Spinner, Shield, ExternalLink, canViewEcosystem, etc.) on touch. They were left in place — out of scope for TASKSET 2, would otherwise pad the diff and make the seam refactor harder to review.

Live punch list

The smoke script needs a real airlock cookie to run; it has not yet been exercised against mcp.devarno.cloud in this session. Fill in once the first run lands:
RouteStatusNotes
GET /decisionsTBD
GET /decisions/:idTBDneeds DECISION_ID
POST /decisions/:id/annotateTBDgated, EXERCISE_WRITES=1
POST /decisions/:id/archiveTBDgated, EXERCISE_WRITES=1
GET /statsTBD
GET /stats/timelineTBD
GET /stats/distributionTBD
GET /agentsTBD
GET /council/:repoTBD
GET /reposTBD
GET /graph/:idTBDneeds DECISION_ID
Run command for the next session:
cd lore
AIRLOCK_COOKIE='better-auth.session_token=...' \
  REPO=devarno-cloud/kb \
  DECISION_ID=<paste-real-id> \
  node scripts/verify-pebble-seam.mjs
Update this table with the punch list. Any drift entries become pebble fix tickets; any red entries become joint LORE/pebble debug sessions.

Verification checklist (doctrine §“Verification checklist (TASKSET 2 exit)”)

  • All callers use Result<T>safe() removed.
  • Every page rendering Result data handles all four failure reasons.
  • Smoke harness exists and is shape-aware.
  • No silent null fallback remains in lore/src/app/**.
  • Live run filed (this learning’s punch-list table) — pending real airlock cookie.
The last item is not blocking TASKSET 3 (CausalityGraph) — it’s an observability checkpoint that closes once anyone with a session runs the script. The seam refactor itself ships green.