From d38588a4dfe0dca68dedba29fe317e46c2cb60d4 Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Sat, 24 Oct 2026 19:58:36 +0100 Subject: [PATCH 1/3] docs(guides): describe views as a query with a sort and group --- content/docs/(overview)/devices.mdx | 2 +- .../docs/(reference)/api-reference/index.mdx | 2 +- content/docs/(reference)/cli.mdx | 6 +-- content/docs/catalog/(system)/view.mdx | 2 +- content/docs/guides/list-filter-search.mdx | 23 ++++----- content/docs/guides/read-a-view.mdx | 48 +++++++++---------- 6 files changed, 42 insertions(+), 41 deletions(-) diff --git a/content/docs/(overview)/devices.mdx b/content/docs/(overview)/devices.mdx index 37d3702e..504d21d9 100644 --- a/content/docs/(overview)/devices.mdx +++ b/content/docs/(overview)/devices.mdx @@ -11,7 +11,7 @@ An app built on the [Swift SDK](/swift-sdk) is a device, and so is the [CLI](/cl A device chooses the **slice** of the instance it keeps: a list of types, a tier, and the edge types it holds. For example, every `core.note` and `core.task` in the library. It fills its copy once by reading every item in the slice, then keeps it current from the instance's [event stream](/events). To hold every type except `system.*`, declare `*` alone: the device reads the whole slice in one pass, with no need to read the catalog and name its types first. `*` beside `system.*` types adds them, and `*` beside any other type is refused `invalid` before anything is read. -Reads come from the copy, with no connection needed. A file's bytes are there once the device has fetched them. The core also [reads saved views](/guides/read-a-view), including layout order, groups, counts and sums. **A view's item or group read needs the whole query held** and is refused with a reason if the copy cannot answer it completely. +Reads come from the copy, with no connection needed. A file's bytes are there once the device has fetched them. The core also [reads saved views](/guides/read-a-view), including sort order, groups and counts. **A view's item or group read needs the whole query held** and is refused with a reason if the copy cannot answer it completely. **While a device fills its copy, it answers direct item reads from what has arrived.** A list or search without a saved view returns the items the copy holds so far and marks the answer `partial`; once the copy is whole, the same read is marked `complete`. A read by ID answers for an item that has arrived, and is refused `hydration_incomplete` for one that hasn't. Until the copy is whole, edge reads are refused `hydration_incomplete`, because an edge's two ends can arrive on different pages. A write to an item that has arrived, or a new item, is queued against the version the copy holds and sent once the copy is whole. This includes a file added on its own or attached to an item that has arrived: the device keeps its bytes and uploads them with the rest of the queue. A write to an item that hasn't arrived, or a file attached to one, is refused `hydration_incomplete`, because there is no version to base it on, and a drain is refused `hydration_incomplete` until the copy is whole. **Edge writes are refused `hydration_incomplete` until the copy is whole**, because a page still to arrive can bring the instance's edge in place of the one a write was made against. An edit that changes an item's body is still queued, and the device reads the body's links and embeds and makes their edges once the server answers the edit. The device's status reports how many items the fill has brought so far and, where the instance counted them, how many the slice held when the fill began, so an app can show how far the fill has come. Compare those two figures, not the count of items the copy holds, which also includes pinned items and creates that are still queued. The Swift SDK returns these answers as `Rows` with a `completeness`. diff --git a/content/docs/(reference)/api-reference/index.mdx b/content/docs/(reference)/api-reference/index.mdx index 6b92d167..174f5d06 100644 --- a/content/docs/(reference)/api-reference/index.mdx +++ b/content/docs/(reference)/api-reference/index.mdx @@ -63,7 +63,7 @@ The reference groups operations by what they act on. The groups and the operatio | Event stream | Changes to items and edges, streamed as they happen | [Stream change events](/api-reference/event-stream/streamEvents) | | Webhooks | Changes sent to a URL you choose, and each delivery's record | [Create a webhook](/api-reference/webhooks/createWebhook) | | Folders | A folder's settings, held as a `system.folder` item | [Create a folder](/api-reference/folders/createFolder) | -| Views | Saved lists of items and how each app shows them, held as `system.view` items | [Create a view](/api-reference/views/createView) | +| Views | Saved lists of items with their sort and grouping, held as `system.view` items | [Create a view](/api-reference/views/createView) | | Connectors | Processes outside the server that read and write on your behalf, with their heartbeats and runs | [Register a connector](/api-reference/connectors/registerConnector) | | Connector state | What the instance keeps for a connector's source: its hold, its state and its agreements | [Take a connector hold](/api-reference/connector-state/takeConnectorHold) | | Inbound webhooks | Addresses where a service sends requests to a connector, and the deliveries Marfa stores for it | [Create an inbound endpoint](/api-reference/inbound-webhooks/createInboundEndpoint) | diff --git a/content/docs/(reference)/cli.mdx b/content/docs/(reference)/cli.mdx index 091e2393..60fc908b 100644 --- a/content/docs/(reference)/cli.mdx +++ b/content/docs/(reference)/cli.mdx @@ -232,14 +232,14 @@ That's how the owner makes the first key in the [Quickstart](/quickstart). These ## Read a view from a working copy -The `marfa device views` commands read saved views from the database named by `--db`. **They need no server connection.** [Read a view offline](/guides/read-a-view) shows how to fill the copy and read a layout. +The `marfa device views` commands read saved views from the database named by `--db`. **They need no server connection.** [Read a view offline](/guides/read-a-view) shows how to fill the copy and read its items and groups. | Command | Answer | | --- | --- | | `marfa device views list` | Views held in the copy. | | `marfa device views get ` | One view's settings and version. | -| `marfa device views groups --layout ` | A layout's groups, counts and sums; `--time-zone` selects the time zone for date buckets. | -| `marfa device items list --view --layout ` | The view's items in layout order; `--limit` and `--offset` select a page. | +| `marfa device views groups ` | The view's groups and counts; `--time-zone` selects the time zone for date buckets. | +| `marfa device items list --view ` | The view's items in its sort order; `--limit` and `--offset` select a page. | | `marfa device search --view ` | Search matches within the view's query. | Each command takes `--json`. A view whose whole query or grouping is not held is refused with a reason. diff --git a/content/docs/catalog/(system)/view.mdx b/content/docs/catalog/(system)/view.mdx index 78e176c7..8b94d633 100644 --- a/content/docs/catalog/(system)/view.mdx +++ b/content/docs/catalog/(system)/view.mdx @@ -1,6 +1,6 @@ --- title: View -description: "A saved list of items, held here and shared by every app: which items, and how each layout shows them. Written only through /views: created, changed at a version, and deleted, which removes it at once with no trash. Lifecycle bounded to active | revoked. Has no tier." +description: "A saved list of items, held here and shared by every app: which items, in what order and how they group. How an app shows them is the app's own. Written only through /views: created, changed at a version, and deleted, which removes it at once with no trash. Lifecycle bounded to active | revoked. Has no tier." identifier: system.view --- diff --git a/content/docs/guides/list-filter-search.mdx b/content/docs/guides/list-filter-search.mdx index e4de6fbd..c5cae2cc 100644 --- a/content/docs/guides/list-filter-search.mdx +++ b/content/docs/guides/list-filter-search.mdx @@ -231,12 +231,13 @@ The whole window comes in one page, in order of `starts_at`. A window spans at m ## Save a list as a view -A [view](/catalog/view) saves a listing so every app shows the same one: its `query` says which items it lists, and its `layouts` say how each one shows them. [Create a view](/api-reference/views/createView) with a `title`, a `query` of `types`, `tier`, `state`, `filter` and `beneath` that says which items it lists, and at least one layout, each with an `id` and a `kind` (`list`, `table`, `board`, `gallery`, `calendar`, `timeline`, `chart` or `map`): +A [view](/catalog/view) saves a listing so every app shows the same one: its `query` says which items it lists, and its `sort` and `group` say in what order and how they group. **How an app shows the items is the app's own**, so a view holds no layout. [Create a view](/api-reference/views/createView) with a `title`, and optionally a `query` of `types`, `tier`, `state`, `filter` and `beneath`, a `sort` of `by` and `direction` keys, and a `group` with a `by` field: ```bash tab="CLI" tab-group="client" marfa views create --title "Open tasks" \ --query '{"types":["core.task"],"filter":"properties.status neq \"completed\""}' \ - --layouts '[{"id":"main","kind":"board","group":{"by":"properties.priority"}}]' + --sort '[{"by":"updated_at","direction":"desc"}]' \ + --group '{"by":"properties.priority"}' ``` ```http tab="HTTP" @@ -247,9 +248,8 @@ Content-Type: application/json { "title": "Open tasks", "query": { "types": ["core.task"], "filter": "properties.status neq \"completed\"" }, - "layouts": [ - { "id": "main", "kind": "board", "group": { "by": "properties.priority" } } - ] + "sort": [{ "by": "updated_at", "direction": "desc" }], + "group": { "by": "properties.priority" } } ``` @@ -258,20 +258,21 @@ const { data } = await marfa.POST("/views", { body: { title: "Open tasks", query: { types: ["core.task"], filter: 'properties.status neq "completed"' }, - layouts: [ - { id: "main", kind: "board", group: { by: "properties.priority" } }, - ], + sort: [{ by: "updated_at", direction: "desc" }], + group: { by: "properties.priority" }, }, }); ``` -The answer is the view as a `system.view` item at `version` 1. The server returns its settings, not the items it lists. **The device core can read its items and layout groups offline**, as [Read a view offline](/guides/read-a-view) shows. A layout only arranges, so it takes no filter of its own. +The answer is the view as a `system.view` item at `version` 1. The server returns its settings, not the items it lists. **The device core can read its items and groups offline**, as [Read a view offline](/guides/read-a-view) shows. -- **To change a view,** [update it](/api-reference/views/updateView) with the `version` you read and the settings you change. Each one you name is replaced whole, `layouts` included, and a change based on an older version still applies when nobody changed the same setting since. +A view's `filter` takes any expression this guide describes, so a view can list one folder's items with `edge[in-folder] eq ""`, or one source's with `source eq ""`. + +- **To change a view,** [update it](/api-reference/views/updateView) with the `version` you read and the settings you change. Each one you name is replaced whole, and a change based on an older version still applies when nobody changed the same setting since. - **To delete a view,** [delete it](/api-reference/views/deleteView). It's gone at once, with no trash. - **To read a view,** [get it](/api-reference/items/getItem) by its `id`, or list the type `system.view`, like any other item. -Writing a view needs write on `system.view`, and reading one needs read; a `content:*` scope reaches neither. Marfa refuses a malformed setting before it writes anything, and names it in `details.errors[0].path`, such as `layouts.0.group.bucket`. The rules are in the [views](https://github.com/withmarfa/marfa/blob/main/conformance/spec/views.md) chapter of the contract. +Writing a view needs write on `system.view`, and reading one needs read; a `content:*` scope reaches neither. Marfa refuses a malformed setting before it writes anything, and names it in `details.errors[0].path`, such as `group.bucket`. The rules are in the [views](https://github.com/withmarfa/marfa/blob/main/conformance/spec/views.md) chapter of the contract. ## When a query is refused diff --git a/content/docs/guides/read-a-view.mdx b/content/docs/guides/read-a-view.mdx index 03825425..891364e6 100644 --- a/content/docs/guides/read-a-view.mdx +++ b/content/docs/guides/read-a-view.mdx @@ -1,11 +1,11 @@ --- title: Read a view offline -description: Read a saved view's items, groups, counts and sums from a device's working copy. +description: Read a saved view's items and groups from a device's working copy. --- -A [view](/catalog/view) saves which items to show and how to arrange them. **The device core answers its layouts offline** from a [working copy](/devices), so an app uses the same sorting and grouping as the CLI. +A [view](/catalog/view) saves which items to list, in what order and how they group. **The device core answers a view offline** from a [working copy](/devices), so an app uses the same sorting and grouping as the CLI. How the app shows the items is its own. -Start with a view [saved on the instance](/guides/list-filter-search#save-a-list-as-a-view) and a credential that can read `system.view` and every content type its query names. The examples use a view of `core.task` items with a layout whose `id` is `main`. +Start with a view [saved on the instance](/guides/list-filter-search#save-a-list-as-a-view) and a credential that can read `system.view` and every content type its query names. The examples use a view of `core.task` items. ## Fill the working copy @@ -15,11 +15,11 @@ While connected, hydrate the view type and the types its query needs: marfa device --db ./copy.sqlite hydrate --types system.view,core.task --tier all ``` -**Hold the whole query**, including its tier. A query over both tiers needs `--tier all`. A group that names `edge[]` or `backref[]` needs that edge type held whole: add `--edge-type ` to the hydration. A query with `beneath` needs `parent-of` held whole too. +**Hold the whole query**, including its tier. A query over both tiers needs `--tier all`. A group that names `edge[]` or `backref[]`, and a `filter` with a `backref[]` condition, need that edge type held whole: add `--edge-type ` to the hydration. A query with `beneath` needs `parent-of` held whole too. Hydration needs the server and a credential, as [Connect to a server](/cli#connect-to-a-server) describes. The reads below need neither a connection nor a new hydration while the copy remains complete. -## Choose the view and layout +## Choose the view List the views held in the copy, then read the selected view's settings: @@ -28,10 +28,10 @@ marfa device --db ./copy.sqlite views list --json marfa device --db ./copy.sqlite views get --json ``` -Use a layout's `id` from `settings.layouts` to read its items in order: +Read the view's items in its sort order: ```bash -marfa device --db ./copy.sqlite items list --view --layout main --limit 50 --offset 0 --json +marfa device --db ./copy.sqlite items list --view --limit 50 --offset 0 --json ``` **Sort keys apply in their declared order.** Property values use the server's ordering, missing and null values come last in either direction, and item IDs break ties. A property that no item holds leaves every item tied on that key. @@ -42,56 +42,56 @@ To search within the view's query, pass the view ID to search. Search orders mat marfa device --db ./copy.sqlite search "meeting" --view --json ``` -## Read groups and totals +## Read groups -A layout with `group` returns one count per group. A chart whose `y` is `{"sum":"properties."}` also sums the numeric values of that property; missing and non-number values add nothing. +A view with a `group` returns one count per group. A view with no `group` returns one group, with the count of every item it lists. ```bash -marfa device --db ./copy.sqlite views groups --layout main --json +marfa device --db ./copy.sqlite views groups --json ``` -Each entry in `groups` has `key`, `count` and `sum`. **Membership counts once per group**: an item with two tags belongs to both tag groups, so adding their counts can exceed the number of items. Missing values belong to the `null` group. Edge groups use the ID at the other end of each edge. +Each entry in `groups` has `key` and `count`. **Membership counts once per group**: an item with two tags belongs to both tag groups, so adding their counts can exceed the number of items. Missing values belong to the `null` group. Edge groups use the ID at the other end of each edge. -The layout's `group.order` puts named groups first, and `group.hidden` leaves named groups out. Other groups follow value order, with `null` last. +The view's `group.order` puts named groups first, and `group.hidden` leaves named groups out. Other groups follow value order, with `null` last. -A date group uses its layout's `bucket`: `day`, `week`, `month` or `year`. Its key is the bucket's start date as `YYYY-MM-DD`, and weeks start on Monday. Buckets follow calendar boundaries in the device's time zone, including daylight saving changes. To select a time zone explicitly: +A date group uses the view's `bucket`: `day`, `week`, `month` or `year`. Its key is the bucket's start date as `YYYY-MM-DD`, and weeks start on Monday. Buckets follow calendar boundaries in the device's time zone, including daylight saving changes. To select a time zone explicitly: ```bash -marfa device --db ./copy.sqlite views groups --layout main --time-zone Europe/London --json +marfa device --db ./copy.sqlite views groups --time-zone Europe/London --json ``` ## Read a view in an app Use the core's view reads on the same handle as other working-copy reads. Rust exposes `list_views`, `view`, `list_in_view`, `search_in_view` and `view_groups`; the core's Node binding exposes the corresponding camel-case methods, and the [Swift SDK](/swift-sdk#saved-views) wraps them in its own types. -For example, with an open Rust `Core` handle, read a page and the layout's totals: +For example, with an open Rust `Core` handle, read a page and the view's groups: ```rust -let page = core.list_in_view(&view_id, Some("main"), Some(50), None)?; -let groups = core.view_groups(&view_id, "main", None)?; +let page = core.list_in_view(&view_id, Some(50), None)?; +let groups = core.view_groups(&view_id, None)?; ``` With a Swift `WorkingCopy`, the same two reads are: ```swift -let page = try await copy.items.list(inView: viewID, layout: "main", limit: 50) -let groups = try await copy.items.groups(inView: viewID, layout: "main") +let page = try await copy.items.list(inView: viewID, limit: 50) +let groups = try await copy.items.groups(inView: viewID) ``` | Core read | Swift SDK | Answer | | --- | --- | --- | | `list_views` | `items.views()` | The views held in the copy. | | `view` | `items.view(_:)` | One view's settings and version. | -| `list_in_view` | `items.list(inView:layout:limit:offset:)` | A page of matching items, ordered by the selected layout. | +| `list_in_view` | `items.list(inView:limit:offset:)` | A page of matching items, in the view's sort order. | | `search_in_view` | `search(_:inView:limit:)` | Search matches within the view's query. | -| `view_groups` | `items.groups(inView:layout:timeZone:)` | The selected layout's group keys, counts and sums. | +| `view_groups` | `items.groups(inView:timeZone:)` | The view's group keys and counts. | -Read the selected layout to choose the presentation, and pass the device's time zone when asking for date groups. The Swift SDK also writes views and follows a layout with a live query: [Saved views](/swift-sdk#saved-views) shows both. +Pass the device's time zone when asking for date groups. The Swift SDK also writes views and follows one with a live query: [Saved views](/swift-sdk#saved-views) shows both. ## Handle a refused view -**A view's items and groups are never answered in part.** If the working copy cannot answer the whole query or grouping, the read is refused with a reason. This includes an unfinished hydration, a query outside the held types or tiers, and an edge group whose edge type is not held whole. Listing the view settings during a fill can return `partial`; that does not make their item queries answerable. +**A view's items and groups are never answered in part.** If the working copy cannot answer the whole query or grouping, the read is refused with a reason. This includes an unfinished hydration, a query outside the held types or tiers, and an edge group or a `backref` condition whose edge type is not held whole. Listing the view settings during a fill can return `partial`; that does not make their item queries answerable. -Show the reason and, when connected, hydrate the slice and edge types the view needs before reading again. A field that no matching item holds is not a refusal: it sorts as missing, groups under `null`, and contributes nothing to a sum. +Show the reason and, when connected, hydrate the slice and edge types the view needs before reading again. A field that no matching item holds is not a refusal: it sorts as missing and groups under `null`. The rules are in the contract's [views read from a copy](https://github.com/withmarfa/marfa/blob/main/conformance/spec/device.md#views-read-from-a-copy) section and [views](https://github.com/withmarfa/marfa/blob/main/conformance/spec/views.md) chapter. From 6c3d14d782c1319b391633c3d806572ccaeeef1e Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Sat, 24 Oct 2026 20:06:12 +0100 Subject: [PATCH 2/3] docs(views): name the group bucket setting --- content/docs/guides/read-a-view.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/guides/read-a-view.mdx b/content/docs/guides/read-a-view.mdx index 891364e6..94c1ef59 100644 --- a/content/docs/guides/read-a-view.mdx +++ b/content/docs/guides/read-a-view.mdx @@ -54,7 +54,7 @@ Each entry in `groups` has `key` and `count`. **Membership counts once per group The view's `group.order` puts named groups first, and `group.hidden` leaves named groups out. Other groups follow value order, with `null` last. -A date group uses the view's `bucket`: `day`, `week`, `month` or `year`. Its key is the bucket's start date as `YYYY-MM-DD`, and weeks start on Monday. Buckets follow calendar boundaries in the device's time zone, including daylight saving changes. To select a time zone explicitly: +A date group uses the view's `group.bucket`: `day`, `week`, `month` or `year`. Its key is the bucket's start date as `YYYY-MM-DD`, and weeks start on Monday. Buckets follow calendar boundaries in the device's time zone, including daylight saving changes. To select a time zone explicitly: ```bash marfa device --db ./copy.sqlite views groups --time-zone Europe/London --json From ab04334b6d87e9ef1b712a7565ccd1f0e605308b Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Sat, 24 Oct 2026 20:13:40 +0100 Subject: [PATCH 3/3] docs(guides): say how to clear a view's grouping --- content/docs/guides/list-filter-search.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/guides/list-filter-search.mdx b/content/docs/guides/list-filter-search.mdx index c5cae2cc..cb6a5d3d 100644 --- a/content/docs/guides/list-filter-search.mdx +++ b/content/docs/guides/list-filter-search.mdx @@ -268,7 +268,7 @@ The answer is the view as a `system.view` item at `version` 1. The server return A view's `filter` takes any expression this guide describes, so a view can list one folder's items with `edge[in-folder] eq ""`, or one source's with `source eq ""`. -- **To change a view,** [update it](/api-reference/views/updateView) with the `version` you read and the settings you change. Each one you name is replaced whole, and a change based on an older version still applies when nobody changed the same setting since. +- **To change a view,** [update it](/api-reference/views/updateView) with the `version` you read and the settings you change. Each one you name is replaced whole, and a change based on an older version still applies when nobody changed the same setting since. To stop a view grouping its items, send `group: null`, or run `marfa views change --version --group null`; the change is to the `group` setting alone, so it follows the same rule. - **To delete a view,** [delete it](/api-reference/views/deleteView). It's gone at once, with no trash. - **To read a view,** [get it](/api-reference/items/getItem) by its `id`, or list the type `system.view`, like any other item.