diff --git a/content/docs/catalog/edges.mdx b/content/docs/catalog/edges.mdx
index 154935f7..7e8f83f1 100644
--- a/content/docs/catalog/edges.mdx
+++ b/content/docs/catalog/edges.mdx
@@ -3,11 +3,11 @@ title: Edges
description: The edge types every instance ships, and what each one carries.
---
-Any type can be the source of every edge type.
+An edge is a typed, directed link from one item, its source, to another, its target, as [Properties, metadata and edges](/properties-metadata-and-edges#edges) explains. Any type can be the source of every edge type.
src/generated/catalog/edge-types.md
-`reverse_name` is the name an edge goes by read from its target. In a folder, the source item's file writes an edge. Where an edge's section gives `written_at` as `target`, the target item's file writes it instead, under its reverse name. Where it gives `none`, no file writes the edge: `in-folder` places an item in a folder, and a folder never renders it. An edge type that gives no `written_at` is written at its source.
+The reverse name is the edge's name read from its target. In a [synced folder](/guides/sync-a-folder), the source item's file writes the edge, unless its section below gives `written_at`: `target` puts it in the target item's file under its reverse name, and `none` in no file.
## Fields every edge has
diff --git a/content/docs/catalog/field-kinds.mdx b/content/docs/catalog/field-kinds.mdx
index a15bab27..f5464fed 100644
--- a/content/docs/catalog/field-kinds.mdx
+++ b/content/docs/catalog/field-kinds.mdx
@@ -3,51 +3,36 @@ title: Field kinds
description: The kinds of value a type's field can hold, and what Marfa accepts for each.
---
-Each kind and rule links to the contract rule behind it.
+Each field of a type holds one kind of value, named by its `type`. **A value Marfa doesn't accept for its kind is refused `400 invalid_properties`**, and `details.errors` names the field. A device applies these rules to whole numbers and URLs offline, and queues nothing it refuses.
## Kinds
| Kind | Holds |
| --- | --- |
-| [`string`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemsproperty-length-cap) | Text of up to `maxLength` UTF-16 code units, or 100,000 where the field sets none. |
-| [`number`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemsproperty-number-exact) | A number. A whole number must be one Marfa serves back with the same digits (see Values). |
-| [`integer`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemsproperty-number-exact) | A whole number, with the same rule on its digits. |
-| [`boolean`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemsproperty-invalid) | `true` or `false`. |
-| [`url`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesurl-value) | An absolute URL that the WHATWG URL Standard accepts (see Values). |
-| [`email`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemsproperty-invalid) | An email address. |
-| [`datetime`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesdatetime-value) | A date as `YYYY-MM-DD`, or a date and time with a zone, such as `2026-03-01T09:30:00Z`. |
-| [`date`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesdatetime-refused) | A calendar date as `YYYY-MM-DD`, with no time. |
-| [`enum`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesfield-grammar) | One of the field's `enum_values`. |
-| [`array`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesarray-items) | A list. `items_type` names the kind of its elements, and `maxItems` caps their number. |
-| [`object`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemsproperty-invalid) | A JSON object. |
-| [`thumbnail`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemsthumbnail-format) | A PNG, JPEG or WebP data URI that decodes to at most 16 KiB. |
-
-A value of the wrong shape is refused `400 invalid_properties`, and `details.errors` names the field.
+| `string` | Text of up to `maxLength` UTF-16 code units, or 100,000 where the field sets none. It can't hold U+0000. |
+| `number` | A number. A whole number must be written in the shortest digits of its 64-bit float, so Marfa serves it back unchanged: `1500000000000000001` and `-0` are refused. Send a whole number that must keep every digit as a `string`. |
+| `integer` | A whole number, written the same way. |
+| `boolean` | `true` or `false`. |
+| `url` | An absolute URL that the WHATWG URL Standard accepts once whitespace is trimmed from both ends. Marfa stores it without that whitespace and without tab, line feed and carriage return characters. |
+| `email` | An email address. |
+| `datetime` | A date and time with a zone, such as `2026-03-01T09:30:00Z`, or a date alone, as `YYYY-MM-DD`. A time with no zone is refused. |
+| `date` | A date alone, as `YYYY-MM-DD`. A date with a time is refused. |
+| `enum` | One of the field's `enum_values`. |
+| `array` | A list. `items_type` names the kind of its elements, and `maxItems` caps their number. |
+| `object` | A JSON object. |
+| `thumbnail` | A PNG, JPEG or WebP data URI that decodes to at most 16 KiB. |
+
+The rule on whole numbers holds at any depth, and also in an edge's `properties` and an extension's data. A bulk write that breaks it is refused whole, and `details.index` gives the entry's position.
## Formats
-| Format | What Marfa does |
-| --- | --- |
-| [`url`, `email`, `datetime`, `date`, `thumbnail`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesformat-collapses) | On a `string` field, stores the field as that kind. On an array of strings, stores an array of that kind. |
-| [Its own kind](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesformat-own-type) | On a field of the kind the format names, such as a `url` field, stores the field without the `format`. |
-| [`bcp47`, `iso3166`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesformat-string-only) | Keeps the format as a label on a `string` field: `bcp47` for a language tag, as on `language`, and `iso3166` for a country code, as on `country`. Marfa doesn't check the value. |
-| [Kind-naming format elsewhere](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesformat-refused) | Refuses `url`, `email`, `datetime`, `date` or `thumbnail` on any other kind, and `thumbnail` on an array, with `400` `invalid_schema`. |
-| [`bcp47`, `iso3166` elsewhere](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesformat-string-only-refused) | Refuses them on any kind but `string` with `400` `invalid_schema`. |
-
-## Values
+A field's `format` refines its kind:
-| Rule | Marfa allows or refuses |
+| Format on the field | What Marfa does |
| --- | --- |
-| [Null character](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemsproperty-nul) | A string that holds U+0000 is refused `400` `invalid_properties`. |
-| [Exact whole numbers](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemsproperty-number-exact) | A whole number must be written in the shortest digits of its 64-bit float. `1500000000000000001`, 2^60 (`1152921504606846976`) and `-0` are refused `400` `invalid_properties`. Send a value that must keep every digit as a `string`. |
-| [Where it applies](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemsproperty-number-exact-bulk) | At any depth, including an edge's `properties` and an extension's data. A bulk write is refused whole, and `details.index` gives the entry's position. |
-| [Numbers Marfa takes](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemsproperty-number-kept) | A whole number in its shortest digits, such as `1152921504606847000`, and a number with a fraction or an exponent. |
-| [URL trimming](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesurl-value) | Marfa trims whitespace from both ends, as JavaScript's `String.prototype.trim` does, then parses the value as an absolute URL. |
-| [URL storage](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesurl-stored) | Marfa stores the URL without the trimmed whitespace and without tab, line feed and carriage return characters. |
-| [ASCII host](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesurl-punycode) | On a special scheme such as `http`, a host that is ASCII after percent-decoding is taken whatever its labels hold, so `http://xn--` is a URL. |
-| [Non-ASCII host](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesurl-idna) | Must be valid IDNA. `http://bücher.xn--` is refused `400` `invalid_properties`. |
-| [Forbidden host](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesurl-host-refused) | A forbidden domain code point, or a last label that is a number where the host isn't a valid IPv4 address, is refused `400` `invalid_properties`. |
-| [URL on a device](https://github.com/withmarfa/marfa/blob/main/conformance/spec/device.md#deviceurl-idna-unassigned) | A device applies the same URL rules offline. It also refuses a special-scheme URL whose host holds a code point its IDNA tables leave unassigned. |
-| [Numbers on a device](https://github.com/withmarfa/marfa/blob/main/conformance/spec/device.md#devicenumber-exact) | A device refuses the same whole numbers offline and queues nothing. |
-
-The rules behind this page are in the [items](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md), [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.
+| `url`, `email`, `datetime`, `date` or `thumbnail` on a `string` field | Stores the field as that kind. An array of strings takes any of the first four, and becomes an array of that kind. |
+| A format that names the field's own kind, such as `url` on a `url` field | Stores the field without the `format`. |
+| `bcp47` or `iso3166` on a `string` field | Keeps the format as a label, for a language tag (as on `language`) or a country code (as on `country`). Marfa doesn't check the value. |
+| `url`, `email`, `datetime`, `date` or `thumbnail` where the rows above don't allow it, and `bcp47` or `iso3166` on any kind but `string` | Refuses the type with `400 invalid_schema`. |
+
+The rules behind this page are in the [items](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md), [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, including how Marfa reads a URL's host.
diff --git a/content/docs/catalog/your-own-types.mdx b/content/docs/catalog/your-own-types.mdx
index 4c5d384f..3f39f305 100644
--- a/content/docs/catalog/your-own-types.mdx
+++ b/content/docs/catalog/your-own-types.mdx
@@ -3,19 +3,15 @@ title: Type rules
description: "The rules a type you register must meet: its identifier, fields, parent, roles, merge policy and retention policy."
---
-[Define a type](/guides/define-a-type) shows how to register one. Each rule links to the contract rule behind it.
+A type you register must meet the rules on this page, for its identifier, its fields, its parent, its roles and its policies. **Marfa refuses a definition that breaks a rule with `400 invalid_schema`** unless the rule names another answer. [Define a type](/guides/define-a-type) shows how to register one.
## Identifier
-| Rule | Marfa allows or refuses |
-| --- | --- |
-| [Grammar](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesid-grammar) | Two or more dot-separated segments, each starting with a lowercase letter and holding lowercase letters, digits, hyphens and underscores. At most 128 characters. |
-| [Malformed](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesid-malformed) | An identifier the grammar refuses is refused `400` `validation_error`. |
-| [`app` root](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesid-grammar) | `app..`, with exactly two segments after `app`. |
-| [`user` root](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesid-grammar) | `user.`, for types you keep for yourself. |
-| [Publisher root](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesid-root-not-prefix) | `.`, such as `acme.deal`. The publisher name is the whole first segment, so `core-pub` is allowed. |
-| [`core`, `system`, `marfa`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesid-reserved-namespace) | Refused `403` `forbidden`, whatever the credential. |
-| [Other reserved root](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesid-reserved-scope-root) | Any other root in the list below, except `app` and `user`, is refused `400` `validation_error`. |
+[Items and types](/items-and-types) explains the namespaces a type can sit in.
+
+- **Grammar:** two or more dot-separated segments, each starting with a lowercase letter and holding only lowercase letters, digits, hyphens and underscores. At most 128 characters, and exactly two segments after `app`, as in `app.readinglist.entry`. Anything else is refused `400 validation_error`.
+- **`core`, `system` and `marfa`:** refused `403 forbidden`, whatever the credential.
+- **Other reserved roots:** every root in the list below except `app`, `user`, `core`, `system` and `marfa` is refused `400 validation_error`. A root is the whole first segment, so `core-pub` is allowed.
### Reserved type roots
@@ -25,18 +21,16 @@ description: "The rules a type you register must meet: its identifier, fields, p
| Rule | Marfa allows or refuses |
| --- | --- |
-| [Keys](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesunknown-key) | A field definition takes `type`, `description`, `required`, `enum_values`, `items_type`, `format`, `searchable`, `maxLength` and `maxItems`. Any other key, such as `minimum`, is refused `400` `invalid_schema`. |
-| [Kind](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesfield-grammar) | `type` is one of the [field kinds](/catalog/field-kinds). An unknown `type` or `format` is refused `400` `invalid_schema`. |
-| [`enum`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesfield-grammar) | Needs `enum_values`, a list of at least one string. |
-| [`array`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesfield-grammar) | Needs `items_type`. |
-| [`maxLength`, `maxItems`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesfield-grammar) | A whole number of at least 1. `maxLength` applies to `string` and `enum`, `maxItems` to `array`. |
-| [Array elements](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesarray-items-refused) | Each element must be a value of the field's `items_type`. A write that leaves a wrong element is refused `400` `invalid_properties`, and `details.errors` names the field and position, as `names.1`. |
-| [Unnamed `items_type`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesarray-items-unnamed) | An `items_type` that isn't a field kind leaves the elements unchecked. |
-| [Name](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesfield-shadows-item) | A name in the reserved field names below is refused `400` `property_shadows_field`. |
-| [Thumbnail count](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesthumbnail-one) | At most one `thumbnail` field, counting inherited ones. A second is refused `400` `invalid_schema`. |
-| [Thumbnail name](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesthumbnail-name) | Not `title`, `body`, `description` or `name`. An array's `items_type` can't be `thumbnail`. Refused `400` `invalid_schema`. |
-| [`title_field`, `body_field`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesfield-grammar) | Must name a field the type declares or inherits. Refused `400` `invalid_schema`. |
-| [Thumbnail as hint](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesthumbnail-hint) | Neither `title_field` nor `body_field` can name a `thumbnail` field. Refused `400` `invalid_schema`. |
+| Keys | A field definition takes `type`, `description`, `required`, `enum_values`, `items_type`, `format`, `searchable`, `maxLength` and `maxItems`, and nothing else, such as `minimum`. |
+| `type` | One of the [field kinds](/catalog/field-kinds). |
+| `enum` | Needs `enum_values`, a list of at least one string. |
+| `array` | Needs `items_type`. An `items_type` that isn't a field kind leaves the elements unchecked. |
+| `maxLength`, `maxItems` | A whole number of at least 1. `maxLength` applies to `string` and `enum`, `maxItems` to `array`. |
+| Name | Not one of the reserved field names below. Refused `400 property_shadows_field`. |
+| `thumbnail` | At most one, counting inherited ones. Not named `title`, `body`, `description` or `name`, and not an array's `items_type`. |
+| `title_field`, `body_field` | Name a field the type declares or inherits, and not a `thumbnail` field. |
+
+**A write that leaves a wrong element in an `array` field** is refused `400 invalid_properties`, and `details.errors` names the field and position, as `names.1`.
### Reserved field names
@@ -46,55 +40,45 @@ description: "The rules a type you register must meet: its identifier, fields, p
| Rule | Marfa allows or refuses |
| --- | --- |
-| [`parent`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesparent-unknown) | Names a registered type. Anything else is refused `400` `validation_error`. |
-| [Depth](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesparent-chain) | No loop, and at most ten ancestors. |
-| [Subtype may](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typeschild-adds) | Add fields, redeclare an inherited field unchanged or with a new `description`, and make an inherited field required. |
-| [Subtype may not](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typeschild-shape) | Change an inherited field's `type`, `format`, `items_type`, `searchable`, `maxLength`, `maxItems` or `enum_values`, or make a required field optional. Refused `400` `inheritance_violation`. |
-| [Write access](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesparent-reach) | A new `parent` that Marfa doesn't ship needs write on that type; keeping one doesn't. Refused `403` `type_not_permitted`. |
+| `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`, and make an inherited field required. |
+| A subtype may not | Change an inherited field's `type`, `format`, `items_type`, `searchable`, `maxLength`, `maxItems` or `enum_values`, or make a required field optional. Refused `400 inheritance_violation`. |
+| Write access | A new `parent` that Marfa doesn't ship needs write on that type; keeping one doesn't. Refused `403 type_not_permitted`. |
### Compatible with
-`compatible_with` names a type that yours is readable as, without inheriting from it.
+`compatible_with` names a type that yours is readable as, without inheriting from it. A claim that doesn't hold, or that names an unregistered type, is refused `422 compatible_with_violation`. The claim holds when:
-| Rule | Marfa allows or refuses |
-| --- | --- |
-| [Required fields](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with) | Yours requires every field the target requires. |
-| [Field kinds](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with) | Each target field that yours declares or inherits has the same kind. A `string` target field also accepts `url`, `email`, `datetime`, `date`, `thumbnail` or `enum`, and a `number` accepts `integer`. |
-| [Arrays and enums](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with) | An `array` has the same `items_type`. An `enum` takes values from the target's. |
-| [Format](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with) | Where both name a `format`, it is the same one. |
-| [Refusal](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with-refused) | A claim that doesn't hold, or names an unregistered type, is refused `422` `compatible_with_violation`. |
-| [Later changes](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with-preserved) | A replacement or deletion that breaks another type's claim is refused the same way, and `details.dependent_type_ids` names the claimants. |
-| [Write access](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with-reach) | A new target that Marfa doesn't ship needs write on it; keeping one doesn't. Refused `403` `type_not_permitted`. |
+- 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` or `enum`, and a `number` accepts `integer`.
+- An `array` has the same `items_type`, and an `enum` takes values from the target's.
+- Where both name a `format`, it's the same one.
+
+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 Marfa doesn't ship needs write on it, as a `parent` does.
## Roles
-| Rule | Marfa allows or refuses |
-| --- | --- |
-| [`roles`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesfield-grammar) | Takes `container` alone. Any other role is refused `400` `invalid_schema`. |
-| [`container`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/edges.md#edgescollection-target) | The type can be the target of an [`in-collection`](/catalog/edges#in-collection) edge. |
+`roles` takes `container` alone. A `container` type can be the target of an [`in-collection`](/catalog/edges#in-collection) edge.
## Merge policy
-| Rule | Marfa allows or refuses |
-| --- | --- |
-| [Shape](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesmerge-policy) | `fields` maps a field the type declares or inherits to a strategy. `default` covers the rest, and is `last_writer_wins` when left out. |
-| [Refusal](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesmerge-policy-refused) | A field the type doesn't declare, or another strategy, is refused `400` `invalid_schema`. |
+`fields` maps a field the type declares or inherits to a strategy, and `default` covers the rest. Left out, `default` is `last_writer_wins`.
-| Strategy | When a later write resolves a collision on the field |
+| Strategy | When a later write collides with another on the field |
| --- | --- |
-| [`last_writer_wins`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/versions.md#versionsauto-last-writer) | The item takes the later writer's value. |
-| [`keep_both_copies`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/versions.md#versionscopy-written) | The item keeps its value, and the later writer's value goes to a new item tagged `conflicted-copy`. |
+| `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`. |
## Retention policy
-| Rule | Marfa allows or refuses |
+`retention_policy` takes only `feed_days`:
+
+| `feed_days` | Effect |
| --- | --- |
-| [Key](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesunknown-key) | Only `feed_days`. |
-| [`feed_days` of 1 to 36500](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesretention-policy-values) | A whole number of days. The `feed-expiry` [housekeeping job](/guides/housekeeping) moves the type's feed items to the bin once more than that many days have passed since they arrived. |
-| [Counting](https://github.com/withmarfa/marfa/blob/main/conformance/spec/housekeeping.md#housekeepingfeed-expiry-arrival) | From the item's `created_at`. A natural-key upsert keeps it, so a refresh doesn't restart the count. |
-| [`feed_days` of `null`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesretention-policy-merge) | The type keeps its feed items, even when a parent sets a window. |
-| [`feed_days` left out](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesretention-policy-merge) | The type takes its parent's window. Where no type in the chain sets one, it keeps its feed items. |
-| [Other value](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesretention-policy-values) | Refused `400` `invalid_schema`. |
-| [Reading](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typeslist-declared-policy) | `GET /types/{id}` shows the window after inheritance. `GET /types` shows only the window the type declares. |
+| A whole number from 1 to 36500 | The `feed-expiry` [housekeeping job](/guides/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. |
+
+`GET /types/{id}` shows the window after inheritance, and `GET /types` shows only the window the type declares.
The rules behind this page are in the [types](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.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/src/lib/catalog.test.ts b/src/lib/catalog.test.ts
index 3bd805f8..01dd4764 100644
--- a/src/lib/catalog.test.ts
+++ b/src/lib/catalog.test.ts
@@ -506,8 +506,8 @@ describe('the example check', () => {
describe('the Field kinds page', () => {
const page = PAGES.find((candidate) => candidate.slug === 'field-kinds')!.text;
- // A kind is a table row's first cell, linked to its contract rule.
- const kinds = [...page.matchAll(/^\|\s*\[?`([a-z]+)`(?:\]\([^)]*\))?\s*\|/gm)].map((match) => match[1]);
+ // A kind is a table row's first cell.
+ const kinds = [...page.matchAll(/^\|\s*`([a-z]+)`\s*\|/gm)].map((match) => match[1]);
it('lists every field kind the catalog names, once', () => {
expect(kinds.length).toBeGreaterThan(0);
@@ -525,7 +525,18 @@ describe('the Field kinds page', () => {
// ── Links ────────────────────────────────────────────────────────────────
-const RULE_LINK = /github\.com\/withmarfa\/marfa\/blob\/main\/conformance\/spec\/([a-z-]+)\.md#([a-z0-9-]+)/g;
+// A link to a contract chapter, or to one rule in it.
+const RULE_LINK = /github\.com\/withmarfa\/marfa\/blob\/main\/conformance\/spec\/([a-z-]+)\.md(?:#([a-z0-9-]+))?/g;
+
+// 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(['your-own-types', '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 /);
+ expect([...footer.matchAll(RULE_LINK)].length).toBeGreaterThan(0);
+ });
+});
describe('the links', () => {
it('point at rules the contract holds', () => {
@@ -536,8 +547,8 @@ describe('the links', () => {
const missing: string[] = [];
for (const { file, text } of PAGES) {
for (const [, chapter, anchor] of text.matchAll(RULE_LINK)) {
- found.push(`${chapter}#${anchor}`);
- if (!anchors.get(chapter)?.has(anchor)) missing.push(`${file}: ${chapter}#${anchor}`);
+ found.push(anchor ? `${chapter}#${anchor}` : chapter);
+ if (!anchors.has(chapter) || (anchor && !anchors.get(chapter)?.has(anchor))) missing.push(`${file}: ${chapter}${anchor ? `#${anchor}` : ''}`);
}
}
expect(found.length).toBeGreaterThan(10);