Foundry Foundry

Foundry on Autri — Wave 2 (M4 Authoring/Review UI) Build Epic

Status: Build epic — storified session 28 (2026-06-29), after the char-offset renderer was de-risked by a working spike. Grounds the build of milestone M4 in the requirements contract projects/autri/requirements/foundry-integration (authoritative on scope) and follows Wave 1 (projects/autri/epics/foundry-v1, S0–S4 shipped to the held foundry-v1 branch). Consumes the shipped @autri/authoring domain (annotations + reviews + read adapters + M3 access). Authored as a standalone doc — Form-A input for /hl:ship.

Grounding & spike outcome

A Wave-2 renderer spike (session 28) built a working vertical slice — /foundry surface → getSection → offset-faithful renderer → select → @autri/authoring create → persist → rehydrate — against a real ingested markdown doc on the local dev DB (migration 029 applied additively). Proven: rendered markdown carries exact source offsets per text run (domTextLen === end − start, syntax chars excluded); a selection maps to the exact source range; the annotation persists with char_start/char_end/quoted_text and the correct server-derived user_id/author_type; a fresh page load re-resolves the highlight at the exact span. Dark-themed, zero console errors. The spike code lives uncommitted in the foundry-wave2 worktree and is the foundation these stories harden — not throwaway.

Load-bearing decisions

  • Anchor offset space = section-local offsets into getSection().content (validated by the spike). Create-time and display-time use the identical domain-returned string, so the round-trip is exact by construction, and deterministic chunking reproduces it byte-for-byte on re-ingest of unchanged content.
  • Data layer = route handlers (not server actions): durable across redeploys (server-action ids are per-build hashed → stale tabs 404), and they fit the lifted Foundry components' fetch shape.
  • Renderer = a rehype pass over react-markdown's hast (positions preserved) that wraps each text run in an offset-tagged element, splitting at highlight boundaries into <mark>. Dependency-free. Replaces Foundry's brittle quoted-text DOM search — the structural fix for the orphaning cluster (#163/#161/#127).

Open question owed to Wave 3 (flag, not a Wave-2 blocker)

M2's "survives a re-ingest of unchanged content" bar depends on section-identity stability, which the renderer does not own: the shipped anchor keys on sections.id (a UUID). The offset within a section is proven stable; whether the anchor survives re-ingest hinges on whether that section UUID (or the M5 unit_index) is reused for unchanged content — an ingestion/versioning property. Resolve in the Wave-3 (M5) storify: confirm sections.id stability across re-ingest, or re-key the anchor on (unit_index, char range). Wave 2's own bar is M4's: highlights map to the correct spans.

Isolation — parallel-track discipline

All Wave-2 work branches off the held foundry-v1 branch (HEAD at integration time), not main — same discipline as Wave 1. PRs merge into foundry-v1; integration to main stays deliberate (alongside Track 1's FIA work). The /hl:ship orchestration must override its default main base to foundry-v1 for both worktree creation and merge target. The spike foundation is committed to the Wave-2 base branch first so the renderer story builds on it rather than rebuilding from scratch.

QA backend

Headless-preview + dev-auth (backend (b)). The dev-auth bypass (AUTRI_DEV_AUTH=1 + the dev Credentials provider) is already present and was validated end-to-end in the spike. Headless-login recipe for every QA-agent prompt: GET /api/auth/csrf → POST /api/auth/callback/dev (form-encoded csrfToken + json=true) sets the session cookie; then drive the worktree dev server. Watch the two known traps: the dev server must run from the worktree (a foundry-wave2-pinned .claude/launch.json config exists), and headless tabs report visibilityState='hidden' — don't gate verified behavior on visibility. Crucible is the post-deploy prod regression, not the pre-merge gate.

Story Summary

StoryTitleScopeDepsVisualPhase
W2-S0Surface foundation + Shell mount + auth adapterThe /foundry route section inside <Shell> (new nav key), the foundryScope() session→domain adapter, doc picker + section nav, loading/error states. Hardens the spike's bare mount.—yesM4
W2-S1Char-offset renderer (hardened) + mechanized testHarden the spike renderer: multi-chunk-section join fidelity, selection edge cases (element boundaries, triple-click, keyboard), the entity/value-mismatch guard on code/tables; add the deterministic offset round-trip test across ≥2 doc shapes.W2-S0yesM4
W2-S2Annotate + thread (reviewer flow 1)Create a comment from a selection; threaded replies (parent_id); resolve/reopen; highlight↔comment interaction (click-to-scroll, hover); the annotation list.W2-S1yesM4
W2-S3Review batch + submit (reviewer flow 2)Review composer: batch draft annotations → the domain's atomic submit; view a review; review status lifecycle.W2-S2yesM4
W2-S4Author shellEdit-mode UI shell (editor view + edit toggle) that the M5 keystone plugs into in Wave 3. NO versioned save this wave — the save action is stubbed/disabled.W2-S0yesM4

Stories

W2-S0 — Surface foundation + Shell mount + auth adapter [foundation]

Depends on: none. Visual: yes.

Scope. Promote the spike's bare /foundry surface to the real surface: mount inside the existing <Shell> chrome with a new nav key (sidebar entry), keep the foundryScope() adapter (Cognito session → AuthContext + read/write scope; done in the spike), the document picker (org-scoped markdown docs), the section nav, and real loading/error/empty states. Autri-themed, dark mode default (inherits au-* primitives + token palette; no new theme work).

Acceptance.

  • A signed-in user opens /foundry, sees their org's documents, opens one, and navigates sections — all within the Autri <Shell>. 🧑 (headless-preview visual QA)
  • Dark mode renders correctly; no console errors. 🤖 (preview_console_logs in QA)
  • A cross-org document id 404-hides (the domain's assertCanReadDocument is the boundary; the route surfaces it as not-found). 🤖 (route test)

Owned surfaces. app/app/foundry/{layout,page}.tsx, app/app/foundry/[docId]/page.tsx, app/lib/foundry/scope.ts, app/components/foundry/* (shell/nav/picker), nav registration in the Shell nav config.

W2-S1 — Char-offset renderer (hardened) + mechanized test [substrate — the crux]

Depends on: W2-S0 (mounts in the surface). Visual: yes.

Scope. Harden the spike's rehype-offsets + source-markdown into the production renderer:

  • Multi-chunk-section fidelity. getSection().content joins chunks with \n, normalizing the original paragraph gaps. Decide + implement the join/render fidelity (render per-chunk, or join on the true separator) so multi-chunk sections render faithfully while offsets stay exact. Single-chunk sections already proven.
  • Selection edge cases. Selections that start/end on element boundaries (not text nodes), triple-click, keyboard selection, selections spanning block elements — all map to correct source offsets.
  • The value/offset guard. Validate the value.length !== end − start fallback on docs with HTML entities, code blocks, and tables (where rendered text ≠ source slice); confirm it degrades to node granularity and never lands a highlight wrong.

Acceptance.

  • Mechanized offset round-trip test across ≥2 doc shapes (single-chunk prose + a multi-chunk and/or code/table doc): for a known range, select → offset → quoted === content.slice(start,end); persisted offsets re-resolve to the same span. 🤖 (the M4 "highlights map to correct spans" bar; validate ≥2 cases per [[feedback_validate_across_cases_not_n1]])
  • A highlight renders on a multi-chunk section at the correct span, visually. 🧑 (visual QA)
  • No console errors on render. 🤖

Owned surfaces. app/lib/foundry/rehype-offsets.ts, app/components/foundry/source-markdown.tsx, their *.test.ts.

W2-S2 — Annotate + thread (reviewer flow, part 1) [app]

Depends on: W2-S1. Visual: yes.

Scope. The reviewer's core loop, lifting Foundry's AnnotationThread/CommentDraft UI rewired to the new anchor + route handlers (not the old REST/localStorage): create a comment from a selection; threaded replies (parent_id, inheriting review_id); resolve/reopen; the highlight↔comment interaction (click a highlight → scroll to the comment; hover); the annotation list/thread panel. Honor the shipped domain's behaviors (30s create-dedup, one-level reply cascade on delete).

Acceptance.

  • A user highlights source text, leaves a comment, replies to it, and resolves it — all persisted via @autri/authoring, all re-resolving on reload. 🧑 (visual QA, headless recipe)
  • Clicking a highlight scrolls to its comment; the comment shows author_type + status + char range. 🧑
  • Route handlers map ServiceError → correct HTTP status (404 hide, 400 validation). 🤖 (route test)

Owned surfaces. app/components/foundry/section-reader.tsx (+ thread components), app/app/foundry/api/annotations/route.ts (extend: edit/resolve/reopen/delete + list).

W2-S3 — Review batch + submit (reviewer flow, part 2) [app]

Depends on: W2-S2. Visual: yes.

Scope. The review aggregate: a review composer that batches draft annotations and submits them via the domain's atomic submit (draft → submitted, stamps review_id, all in one transaction); viewing a submitted review and its annotations; the review status lifecycle (draft → submitted → complete).

Acceptance.

  • A user batches several draft comments into a review and submits; all targeted annotations flip to submitted and carry the review_id; the review shows submitted_at. 🧑 (visual QA)
  • The submit is atomic (the domain guarantees it; verify the UI reflects all-or-nothing). 🤖 (route/domain test already covers atomicity; UI integration verified visually)

Owned surfaces. review components under app/components/foundry/, app/app/foundry/api/reviews/route.ts.

W2-S4 — Author shell [app]

Depends on: W2-S0. Visual: yes. (Co-waves with W2-S1; lives in its own files/route to avoid the section-reader collision.)

Scope. The author-side surface shell only — the editing affordances the M5 versioning keystone plugs into in Wave 3, with no real save this wave. An edit-mode toggle, a markdown editor view of a section/doc, and a save action that is present but stubbed/disabled (wired to the M5 atomic section-flip in Wave 3). Reviewer-first, author-shell-only per the contract's M4 framing.

Acceptance.

  • A user toggles to author/edit mode on a doc and sees the editor view; the save control is visibly present but inert (clearly "coming in versioning"). 🧑 (visual QA)
  • No write path is reachable from the shell yet (no annotations/version mutation) — the shell is inert by construction. 🤖 (no write route added)

Owned surfaces. author components under app/components/foundry/author/, an author view/route distinct from the reviewer surface.

Wave plan

Topo sort of the deps:

  • Wave A: W2-S0 (foundation).
  • Wave B: W2-S1 (renderer) + W2-S4 (author shell) — both depend only on S0; S4 in its own files → parallel-safe.
  • Wave C: W2-S2 (annotate + thread — needs S1).
  • Wave D: W2-S3 (review batch + submit — needs S2).

5 stories, 4 waves, all visual → headless-preview + dev-auth QA; W2-S1 also carries the mechanized renderer test. Base off foundry-v1; merge into foundry-v1.

Review

🔒

Enter your access token to view annotations