diff --git a/content/AGENTS.md b/content/AGENTS.md index 0849dc93..d9549d24 100644 --- a/content/AGENTS.md +++ b/content/AGENTS.md @@ -6,7 +6,7 @@ This file sits above `content/docs/` on purpose: the docs collection publishes e ## Placing a page -- Each folder directly under `content/docs/` is a tab. `(overview)/` and `(reference)/` are folder groups, so their pages sit at the site root (`/files`, `/cli`). `build/`, `run/` and `catalog/` keep their names in the URL, and so does `(reference)/api-reference/`. The Catalog sidebar has three labeled sections: Data (Items, Types, Edges), Events and access, and Integrations (Connectors). The Catalog tab's Types pages sit in a folder group, `catalog/(types)/`, which the sidebar shows as a closed folder named Types. Its own page is `types.mdx` (`pagesIndex` in its `meta.json`, so the folder opens on `/catalog/types`), and it holds Type rules and Field kinds and two more folder groups, `(core)/` and `(system)/`, shown as closed folders, "Core types" and "System types". No group adds to an address: a type page stays at `/catalog/note`. +- Each folder directly under `content/docs/` is a tab. `(overview)/` and `(reference)/` are folder groups, so their pages sit at the site root (`/files`, `/cli`). `build/`, `run/` and `catalog/` keep their names in the URL, and so does `(reference)/api-reference/`. The Catalog sidebar has three labeled sections: Data (Items, Types, Edges), Events and access, and Integrations (Connectors). The Catalog tab's Types pages sit in a folder group, `catalog/(types)/`, which the sidebar shows as a closed folder named Types. Its own page is `types.mdx` (`pagesIndex` in its `meta.json`, so the folder opens on `/catalog/types`), and it holds Type rules, Display hints and Field kinds and two more folder groups, `(core)/` and `(system)/`, shown as closed folders, "Core types" and "System types". No group adds to an address: a type page stays at `/catalog/note`. - A new page needs its file and an entry in its folder's `meta.json`. `content/docs/meta.json` orders the tabs. Build, Run and the Catalog each open on an `index.mdx` that their `meta.json` lists first; a page that belongs in Build (code against an instance) or Run (an instance and its care, with no code) goes in that folder, under the group a reader would name. - A slug in `(overview)/` or `(reference)/` must not repeat a slug in either group. It must not be a name the Worker answers at the root (`api`, `assets`, `mcp`, `llms`, `llms-full`, `docs-map`, `version`) or a tab folder (`build`, `run`, `catalog`), or `docs`, which `marfa docs` strips as a prefix. - The Catalog tab lists names and shapes and explains nothing. Each top-level type, core or system, has a page (`src/lib/catalog.test.ts` holds them to `src/generated/catalog/builtin-catalog.json`) that takes its title and description from the type and includes the type's generated file. A core type's page ends with one `## Example` item that the test checks; Marfa writes system items itself, so their pages have none. Add a type's page, in the folder of its family and in that folder's `meta.json`, when marfa ships a type, and leave explanation to the Overview, Build and Run. Include `catalog/edge-types.md`, `permissions.md` and `event-types.md` on Catalog pages only; every other page links to the Catalog page. diff --git a/content/docs/(overview)/items-and-types.mdx b/content/docs/(overview)/items-and-types.mdx index 0abf5867..e95a6bcc 100644 --- a/content/docs/(overview)/items-and-types.mdx +++ b/content/docs/(overview)/items-and-types.mdx @@ -59,7 +59,7 @@ A type also tells an app how to show its items, through three **display hints**: - **The text is Markdown.** An app renders it, and a device reads the links in it to make [edges](/edges). - **The icon is an ID** such as `lucide:calendar`, or `simple-icons:github` for a brand. The owner can choose another for any type, including the ones Marfa ships. -Each hint comes from the nearest type that sets it, so a child that sets only an icon keeps its parent's title and text. [Type rules](/catalog/type-rules#display-hints) has the details. +Each hint comes from the nearest type that sets it, so a child that sets only an icon keeps its parent's title and text. [Display hints](/catalog/display-hints) has the details. ## Where an item came from diff --git a/content/docs/(reference)/swift-sdk/items.mdx b/content/docs/(reference)/swift-sdk/items.mdx index c6d80169..a97f24ce 100644 --- a/content/docs/(reference)/swift-sdk/items.mdx +++ b/content/docs/(reference)/swift-sdk/items.mdx @@ -161,7 +161,7 @@ do { ## Names and previews -`items.displays(of:)` gives each item's `ItemDisplay`: its `name` and a `preview` of its body field with the Markdown removed, up to 240 characters. `displays(for:)` takes `ItemDisplayInput` values for an item not yet saved. **Marfa's core works both out from the copy's catalog**, so every app names an item alike. Where the instance would say `Untitled`, `name` is `nil`, and the app shows its own words. +`items.displays(of:)` gives each item's `ItemDisplay`: its `name` and a `preview` of its body field with the Markdown removed, up to 240 characters. `displays(for:)` takes `ItemDisplayInput` values for an item not yet saved. **Marfa's core works both out from the copy's catalog**, by the rule in [Names and previews](/catalog/display-hints#names-and-previews), so every app names an item alike. Where the instance would say `Untitled`, `name` is `nil`, and the app shows its own words. ```swift let notes = try await copy.items.list(ListFilters(types: ["core.note"])) diff --git a/content/docs/(reference)/swift-sdk/server-calls.mdx b/content/docs/(reference)/swift-sdk/server-calls.mdx index cc8fe050..41ee7919 100644 --- a/content/docs/(reference)/swift-sdk/server-calls.mdx +++ b/content/docs/(reference)/swift-sdk/server-calls.mdx @@ -157,7 +157,7 @@ A move throws `server` with the code `type_move_blocked` when a setting names th ## Draw a type's icon -A type's icon is an Iconify ID: `lucide:book-open` from the [Lucide](https://lucide.dev/icons/) release Marfa pins, or `simple-icons:github` for a brand mark from [Simple Icons](https://simpleicons.org). **`ItemType.icon` holds the icon the type resolves to**: the owner's choice for the type or its nearest parent, else the nearest author's. It's `nil` when no type in the chain sets one. [Display hints](/catalog/type-rules#display-hints) explains. +A type's icon is an Iconify ID from one of the two icon sets Marfa pins: `lucide:book-open` from [Lucide](https://lucide.dev/icons/), or `simple-icons:github` for a brand mark from [Simple Icons](https://simpleicons.org). **`ItemType.icon` holds the icon the type resolves to**: the owner's choice for the type or its nearest parent, else the nearest author's. It's `nil` when no type in the chain sets one. [Display hints](/catalog/display-hints) explains. The `MarfaIcons` product draws the ID: diff --git a/content/docs/build/change-a-type.mdx b/content/docs/build/change-a-type.mdx index ba3e2f04..f255ca2a 100644 --- a/content/docs/build/change-a-type.mdx +++ b/content/docs/build/change-a-type.mdx @@ -37,9 +37,9 @@ let normalized = try await api.check(itemType: definition) The answer's `type` is your definition with its defaults made explicit, and `registered_type` is the registered one in the same form. Compare those two, not `resolved_type`, which adds what the registered type inherits. Both registered forms are `null` when the identifier isn't registered. An unknown schema key, anywhere in a definition, is refused `400 invalid_schema`. -**A replacement keeps the icon the owner chose** for the type (see [the owner's icon](/catalog/type-rules#the-owners-icon)), and `registered_type` shows the icon you registered, not the owner's. +**A replacement keeps the icon the owner chose** for the type (see [the owner's icon](/catalog/display-hints#the-owners-icon)), and `registered_type` shows the icon you registered, not the owner's. -**Sending back what a `GET` answered changes only what you changed.** A `GET` answers the display hints a type inherits as well as its own. A hint you send with the value the type inherits isn't stored, so the type keeps following its parent and the owner's icon. A hint the type sets itself stays as it is. A change to `title_field` or `body_field` changes how the server and every device [name and preview](/catalog/type-rules#names-and-previews) the type's items. +**Sending back what a `GET` answered changes only what you changed.** A `GET` answers the display hints a type inherits as well as its own. A hint you send with the value the type inherits isn't stored, so the type keeps following its parent and the owner's icon. A hint the type sets itself stays as it is. A change to `title_field` or `body_field` changes how the server and every device [name and preview](/catalog/display-hints#names-and-previews) the type's items. Then replace the type: diff --git a/content/docs/build/connector-pull-and-push.mdx b/content/docs/build/connector-pull-and-push.mdx index 71eaf65d..23d2cb5c 100644 --- a/content/docs/build/connector-pull-and-push.mdx +++ b/content/docs/build/connector-pull-and-push.mdx @@ -62,7 +62,7 @@ An item's natural key is its `source`, from the connector's key, and its `source `tier: "feed"` puts the item in the feed, where a person or an app decides whether to keep it. `occurred_at` is when the record happened at the vendor. `atomic: false` writes the records that pass and reports a refused one as `errored`; by default, one refused record **rolls the whole batch back**. -**Write an item's text as Markdown.** Marfa stores the field its type names in [`display_hints.body_field`](/catalog/type-rules#display-hints) unchecked, so convert a vendor's HTML first, or an app shows the tags. +**Write an item's text as Markdown.** Marfa stores the field its type names in [`display_hints.body_field`](/catalog/display-hints#the-hints) unchecked, so convert a vendor's HTML first, or an app shows the tags. ## Write only what changed diff --git a/content/docs/build/define-a-type.mdx b/content/docs/build/define-a-type.mdx index 7ea19b56..1a6a4ff3 100644 --- a/content/docs/build/define-a-type.mdx +++ b/content/docs/build/define-a-type.mdx @@ -37,7 +37,7 @@ marfa types scaffold app.example.receipt --parent core.file.image \ --required merchant > receipt.json ``` -A field is `NAME:TYPE`, an enum `NAME:enum:one,two` and an array `NAME:array:TYPE`. The command refuses what registration would refuse, and says why. It can't set a field's `description` or `format`, so add them to the file. +A field is `NAME:TYPE`, an enum `NAME:enum:one,two`, an array `NAME:array:TYPE` and a [status field](/catalog/field-kinds#status-words) `NAME:status:WORD=GROUP,WORD=GROUP`, such as `stage:status:open=not_started,closed=completed`. The command refuses what registration would refuse, and says why. It can't set a field's `description` or `format`, so add them to the file. ### Make it readable as another type @@ -115,7 +115,7 @@ Marfa made the label from the identifier, and `provenance.created_by` names who `marfa types get app.example.receipt` (`GET /types/{id}`) answers the type with the properties it inherits, such as `blob_ref`, `mime_type`, `width` and `height`. An item must hold **every required property along the chain**, so a receipt needs a `blob_ref`, a `mime_type` and a `merchant`. A write that lacks one is refused `400 invalid_properties`, naming it. -A type can also set [`display_hints`](/catalog/type-rules#display-hints): the field that names an item, the field that holds its Markdown text, and an `icon`, an Iconify ID such as `lucide:receipt`. Each one comes from the nearest type that sets it, so a receipt that sets only an icon keeps the title its parent names. The server and a device [name an item](/catalog/type-rules#names-and-previews) from these hints by one rule, which a Swift app reads with [`displays(of:)`](/swift-sdk/items#names-and-previews). +A type can also set [`display_hints`](/catalog/display-hints): the field that names an item, the field that holds its Markdown text, and an `icon`, an Iconify ID such as `lucide:receipt`. Each one comes from the nearest type that sets it, so a receipt that sets only an icon keeps the title its parent names. The server and a device [name an item](/catalog/display-hints#names-and-previews) from these hints by one rule, which a Swift app reads with [`displays(of:)`](/swift-sdk/items#names-and-previews). Once the type is registered, [change or delete it](/build/change-a-type) as your app grows. diff --git a/content/docs/catalog/(types)/display-hints.mdx b/content/docs/catalog/(types)/display-hints.mdx new file mode 100644 index 00000000..7a472849 --- /dev/null +++ b/content/docs/catalog/(types)/display-hints.mdx @@ -0,0 +1,54 @@ +--- +title: Display hints +description: "How a type says what names an item, where its text is and which icon it shows, how Marfa names an item from them, and the icon the owner chooses." +--- + +A type's `display_hints` tell an app how to show its items: the field that names an item, the field that holds its text and an icon. [Type rules](/catalog/type-rules) has the rest of what a type you register must meet, and [Define a type](/build/define-a-type) shows how to set them. + +## The hints + +`display_hints` takes three optional hints: + +| Hint | What it holds | Marfa refuses | +| --- | --- | --- | +| `title_field` | The name of the field that names an item. | A field the type and its ancestors don't declare, or a `thumbnail` field. | +| `body_field` | The name of the field that holds the item's text, **in Markdown**. Apps render it, and a device makes [edges](/catalog/edges) from its links. | The same as `title_field`. | +| `icon` | An [Iconify](https://iconify.design/) icon ID, `:`, from the `lucide` set (`@iconify-json/lucide` 1.2.149) or, for brand marks, the `simple-icons` set (`@iconify-json/simple-icons` 1.2.101), such as `lucide:star` or `simple-icons:github`. | A bare name such as `star`, another set, a name the set's version lacks, an alias such as `lucide:alert-circle`, an icon the set marks hidden, or a wrong-case or padded ID. | + +Marfa refuses a hint with `400 invalid_schema`, naming the member, such as `display_hints.icon`. + +**Marfa doesn't check the text in `body_field`.** It stores the value as sent and never refuses it as malformed Markdown, so a [connector](/build/connector-pull-and-push) converts a vendor's HTML first, or an app shows the tags. + +**The icon sets are fixed because apps carry their own copy**, so an icon draws offline and looks the same in every app. The accepted IDs are in [`icon-set-lucide.json`](https://github.com/withmarfa/marfa/blob/main/packages/types/generated/icon-set-lucide.json) and [`icon-set-simple-icons.json`](https://github.com/withmarfa/marfa/blob/main/packages/types/generated/icon-set-simple-icons.json). + +**Each hint comes from the nearest type that sets it**, so a type that sets only an `icon` keeps the title and body its parent names. `GET /types/{id}` answers each hint after inheritance, and `GET /types` lists only the hints each type sets itself. + +## Names and previews + +The server and a device name an item by one rule, so they show the same name, offline included. + +- **The name** comes from the first of these fields that holds text: `title_field`, `title`, `name`, `body_field`, `body`, `text`, `filename`, `url` and `address`. Marfa takes the first line, collapses each run of whitespace and keeps at most 120 characters, ending in `…` where it cuts. +- **A name taken from `body_field`, `body` or `text` loses its Markdown**, so a note whose body starts `# Plan` is named `Plan`. A name from any other field keeps every character. +- **An item with no name** is `Untitled` to the server. A device answers that it has none, so each app chooses its own word. +- **A preview** is the body field's text without its Markdown, at most 240 characters. Where the name came from the body field, the preview leaves out that line and every line before it. Only a device makes one. + +A Swift app reads both with [`displays(of:)`](/swift-sdk/items#names-and-previews). + +## The owner's icon + +**The owner can choose the icon of any type**, a shipped or connector type included, without replacing it. The choice sits beside the type's definition, so the author's icon stays the default. [Set a type's icon](/api-reference/types/setTypeIcon) and [clear a type's icon](/api-reference/types/clearTypeIcon) are the operations: + +```bash +marfa types set-icon user.recipe lucide:chef-hat +marfa types clear-icon user.recipe +``` + +- **Reads answer it in place of the author's icon**, and a subtype with no icon of its own shows it. +- **Replacing the type keeps it, and deleting the type discards it.** A type registered again under the same identifier starts without one. +- **Devices take it at their next catch-up**, or when a follow next opens its stream. No event announces it. +- **The icon follows the same rules as the author's.** One the sets don't hold is refused `400 invalid_schema`, naming `icon`, and a type nothing registered is refused `404 type_not_found`. A refused call leaves the icon the type had. +- **Who can change it:** the owner, a local process, or a key with `schema.write` and write on the type. A key without `schema.write` is refused `403 forbidden`, and one without write on the type `403 type_not_permitted`. + +[Change a type](/build/change-a-type) explains what a replacement does with the owner's icon when you send back what a `GET` answered. + +The rules behind this page are in the [types](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md) and [device](https://github.com/withmarfa/marfa/blob/main/conformance/spec/device.md) chapters of the contract. diff --git a/content/docs/catalog/(types)/meta.json b/content/docs/catalog/(types)/meta.json index 4add9284..cfe66772 100644 --- a/content/docs/catalog/(types)/meta.json +++ b/content/docs/catalog/(types)/meta.json @@ -6,6 +6,7 @@ "(core)", "(system)", "type-rules", + "display-hints", "field-kinds" ] } diff --git a/content/docs/catalog/(types)/type-rules.mdx b/content/docs/catalog/(types)/type-rules.mdx index a4d31602..07db1792 100644 --- a/content/docs/catalog/(types)/type-rules.mdx +++ b/content/docs/catalog/(types)/type-rules.mdx @@ -1,6 +1,6 @@ --- title: Type rules -description: "The rules a type you register must meet: its identifier, label and plural, parent, compatible types, display hints, roles, merge policy and retention policy." +description: "The rules a type you register must meet: its identifier, label and plural, parent, compatible types, display hints, roles, link field, and merge, version and retention policies." --- **Marfa refuses a definition that breaks a rule with `400 invalid_schema`** unless the rule names another answer. [Define a type](/build/define-a-type) shows how to register one, and [Field kinds](/catalog/field-kinds) has the rules for its fields. @@ -19,10 +19,10 @@ description: "The rules a type you register must meet: its identifier, label and ## Label and plural -A type's `label` names one item, such as `Recipe`, and its `plural` names a list of them, such as `Recipes`. Each is optional text of at most 100 characters. +A type's `label` names one item, such as `Recipe`, and its `plural` a list of them, such as `Recipes`. Each is optional, at most 100 characters. -- **A missing label is made** from the identifier's last segment, split into words at each hyphen and underscore, each word capitalized: `user.reading-list` is `Reading List`. -- **A missing plural is made** from the label when the type is read, by the regular English rule, so `Story` makes `Stories`. Name the plural where the rule is wrong, as in `People`. `GET /types` answers only a declared plural. +- **A missing label is made** from the identifier's last segment, split into capitalized words at each hyphen and underscore: `user.reading-list` is `Reading List`. +- **A missing plural is made** from the label when the type is read, by the regular English rule: `Story` makes `Stories`. Name it where the rule is wrong, as in `People`. `GET /types` answers only a declared plural. ## Fields @@ -37,15 +37,15 @@ A type's `fields` follow [Field kinds](/catalog/field-kinds). A field can't take | Rule | Marfa allows or refuses | | --- | --- | | `parent` | Names a registered type, with no loop and at most ten ancestors. Refused `400 validation_error`. | -| A subtype may | Add fields. Redeclare an inherited field unchanged, or with a new `description` or `label`. Make an inherited field required. Add a range to an inherited `number` or `integer` field, or narrow its range. Add status words. | +| A subtype may | Add fields. Redeclare an inherited field, or give it a new `description` or `label`. Make an inherited field required. Add a range to an inherited `number` or `integer` field, or narrow its range. Add status words. | | A subtype may not | Change an inherited field's `type`, `format`, `items_type`, `items_fields`, `searchable`, `maxLength`, `maxItems` or `enum_values`. Widen or drop its range. Make a required field optional. Refused `400 inheritance_violation`, naming the field. | | Write access | A new `parent` that isn't a platform type needs write on that type; keeping one doesn't. Refused `403 type_not_permitted`. | -**A change to a parent that a subtype would break is refused the same way**, such as a new parent field in another shape than a subtype's. +**A parent change that breaks a subtype is refused the same way.** ### Status words -A subtype inherits its parent's [status field](/catalog/field-kinds#status-words) and its words. To add words, it declares the same field with only the new ones, such as `blocked` in the `in_progress` group, and `GET /types/{id}` answers them after its parent's. A word a type above already declares, the field under another name, and a parent change that leaves a subtype's field with no words are refused `400 inheritance_violation`. +A subtype inherits its parent's [status field](/catalog/field-kinds#status-words) and its words. To add words, it declares the same field with only the new ones, such as `blocked` in the `in_progress` group. A word a type above already declares, the field under another name, and a parent change that leaves a subtype's field with no words are refused `400 inheritance_violation`. ### Compatible with @@ -53,49 +53,14 @@ A subtype inherits its parent's [status field](/catalog/field-kinds#status-words - Yours requires every field the target requires. - Each target field that yours declares or inherits has the same kind. A `string` target field also accepts `url`, `email`, `datetime`, `date`, `thumbnail`, `enum` or `status`, and a `number` accepts `integer`. -- An `array` has the same `items_type`, and the same `items_fields` where either names them. An `enum` takes values from the target's, and a `status` field takes words from the target's, each in the same group. -- A range is no wider than the target's. -- Where both name a `format`, it's the same one. A target's `currency` field needs `currency` on yours, and a `currency` field reads as a plain `string`. +- An `array` has the same `items_type` and `items_fields`. An `enum` takes values from the target's, and a `status` field takes words from the target's, each in the same group. +- A range is no wider than the target's, and a `format` the target names is the same on yours (a `currency` field also reads as a plain `string`). -A replacement or deletion that breaks another type's claim is refused the same way, and `details.dependent_type_ids` names the claimants. A new target that isn't a platform type needs write on it, as a `parent` does. +A replacement or deletion that breaks another type's claim is refused the same way, naming the claimants in `details.dependent_type_ids`. Naming a new target that isn't a platform type needs write on it, as a new `parent` does; keeping one doesn't. Refused `403 type_not_permitted`. ## Display hints -`display_hints` says how an app shows an item of the type. It takes three optional hints: - -| Hint | What it holds | Marfa refuses | -| --- | --- | --- | -| `title_field` | The name of the field that names an item. | A field the type and its ancestors don't declare, or a `thumbnail` field. | -| `body_field` | The name of the field that holds the item's text, **in Markdown**. Apps render it, and a device makes [edges](/catalog/edges) from its links. Marfa doesn't check it. | The same as `title_field`. | -| `icon` | An [Iconify](https://iconify.design/) icon ID, `:`, from the `lucide` set (`@iconify-json/lucide` 1.2.149) or, for brand marks, the `simple-icons` set (`@iconify-json/simple-icons` 1.2.101), such as `lucide:star` or `simple-icons:github`. | A bare name such as `star`, another set, a name the set's version lacks, an alias such as `lucide:alert-circle`, an icon the set marks hidden, or a wrong-case or padded ID. Refused `400 invalid_schema`, naming `display_hints.icon`. | - -**The icon sets are fixed because apps carry their own copy**, so an icon draws offline and looks the same in every app. The accepted IDs are in [`icon-set-lucide.json`](https://github.com/withmarfa/marfa/blob/main/packages/types/generated/icon-set-lucide.json) and [`icon-set-simple-icons.json`](https://github.com/withmarfa/marfa/blob/main/packages/types/generated/icon-set-simple-icons.json). - -**Each hint comes from the nearest type that sets it**, so a type that sets only an `icon` keeps the title and body its parent names. `GET /types` lists only the hints each type declares. - -### Names and previews - -The server and a device name an item by one rule, so they show the same name, offline included. - -- **The name** comes from the first of these fields that holds text: `title_field`, `title`, `name`, `body_field`, `body`, `text`, `filename`, `url` and `address`. Marfa takes the first line, collapses each run of whitespace and keeps at most 120 characters, ending in `…` where it cuts. -- **A name taken from `body_field`, `body` or `text` loses its Markdown**, so a note whose body starts `# Plan` is named `Plan`. A name from any other field keeps every character. -- **An item with no name** is `Untitled` to the server. A device answers that it has none, so each app chooses its own word. -- **A preview** is the body field's text without its Markdown, at most 240 characters, leaving out the line the name came from and every line before it. Only a device makes one. - -### The owner's icon - -**The owner can choose the icon of any type**, a shipped or connector type included, without replacing it: - -```bash -marfa types set-icon user.recipe lucide:chef-hat -marfa types clear-icon user.recipe -``` - -The same two operations are [set a type's icon](/api-reference/types/setTypeIcon) and [clear a type's icon](/api-reference/types/clearTypeIcon). - -- **Reads answer it in place of the author's icon**, and a subtype with no icon of its own shows it. Replacing the type keeps it, and deleting the type discards it. -- **Devices take it at their next catch-up**, or when a follow next opens its stream. No event announces it. -- **Who can change it:** the owner, a local process, or a key with `schema.write` and write on the type. Otherwise it's refused `403 forbidden` or `403 type_not_permitted`. +`display_hints` names the field that names an item, the field that holds its Markdown text and an icon. [Display hints](/catalog/display-hints) has their rules, how Marfa names an item from them and the icon the owner chooses. ## Roles @@ -103,6 +68,14 @@ The same two operations are [set a type's icon](/api-reference/types/setTypeIcon A type also plays its ancestors' roles. [List types](/api-reference/types/listTypes) answers `roles`, every role it plays, and `own_roles`, those it declares. **When you [replace a type](/api-reference/types/replaceType), send its `own_roles` as `roles`.** +## Link field + +`link_field` names a `string` field, declared or inherited, that holds each item's own ID at the vendor that writes the type. A [connector](/build/connector-pull-and-push) finds an item by it. + +- **The value is unique within the type**, in every state. A write that gives it to a second item is refused `409 link_taken`, and `details.existing_id` names the holder. An empty value is no link. +- **Marfa refuses** a field that isn't a declared or inherited `string`, or whose name holds `"` or `\`: `400 invalid_schema`. +- **A subtype inherits the field, not the link**, and may name its own. + ## Merge policy `fields` maps a field the type declares or inherits to a strategy, and `default` covers the rest. Left out, `default` is `last_writer_wins`. @@ -112,6 +85,19 @@ A type also plays its ancestors' roles. [List types](/api-reference/types/listTy | `last_writer_wins` | The item takes the later writer's value. | | `keep_both_copies` | The item keeps its value, and the later writer's value goes to a new item tagged `conflicted-copy`. | +## Version policy + +`version_policy` sets how long Marfa keeps an item's earlier versions. The `version-thinning` [housekeeping job](/run/housekeeping) removes the older ones it doesn't keep, and leaves an item with two versions or fewer alone. + +| Key | Marfa keeps | +| --- | --- | +| `recent_days` | Every version, back this many days. | +| `daily_snapshot_days` | One a day, back this many days. | +| `weekly_snapshot_days` | One a week, back this many days. | +| `max_versions` | At most this many, dropping the oldest first. | + +Each is a whole number of at least 1, and no window ends sooner than the one above it. Marfa refuses a policy that breaks either rule with `400 invalid_schema`, naming the key. **A key left out** comes from the parent type, then the instance's defaults. + ## Retention policy `retention_policy` takes only `feed_days`: @@ -120,8 +106,8 @@ A type also plays its ancestors' roles. [List types](/api-reference/types/listTy | --- | --- | | A whole number from 1 to 36500 | The `feed-expiry` [housekeeping job](/run/housekeeping) moves the type's feed items to the bin once more than that many days have passed since their `created_at`. A natural-key upsert keeps `created_at`, so a refresh doesn't restart the count. | | `null` | The type keeps its feed items, even when a parent sets a window. | -| Left out | The type takes its parent's window, and keeps its feed items where no type in the chain sets one. | +| Left out | The type takes its parent's window, and keeps its feed items where no type above sets one. | -`GET /types/{id}` shows the window after inheritance, and `GET /types` shows only the window the type declares. +`GET /types/{id}` shows the merge, version and retention policies after inheritance, and `GET /types` only those the type declares. -The rules behind this page are in the [types](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md), [device](https://github.com/withmarfa/marfa/blob/main/conformance/spec/device.md), [versions](https://github.com/withmarfa/marfa/blob/main/conformance/spec/versions.md), [edges](https://github.com/withmarfa/marfa/blob/main/conformance/spec/edges.md) and [housekeeping](https://github.com/withmarfa/marfa/blob/main/conformance/spec/housekeeping.md) chapters of the contract. +The rules behind this page are in the [types](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md), [items](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md), [versions](https://github.com/withmarfa/marfa/blob/main/conformance/spec/versions.md), [edges](https://github.com/withmarfa/marfa/blob/main/conformance/spec/edges.md) and [housekeeping](https://github.com/withmarfa/marfa/blob/main/conformance/spec/housekeeping.md) chapters of the contract. diff --git a/content/docs/catalog/(types)/types.mdx b/content/docs/catalog/(types)/types.mdx index a5424280..093cbd32 100644 --- a/content/docs/catalog/(types)/types.mdx +++ b/content/docs/catalog/(types)/types.mdx @@ -1,9 +1,9 @@ --- title: Types -description: The core and system types Marfa ships, and the pages for their rules and field kinds. +description: The core and system types Marfa ships, and the pages for their rules, display hints and field kinds. --- -Marfa ships two families of types: **core types** for what people keep, such as notes and tasks, and **system types** for Marfa's own records, which it writes itself. Each type's page lists its fields and any subtypes. [Type rules](/catalog/type-rules) and [Field kinds](/catalog/field-kinds) list what a type you register must meet and what a field can hold. +Marfa ships two families of types: **core types** for what people keep, such as notes and tasks, and **system types** for Marfa's own records, which it writes itself. Each type's page lists its fields and any subtypes. [Type rules](/catalog/type-rules), [Display hints](/catalog/display-hints) and [Field kinds](/catalog/field-kinds) list what a type you register must meet, how its items are shown and what a field can hold. [Items and types](/items-and-types) explains how types work, and [Define a type](/build/define-a-type) shows how to add your own. diff --git a/content/docs/run/export-and-restore.mdx b/content/docs/run/export-and-restore.mdx index e30ae5fb..6d9d78e2 100644 --- a/content/docs/run/export-and-restore.mdx +++ b/content/docs/run/export-and-restore.mdx @@ -171,7 +171,7 @@ The same archive restored again answers `"imported": 0`, `"duplicates": 6` and ` - **`blobs_missing`** lists the blobs the archive carries no bytes for and no store of the instance holds. Their items are restored, without bytes until you [upload them again](#when-an-archive-lacks-a-files-bytes), and the CLI exits with code 1 and `blobs_missing`. - **`edges_skipped_reasons`** counts why edges were skipped, such as `already_present` or `endpoint_missing`. An edge to an item in the bin is restored. -- **`type_icons_added`, `type_icons_skipped` and `type_icons_skipped_reasons`** count [the icons you chose](/catalog/type-rules#the-owners-icon). A restore never replaces an icon you chose since the export, and counts that skip as `already_set`. An icon the [icon rule](/catalog/type-rules#display-hints) refuses is skipped as `unknown_icon`. A damaged icon line, or one naming a type neither the archive nor the instance registers, refuses the whole archive. +- **`type_icons_added`, `type_icons_skipped` and `type_icons_skipped_reasons`** count [the icons you chose](/catalog/display-hints#the-owners-icon). A restore never replaces an icon you chose since the export, and counts that skip as `already_set`. An icon the [icon rule](/catalog/display-hints#the-hints) refuses is skipped as `unknown_icon`. A damaged icon line, or one naming a type neither the archive nor the instance registers, refuses the whole archive. A restore writes in one transaction: refused, failed or interrupted, it leaves nothing behind, so you can run it again. Other writes wait while it runs, so let it answer before you start your apps. diff --git a/src/lib/catalog.test.ts b/src/lib/catalog.test.ts index e8eb6201..fd786cf7 100644 --- a/src/lib/catalog.test.ts +++ b/src/lib/catalog.test.ts @@ -242,13 +242,13 @@ const SYSTEM_TYPES = TOP_LEVEL.filter((type) => type.id.startsWith('system.')); const CORE_SLUGS = CORE_TYPES.map(slugOf); const SYSTEM_SLUGS = SYSTEM_TYPES.map(slugOf); /** The pages that are not the page of one type. */ -const OTHER_PAGES = ['index', 'items', 'types', 'field-kinds', 'type-rules', 'edges', 'events', 'permissions', 'connectors']; +const OTHER_PAGES = ['index', 'items', 'types', 'field-kinds', 'type-rules', 'display-hints', 'edges', 'events', 'permissions', 'connectors']; /** The connectors Marfa ships, one page each beneath the Connectors page. */ const CONNECTORS_DIR = 'connectors'; const CONNECTOR_SLUGS = ['github', 'google-calendar', 'todoist', 'readwise', 'readwise-reader', 'rss']; /** Where the pages about types sit: in the Types folder, with the two families of types inside it. */ const TYPES_DIR = '(types)'; -const TYPES_PAGES = ['types', 'type-rules', 'field-kinds']; +const TYPES_PAGES = ['types', 'type-rules', 'display-hints', 'field-kinds']; describe('the catalog the pages follow', () => { it('is the synced copy, with the types the pages cover', () => { @@ -588,7 +588,7 @@ const RULE_LINK = /github\.com\/withmarfa\/marfa\/blob\/main\/conformance\/spec\ // The pages that set out rules name the chapters behind them in a footer, in place of a link on each rule. describe('the rule pages', () => { - it.each(['type-rules', 'field-kinds'])('%s ends with a footer naming the contract chapters behind its rules', (name) => { + it.each(['type-rules', 'display-hints', 'field-kinds'])('%s ends with a footer naming the contract chapters behind its rules', (name) => { const text = PAGES.find((candidate) => candidate.slug === name)!.text; const footer = text.trimEnd().split('\n').at(-1)!; expect(footer).toMatch(/^The rules behind this page are in /); @@ -664,7 +664,9 @@ describe('the links', () => { if (!file.includes('/catalog/') && !target.startsWith('/catalog')) continue; const page = target.startsWith('/catalog') ? PAGES.find((candidate) => candidate.slug === (target.split('/').at(-1) === 'catalog' ? 'index' : target.split('/').at(-1)))?.file ?? target - : `content/docs${/^\/(build|run)\//.test(target) ? target : `/(overview)${target}`}.mdx`; + : /^\/(build|run)\//.test(target) + ? `content/docs${target}.mdx` + : ['(overview)', '(reference)'].map((group) => `content/docs/${group}${target}.mdx`).find((file) => existsSync(path.join(ROOT, file))) ?? target; const ids = headingIds(page); checked += 1; if (!ids?.has(id)) broken.push(`${file}: ${target}#${id}`); diff --git a/src/lib/docs-map.test.ts b/src/lib/docs-map.test.ts index 869e5da5..86e57154 100644 --- a/src/lib/docs-map.test.ts +++ b/src/lib/docs-map.test.ts @@ -53,6 +53,7 @@ describe('buildDocsMap', () => { ' - System types', ...['Connection', 'Folder', 'View'].map((title) => ` - ${title}]`), ' - Type rules]', + ' - Display hints]', ' - Field kinds]', '- Edges]', '- Events]', diff --git a/src/lib/page-limits.json b/src/lib/page-limits.json index 70100acb..2db26b94 100644 --- a/src/lib/page-limits.json +++ b/src/lib/page-limits.json @@ -73,6 +73,9 @@ "catalog/(types)/type-rules.mdx": { "kind": "guide" }, + "catalog/(types)/display-hints.mdx": { + "kind": "guide" + }, "(reference)/cli.mdx": { "limit": 3000, "reason": "Lookup page: every command group of the CLI, around the generated command reference."