Skip to content

Commit 9804ff9

Browse files
committed
docs(resources): describe the extended axis where the rules already live
CLAUDE.md, .claude/rules and .cursor/rules all still spelled `grants` as `{ write, run }` and `hrefFor` as self-or-resource. Adds `manage`/`settled` and the `{ to: 'list' }` destination, plus the one thing a reader has to know about `settled`: a denied member and a loading one produce identical capability booleans, so `write === false` is not a decision until `settled` says it is. Also splits the TSDoc that `resourceListHref` landed under — it was describing `resourceHref` and would have documented the wrong function.
1 parent 01f01ab commit 9804ff9

4 files changed

Lines changed: 15 additions & 8 deletions

File tree

.claude/rules/sim-resource-views.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Enforced by `bun run check:resources` (strict CI gate: `bun run check:resources:
2525
| Axis | Type | Replaces |
2626
| --- | --- | --- |
2727
| `source` | `WorkspaceSource<K> \| ShareSource<K>`, discriminated on `via` | `workspaceId`, `token`, `contentSource`, `isPublic`, `isShared` |
28-
| `grants` | `{ write: boolean; run: boolean }` | `canEdit`, `canRun`, `canAdmin`, `canDelete`, `disableEdit/Insert/Delete` |
28+
| `grants` | `{ write; run; manage; settled }` | `canEdit`, `canRun`, `canAdmin`, `canDelete`, `disableEdit/Insert/Delete` |
2929
| `host` | `'page' \| 'panel' \| 'public'` | `embedded`, `isEmbedded`, `compact`, `minimal` |
3030

3131
There is no fourth axis. Agent streaming is **one optional prop on `FileView`** (`streaming?: FileViewStreaming`), because only files stream.
@@ -87,7 +87,8 @@ return <FileView source={source} grants={grants} host='panel' streaming={streami
8787

8888
- Import from the **unit barrel** (`@/components/resources/file-view`), never a file inside it.
8989
- 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.
90-
- Links belong on the source too (`source.hrefFor(link)`), which returns `null` in share scope so nobody hand-builds `/workspace/${token}/…`.
90+
- Links belong on the source too (`source.hrefFor(link)`), which returns `null` in share scope so nobody hand-builds `/workspace/${token}/…`. Three destinations: `{ to: 'self' }`, `{ to: 'resource', kind, id }`, and `{ to: 'list' }` for the index route the kind lives undera breadcrumb root, or where to go after the resource being shown is deleted.
91+
- `grants.settled` says whether `write`/`run`/`manage` are final. A resolving membership and a denied one produce identical booleans, so a surface that renders an affordance disabled while permissions loador fires a one-shot effectmust check it rather than reading `write === false` as a decision.
9192
- `host` decides chrome and URL ownership. `hostOwnsUrl(host)` is the single place the "embedded views do not write nuqs keys" rule lives.
9293

9394
## Never do this

.cursor/rules/sim-resource-views.mdc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Enforced by `bun run check:resources`; strict gate `bun run check:resources:stri
1515
| Axis | Type | Replaces |
1616
| --- | --- | --- |
1717
| `source` | `WorkspaceSource<K> \| ShareSource<K>`, discriminated on `via` | `workspaceId`, `token`, `contentSource`, `isPublic` |
18-
| `grants` | `{ write: boolean; run: boolean }` | `canEdit`, `canRun`, `canAdmin`, `disableEdit/Insert/Delete` |
18+
| `grants` | `{ write; run; manage; settled }` | `canEdit`, `canRun`, `canAdmin`, `disableEdit/Insert/Delete` |
1919
| `host` | `'page' \| 'panel' \| 'public'` | `embedded`, `isEmbedded`, `compact`, `minimal` |
2020

2121
There is no fourth axis. Agent streaming is one optional prop on `FileView` (`streaming?`), because only files stream.
@@ -41,7 +41,7 @@ const source = workspaceSource({ kind: 'file', workspaceId, resourceId: file.id
4141
return <FileView source={source} grants={grantsFromPermissions(permissions)} host='panel' />
4242
```
4343

44-
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.
44+
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)` — `{ to: 'self' | 'resource' | 'list' }`, returning `null` in share scope. `hostOwnsUrl(host)` is the one place the "embedded views do not write nuqs keys" rule lives.
4545

4646
## Never
4747

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ A **resource** is a thing a workspace holds that can also be shared — a file,
422422
Views are mounted against exactly **three axes**, defined in `apps/sim/resources/**` (pure TypeScript — no React, no `'use client'`, because a Server Component builds a share source during SSR):
423423

424424
- `source` — where the data comes from and by what address: `WorkspaceSource<K> | ShareSource<K>`, discriminated on `via`. Replaces `workspaceId`, `token`, `contentSource`, `isPublic`. `ShareSource` declares `workspaceId?: never`, so a share token can no longer be laundered through a workspace-shaped slot.
425-
- `grants` — what this viewer may do: `{ write, run }`. Replaces `canEdit`, `canRun`, `canAdmin`, `disableEdit/Insert/Delete`.
425+
- `grants` — what this viewer may do: `{ write, run, manage }`, plus `settled` (whether those three are final, or still resolving — a denied member and a loading one are otherwise indistinguishable). Replaces `canEdit`, `canRun`, `canAdmin`, `disableEdit/Insert/Delete`.
426426
- `host` — who owns the URL, the router, the document frame: `'page' | 'panel' | 'public'`. Replaces `embedded`. `hostOwnsUrl(host)` is the one place the "embedded views do not write nuqs keys" rule lives.
427427

428428
There is no fourth axis; agent streaming is one optional prop on `FileView`. Consumers CONSTRUCT the axes and MOUNT the view — never wrap it in a passthrough, never reach past its barrel, never reimplement its UI because it lacks a seam (add the seam), never import `@/app/workspace/[workspaceId]/**` from an anonymous surface (`app/f/**`, `app/(interfaces)/**`), and never read `useRouter`/`useParams`/`useQueryState`/`useUserPermissionsContext` inside a unit. A kind with no canonical view yet — `table` alone today — is simply absent from the view list in the check's `CANONICAL_UNITS` — no flag, shim, or placeholder. Every unit has the same layout (`<unit>.tsx` · `index.ts` · `components/<child>/` · `hooks/` · `utils/` · `types.ts`), so moving between them costs nothing.

apps/sim/resources/source.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@ export type ResourceSource<K extends ResourceKind = ResourceKind> = K extends Re
8989
: never
9090

9191
/**
92-
* The in-app route for a resource, used by {@link workspaceSource}'s `hrefFor`.
93-
* The one table — every in-app destination for a resource is spelled here and
94-
* nowhere else, so a surface cannot drift onto a route that no longer exists.
92+
* The in-app index route a kind lives under, used by {@link workspaceSource}'s
93+
* `hrefFor` for `{ to: 'list' }`. Sibling to {@link resourceHref}, and exhaustive
94+
* for the same reason: a kind added without a list route fails to compile here
95+
* rather than sending a breadcrumb somewhere that does not exist.
9596
*/
9697
function resourceListHref(workspaceId: string, kind: ResourceKind): string {
9798
const workspace = `/workspace/${encodeURIComponent(workspaceId)}`
@@ -107,6 +108,11 @@ function resourceListHref(workspaceId: string, kind: ResourceKind): string {
107108
}
108109
}
109110

111+
/**
112+
* The in-app route for a resource, used by {@link workspaceSource}'s `hrefFor`.
113+
* The one table — every in-app destination for a resource is spelled here and
114+
* nowhere else, so a surface cannot drift onto a route that no longer exists.
115+
*/
110116
function resourceHref(workspaceId: string, kind: ResourceKind, id: string): string {
111117
const workspace = `/workspace/${encodeURIComponent(workspaceId)}`
112118
const resource = encodeURIComponent(id)

0 commit comments

Comments
 (0)