Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .claude/rules/studio-bff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
matlatl: orphan-intentional
paths:
- "apps/server/**"
- "apps/contracts/**"
---
# Studio BFF and contracts

A BFF route is a `createRoute` from `@hono/zod-openapi` in `apps/server/src/routes/<surface>.ts`, with an `operationId` and an `/api/v1/` path, registered in `app.ts`. For an SDK-backed surface, the SDK calls live in `apps/server/src/mecatl/<surface>.ts`. Route tests build `createApp()` with the fakes in `apps/server/src/testing/fakes.ts` and send mutations with its `csrfHeaders()`. Body and response schemas are Zod schemas in `apps/contracts/src/schemas/`, exported from `apps/contracts/src/index.ts`; path and query parameter schemas stay in the route file.

Errors are RFC 9457 problem details. Return `problem()` from `apps/server/src/http/problem.ts`, declare each error status with `problemDetailsSchema` as `application/problem+json`, and pass upstream error text through `sanitizeUpstreamDetail`, which redacts bearer tokens and upstream addresses.

A schema or route change also regenerates the contracts artifacts, as [the Studio agent contract](../../apps/AGENTS.md#generated-files) describes.

Keep these security invariants (`http/security.ts`, `auth/service.ts`, `app.ts`, and `bootstrap.ts` under `apps/server/src/`):

- Session cookies are sealed and HttpOnly, with the `Secure` flag derived from `STUDIO_PUBLIC_URL` (the request scheme when it is unset).
- Requests must name the `STUDIO_PUBLIC_URL` host, or a loopback host when it is unset; only `/api/health` is exempt, for probes.
- State-changing `/api/v1` requests must be same-origin (`Origin` or `Sec-Fetch-Site`) and carry the double-submit token: the `X-Studio-CSRF` header equals the `studio_csrf` cookie.
- `/api/v1/auth/*` (except the session check), `/oauth/callback`, and `GET /api/v1/status` are rate-limited per client address, trusting `X-Forwarded-For` only through `STUDIO_TRUSTED_PROXY_HOPS`.
- Concurrent requests holding one refresh token share a single refresh.
- With interactive login, `/api/v1` answers `401` without a session, except the status and auth routes.
- With `STUDIO_IMAGE=1`, spawn and mock modes are refused, and a static-token or no-auth target needs `STUDIO_ALLOW_UNAUTHENTICATED=1`.

The [Studio guide](../../user-docs/operating/studio.md) describes the security model for operators.
22 changes: 22 additions & 0 deletions .claude/rules/studio-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
matlatl: orphan-intentional
paths:
- "apps/**/*.test.ts"
- "apps/**/*.test.tsx"
- "apps/web/src/test-*.ts"
- "apps/web/e2e/**"
- "apps/server/test/**"
---
# Studio tests

Studio tests are offline. Vitest runs both apps with `isolate: false`, so the files in a worker share one module cache, Web Storage, and the happy-dom `window`. Web tests run in Node unless the file starts with `// @vitest-environment happy-dom`. The NO-ISOLATE RULES in `apps/web/src/test-setup.ts` give the fixes, and Biome fails on the first two hazards:

- Render the real module inside real providers, with a fresh `QueryClient` and memory router per test, and fake the network with `apiClient.setConfig({ baseUrl: window.location.origin, fetch })`, where `apiClient` is `client` from `@mecatl-studio/contracts/client`; `vi.mock()` and `vi.doMock()` are banned.
- Spy on storage with `spyOnLocalStorage()` from `apps/web/src/test-storage.ts`; `vi.spyOn` on Web Storage is banned.
- Replace globals with `vi.stubGlobal()`, and give module-level state a reset that the shared hook in `test-setup.ts` calls.

Browser journeys in `apps/web/e2e/` import `test` and `expect` from `./fixtures`. Its automatic `offlineBff` fixture answers `/api/` calls only from the handlers a test registers, and fails the test on an unregistered BFF call or on network access outside its fixtures. Playwright runs each journey in the `chromium-desktop` (1280x800) and `chromium-mobile` (390x844) projects.

The approval journey and `apps/server/test/integration/` drive a real `mecated --mock` from `bin/mecated`; [the Studio agent contract](../../apps/AGENTS.md#commands) lists the commands.

Before changing behavior, pin the current behavior in its own commit, with `*.pinning.test.tsx` tests that pass on the base commit and on the branch.
18 changes: 18 additions & 0 deletions .claude/rules/studio-web.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
matlatl: orphan-intentional
paths:
- "apps/web/src/**"
---
# Studio web app

Routes are TanStack file routes in `apps/web/src/routes/`. A dot nests a route in its parent's layout (`workspace.chat.tsx` renders inside `workspace.tsx`), `$name` is a path parameter, and a trailing `_` on a segment leaves the parent's layout (`workspace.schedules_.$scheduleName.tsx`). A route file validates search and path parameters, redirects, and renders a component from `features/` or `components/shell/`. `routeTree.gen.ts` is [generated](../../apps/AGENTS.md#generated-files) from these files.

Each surface keeps its components and helpers in `features/<surface>/`. `components/shell/` holds the app frame, and `lib/` and `hooks/` hold shared helpers. Some features import each other today; #1777 owns the layout decision.

Request data only through the generated client (`@mecatl-studio/contracts/generated`, `/query`, and `/client`), configured once in `lib/api-client.ts` with the CSRF header and auth recovery. The only hand-built BFF URLs are browser navigations: the login URL in `features/auth/auth-gate.tsx` and the authorization presentation in `features/chat/authorization-review.tsx`.

Build controls from the shadcn/ui ports in `components/ui/`. When shadcn/ui provides a control that is missing there, port the primitive into `components/ui/` instead of hand-rolling it (#2201).

Layouts pivot at 500px, like the prototype: `min-[500px]:` and `max-[499px]:` in classes, and `useIsMobile()` from `lib/use-mobile.ts` in code. The chat list and side panels also pivot at 760px (`min-[760px]:`). `features/breakpoints.test.ts` fails on a new viewport `sm:` or `md:` class until the test lists it with a reason (it skips `features/schedules/` for now); `components/ui/` keeps its upstream shadcn classes, and container queries pass.

A visual change needs the screenshots described in [the Studio agent contract](../../apps/AGENTS.md#ui-pull-requests).
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ go run ./cmd/mecademo # offline session smoke test
## Module layout

The root is one Go module; `engine/` is its own. Root `go test ./...` does not cross
the module boundary; `task test` does. [`engine/`](engine/AGENTS.md),
[`internal/`](internal/AGENTS.md), and [`website/`](website/AGENTS.md) have their own
`AGENTS.md`. When the code doesn't explain how parts fit together, read the chapter
for that area from [`docs/READING.md`](docs/READING.md).
the module boundary; `task test` does. [`apps/`](apps/AGENTS.md) (Mecatl Studio),
[`engine/`](engine/AGENTS.md), [`internal/`](internal/AGENTS.md), and
[`website/`](website/AGENTS.md) have their own `AGENTS.md`. When the code doesn't
explain how parts fit together, read the chapter for that area from
[`docs/READING.md`](docs/READING.md).

## Implementation boundaries

Expand Down
94 changes: 94 additions & 0 deletions apps/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# AGENTS.md: Studio workspace

`apps/` holds Mecatl Studio, a self-contained pnpm workspace outside the Go build:
`contracts/` (Zod schemas and the generated client), `server/` (the Hono backend for
frontend, or BFF), and `web/` (the Vite and React app). The [README](README.md)
covers local runtime modes. The public [Studio guide](../user-docs/operating/studio.md)
owns deployment, configuration, and the security model.

## Commands

Run these from the repository root:

```sh
task studio:check # local gate: Biome, typecheck, Vitest, contracts drift
task studio:browser # offline Playwright journeys, desktop and phone
task studio:test:integration # BFF over the real SDK against a spawned mecated --mock
task studio:generate # regenerate the contracts artifacts
task studio:format # apply Biome formatting and import order
```

- `task studio:check` must pass for any change under `apps/`. `task test` and
`task lint` cover only the Go modules. Biome formats and lints all three packages
from `biome.json`, so run `task studio:format` before committing. CI also runs
`task studio:audit`, which fails on a production dependency with an advisory.
- The approval journey in `task studio:browser` spawns `mecated --mock`, so run
`task build` first. Install Chromium once with `task studio:browser:install`.
- To run one journey, use `pnpm run test:browser <SPEC> --project=chromium-mobile`
from `apps/`. Keep the `=`, because `--project` accepts several names. Leave out
`--`, because Playwright ignores the arguments after it and runs every journey.
- `task studio:test:integration` runs the full `task build` first. To rerun the
suite against an existing `bin/mecated`, run
`cd apps && STUDIO_REQUIRE_MECATED=1 STUDIO_MECATED_BIN=$PWD/../bin/mecated pnpm --filter @mecatl-studio/server run test:integration`.
Without `STUDIO_REQUIRE_MECATED=1`, a missing binary skips the suite.

## Generated files

Never edit these by hand; Biome skips them.

- `contracts/openapi.json` and `contracts/src/generated/` (the Hey API client and
its TanStack Query helpers) come from the Hono routes. Regenerate them with
`task studio:generate` (`pnpm run generate` in `apps/`) and commit them with the
route or schema change; `task studio:generated-check` fails CI when they drift.
- `web/src/routeTree.gen.ts` comes from the files in `web/src/routes/`. The
TanStack Router Vite plugin rewrites it during `dev` and `build`, and the web
`typecheck` script runs `tsr generate`. Commit it with the route change.

## Boundaries

- The BFF is the single source of truth for the data the UI shows. `web/` requests
data only from the BFF's `/api/v1`, through the generated client, and only labels
or formats what comes back. It never re-derives state or eligibility; when the UI
needs a value the contract lacks, add it to the schema and the BFF route.
- The design prototype, branch `stack/08-tooling` of `stacklok/mecatl-prototypes`,
is the source of truth for components, layout, interaction, and copy. #2210 lists
what Studio keeps, including the BFF, contracts, security, and contrast tokens.
- `server/` builds the checked-out `sdk/typescript` through a frozen `file:`
dependency and imports it only as `@stacklok-oss/mecatl-sdk`.
`server/src/sdk-source.test.ts` guards the dependency, and Biome rejects relative
imports of `sdk/`. `web/` imports neither the SDK nor its protocol types.
- `MECATL_*` variables describe the target (`server/src/mecatl/runtime.ts`), and
`STUDIO_*` variables configure Studio itself (`server/src/config.ts`). The Studio
guide's [configuration reference](../user-docs/operating/studio.md#configuration-reference)
owns the deployment variables.

## Path-scoped rules

These load with the files they cover:

- [`studio-bff.md`](../.claude/rules/studio-bff.md): `server/` and `contracts/`
routes, problem details, and security invariants.
- [`studio-web.md`](../.claude/rules/studio-web.md): `web/src/` routes, features,
data access, UI primitives, and the 500px breakpoint.
- [`studio-tests.md`](../.claude/rules/studio-tests.md): Vitest without isolation,
the offline Playwright fixtures, and pinning tests.

## UI pull requests

A PR that changes what Studio renders shows screenshots in Before, After, and
Prototype columns at desktop (1280x800) and phone (390x844) sizes, in light and
dark, with the changed regions highlighted.

Capture Before and After with a throwaway `<NAME>.spec.ts` in `web/e2e/` that you
don't commit. Import `test` from `./fixtures`, answer each JSON call with
`offlineBff.json()` and each event stream with `offlineBff.on()` returning
`text/event-stream`, and fix the clock with
`page.clock.install({ time: new Date("<ISO_TIME>") })`. Save each shot with
`page.screenshot({ path: testInfo.outputPath("<NAME>.png") })`. The
`chromium-desktop` and `chromium-mobile` projects give both sizes, and
`page.emulateMedia({ colorScheme: "dark" })` switches the theme. Run the spec on
the base commit for Before and on the branch for After, copying the shots out of
`.scratch/studio-browser-playwright/` between runs, because Playwright empties it
at the start of each run. Take the Prototype column from the prototype branch
above. Recent PRs host the images on the `pr-assets` branch and link them as
`raw.githubusercontent.com` URLs.
1 change: 1 addition & 0 deletions apps/CLAUDE.md
Loading