Foundry Foundry

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

  • id uuid PK
  • document_id uuid NOT NULL → documents(id) ON DELETE CASCADE (replaces Foundry doc_path)
  • section_id uuid, char_start int, char_end int (the new anchor — replaces the fragile heading_path/content_hash; resolves into chunks.source_ranges)
  • quoted_text text (display + best-effort recovery snapshot)
  • content text NOT NULL
  • parent_id uuid → annotations(id) (reply threading)
  • review_id uuid → reviews(id)
  • user_id text NOT NULL (Cognito sub)
  • author_type text NOT NULL CHECK in ('human','agent')
  • status text NOT NULL CHECK in ('draft','submitted','replied','resolved','orphaned') DEFAULT 'draft'
  • created_at, updated_at timestamptz NOT NULL

reviews

  • id uuid PK
  • document_id uuid NOT NULL → documents(id) ON DELETE CASCADE
  • user_id text NOT NULL
  • status text NOT NULL CHECK in ('draft','submitted','complete') DEFAULT 'draft'
  • submitted_at, completed_at timestamptz NULL
  • created_at, updated_at timestamptz 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_id from parent; author_type drives 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 / listDocs re-pointed onto Autri's existing model — documents / pages / sections + chunks.source_ranges for 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_type derived at the boundary (web=human / MCP=agent), never client-supplied.
  • Tenant-isolation probe: extend + run scripts/probe-tenant-isolation.sh to 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 by content_hash → an every-save edit + comment changelog. Activates the dormant orphaned trigger (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.

Review

🔒

Enter your access token to view annotations