Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f9d201a
interfaces
emir-karabeg Jul 19, 2026
5a1b100
final cleanup
emir-karabeg Jul 29, 2026
ca6bed4
merge: bring origin/staging into feat/interfaces
waleedlatif1 Aug 1, 2026
a6ac53a
merge: bring the newer origin/staging into feat/interfaces
waleedlatif1 Aug 1, 2026
68a6991
fix(interfaces): add the missing workspace_interface migration + clea…
waleedlatif1 Aug 1, 2026
a8874df
chore: apply reviewed action items; drop the ripgrep-dependent review…
waleedlatif1 Aug 2, 2026
56c33a3
fix(tables): format interface-module cells through the column-type re…
waleedlatif1 Aug 2, 2026
d8b10aa
refactor(tables): clear the four blockers to moving the grid's view l…
waleedlatif1 Aug 2, 2026
634eef7
refactor(tables): inject the cell editor instead of importing it
waleedlatif1 Aug 2, 2026
10d661a
refactor(tables): move the grid's view layer to components/resources/…
waleedlatif1 Aug 2, 2026
4e66399
feat(tables): give the table view source/grants/host and delete the m…
waleedlatif1 Aug 2, 2026
35494f3
docs(resources): record what the table migration actually did
waleedlatif1 Aug 2, 2026
f89a6f5
refactor(tables): give the editing shell the host axis and an explici…
waleedlatif1 Aug 2, 2026
37519d4
refactor(resources): take the last cross-tree edges out of the canoni…
waleedlatif1 Aug 2, 2026
3cecc86
refactor(resources): correct the row-cache premise and clear an R1b f…
waleedlatif1 Aug 2, 2026
e2f389e
fix(interfaces): close a public output leak and three correctness gaps
waleedlatif1 Aug 2, 2026
58c553a
fix(interfaces): bound descriptions in the service and rate-limit OTP…
waleedlatif1 Aug 2, 2026
58206cf
refactor(chat): move the scaling test helper out of the route tree
waleedlatif1 Aug 2, 2026
9fae741
Merge remote-tracking branch 'origin/staging' into feat/interfaces-merge
waleedlatif1 Aug 2, 2026
fda21df
fix(table-view): restore one-line truncation and pin the header
waleedlatif1 Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
50 changes: 50 additions & 0 deletions .claude/rules/canon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Canon — The 7 Canonical Concerns

The non-negotiable conventions every change is measured against. This file is the index — the full rules live in the referenced files. When a canon rule and any other guidance conflict, canon wins. Known gaps are flagged so we close them deliberately instead of re-discovering them.

## 1. Next.js best practices

Server Components by default; `'use client'` only on the smallest leaf. `page.tsx` owns metadata. `next/image` with `priority` on the LCP element; `next/dynamic` for below-fold. Client refs are never called server-side (enforced by `scripts/check-client-boundary-imports.ts`).

- Full rules: `apps/sim/app/(landing)/CLAUDE.md` (landing), `.claude/rules/sim-architecture.md` (server boundary)
- **Gap**: no general app-router conventions rule — when to add `error.tsx` / `loading.tsx` / `not-found.tsx`, `generateMetadata` vs static `metadata`, ISR/`revalidate`, Suspense/streaming. Landing doc covers landing only.

## 2. SEO / GEO (landing only)

One `<h1>` (Hero), strict heading hierarchy, `<section aria-labelledby>`, server-rendered navbar, JSON-LD, answer-first H2s, atomic extractable blocks, entity consistency ("Sim", never "the platform"), sr-only summaries, concrete numbers. Copy follows `.claude/rules/constitution.md`.

- Full rules: `.claude/rules/landing-seo-geo.md`, `apps/sim/app/(landing)/CLAUDE.md`
- **Gap**: docs reference a single `structured-data.tsx` but code is split into `site-structured-data/`, `home-structured-data/`, `json-ld/`. `sitemap.ts` / `robots.ts` / `manifest.ts` conventions (app root) are undocumented.

## 3. Feature file structure

Every feature dir: `feature.tsx` + `page.tsx` (+ `error.tsx`, `loading.tsx`, `search-params.ts` where applicable) + `utils/` (only for 2+ consumers — single-consumer helpers stay in `feature.tsx`) + `hooks/` + `components/`. Every component lives in its own kebab-case folder holding `<name>.tsx` + `index.ts` barrel; children nest under that folder's own `components/`, recursively. Never a bare `<name>.tsx` flat inside a `components/` directory. Reference implementation: `apps/sim/app/workspace/[workspaceId]/scheduled-tasks/`.

- Full rules: `apps/sim/app/(landing)/CLAUDE.md` "Structure", `.claude/rules/sim-architecture.md`
- **Gap**: the recursion + barrel rule is only fully written in the landing CLAUDE.md; `sim-architecture.md` shows a flatter sketch and omits `search-params.ts` / `error.tsx` / `loading.tsx` co-location.

## 4. EMCN components only (platform)

No custom buttons/inputs/menus — always the `@sim/emcn` chip-family equivalent. Components own their chrome; consumers pass props, never chrome via `className` (layout/sizing only).

- Full rules: `.claude/rules/emcn-components.md` (authoring), `.claude/rules/sim-styling.md` (consumer)
- **Gap**: stale paths — EMCN moved to `packages/emcn/` but `emcn-components.md` frontmatter still scopes `apps/sim/components/emcn/**`, and the landing CLAUDE.md still says import from `@/components/emcn`.

## 5. No ad-hoc animations or colors

Colors come from tokens in `apps/sim/app/_styles/globals.css` / `tailwind.config.ts` — never raw hex in components. Animations: prefer CSS, respect `prefers-reduced-motion`, no new keyframes outside the Tailwind config / scoped `.module.css`. Never touch global styles.

- Full rules: `.claude/rules/sim-styling.md` (tokens), `apps/sim/app/(landing)/CLAUDE.md` (motion)
- **Gap**: the positive rule ("declare custom keyframes/tokens HERE and nowhere else") is unwritten — only the prohibition exists.

## 6. State placement (useState / Zustand / React Query / URL)

One four-way decision: React Query = all server state · nuqs URL params = shareable view-state · Zustand = high-frequency, ephemeral, or socket-synced state · useState = purely local UI. Never `useState` + `fetch`; never store-synced-with-effects for view-state.

- Full rules: `.claude/rules/sim-url-state.md` (the canonical 4-way table), `.claude/rules/sim-queries.md`, `.claude/rules/sim-stores.md`

## 7. Meta — authoring skills, rules, and CLAUDE.md files

How we write the docs themselves: CLAUDE.md stays a lean index; detailed conventions go in `.claude/rules/*.md` with `paths:` frontmatter globs so they load only when matching files are touched; repeatable multi-step procedures become skills (`.claude/skills/` or `.claude/commands/`); one-off preferences go in memory, not the repo.

- **Gap**: no authoring guide exists — rule/skill conventions are learned by imitating existing files. Needs a short `.claude/rules/meta-authoring.md`.
134 changes: 134 additions & 0 deletions .claude/rules/sim-resource-views.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
paths:
- "apps/sim/resources/**/*.ts"
- "apps/sim/components/resources/**/*.ts"
- "apps/sim/components/resources/**/*.tsx"
- "apps/sim/app/workspace/[workspaceId]/**/*.tsx"
- "apps/sim/app/f/**/*.tsx"
- "apps/sim/app/i/**/*.tsx"
- "apps/sim/app/(interfaces)/**/*.tsx"
- "apps/sim/hooks/queries/workspace-files.ts"
---

# Resource Views

A **resource** is a thing a workspace holds that can also be shared: a file, a table, an interface, a knowledge base, a log, a scheduled task. A resource with a canonical view has **exactly one**, and every consumer mounts that one — the workspace route page, the mothership panel, an interface module, and the public share page.

**One view per resource. Consumers construct the axes and mount it. They never wrap it.**

Enforced by `bun run check:resources` (strict CI gate: `bun run check:resources:strict`), which is `scripts/check-resource-views.ts`.

## The three axes

`apps/sim/resources/**` is pure TypeScript — no React, no `'use client'` — because `app/i/[token]/page.tsx` builds a share source during SSR.

| Axis | Type | Replaces |
| --- | --- | --- |
| `source` | `WorkspaceSource<K> \| ShareSource<K>`, discriminated on `via` | `workspaceId`, `token`, `contentSource`, `isPublic`, `isShared` |
| `grants` | `{ write: boolean; run: boolean }` | `canEdit`, `canRun`, `canAdmin`, `canDelete`, `disableEdit/Insert/Delete` |
| `host` | `'page' \| 'panel' \| 'public'` | `embedded`, `isEmbedded`, `compact`, `minimal` |

There is no fourth axis. Agent streaming is **one optional prop on `FileView`** (`streaming?: FileViewStreaming`), because only files stream.

`ShareSource` declares `workspaceId?: never` and `resourceId?: never`, and `WorkspaceSource` declares `token?: never` and `seed?: never`. A share source **cannot** carry a workspace id — that is a compile error, not a convention. A kind whose seed is typed `never` (`knowledge`, `log`, `schedule`) structurally cannot construct a share source at all: "no public surface" is a compile-time fact.

```
apps/sim/resources/ # kinds.ts · source.ts · grants.ts · host.ts — pure TS
apps/sim/components/resources/<unit>/ # 'use client' — THE view, one per resource
```

A resource kind with no canonical view yet (`table`, `knowledge`, `log`, `schedule`) is simply **absent** from `CANONICAL_UNITS` in the check. That is the correct state for an unmigrated kind. Do not add a flag, a shim, or a placeholder entry for it.

## Consume: construct the axes, then mount

That is the whole job. Same component, same props; only the constructed values differ.

```typescript
// app/f/[token]/public-file-view.tsx — anonymous share
const source = useMemo(
() => shareSource({ kind: 'file', token, grantId: token, seed: { name, type, size, version } }),
[token, name, type, size, version]
)
return <FileView source={source} grants={grantsForShare('file')} host='public' readOnly />
```

```typescript
// .../mothership-view/.../resource-content.tsx — panel, same view
const source = useMemo(
() => workspaceSource({ kind: 'file', workspaceId, resourceId: file.id }),
[workspaceId, file.id]
)
const grants = useMemo(() => grantsFromPermissions(permissions), [permissions])
return <FileView source={source} grants={grants} host='panel' streaming={streaming} />
```

- Import from the **unit barrel** (`@/components/resources/file-view`), never a file inside it.
- Copy that differs between workspace and share belongs on the **source** (`source.unavailableCopy`), not in the view. A share must never say "workspace" — that is what stops the view becoming an existence oracle.
- Links belong on the source too (`source.hrefFor(link)`), which returns `null` in share scope so nobody hand-builds `/workspace/${token}/…`.
- `host` decides chrome and URL ownership. `hostOwnsUrl(host)` is the single place the "embedded views do not write nuqs keys" rule lives.

## Never do this

**Never wrap a view.** A component whose body is a canonical view with its own props forwarded in is a wrapper. `check:resources` fails on the first one (`wrapperMounts` is at `0`).

```typescript
// ✗ Bad — adds a name, a file, and an import hop; adds no behavior.
export function EmbeddedFilePanel({ source, grants, host }: EmbeddedFilePanelProps) {
return <FileView source={source} grants={grants} host={host} />
}

// ✓ Good — the consumer constructs the axes and mounts the view itself.
const source = workspaceSource({ kind: 'file', workspaceId, resourceId })
return <FileView source={source} grants={grants} host='panel' />
```

**Never add a fourth spelling.** If the view cannot express what you need, change `source` / `grants` / `host` — one place, every consumer — or collapse the need into an existing optional object (`streaming`, `editing`). Do not add a loose prop.

```typescript
// ✗ Bad — three axes, spelled four wrong ways.
<FileView workspaceId={id} canEdit embedded streamingContent={text} isAgentEditing />

// ✓ Good
<FileView source={source} grants={grants} host='panel' streaming={{ content: text, isAgentEditing }} />
```

**Never reimplement.** If a view has no seam for what you need, **add the seam**. A hand-rolled mini-table loses booleans, JSON, dates, links, resource chips, pinned columns and windowing — every one of which the real view already handles.

**Never reach past the barrel.**

```typescript
// ✗ Bad — binds you to the unit's private layout
import { resolveFileCategory } from '@/components/resources/file-view/file-category'

// ✓ Good
import { resolveFileCategory } from '@/components/resources/file-view'
```

The one sanctioned exception is a `lazy()` code-split point, where routing through the barrel silently re-attaches the split chunk (`apps/sim` has no `sideEffects: false`). Those go in `INTERNAL_IMPORT_ALLOWLIST` in the check, keyed by importer **and** specifier.

**Never import the workspace route tree from an anonymous surface.** `app/f/**`, `app/i/**`, `app/(interfaces)/**`, `app/(shared)/**` and public API routes may not import `@/app/workspace/[workspaceId]/**`. Shared units live in `apps/sim/components/resources/**`. Nesting under a `[workspaceId]` segment is exactly why `workspaceId: string` once read as natural on a component anonymous visitors mounted with a **share token**.

**Never read route or permission context inside a unit.** No `useRouter`, `useParams`, `useSearchParams`, `usePathname`, `useQueryState(s)`, or `useUserPermissionsContext` under `apps/sim/components/resources/**`. Addressing is `source`, navigation targets are `source.hrefFor(link)`, capability is `grants`, URL ownership is `host`. A component that falls back to `useParams()` can only ever exist once per page.

**Never put `'use client'` in `apps/sim/resources/**`.** Next rewrites every export of a `'use client'` module into a client reference in the server bundle, so the Server Component that builds a share source would throw at runtime.

## Escape hatch

Four annotations, reason mandatory, on the line directly above the offending mount / import / attribute (up to three preceding comment lines of extra context are tolerated):

```typescript
// boundary-resource-wrapper: <reason>
// boundary-resource-internal: <reason>
// boundary-resource-tree: <reason>
// boundary-resource-prop: <reason>
```

An annotation with an empty reason is still a finding **and** trips `annotationsMissingReason`. Whole-file exceptions go through `INTERNAL_IMPORT_ALLOWLIST` / `CROSS_TREE_ALLOWLIST` in `scripts/check-resource-views.ts`, not per-line annotations.

## Checklist before you add a component near a resource

1. Does a canonical view already exist for this kind? Mount it.
2. Does it exist but lack a seam? Add the seam in the unit and thread it — do not fork the UI.
3. Is your new component only forwarding props into a view? Delete it; mount the view at the call site.
4. Are you about to write `embedded`, `canEdit`, `canRun`, `isPublic`, `token` or `workspaceId` on a view? Map it to `source` / `grants` / `host`.
5. Run `bun run check:resources`. The success metric is **consumers per view going up and component count going down**.
99 changes: 99 additions & 0 deletions .cursor/rules/sim-resource-views.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
description: Resource views — one view per resource, many consumers, no wrappers
globs: ["apps/sim/resources/**/*.ts", "apps/sim/components/resources/**/*.ts", "apps/sim/components/resources/**/*.tsx", "apps/sim/app/workspace/[workspaceId]/**/*.tsx", "apps/sim/app/f/**/*.tsx", "apps/sim/app/i/**/*.tsx", "apps/sim/app/(interfaces)/**/*.tsx"]
---
# Resource Views

A **resource** is a thing a workspace holds that can also be shared: a file, a table, an interface, a knowledge base, a log, a scheduled task. A resource with a canonical view has **exactly one**, and every consumer mounts that one — the workspace page, the mothership panel, an interface module, the public share page.

**One view per resource. Consumers construct the axes and mount it. They never wrap it.**

Enforced by `bun run check:resources`; strict gate `bun run check:resources:strict`.

## Three axes, nothing else

| Axis | Type | Replaces |
| --- | --- | --- |
| `source` | `WorkspaceSource<K> \| ShareSource<K>`, discriminated on `via` | `workspaceId`, `token`, `contentSource`, `isPublic` |
| `grants` | `{ write: boolean; run: boolean }` | `canEdit`, `canRun`, `canAdmin`, `disableEdit/Insert/Delete` |
| `host` | `'page' \| 'panel' \| 'public'` | `embedded`, `isEmbedded`, `compact`, `minimal` |

There is no fourth axis. Agent streaming is one optional prop on `FileView` (`streaming?`), because only files stream.

`ShareSource` declares `workspaceId?: never`; `WorkspaceSource` declares `token?: never`. A share source cannot carry a workspace id — compile error, not convention.

```
apps/sim/resources/ # kinds/source/grants/host — pure TS, NO 'use client'
apps/sim/components/resources/<unit>/ # 'use client' — THE view, one per resource
```

A kind with no canonical view yet (`table`, `knowledge`, `log`, `schedule`) is simply absent from the check's `CANONICAL_UNITS`. Do not add a flag, shim, or placeholder for it.

## Consume: construct, then mount

```typescript
// Anonymous share
const source = shareSource({ kind: 'file', token, grantId: token, seed })
return <FileView source={source} grants={grantsForShare('file')} host='public' readOnly />

// Panel — same view, same props
const source = workspaceSource({ kind: 'file', workspaceId, resourceId: file.id })
return <FileView source={source} grants={grantsFromPermissions(permissions)} host='panel' />
```

Import from the unit barrel (`@/components/resources/file-view`), never a file inside it. Scope-dependent copy lives on `source.unavailableCopy`; links on `source.hrefFor(link)` (which returns `null` in share scope). `hostOwnsUrl(host)` is the one place the "embedded views do not write nuqs keys" rule lives.

## Never

```typescript
// ✗ Wrapper — adds a name, a file, an import hop; adds no behavior. Fails check:resources.
export function EmbeddedFilePanel({ source, grants, host }: EmbeddedFilePanelProps) {
return <FileView source={source} grants={grants} host={host} />
}

// ✓ The consumer constructs the axes and mounts the view itself
const source = workspaceSource({ kind: 'file', workspaceId, resourceId })
return <FileView source={source} grants={grants} host='panel' />
```

```typescript
// ✗ A fourth spelling for capability / chrome / address
<FileView workspaceId={id} canEdit embedded streamingContent={text} isAgentEditing />

// ✓
<FileView source={source} grants={grants} host='panel' streaming={{ content: text, isAgentEditing }} />
```

```typescript
// ✗ Reaching past the barrel
import { resolveFileCategory } from '@/components/resources/file-view/file-category'

// ✓
import { resolveFileCategory } from '@/components/resources/file-view'
```

- **Never reimplement** a resource's UI because the view has no seam — add the seam. A hand-rolled mini-table loses booleans, JSON, dates, links, chips, pinned columns and windowing.
- **Never import `@/app/workspace/[workspaceId]/**`** from `app/f/**`, `app/i/**`, `app/(interfaces)/**`, `app/(shared)/**`, or a public API route. Shared units live in `apps/sim/components/resources/**`.
- **Never** use `useRouter` / `useParams` / `useSearchParams` / `usePathname` / `useQueryState(s)` / `useUserPermissionsContext` inside `apps/sim/components/resources/**`. Addressing is `source`, navigation is `source.hrefFor`, capability is `grants`, URL ownership is `host`.
- **Never** put `'use client'` in `apps/sim/resources/**` — a Server Component builds a share source from it during SSR.

## Escape hatch

Reason mandatory, on the line directly above the offending mount / import / attribute:

```typescript
// boundary-resource-wrapper: <reason>
// boundary-resource-internal: <reason>
// boundary-resource-tree: <reason>
// boundary-resource-prop: <reason>
```

An empty reason is still a finding. Whole-file exceptions go in `INTERNAL_IMPORT_ALLOWLIST` / `CROSS_TREE_ALLOWLIST` in `scripts/check-resource-views.ts`. The only sanctioned deep import is a `lazy()` split point, where the barrel would re-attach the split chunk.

## Before adding a component near a resource

1. Does a canonical view exist for this kind? Mount it.
2. Exists but lacks a seam? Add the seam in the unit — do not fork.
3. Only forwarding props into a view? Delete it; mount the view at the call site.
4. About to write `embedded` / `canEdit` / `canRun` / `isPublic` / `token` / `workspaceId` on a view? Map it to `source` / `grants` / `host`.
5. Run `bun run check:resources`. Success is consumers-per-view up, component count down.
8 changes: 3 additions & 5 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ jobs:
- name: API contract boundary audit
run: bun run check:api-validation:strict

- name: Resource view boundary audit
run: bun run check:resources

- name: Desktop bridge contract audit
run: bun run check:desktop-bridge

Expand Down Expand Up @@ -178,11 +181,6 @@ jobs:
- name: Type-check realtime server
run: bunx turbo run type-check --filter=@sim/realtime

# cloud-review-tools.test.ts runs the real helper on the runner, which shells
# out to rg. Blacksmith's image ships it, GitHub's doesn't.
- name: Install ripgrep
run: command -v rg || (sudo apt-get update && sudo apt-get install -y ripgrep)

# Named for what it does: `bun run test` is `vitest run`, with no
# `--coverage`. See the Codecov note below.
- name: Run tests
Expand Down
Loading
Loading