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 returnsResult<T> = { ok: true, data } | { ok: false, status, reason, detail }instead ofT | null. Thesafe()wrapper is gone.reasonis 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 (outagefor network/5xx,permissionfor 4xx,shapefor parse/predicate failures).- Pages updated to consume
Result<T>:search,decisions/[id],governance,agents,analytics,graph. Detail pages distinguish4xx → notFound()from5xx/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 mirroringlore/src/lib/types.ts.
Decisions that ran against the spec, and why
No zod. The doctrine said “prefer zod” because pebble already pinszod@^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
-
getDecisionwas returningnullon 404. Pre-refactor pages callednotFound()on null, which collapsed permission denials and missing rows into the same UX. Post-refactor, onlyreason: "4xx"triggersnotFound();5xx/network/shaperender the banner so an operator can tell why the page is empty. -
Annotate handler was throwing a generic Error. The replacement
includes
result.reasonandresult.detailin 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.) -
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 againstmcp.devarno.cloud in this session. Fill in once the
first run lands:
| Route | Status | Notes |
|---|---|---|
GET /decisions | TBD | |
GET /decisions/:id | TBD | needs DECISION_ID |
POST /decisions/:id/annotate | TBD | gated, EXERCISE_WRITES=1 |
POST /decisions/:id/archive | TBD | gated, EXERCISE_WRITES=1 |
GET /stats | TBD | |
GET /stats/timeline | TBD | |
GET /stats/distribution | TBD | |
GET /agents | TBD | |
GET /council/:repo | TBD | |
GET /repos | TBD | |
GET /graph/:id | TBD | needs DECISION_ID |
Verification checklist (doctrine §“Verification checklist (TASKSET 2 exit)”)
- All callers use
Result<T>—safe()removed. - Every page rendering
Resultdata 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.