Foundry on Autri — v1 Build Epic
Status: Build epic — Wave 1 storified session 27 (2026-06-29). Grounds the build for the requirements contract projects/autri/requirements/foundry-integration (authoritative on scope, M1–M7) and the scoping doc projects/autri/foundry-integration (rationale, D1–D7). The M5 keystone is designed + locked — see the contract's "Load-bearing design locks." This epic carries the dependency-ordered stories. Wave 1 (M1 domain + M3 identity + the M6 harness) is fully specified below; later waves (M2/M4/M5/M7) are sketched and storified when their wave comes up.
Wave plan
Wave 1 delivers the annotation + review domain on read-only docs (M1), identity & access (M3), and stands up the M6 tool-testing harness in parallel. Document editing (the M5 keystone) and the authoring UI (M4) come in later waves; Wave 1 is the substrate they sit on.
Topo sort of the dependency graph below → execution waves:
- Wave 1a: S0 (foundation) + S4 (harness — independent, parallel)
- Wave 1b: S1 + S2 (both depend only on S0)
- Wave 1c: S3 (depends on S1 + S2)
Isolation — parallel-track discipline
Track 1 (the FIA segmenter) is live in autri's main working tree in a separate session. All Wave 1 work is isolated in the foundry-v1 worktree (.worktrees/foundry-v1, branched off main @ fee1ce2). /hl:ship bases its per-story branches off the pinned foundry-v1 commit, not moving main — so even if Track 1 commits to main mid-ship, Wave 1 stays isolated. Wave 1 PRs merge into foundry-v1; integration to main is deliberate, once both tracks are ready. The real collision surface in parallel ships is shared write paths (the schema file, the migrations dir) — owned-surfaces are scoped per story below to avoid them.
Where the domain lives — S0 structural decision
The annotation/review domain is a new shared workspace package @autri/authoring (sibling to @autri/retrieval), consumed by the web app and — later — the authoring MCP server, mirroring how @autri/retrieval is consumed by app/ and the doc-search Lambda. This is the clean module boundary the scoping doc's "native authoring module on the shared substrate" framing calls for, rather than swelling @autri/retrieval. The Drizzle schema stays in its single source (app/lib/db/schema.ts); the numbered migration lives in db/migrations/. Lambda-packaging discipline (narrow subpath exports, Dockerfile copy lists) applies only when the package is wired into a deployed surface in a later wave — a root-barrel import into a tsx-on-Lambda worker drags its whole transitive graph into cold-start. Wave 1 is local-tested domain, so it does not bite yet.
Wave 1 stories
S0 — Schema + auth context [foundation]
Depends on: none.
Scope. Stand up the @autri/authoring package skeleton (workspace package, tsconfig refs, build wired into the pnpm workspace). Add the annotations + reviews tables to Autri Postgres via an additive numbered Drizzle migration (next free number — verify db/migrations/; expected 029), and to the Drizzle schema (app/lib/db/schema.ts). Define the AuthContext domain type. No behavior.
Schema, mapped from Foundry (packages/api/src/db.ts) with the Autri deltas:
annotations
iduuid PKdocument_iduuid NOT NULL → documents(id) ON DELETE CASCADE (replaces Foundrydoc_path)section_iduuid,char_startint,char_endint (the new anchor — replaces the fragileheading_path/content_hash; resolves intochunks.source_ranges)quoted_texttext (display + best-effort recovery snapshot)contenttext NOT NULLparent_iduuid → annotations(id) (reply threading)review_iduuid → reviews(id)user_idtext NOT NULL (Cognitosub)author_typetext NOT NULL CHECK in ('human','agent')statustext NOT NULL CHECK in ('draft','submitted','replied','resolved','orphaned') DEFAULT 'draft'created_at,updated_attimestamptz NOT NULL
reviews
iduuid PKdocument_iduuid NOT NULL → documents(id) ON DELETE CASCADEuser_idtext NOT NULLstatustext NOT NULL CHECK in ('draft','submitted','complete') DEFAULT 'draft'submitted_at,completed_attimestamptz NULLcreated_at,updated_attimestamptz NOT NULL
Indexes: annotations(document_id), annotations(review_id), annotations(parent_id), reviews(document_id). Library/tenant scoping rides through documents (an annotation FKs a document, which is already library-scoped) — no denormalized library_id in Wave 1 unless S3's authz proves it needs one.
AuthContext type: { user: { sub, scopes[] }, client?: { type: 'web' | 'mcp' } } — server-authoritative identity, threaded as the first arg of every service function (mirrors Foundry's context.ts). author_type is derived from client.type at the boundary (web→human, mcp→agent), never client-supplied (D6).
Acceptance. Migration applies cleanly on a fresh local DB (make db-reset) and is additive + idempotent (re-runnable); Drizzle schema typechecks; @autri/authoring builds and exports the types. No behavior yet.
Owned surfaces. packages/authoring/** (new), app/lib/db/schema.ts (additive — annotations/reviews tables only), db/migrations/029_foundry_annotations_reviews.sql (new).
S1 — Annotation service (async pg re-impl) [substrate]
Depends on: S0.
Scope. Port annotations.ts (packages/api/src/services/annotations.ts, 361 LOC) to async pg inside @autri/authoring: list / get / create / edit / resolve / reopen / del. Preserve the domain behaviors exactly:
- create: 30-second create-dedup window; replies inherit
review_idfrom parent;author_typedrives default status (human→draft, agent→submitted) unless an explicit valid status is supplied. - edit: PATCH status/content/review_id; auto-bump
updated_at. - resolve / reopen: convenience wrappers (→ resolved / → draft).
- del: cascade-delete child replies; orphan-review cleanup (delete a review when its last annotation is removed).
Port the error contract (ServiceError + NotFound 404 / Validation 400 / Conflict 409 / ServiceUnavailable 503) into @autri/authoring — HTTP-status-carrying but transport-agnostic. The orphaned status exists and is settable, but has no auto-trigger in Wave 1 (auto-orphan-on-edit lands with the M5 editing keystone).
Acceptance. The annotation domain behaviors the Foundry annotation test suite encodes hold against the async pg impl (re-implemented async, not copied; the pre-existing Foundry test failures, issue #129, are fixed, not reproduced — D4). An annotation on a real ingested doc threads (parent/child), inherits its parent's review_id, and resolves correctly. Typecheck + tests green.
Owned surfaces. packages/authoring/src/annotations.ts, packages/authoring/src/errors.ts, packages/authoring/src/annotations.test.ts.
S2 — Review service (async pg re-impl) [substrate]
Depends on: S0. (Co-waves with S1 — the submit flow updates annotation rows by review_id directly in SQL, the Foundry approach, so it does not import S1's service module. S2's tests seed annotation rows via direct inserts.)
Scope. Port reviews.ts (packages/api/src/services/reviews.ts, 265 LOC): list / get / create / edit / submit. The submit aggregate flow — create (or reuse) the review → set its target annotations (draft+submitted, or an explicit id list) to status='submitted' and stamp review_id → patch the review to status='submitted' + submitted_at, all in one transaction. Lifecycle draft → submitted → complete (complete is a manual PATCH, reserved).
Acceptance. The review domain behaviors the Foundry review test suite encodes hold against the async pg impl; the submit flow batches the right annotations and flips both annotation- and review-state atomically. Typecheck + tests green.
Owned surfaces. packages/authoring/src/reviews.ts, packages/authoring/src/reviews.test.ts.
S3 — Read adapters + access enforcement [substrate]
Depends on: S1, S2.
Scope.
- Read adapters (
pages):getPage/getSection/listPages/listDocsre-pointed onto Autri's existing model — documents / pages / sections +chunks.source_rangesfor the canonical source string and char ranges. These resolve an annotation's(document_id, section_id, char range)to displayable source. This is adapter work onto Autri's hybrid retrieval + doc model, not an Anvil re-impl. - M3 access enforcement (the teeth): every annotation/review service op is scoped to the caller's library/tenant (via the annotation's
document_id→ its library); cross-tenant reads/writes denied.author_typederived at the boundary (web=human / MCP=agent), never client-supplied. - Tenant-isolation probe: extend + run
scripts/probe-tenant-isolation.shto cover the annotation/review surface — two users, no cross-tenant leakage on any op. Scope the probe by per-call id, not a shared time-window.
Acceptance. Web- and MCP-created annotations carry the correct user_id (Cognito sub) + author_type; an annotation resolves to its exact source span via the read adapters; the tenant-isolation probe shows zero cross-tenant leakage across all annotation + review ops.
Owned surfaces. packages/authoring/src/pages.ts, packages/authoring/src/access.ts, scripts/probe-tenant-isolation.sh (extend), the matching *.test.ts.
S4 — M6 tool-testing harness (stand up) [parallel]
Depends on: none — runs parallel to S0–S3 (pure local tooling, no app/schema/migration overlap).
Scope. A local closed-loop harness that drives an agent through representative authoring/review tasks via an MCP toolset and scores task completion — the instrument that curates the Autri authoring toolset and irons out tool bugs before any prod ship (modify → score → verify; a growing task corpus that hardens over time).
- Bootstrap against the live Foundry 25-tool MCP server (
~/Documents/Code/foundry, Streamable HTTP): build the harness loop + scoring, establish the incumbent's task-success bar, and capture which of the 25 tools actually matter for real authoring/review tasks. - Task corpus: representative jobs (create + thread an annotation; batch + submit a review; navigate a doc; search) each with explicit success criteria.
- Autri re-target hook: wired (stubbed) so the harness measures the Autri tool surface — a thin MCP wrapper over the S1/S2 services — against the same bar as those services land.
Acceptance. The harness runs end-to-end against the Foundry MCP server, scores an agent completing the task corpus, and emits a per-task success report + a tool-usage map (which tools were needed). No prod dependency. The Autri-retarget hook is ready for S1/S2's services.
Owned surfaces. packages/authoring/harness/** (or top-level foundry-harness/), the task-corpus fixtures.
Later waves — sketch (storified when their wave comes up)
- Wave 2 — M4 authoring/review UI [app]. The standalone re-themed "Foundry" Next.js surface (D3): lift the ~3,800 LOC of portable React 19 components, rebuild the ~880 LOC Astro shell, build the char-offset annotation renderer (a remark/rehype pass mapping a highlight to
(section_id, char range)— the single largest piece, ~2.5–3 weeks). Reviewer flow first (highlight → comment → batch → submit), then author affordances. Consumes the S1–S3 domain. - Wave 3 — M5 editable source + versioning [substrate] (the keystone). The locked design: in-app markdown edit → S3 re-publish → per edit tool call, inline embed-then-insert atomic section flip scoped by
(document_id, unit_index), gated bycontent_hash→ an every-save edit + comment changelog. Activates the dormantorphanedtrigger (boundary split/merge + failed re-anchor) and the fuzzy carry-forward. Build only after M4 gives the editing surface. - Wave 4 — M6 curated toolset (productionize) [substrate/app]. Turn the S4 harness findings into the curated, prod Autri authoring MCP toolset over the deployed surface; regression coverage for the fixed-on-port issues; Lambda-packaging discipline applies here.
- Migration — M7 corpus import. One-time import of Foundry's git-backed markdown into S3-backed Autri docs (adapts
mgmt.importRepo). Decide the annotation migration policy (re-anchor existing threads vs. start fresh) — the third owed design lock.