diff --git a/.github/scripts/sync-from-marfa.test.sh b/.github/scripts/sync-from-marfa.test.sh index 90a3330d..696dbba3 100644 --- a/.github/scripts/sync-from-marfa.test.sh +++ b/.github/scripts/sync-from-marfa.test.sh @@ -1176,6 +1176,28 @@ check "a subtype that differs says its hints and merge policy as values" \ check "the parent says its own hints once, and a subtype equal to it adds none" 1 \ "$(grep -cE '^\| \[`title_field`\].*\| `title` +\|$' "${drift_file}")" +# Keys a type and its fields gain: a plural, field labels, a list of entries with members, a range, +# a status field's words and groups, and an edge type's acyclic flag. +echo "a type catalog with plurals, labels, members, ranges and statuses" +fixture labels "${work}/untagged.json" +catalog_with 'n = c["types"][0]; n["plural"] = "Notes"; n["fields"]["title"]["label"] = "Heading"; n["fields"]["rating"] = {"type": "integer", "description": "A rating", "minimum": 1, "maximum": 5}; n["fields"]["progress"] = {"type": "status", "description": "Progress", "status_values": [{"value": "planned", "group": "not_started"}, {"value": "done", "group": "completed"}]}; n["fields"]["links"] = {"type": "array", "description": "Links", "items_type": "object", "items_fields": {"value": {"type": "url", "label": "Link", "description": "The link", "required": True}}}; c["edge_types"][1]["acyclic"] = True' \ + > "${work}/labels/marfa/core/marfa-core/src/builtin_catalog.json" +sync labels +labels_file="${work}/labels/docs/src/generated/catalog/core.note.md" +check "plurals, labels, members, ranges and statuses: the script succeeds" 0 "${status}" +check "the plural is a setting" 1 "$(grep -cF '| `plural`' "${labels_file}")" +check "a field's label has a column that appears with it" 1 "$(grep -cE '^\| `title` +\| Heading +\|' "${labels_file}")" +check "a range is in the kind" 1 "$(grep -cF '`integer` (1 to 5)' "${labels_file}")" +check "a status field's words are listed with their groups" 1 "$(grep -cF 'Values: `planned` in `not_started`, `done` in `completed`.' "${labels_file}")" +check "a member of a list of entries follows its field, with its label" 1 "$(grep -cE '^\| `links\[\]\.value` +\| Link +\| `url` +\| Yes' "${labels_file}")" +check "an acyclic edge type says so" 1 "$(grep -cF '**acyclic:** `true`' "${work}/labels/docs/src/generated/catalog/edges.md")" +refuse_catalog "a field with an empty label" "${cat_file} has a field core.note.body whose label is not a label. Nothing has changed." "$(catalog_with 'c["types"][0]["fields"]["body"]["label"] = ""')" +refuse_catalog "a status with no words" "${cat_file} has a status core.note.body whose status_values are not a list of values and groups. Nothing has changed." "$(catalog_with 'c["types"][0]["fields"]["body"]["type"] = "status"; c["types"][0]["fields"]["body"]["status_values"] = []')" +refuse_catalog "status words on a field that is not a status" "${cat_file} has status_values on core.note.body, which is not a status. Nothing has changed." "$(catalog_with 'c["types"][0]["fields"]["body"]["status_values"] = []')" +refuse_catalog "a range that is not a number" "${cat_file} has a minimum on core.note.body that is not a number. Nothing has changed." "$(catalog_with 'c["types"][0]["fields"]["body"]["minimum"] = "1"')" +refuse_catalog "members on a field that is not a list of objects" "${cat_file} has items_fields on core.note.body, which is not an array of objects. Nothing has changed." "$(catalog_with 'c["types"][0]["fields"]["body"]["items_fields"] = {"a": {"type": "string", "description": "A"}}')" +refuse_catalog "an acyclic flag that is not true or false" "${cat_file} has the edge type in-collection with an acyclic that is not true or false. Nothing has changed." "$(catalog_with 'c["edge_types"][0]["acyclic"] = "yes"')" + # spec_with : the fixture's spec, changed. spec_with() { python3 -c 'import json, sys; s = json.load(open(sys.argv[1])); exec(sys.argv[2]); print(json.dumps(s))' "${work}/untagged.json" "$1" diff --git a/content/docs/catalog/(types)/(core)/entity.mdx b/content/docs/catalog/(types)/(core)/entity.mdx index ba92160c..b2b1988c 100644 --- a/content/docs/catalog/(types)/(core)/entity.mdx +++ b/content/docs/catalog/(types)/(core)/entity.mdx @@ -15,7 +15,7 @@ identifier: core.entity "name": "Priya Raman", "given_name": "Priya", "family_name": "Raman", - "email": "priya.raman@example.com", + "emails": [{ "value": "priya.raman@example.com", "label": "work" }], "organization": "Harbour Press", "job_title": "Managing editor", "birthday": "1988-04-17" diff --git a/content/docs/catalog/(types)/(core)/task.mdx b/content/docs/catalog/(types)/(core)/task.mdx index fd728ca5..1aaed5a5 100644 --- a/content/docs/catalog/(types)/(core)/task.mdx +++ b/content/docs/catalog/(types)/(core)/task.mdx @@ -15,7 +15,7 @@ identifier: core.task "title": "Renew passport", "body": "The form is online. It needs a countersigned photo.", "due_at": "2026-12-01", - "status": "pending", + "status": "not_started", "priority": "high" } } diff --git a/content/docs/catalog/(types)/field-kinds.mdx b/content/docs/catalog/(types)/field-kinds.mdx index f5464fed..b72043ac 100644 --- a/content/docs/catalog/(types)/field-kinds.mdx +++ b/content/docs/catalog/(types)/field-kinds.mdx @@ -18,6 +18,7 @@ Each field of a type holds one kind of value, named by its `type`. **A value Mar | `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`. | +| `status` | One of the words in the field's `status_values`. Each word belongs to one of four groups: `not_started`, `in_progress`, `completed` or `canceled`. | | `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. | diff --git a/content/docs/catalog/(types)/your-own-types.mdx b/content/docs/catalog/(types)/your-own-types.mdx index 33698181..f322cc49 100644 --- a/content/docs/catalog/(types)/your-own-types.mdx +++ b/content/docs/catalog/(types)/your-own-types.mdx @@ -21,7 +21,7 @@ description: "The rules a type you register must meet: its identifier, fields, p | Rule | Marfa allows or refuses | | --- | --- | -| Keys | A field definition takes `type`, `description`, `required`, `enum_values`, `items_type`, `format`, `searchable`, `maxLength` and `maxItems`, and nothing else, such as `minimum`. | +| Keys | A field definition takes `type`, `label`, `description`, `required`, `enum_values`, `status_values`, `items_type`, `items_fields`, `format`, `searchable`, `maxLength`, `maxItems`, `minimum` and `maximum`, and nothing else. | | `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. | @@ -64,9 +64,9 @@ A replacement or deletion that breaks another type's claim is refused the same w | --- | --- | --- | | `title_field` | The name of the field that gives an item its name. | 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. **The value is Markdown.** Apps render it, and a device reads it to make [edges](/catalog/edges) from the links it contains. Marfa stores the value as sent and doesn't check it. | The same as `title_field`. | -| `icon` | The name of a [Lucide](https://lucide.dev/icons/) icon in kebab-case, such as `star` or `file-music`. | A name outside Lucide 1.61.0, the one release Marfa pins: a different case, a name from a later release, or an icon Lucide removed. **Use the canonical name, not an alias:** `home` is refused and `house` is accepted. Refused `400 invalid_schema`, naming `display_hints.icon`. | +| `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`. | -Every operation that takes a type definition applies the same rule, including [normalize a type definition](/api-reference/types/normalizeType). The names Marfa accepts are in the [generated list](https://github.com/withmarfa/marfa/blob/main/packages/types/generated/lucide-icons.json). +Every operation that takes a type definition applies the same rule, including [normalize a type definition](/api-reference/types/normalizeType). The IDs Marfa accepts are in the generated lists [`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:** the type itself, then its parent, then that type's parent. A type that sets only an `icon` still shows the title and body its parent names. `GET /types/{id}` answers each hint after that choice, and `GET /types` lists the hints each type declares itself, with the owner's icon in place of the author's. diff --git a/openapi.json b/openapi.json index 6755b80a..f117ae7f 100644 --- a/openapi.json +++ b/openapi.json @@ -96,6 +96,14 @@ "shipped": { "type": "boolean", "description": "`true` if Marfa ships the edge type: it exists on every instance, and you can't register or delete it. `false` for one registered through `POST /edge-types`." + }, + "acyclic": { + "type": "boolean", + "description": "`true` if the server refuses an edge of this type that closes a cycle through other edges of the type. Every edge type refuses an edge from an item to itself." + }, + "flat": { + "type": "boolean", + "description": "`true` if the server refuses an edge of this type whose source type its `target_type_constraints` admit, so an item that can be a target is never a source." } }, "required": [ @@ -108,7 +116,9 @@ "cascade_on_delete", "property_schema", "written_at", - "shipped" + "shipped", + "acyclic", + "flat" ], "description": "An edge type says how edges of that type behave: its cardinality, what a delete does to the items they join, and which item types they can join." }, @@ -117,7 +127,7 @@ "properties": { "type": { "type": "string", - "description": "The property's type: one of the field types a type's `fields` take, except `thumbnail`." + "description": "The property's type: one of the field types a type's `fields` take, except `thumbnail` and `status`." }, "description": { "type": "string", @@ -140,7 +150,7 @@ }, "format": { "type": "string", - "description": "A refinement of a `string` property. `url`, `email`, `date` and `datetime` store it as that type. `bcp47` and `iso3166` only label it. It can't be `thumbnail`." + "description": "A refinement of a `string` property. `url`, `email`, `date` and `datetime` store it as that type. `bcp47` and `iso3166` only label it, and `currency` limits it to three capital letters. It can't be `thumbnail`." } }, "required": [ @@ -461,6 +471,13 @@ "type": "string", "description": "When the item was last written, in UTC. A tag or extension write moves it too." }, + "trashed_at": { + "type": [ + "string", + "null" + ], + "description": "When the item went to the trash, in UTC, or when a `system.*` item was revoked. A later write to the item leaves it as it is, and a restore clears it. `null` for an item outside the trash, and for one an archive restored to the trash without a time." + }, "trashed_by_cascade": { "type": "boolean", "description": "Always `true` where present: the item went to the trash with another item, through a cascading edge such as `parent-of`. Present while it stays in the trash. Absent on an item trashed on its own." @@ -495,7 +512,8 @@ "source", "occurred_at", "created_at", - "updated_at" + "updated_at", + "trashed_at" ], "description": "An item is one record in Marfa." }, @@ -894,7 +912,7 @@ }, "icon": { "type": "string", - "description": "The icon that stands for the type, as the kebab-case name of an icon in the Lucide release Marfa pins, such as `calendar-days`. On a type read back, the owner's own choice replaces the author's." + "description": "The type's icon, as an Iconify ID, `:`, from a pinned version of `lucide` or `simple-icons`, such as `lucide:calendar-days`. On a type read back, the owner's own choice replaces the author's." } }, "additionalProperties": {}, @@ -1337,6 +1355,230 @@ ], "description": "An error response." }, + "Health": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "ok", + "degraded", + "down" + ], + "description": "`ok`: serving normally. `degraded`: serving, but something is slow, low or unknown. `down`: something refused." + }, + "components": { + "type": "object", + "properties": { + "database": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "ok", + "degraded", + "down" + ], + "description": "`ok`: serving normally. `degraded`: serving, but something is slow, low or unknown. `down`: something refused." + }, + "latency_ms": { + "type": "integer", + "minimum": 0, + "description": "How long the check took, in milliseconds." + }, + "error": { + "type": "string", + "description": "What went wrong. Present only for a caller that holds `instance.read`." + } + }, + "required": [ + "status" + ], + "description": "Whether the database answers a read." + }, + "database_write": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "ok", + "degraded", + "down" + ], + "description": "`ok`: serving normally. `degraded`: serving, but something is slow, low or unknown. `down`: something refused." + }, + "latency_ms": { + "type": "integer", + "minimum": 0, + "description": "How long the check took, in milliseconds." + }, + "error": { + "type": "string", + "description": "What went wrong. Present only for a caller that holds `instance.read`." + } + }, + "required": [ + "status" + ], + "description": "Whether the database commits a write." + }, + "disk": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "ok", + "degraded", + "down" + ], + "description": "`ok`: serving normally. `degraded`: serving, but something is slow, low or unknown. `down`: something refused." + }, + "latency_ms": { + "type": "integer", + "minimum": 0, + "description": "How long the check took, in milliseconds." + }, + "error": { + "type": "string", + "description": "What went wrong. Present only for a caller that holds `instance.read`." + } + }, + "required": [ + "status" + ], + "description": "Whether the disk has room to write." + }, + "blob_storage": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "ok", + "degraded", + "down" + ], + "description": "`ok`: serving normally. `degraded`: serving, but something is slow, low or unknown. `down`: something refused." + }, + "latency_ms": { + "type": "integer", + "minimum": 0, + "description": "How long the check took, in milliseconds." + }, + "error": { + "type": "string", + "description": "What went wrong. Present only for a caller that holds `instance.read`." + } + }, + "required": [ + "status" + ], + "description": "Whether the blob store on disk answers." + } + }, + "required": [ + "database", + "database_write", + "disk", + "blob_storage" + ], + "description": "Each component checked: reading and writing the database, free disk space and the blob store." + }, + "platform_types": { + "type": "object", + "properties": { + "retired": { + "type": "integer", + "minimum": 0, + "description": "How many platform types the instance holds that this build no longer ships." + } + }, + "required": [ + "retired" + ], + "description": "The platform types this build no longer ships." + }, + "unrecognized_stored_values": { + "type": "object", + "properties": { + "rows": { + "type": "integer", + "minimum": 0, + "description": "How many stored rows hold a value this build does not recognize." + }, + "scanned": { + "type": "boolean", + "description": "`true` if the scan for such rows ran. `false` means `rows` is unknown." + } + }, + "required": [ + "rows", + "scanned" + ], + "description": "Stored values this build does not recognize." + }, + "placement": { + "type": "object", + "properties": { + "region": { + "type": "string", + "description": "The region the server runs in." + }, + "location": { + "type": "string", + "description": "The location the server runs in." + }, + "country": { + "type": "string", + "description": "The country the server runs in." + } + }, + "description": "Where the server runs. Present only when it is configured." + }, + "version": { + "type": "string", + "description": "The build the instance runs, when it is known." + } + }, + "required": [ + "status", + "components", + "platform_types", + "unrecognized_stored_values" + ], + "description": "The instance's health: the worst state of its components, and each one's state.", + "example": { + "status": "ok", + "components": { + "database": { + "status": "ok", + "latency_ms": 1 + }, + "database_write": { + "status": "ok", + "latency_ms": 2 + }, + "disk": { + "status": "ok", + "latency_ms": 0 + }, + "blob_storage": { + "status": "ok", + "latency_ms": 0 + } + }, + "platform_types": { + "retired": 0 + }, + "unrecognized_stored_values": { + "rows": 0, + "scanned": true + } + } + }, "TrashPage": { "allOf": [ { @@ -1994,7 +2236,7 @@ ], "description": "A write Marfa can't merge, because it holds no snapshot you can read of the `version` you sent: the error and the item now." }, - "ConflictOrIdReusedOrLinkTakenOrTypeMismatchRefusal": { + "ConflictOrIdReusedOrItemExistsOrLinkTakenOrTypeMismatchRefusal": { "type": "object", "properties": { "error": { @@ -2005,6 +2247,7 @@ "enum": [ "conflict", "id_reused", + "item_exists", "link_taken", "type_mismatch" ], @@ -2533,102 +2776,293 @@ ], "description": "An error response." }, - "InvalidIdOrInvalidTransitionOrValidationErrorRefusal": { + "TrashPreview": { "type": "object", "properties": { - "error": { - "type": "object", - "properties": { - "code": { - "type": "string", - "enum": [ - "invalid_id", - "invalid_transition", - "validation_error" - ], - "description": "A machine-readable code for the error. Use it in your logic." - }, - "message": { - "type": "string", - "description": "A description of the error for a person to read. It can change, so don't match on it." - }, - "details": { - "type": "object", - "additionalProperties": {}, - "description": "More about the error, such as the field it concerns. Each code defines its own details." - } + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TrashPreviewItem" }, - "required": [ - "code", - "message" - ], - "description": "What went wrong." + "description": "Each item you named, once, in the order you named them." + }, + "count": { + "type": "integer", + "description": "How many items you can read that trashing the named items would move to the trash, each counted once, the named items included." + }, + "digest": { + "type": "string", + "description": "A SHA-256 hash, in hex, of every item `count` counts with its version, listed or not. It changes if any of them changes, joins or leaves. Pass it to `POST /items/trash` with the same `ids` to trash only what it covers.", + "example": "9f2c1d3b5a7e4f60b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5" } }, "required": [ - "error" + "items", + "count", + "digest" ], - "description": "An error response." + "description": "What trashing items would move to the trash. Items you can't read are taken too and aren't listed or counted.", + "example": { + "items": [ + { + "id": "019537a0-7b80-7000-8000-000000000001", + "type": "core.project", + "version": 4, + "takes": [ + { + "id": "019537a0-7b80-7000-8000-000000000002", + "type": "core.task", + "version": 2 + } + ], + "count": 2 + } + ], + "count": 2, + "digest": "9f2c1d3b5a7e4f60b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5" + } }, - "EdgeConstraintViolationOrInvalidIdOrInvalidTransitionOrMissingRequiredFieldOrValidationErrorRefusal": { + "TrashPreviewItem": { "type": "object", "properties": { - "error": { - "type": "object", - "properties": { - "code": { - "type": "string", - "enum": [ - "edge_constraint_violation", - "invalid_id", - "invalid_transition", - "missing_required_field", - "validation_error" - ], - "description": "A machine-readable code for the error. Use it in your logic." - }, - "message": { - "type": "string", - "description": "A description of the error for a person to read. It can change, so don't match on it." - }, - "details": { - "type": "object", - "additionalProperties": {}, - "description": "More about the error, such as the field it concerns. Each code defines its own details." - } + "id": { + "type": "string", + "description": "The ID of an item you named.", + "example": "019537a0-7b80-7000-8000-000000000001" + }, + "type": { + "type": "string", + "description": "The item's type.", + "example": "core.project" + }, + "version": { + "type": "integer", + "minimum": 1, + "description": "The item's version." + }, + "takes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TrashPreviewEntry" }, - "required": [ - "code", - "message" + "description": "The other items you can read that a cascading edge such as `parent-of` would take into the trash with this item, leaves first, at most 100." + }, + "count": { + "type": "integer", + "description": "How many items you can read that trashing this item would move to the trash, the item itself included." + }, + "truncated": { + "type": "boolean", + "enum": [ + true ], - "description": "What went wrong." + "description": "`true` if `takes` was cut at its limit. Absent when it lists every item." } }, "required": [ - "error" + "id", + "type", + "version", + "takes", + "count" ], - "description": "An error response." + "description": "An item you named and what trashing it would take with it." }, - "MetadataResponse": { + "TrashPreviewEntry": { "type": "object", "properties": { - "metadata": { - "allOf": [ - { - "$ref": "#/components/schemas/Metadata" - }, - { - "description": "The item's metadata." - } - ] + "id": { + "type": "string", + "description": "Unique identifier for the item.", + "example": "019537a0-7b80-7000-8000-000000000002" + }, + "type": { + "type": "string", + "description": "The item's type.", + "example": "core.task" + }, + "version": { + "type": "integer", + "minimum": 1, + "description": "The item's version." } }, "required": [ - "metadata" + "id", + "type", + "version" ], - "description": "An item's metadata." + "description": "An item that trashing another item would take with it." }, - "InvalidIdOrMissingRequiredFieldOrValidationErrorRefusal": { + "EdgeConstraintViolationOrInvalidIdOrMissingRequiredFieldOrValidationErrorRefusal": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "edge_constraint_violation", + "invalid_id", + "missing_required_field", + "validation_error" + ], + "description": "A machine-readable code for the error. Use it in your logic." + }, + "message": { + "type": "string", + "description": "A description of the error for a person to read. It can change, so don't match on it." + }, + "details": { + "type": "object", + "additionalProperties": {}, + "description": "More about the error, such as the field it concerns. Each code defines its own details." + } + }, + "required": [ + "code", + "message" + ], + "description": "What went wrong." + } + }, + "required": [ + "error" + ], + "description": "An error response." + }, + "TrashPlanChangedRefusal": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "trash_plan_changed" + ], + "description": "A machine-readable code for the error. Use it in your logic." + }, + "message": { + "type": "string", + "description": "A description of the error for a person to read. It can change, so don't match on it." + }, + "details": { + "type": "object", + "additionalProperties": {}, + "description": "More about the error, such as the field it concerns. Each code defines its own details." + } + }, + "required": [ + "code", + "message" + ], + "description": "What went wrong." + } + }, + "required": [ + "error" + ], + "description": "An error response." + }, + "InvalidIdOrInvalidTransitionOrValidationErrorRefusal": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "invalid_id", + "invalid_transition", + "validation_error" + ], + "description": "A machine-readable code for the error. Use it in your logic." + }, + "message": { + "type": "string", + "description": "A description of the error for a person to read. It can change, so don't match on it." + }, + "details": { + "type": "object", + "additionalProperties": {}, + "description": "More about the error, such as the field it concerns. Each code defines its own details." + } + }, + "required": [ + "code", + "message" + ], + "description": "What went wrong." + } + }, + "required": [ + "error" + ], + "description": "An error response." + }, + "EdgeConstraintViolationOrInvalidIdOrInvalidTransitionOrMissingRequiredFieldOrValidationErrorRefusal": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "edge_constraint_violation", + "invalid_id", + "invalid_transition", + "missing_required_field", + "validation_error" + ], + "description": "A machine-readable code for the error. Use it in your logic." + }, + "message": { + "type": "string", + "description": "A description of the error for a person to read. It can change, so don't match on it." + }, + "details": { + "type": "object", + "additionalProperties": {}, + "description": "More about the error, such as the field it concerns. Each code defines its own details." + } + }, + "required": [ + "code", + "message" + ], + "description": "What went wrong." + } + }, + "required": [ + "error" + ], + "description": "An error response." + }, + "MetadataResponse": { + "type": "object", + "properties": { + "metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/Metadata" + }, + { + "description": "The item's metadata." + } + ] + } + }, + "required": [ + "metadata" + ], + "description": "An item's metadata." + }, + "InvalidIdOrMissingRequiredFieldOrValidationErrorRefusal": { "type": "object", "properties": { "error": { @@ -3786,7 +4220,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/RegisteredType" + "$ref": "#/components/schemas/ListedType" }, "description": "Every type." }, @@ -3804,7 +4238,7 @@ ], "description": "A page holding every type." }, - "RegisteredType": { + "ListedType": { "allOf": [ { "$ref": "#/components/schemas/TypeDefinition" @@ -3821,14 +4255,22 @@ "description": "The type's recorded origin and creator." } ] + }, + "own_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TypeRole" + }, + "description": "The structural roles the type declares itself, without those it only inherits. Empty if it declares none." } }, "required": [ - "provenance" + "provenance", + "own_roles" ] } ], - "description": "A type says which fields the items of that type hold, and what else Marfa does for them." + "description": "A type in the list of types: its own definition, with the roles it declares and the roles it plays." }, "TypeRole": { "type": "string", @@ -3854,9 +4296,14 @@ "enum", "array", "object", - "thumbnail" + "thumbnail", + "status" ], - "description": "The field's type. A `thumbnail` holds an image of at most 16 KiB, as a base64 `data:` URI of type `image/png`, `image/jpeg` or `image/webp`. A type has at most one, never as an array's `items_type` or under `title`, `body`, `description` or `name`." + "description": "The field's type. A `thumbnail` is a base64 `data:` URI of a PNG, JPEG or WebP image up to 16 KiB, never `title`, `body`, `description` or `name`; a `status` is a word from `status_values`. A type has at most one of each, never as `items_type`." + }, + "label": { + "type": "string", + "description": "A name for people to read, up to 100 characters. Leave it out and a type read answers the nearest one a type above names, else one made from the field's name, such as `Due At` for `due_at`. The type list answers only the declared label." }, "description": { "type": "string", @@ -3873,10 +4320,24 @@ }, "description": "The values an `enum` field allows." }, + "status_values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StatusValue" + }, + "description": "The words a `status` field takes, each in a group: `not_started`, `in_progress`, `completed` or `canceled`. A subtype lists only the words it adds. A type read answers every word, inherited first; the type list answers the type's own." + }, "items_type": { "type": "string", "description": "The type of each element of an `array` field. A write is refused if an element is not a value of that type." }, + "items_fields": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/EntryMember" + }, + "description": "The members of each entry of an `array` field whose `items_type` is `object`, by name. A write is refused if an entry has another member, lacks a required one, holds a value a member does not take, or has none." + }, "format": { "type": "string", "enum": [ @@ -3886,9 +4347,11 @@ "date", "thumbnail", "bcp47", - "iso3166" + "iso3166", + "currency", + "phone" ], - "description": "A refinement of a `string` field, or of an array of strings. `url`, `email`, `datetime`, `date` and `thumbnail` become the field's `type`, and all but `thumbnail` an array's `items_type`. `bcp47` and `iso3166` stay as `format`." + "description": "Refines a `string` field or an array of strings. `url`, `email`, `datetime`, `date` and `thumbnail` become the `type` or `items_type`. `bcp47`, `iso3166`, `currency` and `phone` stay as `format`; a `currency` is three capital letters, such as `GBP`." }, "searchable": { "type": "boolean", @@ -3901,6 +4364,14 @@ "maxItems": { "type": "integer", "description": "The most elements an `array` field allows." + }, + "minimum": { + "type": "number", + "description": "The lowest value the field allows, itself included. Only a `number` or `integer` field takes it. A subtype may raise it, never lower or drop it." + }, + "maximum": { + "type": "number", + "description": "The highest value the field allows, itself included. Only a `number` or `integer` field takes it. A subtype may lower it, never raise or drop it." } }, "required": [ @@ -3909,6 +4380,105 @@ "additionalProperties": {}, "description": "One field of a type: its type, and how items may fill it." }, + "StatusValue": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "The word, such as `in_review`: a lowercase letter, then up to 63 lowercase letters, digits and underscores." + }, + "group": { + "type": "string", + "enum": [ + "not_started", + "in_progress", + "completed", + "canceled" + ], + "description": "The group the word belongs to." + } + }, + "required": [ + "value", + "group" + ], + "description": "One word a `status` field takes, with its group." + }, + "EntryMember": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "url", + "email", + "datetime", + "date", + "enum", + "array", + "object", + "thumbnail", + "status" + ], + "description": "The member's type: `string`, `email`, `url`, `enum`, `boolean`, `integer`, `number`, `date` or `datetime`." + }, + "label": { + "type": "string", + "description": "A name for people to read, up to 100 characters. Leave it out and a type read answers the nearest one a type above names, else one made from the member's name, such as `Due At` for `due_at`. The type list answers only the declared label." + }, + "description": { + "type": "string", + "description": "What the member holds." + }, + "required": { + "type": "boolean", + "description": "`true` if every entry must have the member." + }, + "enum_values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The values an `enum` member allows." + }, + "format": { + "type": "string", + "enum": [ + "url", + "email", + "datetime", + "date", + "thumbnail", + "bcp47", + "iso3166", + "currency", + "phone" + ], + "description": "A refinement of a `string` member, as a field's `format`." + }, + "maxLength": { + "type": "integer", + "description": "The longest value a `string` or `enum` member allows." + }, + "minimum": { + "type": "number", + "description": "The lowest value the member allows, itself included. Only a `number` or `integer` member takes it." + }, + "maximum": { + "type": "number", + "description": "The highest value the member allows, itself included. Only a `number` or `integer` member takes it." + } + }, + "required": [ + "type" + ], + "additionalProperties": {}, + "description": "One member of the entries of an `array` field." + }, "TypeDefinition": { "type": "object", "properties": { @@ -3920,6 +4490,10 @@ "type": "string", "description": "The type's name for people to read." }, + "plural": { + "type": "string", + "description": "The type's plural name for people to read, such as `People`. Never inherited. Left out of the list of types where the type declares none." + }, "description": { "type": "string", "description": "What the type is for." @@ -4006,6 +4580,37 @@ ], "description": "A type says which fields the items of that type hold, and what else Marfa does for them." }, + "RegisteredType": { + "allOf": [ + { + "$ref": "#/components/schemas/TypeDefinition" + }, + { + "type": "object", + "properties": { + "plural": { + "type": "string", + "description": "The type's plural name for people to read, such as `People`: the one the type declares, else one made from its label. Never inherited." + }, + "provenance": { + "allOf": [ + { + "$ref": "#/components/schemas/TypeProvenance" + }, + { + "description": "The type's recorded origin and creator." + } + ] + } + }, + "required": [ + "plural", + "provenance" + ] + } + ], + "description": "A type says which fields the items of that type hold, and what else Marfa does for them." + }, "TypeNotFoundRefusal": { "type": "object", "properties": { @@ -4214,7 +4819,11 @@ }, "label": { "type": "string", - "description": "A name for people to read. Leave it out and Marfa derives one from the last segment of the identifier." + "description": "A name for people to read, up to 100 characters. Leave it out and Marfa derives one from the last segment of the identifier." + }, + "plural": { + "type": "string", + "description": "The label's plural, up to 100 characters, such as `People`. Leave it out and Marfa adds `s`, `es` or `ies` to the label by the regular English rule." }, "description": { "type": "string", @@ -4452,7 +5061,11 @@ }, "label": { "type": "string", - "description": "A name for people to read. Leave it out and Marfa derives one from the last segment of the identifier." + "description": "A name for people to read, up to 100 characters. Leave it out and Marfa derives one from the last segment of the identifier." + }, + "plural": { + "type": "string", + "description": "The label's plural, up to 100 characters, such as `People`. Leave it out and Marfa adds `s`, `es` or `ies` to the label by the regular English rule." }, "description": { "type": "string", @@ -4592,7 +5205,7 @@ "properties": { "icon": { "type": "string", - "description": "The kebab-case name of an icon in the Lucide release Marfa pins, such as `calendar-days`." + "description": "An Iconify ID, `:`, from a set and version Marfa pins: `lucide` or `simple-icons`, such as `lucide:calendar-days` or `simple-icons:github`." } }, "required": [ @@ -4835,7 +5448,7 @@ "items": { "$ref": "#/components/schemas/SearchResult" }, - "description": "The results, best match first." + "description": "The results, in the order of `sort`." }, "next_cursor": { "type": [ @@ -5089,6 +5702,295 @@ ], "description": "A tag, and how many items carry it." }, + "Overview": { + "type": "object", + "properties": { + "instance": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The instance's display name." + }, + "version": { + "type": "string", + "description": "The build the instance runs." + }, + "release": { + "type": "string", + "description": "The release the build was made as. Absent for a build that is not a release." + } + }, + "required": [ + "name", + "version" + ], + "description": "The instance's name and the build it runs." + }, + "credential": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "key", + "app" + ], + "description": "`key` for an API key, `app` for a signed-in app's token." + }, + "label": { + "type": "string", + "description": "The key's label. Absent for an app." + }, + "type_permissions": { + "type": "object", + "properties": { + "write": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Type patterns you can read and write, such as `core.*`. Absent when none." + }, + "read": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Type patterns you can only read. Absent when none." + }, + "none": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Type patterns denied even where a wider pattern grants access. Absent when none." + } + }, + "description": "Your type map by level, each list sorted and cut at 30 patterns. Absent when the map is empty." + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Permission" + }, + "description": "The permissions you hold. Absent when you hold none." + } + }, + "required": [ + "kind" + ], + "description": "The credential that sent the request, and what it may do." + }, + "types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OverviewType" + }, + "description": "The types you can read, most items first, then by identifier, at most 50. Types in the `system` namespace are left out. Absent when you can read none." + }, + "total": { + "type": "integer", + "description": "How many active items you can read across every type `types` would list before its cut. Absent when `types` is.", + "example": 128 + }, + "edge_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OverviewEdgeType" + }, + "description": "The edge types you can read or write, sorted by identifier, at most 50. Absent when you can read no type or can use no edge type." + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagCount" + }, + "description": "The most used tags on items you can read, at most 30. Absent when there are none." + }, + "views": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OverviewView" + }, + "description": "The views you can read, sorted by title, at most 30. Absent when there are none." + }, + "folders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OverviewFolder" + }, + "description": "The folders you can read, sorted by title, at most 30. Absent when there are none." + }, + "truncated": { + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "description": "Each list that was cut, with how many entries it held before the cut. Names are `types`, `edge_types`, `tags`, `views`, `folders` and `credential.type_permissions.`. Absent when nothing was cut." + } + }, + "required": [ + "instance", + "credential" + ], + "description": "A summary of what this instance holds and what your credential may reach.", + "example": { + "instance": { + "name": "Marfa", + "version": "dev" + }, + "credential": { + "kind": "key", + "label": "Notes agent", + "type_permissions": { + "write": [ + "core.note" + ], + "read": [ + "*" + ] + }, + "permissions": [ + "audit.read" + ] + }, + "types": [ + { + "id": "core.note", + "description": "A piece of writing.", + "count": 128 + } + ], + "total": 128, + "edge_types": [ + { + "id": "references", + "access": "write" + } + ], + "tags": [ + { + "tag": "work", + "count": 40 + } + ], + "views": [ + { + "id": "019537a0-7b80-7000-8000-000000000000", + "title": "Reading list" + } + ] + } + }, + "Permission": { + "type": "string", + "enum": [ + "webhooks.manage", + "schema.write", + "config.manage", + "audit.read", + "items.purge", + "keys.mint", + "grants.manage", + "instance.read", + "instance.maintain", + "connectors.manage", + "blobs.manage", + "keys.manage" + ], + "description": "A permission a credential can hold, for an operation the type, edge, extension, metadata and profile maps don't cover. `schema.write` replaces and deletes types and edge types; `keys.mint` mints keys and lists, changes and revokes those within the caller's reach; `keys.manage` lists and revokes every key and changes one without widening it; `items.purge` purges trashed items; `webhooks.manage` manages webhooks; `config.manage` reads and replaces `/config`; `audit.read` reads the audit log; `grants.manage` lists and revokes other apps' access; `instance.read` reads health, metrics, housekeeping, retired platform types, blob storage reports and every bulk job's status; `instance.maintain` runs housekeeping, removes retired platform types and cancels bulk jobs; `connectors.manage` administers connector registrations and their endpoints, and clears their retained state; and `blobs.manage` manages every blob." + }, + "OverviewType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The type's identifier.", + "example": "core.note" + }, + "description": { + "type": "string", + "description": "What the type holds, cut at 100 characters. Absent when the type has none." + }, + "count": { + "type": "integer", + "description": "How many active items of the type you can read." + } + }, + "required": [ + "id", + "count" + ], + "description": "A type you can read, and how many active items it has." + }, + "OverviewEdgeType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The edge type's identifier.", + "example": "parent-of" + }, + "description": { + "type": "string", + "description": "What the edge type means, cut at 100 characters. Absent when it has none." + }, + "access": { + "allOf": [ + { + "$ref": "#/components/schemas/PermissionLevel" + }, + { + "description": "`write` where your edge permissions cover writing edges of the type, otherwise `read`. Creating an edge also needs write access to the type of its source item." + } + ] + } + }, + "required": [ + "id", + "access" + ], + "description": "An edge type your edge permissions cover, and at which level." + }, + "OverviewView": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the view.", + "example": "019537a0-7b80-7000-8000-000000000000" + }, + "title": { + "type": "string", + "description": "The view's title, cut at 100 characters." + } + }, + "required": [ + "id", + "title" + ], + "description": "A view you can read." + }, + "OverviewFolder": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the folder.", + "example": "019537a0-7b80-7000-8000-000000000000" + }, + "title": { + "type": "string", + "description": "The folder's title, cut at 100 characters." + } + }, + "required": [ + "id", + "title" + ], + "description": "A folder you can read." + }, "RequestTimeoutRefusal": { "type": "object", "properties": { @@ -7100,6 +8002,30 @@ "additionalProperties": false, "description": "Which items the view lists. Absent lists every item the reading key can read, except `system.*`." }, + "ViewItemPage": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + "description": "The items on this page, in the view's sort order." + }, + "next_cursor": { + "type": [ + "string", + "null" + ], + "description": "Pass as `cursor` for the next page; `null` on the last. A page can be short, or empty, with a cursor still to follow, so a walk stops on `null` and never on a short page." + } + }, + "required": [ + "data", + "next_cursor" + ], + "description": "A page of the items a view lists." + }, "InvalidIdOrInvalidTransitionOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal": { "type": "object", "properties": { @@ -7272,24 +8198,6 @@ ], "description": "A new API key, with its plaintext `key`." }, - "Permission": { - "type": "string", - "enum": [ - "webhooks.manage", - "schema.write", - "config.manage", - "audit.read", - "items.purge", - "keys.mint", - "grants.manage", - "instance.read", - "instance.maintain", - "connectors.manage", - "blobs.manage", - "keys.manage" - ], - "description": "A permission a credential can hold, for an operation the type, edge, extension, metadata and profile maps don't cover. `schema.write` replaces and deletes types and edge types; `keys.mint` mints keys and lists, changes and revokes those within the caller's reach; `keys.manage` lists and revokes every key and changes one without widening it; `items.purge` purges trashed items; `webhooks.manage` manages webhooks; `config.manage` reads and replaces `/config`; `audit.read` reads the audit log; `grants.manage` lists and revokes other apps' access; `instance.read` reads health, metrics, housekeeping, retired platform types, blob storage reports and every bulk job's status; `instance.maintain` runs housekeeping, resets platform type definitions and cancels bulk jobs; `connectors.manage` administers connector registrations and their endpoints, and clears their retained state; and `blobs.manage` manages every blob." - }, "TypePermissionLevel": { "type": "string", "enum": [ @@ -9773,6 +10681,14 @@ "type": "string", "description": "The item's identifier at its source, such as a vendor's row ID. With `source`, it is the item's natural key: creating with a key that exists updates that item." }, + "mode": { + "type": "string", + "enum": [ + "upsert", + "create_only" + ], + "description": "`upsert` (the default) updates the item `source_id` matches. `create_only` writes nothing when an item already holds the `source_id` under the source, or the `id`, in any state, and answers `item_exists` (409) with `details.existing_id`." + }, "version": { "type": "integer", "minimum": 0, @@ -9813,7 +10729,7 @@ "type": "string" } }, - "description": "Edges to create with the item, as edge type to a list of target item IDs. The new item is the source. Marfa creates the item and all its edges, or none. An edge made this way carries no properties, so an edge type that requires one refuses the write." + "description": "Edges to create with the item, as edge type to a list of target item IDs. The new item is the source. Marfa creates the item and all its edges, or none. When the request lands on an item by its natural key, each type you name replaces that item's edges of the type, and an edge to an ID you still list stays as it is. A new edge made this way carries no properties, so an edge type that requires one refuses the write." } }, "required": [ @@ -10012,7 +10928,7 @@ } }, "409": { - "description": "- `id_reused`: `id` names an item of another type.\n- `conflict`: `id` names an item you can't read.\n- `link_taken`: another item of the type holds this link. `details.existing_id` names it.\n- `type_mismatch`: `source_id` matches an item of another type.\n- `version_conflict`, `ancestor_unavailable`: `version` is stale.", + "description": "- `item_exists`: `create_only` found an item holding the `source_id` or `id`.\n- `id_reused`: `id` names an item of another type.\n- `conflict`: `id` names an item you can't read.\n- `link_taken`: another item of the type holds this link. `details.existing_id` names it.\n- `type_mismatch`: `source_id` matches an item of another type.\n- `version_conflict`, `ancestor_unavailable`: `version` is stale.", "content": { "application/json": { "schema": { @@ -10024,7 +10940,7 @@ "$ref": "#/components/schemas/ItemAncestorUnavailable" }, { - "$ref": "#/components/schemas/ConflictOrIdReusedOrLinkTakenOrTypeMismatchRefusal" + "$ref": "#/components/schemas/ConflictOrIdReusedOrItemExistsOrLinkTakenOrTypeMismatchRefusal" }, { "$ref": "#/components/schemas/InstanceChangedRefusal" @@ -10289,20 +11205,20 @@ { "schema": { "type": "string", - "description": "Only return items of this type or a subtype. A wildcard such as `core.*` matches every type under that prefix." + "description": "Only return items of these types and their subtypes: a comma-separated list of up to 10, such as `core.note,app.*`. A wildcard such as `core.*` matches every type under that prefix." }, "required": false, - "description": "Only return items of this type or a subtype. A wildcard such as `core.*` matches every type under that prefix.", + "description": "Only return items of these types and their subtypes: a comma-separated list of up to 10, such as `core.note,app.*`. A wildcard such as `core.*` matches every type under that prefix.", "name": "type", "in": "query" }, { "schema": { "type": "string", - "description": "Only return items in this lifecycle state. Without it, you get `active` items. Send `any` to get every state." + "description": "Only return items in these lifecycle states: one state or a comma-separated list, such as `active,archived`. Without it, you get `active` items. Send `any` on its own to get every state." }, "required": false, - "description": "Only return items in this lifecycle state. Without it, you get `active` items. Send `any` to get every state.", + "description": "Only return items in these lifecycle states: one state or a comma-separated list, such as `active,archived`. Without it, you get `active` items. Send `any` on its own to get every state.", "name": "state", "in": "query" }, @@ -10344,10 +11260,10 @@ { "schema": { "type": "string", - "description": "A filter expression. A term naming an edge type, `edge[]` or `backref[]`, matches by relationship and needs read on that edge type. `edge[]=` also works as a query parameter of its own." + "description": "A filter expression. A term naming an edge type, `edge[]` or `backref[]`, needs read on it; `edge[]=` also works as a query parameter. `status_group` matches by status group; `properties..`, by a list entry." }, "required": false, - "description": "A filter expression. A term naming an edge type, `edge[]` or `backref[]`, matches by relationship and needs read on that edge type. `edge[]=` also works as a query parameter of its own.", + "description": "A filter expression. A term naming an edge type, `edge[]` or `backref[]`, needs read on it; `edge[]=` also works as a query parameter. `status_group` matches by status group; `properties..`, by a list entry.", "name": "filter", "in": "query" }, @@ -10355,10 +11271,10 @@ "schema": { "type": "string", "pattern": "^(created_at|updated_at|occurred_at|properties\\.[a-z0-9_]+)$", - "description": "Field to sort by: `created_at`, `updated_at`, `occurred_at` or `properties.`, such as `properties.due_at`. Properties sort by stored value, so an enum property sorts alphabetically, not by meaning." + "description": "Field to sort by: `created_at`, `updated_at`, `occurred_at` or `properties.`. Properties sort by stored value: an enum sorts alphabetically. Defaults to `created_at`, or for `state=trashed` alone the trash time, which you can't name here." }, "required": false, - "description": "Field to sort by: `created_at`, `updated_at`, `occurred_at` or `properties.`, such as `properties.due_at`. Properties sort by stored value, so an enum property sorts alphabetically, not by meaning.", + "description": "Field to sort by: `created_at`, `updated_at`, `occurred_at` or `properties.`. Properties sort by stored value: an enum sorts alphabetically. Defaults to `created_at`, or for `state=trashed` alone the trash time, which you can't name here.", "name": "sort", "in": "query" }, @@ -10369,10 +11285,10 @@ "asc", "desc" ], - "description": "Sort direction" + "description": "Sort direction. Defaults to `desc`." }, "required": false, - "description": "Sort direction", + "description": "Sort direction. Defaults to `desc`.", "name": "direction", "in": "query" }, @@ -10424,12 +11340,11 @@ "schema": { "type": "integer", "minimum": 1, - "maximum": 200, "default": 50, - "description": "The maximum number of results to return." + "description": "The maximum number of results to return. Up to 500; a larger value is read as 500." }, "required": false, - "description": "The maximum number of results to return.", + "description": "The maximum number of results to return. Up to 500; a larger value is read as 500.", "name": "limit", "in": "query" }, @@ -11224,7 +12139,7 @@ "type": "string" } }, - "description": "Edge types to replace, each mapped to the item IDs it should now point to. An empty list removes every edge of that type. Types you don't name are untouched. An edge made this way carries no properties, so an edge type that requires one refuses the write." + "description": "Edge types to replace, each mapped to the item IDs it should now point to. An edge to an ID you still list stays as it is, with its properties. An empty list removes every edge of that type. Types you don't name are untouched. A new edge made this way carries no properties, so an edge type that requires one refuses the write." } }, "required": [ @@ -12117,58 +13032,54 @@ } } }, - "/items/{id}/restore": { + "/items/trash-preview": { "post": { - "operationId": "restoreItem", + "operationId": "previewItemTrash", "tags": [ "Items" ], - "summary": "Restore an item", - "description": "Restores a trashed item to `active`, along with every item its trash took through a cascading edge such as `parent-of`. Items that were already in the trash before then stay there.", + "summary": "Preview trashing items", + "description": "Returns what `POST /items/trash` would move to the trash for the same `ids`: each item and every item you can read that a cascading edge such as `parent-of` would take with it. Nothing changes.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The ID of the item." - }, - "required": true, - "description": "The ID of the item.", - "name": "id", - "in": "path" - }, - { - "name": "Idempotency-Key", - "in": "header", - "required": false, - "schema": { - "type": "string", - "maxLength": 255 - }, - "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." - }, - { - "name": "X-Marfa-Instance-Id", - "in": "header", - "required": false, - "schema": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - }, - "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 20, + "example": [ + "019537a0-7b80-7000-8000-000000000001" + ], + "description": "The IDs of the items you would trash, at most 20." + } + }, + "required": [ + "ids" + ], + "additionalProperties": false + } + } } - ], + }, "responses": { "200": { - "description": "Returns the restored item and its metadata.", + "description": "Returns the items and what their trash would take.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemWithMetadata" + "$ref": "#/components/schemas/TrashPreview" } } }, @@ -12187,18 +13098,15 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, "400": { - "description": "- `invalid_id`: the ID is not a valid item ID.\n- `invalid_transition`: the item is not in the trash.\n- `validation_error`: `Idempotency-Key` is malformed.", + "description": "- `invalid_id`: an ID in `ids` is not an item ID.\n- `missing_required_field`: `ids` is missing.\n- `edge_constraint_violation`: a `cascade_on_delete: block` edge holds a named item or one its trash would take.\n- `validation_error`: a named item is a live `system.connection`, `ids` is empty or has over 20 IDs, or the body has a field this endpoint doesn't take.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidIdOrInvalidTransitionOrValidationErrorRefusal" + "$ref": "#/components/schemas/EdgeConstraintViolationOrInvalidIdOrMissingRequiredFieldOrValidationErrorRefusal" } } }, @@ -12220,9 +13128,6 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -12287,7 +13192,7 @@ } }, "404": { - "description": "- `item_not_found`: no item has this ID, or its type is one you can't read.", + "description": "- `item_not_found`: an ID in `ids` names no item, or names an item whose type you can't read. If that item is in the trash and you can read its type, `details.trashed` is `true`.", "content": { "application/json": { "schema": { @@ -12313,9 +13218,6 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -12324,14 +13226,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InstanceChangedRefusal" - }, - { - "$ref": "#/components/schemas/IdempotencyKeyInFlightRefusal" - } - ] + "$ref": "#/components/schemas/InstanceChangedRefusal" } } }, @@ -12353,9 +13248,6 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -12380,39 +13272,6 @@ } } }, - "422": { - "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - }, "429": { "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", "content": { @@ -12443,9 +13302,6 @@ }, "Retry-After": { "$ref": "#/components/headers/Retry-After" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -12476,9 +13332,6 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -12509,9 +13362,6 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -12542,60 +13392,11 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } } - } - } - }, - "/items/{id}/purge": { - "post": { - "operationId": "purgeItem", - "tags": [ - "Items" - ], - "summary": "Purge an item", - "description": "Permanently deletes a trashed item with its edges, metadata and extensions. Requires `items.purge` and write on the item's type. Marfa keeps a tombstone of the item's link and natural key, which `POST /items/lookup` reads.", - "security": [ - { - "bearerAuth": [] - } - ], + }, "parameters": [ - { - "schema": { - "type": "string", - "description": "The ID of the item." - }, - "required": true, - "description": "The ID of the item.", - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "description": "The version you read. If the item has changed since, nothing is deleted. Trashing doesn't change the version, so send the one you read before the trash." - }, - "required": false, - "description": "The version you read. If the item has changed since, nothing is deleted. Trashing doesn't change the version, so send the one you read before the trash.", - "name": "version", - "in": "query" - }, - { - "name": "Idempotency-Key", - "in": "header", - "required": false, - "schema": { - "type": "string", - "maxLength": 255 - }, - "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." - }, { "name": "X-Marfa-Instance-Id", "in": "header", @@ -12606,10 +13407,59 @@ }, "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." } + ] + } + }, + "/items/trash": { + "post": { + "operationId": "trashItems", + "tags": [ + "Items" + ], + "summary": "Trash items", + "description": "Moves the items, and every item a cascading edge such as `parent-of` reaches from them, to the trash in one step, all of them or none of them. You can restore them until the retention window ends. Marfa then purges them.", + "security": [ + { + "bearerAuth": [] + } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 20, + "example": [ + "019537a0-7b80-7000-8000-000000000001" + ], + "description": "The IDs of the items to trash, at most 20." + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "example": "9f2c1d3b5a7e4f60b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5", + "description": "The `digest` from `POST /items/trash-preview` for the same `ids`. If what the trash would take has changed since, nothing is trashed. Leave it out to trash what the items take now." + } + }, + "required": [ + "ids" + ], + "additionalProperties": false + } + } + } + }, "responses": { "200": { - "description": "Returns `{ \"ok\": true }`. Marfa announces each edge it deletes as `edge.deleted`, with `purged_with` naming this item.", + "description": "Returns `{ \"ok\": true }`. An item that a cascading edge took into the trash has `trashed_by_cascade`, and `trashed_with` names the named item that took it if you can read its type.", "content": { "application/json": { "schema": { @@ -12639,11 +13489,11 @@ } }, "400": { - "description": "- `invalid_id`: the ID is not a valid item ID.\n- `invalid_transition`: the item is not in the trash.\n- `validation_error`: the item is a live `system.connection` (end it with `DELETE /owner/sign-ins/{id}` first), or `version` is not a positive whole number.", + "description": "- `invalid_id`: an ID in `ids` is not an item ID.\n- `missing_required_field`: `ids` is missing.\n- `edge_constraint_violation`: a `cascade_on_delete: block` edge holds a named item or one its trash would take.\n- `validation_error`: a named item is a live `system.connection`, `ids` is empty or has over 20 IDs, `digest` is not a SHA-256 hash in hex, or the body has a field this endpoint doesn't take.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidIdOrInvalidTransitionOrValidationErrorRefusal" + "$ref": "#/components/schemas/EdgeConstraintViolationOrInvalidIdOrMissingRequiredFieldOrValidationErrorRefusal" } } }, @@ -12702,11 +13552,11 @@ } }, "403": { - "description": "- `forbidden`: you don't have `items.purge`, or the item is in a reserved namespace and is not in the trash.\n- `type_not_permitted`: you can read the item's type but don't have write on it, or your credential reaches no type.", + "description": "- `type_not_permitted`: you can read the item's type but don't have write on it, or your credential reaches no type. `details.grant` names the missing grant.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForbiddenOrTypeNotPermittedRefusal" + "$ref": "#/components/schemas/TypeNotPermittedRefusal" } } }, @@ -12732,7 +13582,7 @@ } }, "404": { - "description": "- `item_not_found`: no item has this ID, including one already purged, or its type is one you can't read.", + "description": "- `item_not_found`: an ID in `ids` names no item, or names an item whose type you can't read. If that item is in the trash and you can read its type, `details.trashed` is `true`.", "content": { "application/json": { "schema": { @@ -12765,13 +13615,13 @@ } }, "409": { - "description": "- `version_conflict`: `version` is stale. `current` is the item now, and nothing is purged.\n- `idempotency_key_in_flight`: a request with this `Idempotency-Key` is still running, and this one wrote nothing. Retry.", + "description": "- `trash_plan_changed`: what the trash would take no longer matches `digest`. `details.digest` is the digest now, and nothing is trashed.", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/ItemStaleVersion" + "$ref": "#/components/schemas/TrashPlanChangedRefusal" }, { "$ref": "#/components/schemas/InstanceChangedRefusal" @@ -12996,44 +13846,8 @@ } } } - } - } - }, - "/items/{id}/transition": { - "post": { - "operationId": "transitionItem", - "tags": [ - "Items" - ], - "summary": "Change an item's state", - "description": "Moves the item to `active`, `archived` or `trashed`. Moving to `trashed` deletes the item as `DELETE /items/{id}` does, and moving from `trashed` to `active` restores it as `POST /items/{id}/restore` does.", - "security": [ - { - "bearerAuth": [] - } - ], + }, "parameters": [ - { - "schema": { - "type": "string", - "description": "The ID of the item." - }, - "required": true, - "description": "The ID of the item.", - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "description": "The item version you read. If it differs, nothing moves. Leave it out to move the item as it is." - }, - "required": false, - "description": "The item version you read. If it differs, nothing moves. Leave it out to move the item as it is.", - "name": "version", - "in": "query" - }, { "name": "Idempotency-Key", "in": "header", @@ -13054,35 +13868,57 @@ }, "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." } + ] + } + }, + "/items/{id}/restore": { + "post": { + "operationId": "restoreItem", + "tags": [ + "Items" ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "state": { - "type": "string", - "enum": [ - "active", - "archived", - "trashed" - ], - "description": "The state to move the item to." - } - }, - "required": [ - "state" - ], - "additionalProperties": false - } - } + "summary": "Restore an item", + "description": "Restores a trashed item to `active`, along with every item its trash took through a cascading edge such as `parent-of`. Items that were already in the trash before then stay there.", + "security": [ + { + "bearerAuth": [] } - }, + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the item." + }, + "required": true, + "description": "The ID of the item.", + "name": "id", + "in": "path" + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "maxLength": 255 + }, + "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." + }, + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + } + ], "responses": { "200": { - "description": "Returns the item in its new state, with its metadata.", + "description": "Returns the restored item and its metadata.", "content": { "application/json": { "schema": { @@ -13112,11 +13948,11 @@ } }, "400": { - "description": "- `invalid_transition`: the type's lifecycle doesn't allow the move, such as `trashed` to `archived`. Restore first.\n- `edge_constraint_violation`: a `block` edge holds an item that a move to `trashed` would take.\n- `validation_error`: `state` is invalid, or the body has a field this endpoint doesn't take.\n- `missing_required_field`: `state` is missing.\n- `invalid_id`: the ID is not valid.", + "description": "- `invalid_id`: the ID is not a valid item ID.\n- `invalid_transition`: the item is not in the trash.\n- `validation_error`: `Idempotency-Key` is malformed.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EdgeConstraintViolationOrInvalidIdOrInvalidTransitionOrMissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/InvalidIdOrInvalidTransitionOrValidationErrorRefusal" } } }, @@ -13205,11 +14041,11 @@ } }, "404": { - "description": "- `item_not_found`: no item has this ID, or its type is one you can't read.\n- `edge_not_found`: a repeat under the `Idempotency-Key` would show an edge you can no longer read.", + "description": "- `item_not_found`: no item has this ID, or its type is one you can't read.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EdgeNotFoundOrItemNotFoundRefusal" + "$ref": "#/components/schemas/ItemNotFoundRefusal" } } }, @@ -13238,14 +14074,11 @@ } }, "409": { - "description": "`version_conflict`: `version` differs from the item. `current` is the item now; nothing moved.", + "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", "content": { "application/json": { "schema": { "anyOf": [ - { - "$ref": "#/components/schemas/ItemStaleVersion" - }, { "$ref": "#/components/schemas/InstanceChangedRefusal" }, @@ -13472,14 +14305,14 @@ } } }, - "/items/{id}/versions": { - "get": { - "operationId": "listItemVersions", + "/items/{id}/purge": { + "post": { + "operationId": "purgeItem", "tags": [ "Items" ], - "summary": "List item versions", - "description": "Returns a page of the item's version snapshots, oldest first. Marfa thins older snapshots over time, so the history can have gaps.", + "summary": "Purge an item", + "description": "Permanently deletes a trashed item with its edges, metadata and extensions. Requires `items.purge` and write on the item's type. Marfa keeps a tombstone of the item's link and natural key, which `POST /items/lookup` reads.", "security": [ { "bearerAuth": [] @@ -13500,33 +14333,41 @@ "schema": { "type": "integer", "minimum": 1, - "maximum": 200, - "default": 50, - "description": "The maximum number of results to return." + "description": "The version you read. If the item has changed since, nothing is deleted. Trashing doesn't change the version, so send the one you read before the trash." }, "required": false, - "description": "The maximum number of results to return.", - "name": "limit", + "description": "The version you read. If the item has changed since, nothing is deleted. Trashing doesn't change the version, so send the one you read before the trash.", + "name": "version", "in": "query" }, { + "name": "Idempotency-Key", + "in": "header", + "required": false, "schema": { "type": "string", - "description": "The `next_cursor` from the previous page. Leave it out to get the first page." + "maxLength": 255 }, + "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." + }, + { + "name": "X-Marfa-Instance-Id", + "in": "header", "required": false, - "description": "The `next_cursor` from the previous page. Leave it out to get the first page.", - "name": "cursor", - "in": "query" + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." } ], "responses": { "200": { - "description": "Returns the snapshots you can read. Each holds the properties before the write that replaced them, and the `type`, `tier`, `occurred_at` and `source_id` the item had at that version. Snapshots written under a type you can't read are left out, and a page fills past them.", + "description": "Returns `{ \"ok\": true }`. Marfa announces each edge it deletes as `edge.deleted`, with `purged_with` naming this item.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VersionPage" + "$ref": "#/components/schemas/Ok" } } }, @@ -13545,15 +14386,18 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, "400": { - "description": "- `invalid_id`: the ID is not a valid item ID.\n- `validation_error`: a query parameter is unknown or out of range, or `cursor` is malformed or came from another listing.", + "description": "- `invalid_id`: the ID is not a valid item ID.\n- `invalid_transition`: the item is not in the trash.\n- `validation_error`: the item is a live `system.connection` (end it with `DELETE /owner/sign-ins/{id}` first), or `version` is not a positive whole number.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidIdOrValidationErrorRefusal" + "$ref": "#/components/schemas/InvalidIdOrInvalidTransitionOrValidationErrorRefusal" } } }, @@ -13575,6 +14419,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -13609,11 +14456,11 @@ } }, "403": { - "description": "- `type_not_permitted`: your credential reaches no type.", + "description": "- `forbidden`: you don't have `items.purge`, or the item is in a reserved namespace and is not in the trash.\n- `type_not_permitted`: you can read the item's type but don't have write on it, or your credential reaches no type.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TypeNotPermittedRefusal" + "$ref": "#/components/schemas/ForbiddenOrTypeNotPermittedRefusal" } } }, @@ -13639,7 +14486,7 @@ } }, "404": { - "description": "- `item_not_found`: no item has this ID, the item is in the trash, or its type is one you can't read.", + "description": "- `item_not_found`: no item has this ID, including one already purged, or its type is one you can't read.", "content": { "application/json": { "schema": { @@ -13665,15 +14512,28 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, - "429": { - "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "409": { + "description": "- `version_conflict`: `version` is stale. `current` is the item now, and nothing is purged.\n- `idempotency_key_in_flight`: a request with this `Idempotency-Key` is still running, and this one wrote nothing. Retry.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RateLimitedRefusal" + "anyOf": [ + { + "$ref": "#/components/schemas/ItemStaleVersion" + }, + { + "$ref": "#/components/schemas/InstanceChangedRefusal" + }, + { + "$ref": "#/components/schemas/IdempotencyKeyInFlightRefusal" + } + ] } } }, @@ -13696,17 +14556,17 @@ "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" }, - "Retry-After": { - "$ref": "#/components/headers/Retry-After" + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, - "500": { - "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InternalErrorRefusal" + "$ref": "#/components/schemas/RequestTooLargeRefusal" } } }, @@ -13717,26 +14577,17 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } } }, - "503": { - "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "422": { + "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WriteContentionRefusal" + "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" } } }, @@ -13758,15 +14609,18 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, - "507": { - "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InsufficientStorageRefusal" + "$ref": "#/components/schemas/RateLimitedRefusal" } } }, @@ -13788,110 +14642,21 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } - } - } - } - }, - "/items/lookup": { - "post": { - "operationId": "lookupItems", - "tags": [ - "Items" - ], - "summary": "Look up items", - "description": "Finds items, in any state, by link, by natural key or by ID, and returns the tombstones that purges left for those keys. Name exactly one of `links`, `source` with `source_ids`, or `ids`, with at most 500 values.", - "security": [ - { - "bearerAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "The type the links are held in and the tombstones are kept under." - }, - "links": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "description": "Link values to name. `type` must name a `link_field`. Links held by a subtype's items aren't included." - }, - "source": { - "type": "string", - "minLength": 1, - "description": "The source the `source_ids` are natural keys under." - }, - "source_ids": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "description": "The `source_id` values to name under `source`." - }, - "ids": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Item IDs to look up, whatever their types." - }, - "include": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "edges" - ] - }, - "description": "`edges` adds each item's outbound edges, as `GET /items?include=edges` does." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - } - } - } - }, - "responses": { - "200": { - "description": "Returns the items found and the tombstones purges left for the keys named. A key that an item holds again has no tombstone. You get no tombstones if you can read only subtypes of `type`.", + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Item" - }, - "description": "The items found, in any state, in the order the request named their keys, each once. Items you can't read, and `system.*` items, are left out." - }, - "tombstones": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Tombstone" - }, - "description": "The tombstones under `type` for the keys named, in the order named. Empty when you look up by `ids`." - } - }, - "required": [ - "data", - "tombstones" - ] + "$ref": "#/components/schemas/InternalErrorRefusal" } } }, @@ -13910,15 +14675,21 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, - "400": { - "description": "- `missing_required_field`: `type` is missing.\n- `validation_error`: `type` is malformed, or the body doesn't name exactly one selector, or has more than 500 values, an empty value, an undeclared key, or `links` for a type with no `link_field`.\n- `unknown_type`: `type` isn't registered.\n- `invalid_id`: an ID in `ids` is malformed.", + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidIdOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" + "$ref": "#/components/schemas/WriteContentionRefusal" } } }, @@ -13940,6 +14711,190 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientStorageRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + } + } + } + }, + "/items/{id}/transition": { + "post": { + "operationId": "transitionItem", + "tags": [ + "Items" + ], + "summary": "Change an item's state", + "description": "Moves the item to `active`, `archived` or `trashed`. Moving to `trashed` deletes the item as `DELETE /items/{id}` does, and moving from `trashed` to `active` restores it as `POST /items/{id}/restore` does.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the item." + }, + "required": true, + "description": "The ID of the item.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "description": "The item version you read. If it differs, nothing moves. Leave it out to move the item as it is." + }, + "required": false, + "description": "The item version you read. If it differs, nothing moves. Leave it out to move the item as it is.", + "name": "version", + "in": "query" + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "maxLength": 255 + }, + "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." + }, + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "active", + "archived", + "trashed" + ], + "description": "The state to move the item to." + } + }, + "required": [ + "state" + ], + "additionalProperties": false + } + } + } + }, + "responses": { + "200": { + "description": "Returns the item in its new state, with its metadata.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemWithMetadata" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "400": { + "description": "- `invalid_transition`: the type's lifecycle doesn't allow the move, such as `trashed` to `archived`. Restore first.\n- `edge_constraint_violation`: a `block` edge holds an item that a move to `trashed` would take.\n- `validation_error`: `state` is invalid, or the body has a field this endpoint doesn't take.\n- `missing_required_field`: `state` is missing.\n- `invalid_id`: the ID is not valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EdgeConstraintViolationOrInvalidIdOrInvalidTransitionOrMissingRequiredFieldOrValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -13974,7 +14929,7 @@ } }, "403": { - "description": "- `type_not_permitted`: your credential reaches no type, or reads nothing under `type`.", + "description": "- `type_not_permitted`: you can read the item's type but don't have write on it, or your credential reaches no type. `details.grant` names the missing grant.", "content": { "application/json": { "schema": { @@ -14003,12 +14958,55 @@ } } }, + "404": { + "description": "- `item_not_found`: no item has this ID, or its type is one you can't read.\n- `edge_not_found`: a repeat under the `Idempotency-Key` would show an edge you can no longer read.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EdgeNotFoundOrItemNotFoundRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, "409": { - "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "description": "`version_conflict`: `version` differs from the item. `current` is the item now; nothing moved.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InstanceChangedRefusal" + "anyOf": [ + { + "$ref": "#/components/schemas/ItemStaleVersion" + }, + { + "$ref": "#/components/schemas/InstanceChangedRefusal" + }, + { + "$ref": "#/components/schemas/IdempotencyKeyInFlightRefusal" + } + ] } } }, @@ -14030,6 +15028,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -14054,6 +15055,39 @@ } } }, + "422": { + "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, "429": { "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", "content": { @@ -14084,6 +15118,9 @@ }, "Retry-After": { "$ref": "#/components/headers/Retry-After" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -14114,6 +15151,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -14144,6 +15184,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -14174,106 +15217,70 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } } - }, - "parameters": [ - { - "name": "X-Marfa-Instance-Id", - "in": "header", - "required": false, - "schema": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - }, - "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." - } - ] + } } }, - "/items/bulk-get": { - "post": { - "operationId": "bulkGetItems", + "/items/{id}/versions": { + "get": { + "operationId": "listItemVersions", "tags": [ "Items" ], - "summary": "Get items in bulk", - "description": "Returns up to 100 items by ID in one call, in the order you named them, each once. Items you can't read, trashed items and IDs that name nothing are left out.", + "summary": "List item versions", + "description": "Returns a page of the item's version snapshots, oldest first. Marfa thins older snapshots over time, so the history can have gaps.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "ids": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The IDs of the items to read, at most 100." - }, - "include": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "edges", - "metadata", - "extensions", - "system" - ] - }, - "description": "`edges`, `metadata` and `extensions` add that data to each item. `system` also returns `system.*` items, which are left out by default." - } - }, - "required": [ - "ids" - ], - "additionalProperties": false - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the item." + }, + "required": true, + "description": "The ID of the item.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50, + "description": "The maximum number of results to return." + }, + "required": false, + "description": "The maximum number of results to return.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "The `next_cursor` from the previous page. Leave it out to get the first page." + }, + "required": false, + "description": "The `next_cursor` from the previous page. Leave it out to get the first page.", + "name": "cursor", + "in": "query" } - }, + ], "responses": { "200": { - "description": "Returns the items you can read, `metadata` if you asked for it, and `listed` if you sent `X-Marfa-Reach`.", + "description": "Returns the snapshots you can read. Each holds the properties before the write that replaced them, and the `type`, `tier`, `occurred_at` and `source_id` the item had at that version. Snapshots written under a type you can't read are left out, and a page fills past them.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Item" - }, - "description": "The items you can read, in the order `ids` named them, each once. IDs that name nothing, a trashed item or an item whose type you can't read are left out." - }, - "metadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Metadata" - }, - "description": "The metadata of each item returned, in the same order. Present only when `include` has `metadata`." - }, - "listed": { - "type": "array", - "items": { - "type": "boolean" - }, - "description": "For each item returned, in the same order: `true` if listings show you the item, `false` if `source_filter` leaves it out of them. Present only when you send `X-Marfa-Reach`." - } - }, - "required": [ - "items" - ] + "$ref": "#/components/schemas/VersionPage" } } }, @@ -14292,18 +15299,15 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Marfa-Reach": { - "$ref": "#/components/headers/X-Marfa-Reach" } } }, "400": { - "description": "- `missing_required_field`: `ids` is missing.\n- `validation_error`: `ids` is not a list or has more than 100 IDs, or the body has a field this endpoint doesn't take.\n- `invalid_id`: an ID in `ids` is not a valid item ID.", + "description": "- `invalid_id`: the ID is not a valid item ID.\n- `validation_error`: a query parameter is unknown or out of range, or `cursor` is malformed or came from another listing.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidIdOrMissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/InvalidIdOrValidationErrorRefusal" } } }, @@ -14359,7 +15363,7 @@ } }, "403": { - "description": "- `type_not_permitted`: your credential reaches no type. If it reaches some types, items of the others are left out instead.", + "description": "- `type_not_permitted`: your credential reaches no type.", "content": { "application/json": { "schema": { @@ -14385,25 +15389,15 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "X-Marfa-Reach": { - "$ref": "#/components/headers/X-Marfa-Reach" } } }, - "409": { - "description": "`reach_changed`: the reach in `X-Marfa-Reach` has changed. Rebuild the working copy.", + "404": { + "description": "- `item_not_found`: no item has this ID, the item is in the trash, or its type is one you can't read.", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ReachChangedRefusal" - }, - { - "$ref": "#/components/schemas/InstanceChangedRefusal" - } - ] + "$ref": "#/components/schemas/ItemNotFoundRefusal" } } }, @@ -14428,27 +15422,6 @@ } } }, - "413": { - "description": "`request_too_large`: the request body is larger than this instance accepts.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestTooLargeRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - } - } - }, "429": { "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", "content": { @@ -14572,41 +15545,17 @@ } } } - }, - "parameters": [ - { - "name": "X-Marfa-Reach", - "in": "header", - "required": false, - "schema": { - "type": "string", - "pattern": "^[0-9a-f]{64}$", - "minLength": 64, - "maxLength": 64 - }, - "description": "The reach from a copy stream, for a working copy. Marfa reads the current data and checks the reach in one snapshot, and returns `409 reach_changed` if it has changed. Leave it out for an ordinary read." - }, - { - "name": "X-Marfa-Instance-Id", - "in": "header", - "required": false, - "schema": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - }, - "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." - } - ] + } } }, - "/items/bulk": { + "/items/lookup": { "post": { - "operationId": "bulkUpsertItems", + "operationId": "lookupItems", "tags": [ "Items" ], - "summary": "Upsert items in bulk", - "description": "Creates or updates up to 5,000 items in one call, matching existing items on `(source, source_id)`. The batch is atomic by default: one failed entry rolls it all back. Returns each entry's outcome.", + "summary": "Look up items", + "description": "Finds items, in any state, by link, by natural key or by ID, and returns the tombstones that purges left for those keys. Name exactly one of `links`, `source` with `source_ids`, or `ids`, with at most 500 values.", "security": [ { "bearerAuth": [] @@ -14619,116 +15568,1509 @@ "schema": { "type": "object", "properties": { - "items": { + "type": { + "type": "string", + "description": "The type the links are held in and the tombstones are kept under." + }, + "links": { "type": "array", "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "A UUIDv7 for the item. Leave it out and Marfa creates one. If no item matches the natural key, an `id` naming an existing item matches that item: under `upsert` only a live item you can read." - }, - "type": { - "type": "string", - "description": "The item's type identifier, such as `core.note`." - }, - "properties": { - "type": "object", - "additionalProperties": {}, - "description": "The item's properties, checked against the type's schema. Strict mode refuses an undeclared property as `invalid_properties` (`details.code` `unknown_property`). A whole number whose digits a 64-bit float doesn't keep is refused: send it as a string." - }, - "properties_mode": { - "type": "string", - "enum": [ - "merge", - "replace" - ], - "description": "How `properties` applies to an existing item, as in `PATCH /items/{id}`. `merge` (the default) lays them over its properties. `replace` takes them as the whole set. A new item takes them whole either way." - }, - "state": { - "$ref": "#/components/schemas/ItemState" - }, - "tier": { - "$ref": "#/components/schemas/Tier" - }, - "occurred_at": { - "type": "string" - }, - "source": { - "type": "string", - "description": "The source to key and stamp the entry's item with, as in `POST /items`. Defaults to your credential's own; it can also name one of your key's `sources`." - }, - "source_id": { - "type": "string", - "description": "The item's identifier at its source. With `source`, it is the natural key that matches an existing item." - }, - "version": { - "type": "integer", - "minimum": 0, - "description": "The version you read, used when the entry matches an existing item: the update then applies only if the item is still at this version." - }, - "tags": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "description": "Tags to put on the item: at most 100, each up to 128 characters." - }, - "edges": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": "Edge types to set, each mapped to the target item IDs the item should now point to. Types you don't name are untouched. An edge made this way carries no properties, so an edge type that requires one refuses the write." - } - }, - "required": [ - "type" - ] + "type": "string", + "minLength": 1 }, - "description": "The entries to write, at most 5,000. An entry with a key this operation doesn't declare is refused as `validation_error`, and `details.unknown_body_fields` names each key." + "description": "Link values to name. `type` must name a `link_field`. Links held by a subtype's items aren't included." }, - "mode": { + "source": { "type": "string", - "enum": [ - "upsert", - "create_only" - ], - "description": "`upsert` (the default) updates the item an entry matches. `create_only` skips it, reporting `skipped` with reason `duplicate_source`, or `duplicate_id` if it matched by `id`." + "minLength": 1, + "description": "The source the `source_ids` are natural keys under." }, - "atomic": { - "type": "boolean", - "description": "Whether one failed entry rolls back the whole batch. Defaults to `true`. With `false`, that entry is `errored` and the rest are written." + "source_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "The `source_id` values to name under `source`." }, - "enable_fanout": { - "type": "boolean", - "description": "Whether each write also calls outbound webhooks. Defaults to `false`. Marfa logs the events either way." + "ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Item IDs to look up, whatever their types." }, - "retype": { - "type": "boolean", - "description": "`true` moves an item to the entry's `type` when the entry resolves an existing item of another type. You need write on both types. Defaults to `false`." + "include": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "edges" + ] + }, + "description": "`edges` adds each item's outbound edges, as `GET /items?include=edges` does." } }, "required": [ - "items" + "type" + ], + "additionalProperties": false + } + } + } + }, + "responses": { + "200": { + "description": "Returns the items found and the tombstones purges left for the keys named. A key that an item holds again has no tombstone. You get no tombstones if you can read only subtypes of `type`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + "description": "The items found, in any state, in the order the request named their keys, each once. Items you can't read, and `system.*` items, are left out." + }, + "tombstones": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tombstone" + }, + "description": "The tombstones under `type` for the keys named, in the order named. Empty when you look up by `ids`." + } + }, + "required": [ + "data", + "tombstones" + ] + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "- `missing_required_field`: `type` is missing.\n- `validation_error`: `type` is malformed, or the body doesn't name exactly one selector, or has more than 500 values, an empty value, an undeclared key, or `links` for a type with no `link_field`.\n- `unknown_type`: `type` isn't registered.\n- `invalid_id`: an ID in `ids` is malformed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidIdOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "403": { + "description": "- `type_not_permitted`: your credential reaches no type, or reads nothing under `type`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeNotPermittedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "409": { + "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceChangedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTooLargeRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + } + } + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WriteContentionRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientStorageRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + } + }, + "parameters": [ + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + } + ] + } + }, + "/items/bulk-get": { + "post": { + "operationId": "bulkGetItems", + "tags": [ + "Items" + ], + "summary": "Get items in bulk", + "description": "Returns up to 100 items by ID in one call, in the order you named them, each once. Items you can't read, trashed items and IDs that name nothing are left out.", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The IDs of the items to read, at most 100." + }, + "include": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "edges", + "metadata", + "extensions", + "system" + ] + }, + "description": "`edges`, `metadata` and `extensions` add that data to each item. `system` also returns `system.*` items, which are left out by default." + } + }, + "required": [ + "ids" ], "additionalProperties": false } } - } - }, - "responses": { - "200": { - "description": "Returns `counts` and a `results` entry for each item, in order: `created`, `updated`, `skipped` or `errored`. Under `upsert`, an entry that matches a trashed item isn't written and is `skipped` with reason `trashed`. Under `create_only`, a matching entry is `skipped` with reason `duplicate_source` or `duplicate_id`.", + } + }, + "responses": { + "200": { + "description": "Returns the items you can read, `metadata` if you asked for it, and `listed` if you sent `X-Marfa-Reach`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + }, + "description": "The items you can read, in the order `ids` named them, each once. IDs that name nothing, a trashed item or an item whose type you can't read are left out." + }, + "metadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Metadata" + }, + "description": "The metadata of each item returned, in the same order. Present only when `include` has `metadata`." + }, + "listed": { + "type": "array", + "items": { + "type": "boolean" + }, + "description": "For each item returned, in the same order: `true` if listings show you the item, `false` if `source_filter` leaves it out of them. Present only when you send `X-Marfa-Reach`." + } + }, + "required": [ + "items" + ] + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Marfa-Reach": { + "$ref": "#/components/headers/X-Marfa-Reach" + } + } + }, + "400": { + "description": "- `missing_required_field`: `ids` is missing.\n- `validation_error`: `ids` is not a list or has more than 100 IDs, or the body has a field this endpoint doesn't take.\n- `invalid_id`: an ID in `ids` is not a valid item ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidIdOrMissingRequiredFieldOrValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "403": { + "description": "- `type_not_permitted`: your credential reaches no type. If it reaches some types, items of the others are left out instead.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeNotPermittedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "X-Marfa-Reach": { + "$ref": "#/components/headers/X-Marfa-Reach" + } + } + }, + "409": { + "description": "`reach_changed`: the reach in `X-Marfa-Reach` has changed. Rebuild the working copy.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ReachChangedRefusal" + }, + { + "$ref": "#/components/schemas/InstanceChangedRefusal" + } + ] + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTooLargeRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + } + } + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WriteContentionRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientStorageRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + } + }, + "parameters": [ + { + "name": "X-Marfa-Reach", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "minLength": 64, + "maxLength": 64 + }, + "description": "The reach from a copy stream, for a working copy. Marfa reads the current data and checks the reach in one snapshot, and returns `409 reach_changed` if it has changed. Leave it out for an ordinary read." + }, + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + } + ] + } + }, + "/items/bulk": { + "post": { + "operationId": "bulkUpsertItems", + "tags": [ + "Items" + ], + "summary": "Upsert items in bulk", + "description": "Creates or updates up to 5,000 items in one call, matching existing items on `(source, source_id)`. The batch is atomic by default: one failed entry rolls it all back. Returns each entry's outcome.", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A UUIDv7 for the item. Leave it out and Marfa creates one. If no item matches the natural key, an `id` naming an existing item matches that item: under `upsert` only a live item you can read." + }, + "type": { + "type": "string", + "description": "The item's type identifier, such as `core.note`." + }, + "properties": { + "type": "object", + "additionalProperties": {}, + "description": "The item's properties, checked against the type's schema. Strict mode refuses an undeclared property as `invalid_properties` (`details.code` `unknown_property`). A whole number whose digits a 64-bit float doesn't keep is refused: send it as a string." + }, + "properties_mode": { + "type": "string", + "enum": [ + "merge", + "replace" + ], + "description": "How `properties` applies to an existing item, as in `PATCH /items/{id}`. `merge` (the default) lays them over its properties. `replace` takes them as the whole set. A new item takes them whole either way." + }, + "state": { + "$ref": "#/components/schemas/ItemState" + }, + "tier": { + "$ref": "#/components/schemas/Tier" + }, + "occurred_at": { + "type": "string" + }, + "source": { + "type": "string", + "description": "The source to key and stamp the entry's item with, as in `POST /items`. Defaults to your credential's own; it can also name one of your key's `sources`." + }, + "source_id": { + "type": "string", + "description": "The item's identifier at its source. With `source`, it is the natural key that matches an existing item." + }, + "version": { + "type": "integer", + "minimum": 0, + "description": "The version you read, used when the entry matches an existing item: the update then applies only if the item is still at this version." + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "description": "Tags to put on the item: at most 100, each up to 128 characters." + }, + "edges": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Edge types to set, each mapped to the target item IDs the item should now point to. An edge to an ID you still list stays as it is, with its properties. Types you don't name are untouched. A new edge made this way carries no properties, so an edge type that requires one refuses the write." + } + }, + "required": [ + "type" + ] + }, + "description": "The entries to write, at most 5,000. An entry with a key this operation doesn't declare is refused as `validation_error`, and `details.unknown_body_fields` names each key." + }, + "mode": { + "type": "string", + "enum": [ + "upsert", + "create_only" + ], + "description": "`upsert` (the default) updates the item an entry matches. `create_only` skips it, reporting `skipped` with reason `duplicate_source`, or `duplicate_id` if it matched by `id`." + }, + "atomic": { + "type": "boolean", + "description": "Whether one failed entry rolls back the whole batch. Defaults to `true`. With `false`, that entry is `errored` and the rest are written." + }, + "enable_fanout": { + "type": "boolean", + "description": "Whether each write also calls outbound webhooks. Defaults to `false`. Marfa logs the events either way." + }, + "retype": { + "type": "boolean", + "description": "`true` moves an item to the entry's `type` when the entry resolves an existing item of another type. You need write on both types. Defaults to `false`." + } + }, + "required": [ + "items" + ], + "additionalProperties": false + } + } + } + }, + "responses": { + "200": { + "description": "Returns `counts` and a `results` entry for each item, in order: `created`, `updated`, `skipped` or `errored`. Under `upsert`, an entry that matches a trashed item isn't written and is `skipped` with reason `trashed`. Under `create_only`, a matching entry is `skipped` with reason `duplicate_source` or `duplicate_id`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkResponse" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "- `validation_error`: the body is malformed, has an undeclared key or has over 5,000 entries.\n- `missing_required_field`: a required field is missing.\n- `invalid_properties`: an entry's `properties` hold a whole number a 64-bit float can't keep.\n- `bulk_atomic_rollback`: an `atomic` entry was refused and nothing was written (`details.code`, `details.index`). The status is the refusal's own.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkAtomicRollbackOrInvalidPropertiesOrMissingRequiredFieldOrValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "403": { + "description": "- `type_not_permitted`: you don't have write on an entry's type, or on the type its natural key matches, or your credential reaches no type.\n- `forbidden`: an entry names a source your key doesn't claim or the instance's source allow-list excludes, or moves a natural key under a source your key doesn't write under.\n- `bulk_atomic_rollback`: one of these under `atomic`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkAtomicRollbackOrForbiddenOrTypeNotPermittedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "404": { + "description": "- `bulk_atomic_rollback`: with `atomic` true, an entry names an item or edge type that isn't there, such as an edge target. `details.code` is `item_not_found` or `edge_type_not_found`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkAtomicRollbackRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "409": { + "description": "- `bulk_atomic_rollback`: with `atomic` true, an entry's item has moved or is taken. `details.code` names the cause, such as `version_conflict`, `link_taken`, `type_mismatch` (the natural key matched an item of another type) or `id_reused` (the entry's `id` belongs to an item it doesn't describe).", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/BulkAtomicRollbackRefusal" + }, + { + "$ref": "#/components/schemas/InstanceChangedRefusal" + } + ] + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTooLargeRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + } + } + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WriteContentionRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientStorageRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + } + }, + "parameters": [ + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + } + ] + } + }, + "/items/stats": { + "get": { + "operationId": "getItemStats", + "tags": [ + "Items" + ], + "summary": "Get item counts", + "description": "Returns item counts by state or type using the filters of `GET /items`. Counts every state unless you name one. The owner can use their browser session.", + "security": [ + { + "bearerAuth": [] + }, + { + "ownerSession": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "enum": [ + "state", + "type" + ], + "description": "Grouping axis. Defaults to `state`." + }, + "required": false, + "description": "Grouping axis. Defaults to `state`.", + "name": "by", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Only return items of these types and their subtypes: a comma-separated list of up to 10, such as `core.note,app.*`. A wildcard such as `core.*` matches every type under that prefix." + }, + "required": false, + "description": "Only return items of these types and their subtypes: a comma-separated list of up to 10, such as `core.note,app.*`. A wildcard such as `core.*` matches every type under that prefix.", + "name": "type", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Only count items in these lifecycle states: one state or a comma-separated list, such as `active,archived`. Without it, every state is counted, as with `any`." + }, + "required": false, + "description": "Only count items in these lifecycle states: one state or a comma-separated list, such as `active,archived`. Without it, every state is counted, as with `any`.", + "name": "state", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Only return items stamped with this source." + }, + "required": false, + "description": "Only return items stamped with this source.", + "name": "source", + "in": "query" + }, + { + "schema": { + "type": "string", + "enum": [ + "library", + "feed", + "all" + ], + "description": "Only return items in this tier. Omit it or send `all` for both tiers." + }, + "required": false, + "description": "Only return items in this tier. Omit it or send `all` for both tiers.", + "name": "tier", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Comma-separated tags. Only return items that carry all of them." + }, + "required": false, + "description": "Comma-separated tags. Only return items that carry all of them.", + "name": "tags", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "A filter expression. A term naming an edge type, `edge[]` or `backref[]`, needs read on it; `edge[]=` also works as a query parameter. `status_group` matches by status group; `properties..`, by a list entry." + }, + "required": false, + "description": "A filter expression. A term naming an edge type, `edge[]` or `backref[]`, needs read on it; `edge[]=` also works as a query parameter. `status_group` matches by status group; `properties..`, by a list entry.", + "name": "filter", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "description": "Only return items whose own time (`occurred_at`, else `created_at`) is after this time. For the modification time, use `updated_after`." + }, + "required": false, + "description": "Only return items whose own time (`occurred_at`, else `created_at`) is after this time. For the modification time, use `updated_after`.", + "name": "occurred_after", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "description": "Only return items whose own time (`occurred_at`, else `created_at`) is before this time." + }, + "required": false, + "description": "Only return items whose own time (`occurred_at`, else `created_at`) is before this time.", + "name": "occurred_before", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "description": "Only return items changed at or after this time. Send the latest `updated_at` you hold, and deduplicate by ID, as items can share an instant. Ordered by `updated_at`, then ID, ascending, so leave out `sort` and `direction`. Purges aren't reported." + }, + "required": false, + "description": "Only return items changed at or after this time. Send the latest `updated_at` you hold, and deduplicate by ID, as items can share an instant. Ordered by `updated_at`, then ID, ascending, so leave out `sort` and `direction`. Purges aren't reported.", + "name": "updated_after", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "description": "Only return items changed before this time. It doesn't change the ordering, so it works with any `sort`." + }, + "required": false, + "description": "Only return items changed before this time. It doesn't change the ordering, so it works with any `sort`.", + "name": "updated_before", + "in": "query" + }, + { + "schema": { + "type": "string", + "enum": [ + "system" + ], + "description": "`system` counts `system.*` items too, which are left out by default. A `type` filter in the `system.` namespace opts in on its own." + }, + "required": false, + "description": "`system` counts `system.*` items too, which are left out by default. A `type` filter in the `system.` namespace opts in on its own.", + "name": "include", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Returns an object that maps each state, or each type, to its count.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "number" + } + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "- `validation_error`: a query parameter is unknown or invalid, or `by` is not `state` or `type`.\n- `unknown_type`: `type` is a concrete type that nothing registers.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownTypeOrValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "403": { + "description": "- `type_not_permitted`: your credential reaches no type, or `type` names a type you can't read with none readable under it.\n- `edge_permission_denied`: the filter has an `edge` or `backref` term for an edge type you can't read.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkResponse" + "$ref": "#/components/schemas/EdgePermissionDeniedOrTypeNotPermittedRefusal" } } }, @@ -14747,15 +17089,18 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" } } }, - "400": { - "description": "- `validation_error`: the body is malformed, has an undeclared key or has over 5,000 entries.\n- `missing_required_field`: a required field is missing.\n- `invalid_properties`: an entry's `properties` hold a whole number a 64-bit float can't keep.\n- `bulk_atomic_rollback`: an `atomic` entry was refused and nothing was written (`details.code`, `details.index`). The status is the refusal's own.", + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkAtomicRollbackOrInvalidPropertiesOrMissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/RateLimitedRefusal" } } }, @@ -14777,15 +17122,18 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" } } }, - "401": { - "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedRefusal" + "$ref": "#/components/schemas/InternalErrorRefusal" } } }, @@ -14810,12 +17158,12 @@ } } }, - "403": { - "description": "- `type_not_permitted`: you don't have write on an entry's type, or on the type its natural key matches, or your credential reaches no type.\n- `forbidden`: an entry names a source your key doesn't claim or the instance's source allow-list excludes, or moves a natural key under a source your key doesn't write under.\n- `bulk_atomic_rollback`: one of these under `atomic`.", + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkAtomicRollbackOrForbiddenOrTypeNotPermittedRefusal" + "$ref": "#/components/schemas/WriteContentionRefusal" } } }, @@ -14840,12 +17188,12 @@ } } }, - "404": { - "description": "- `bulk_atomic_rollback`: with `atomic` true, an entry names an item or edge type that isn't there, such as an edge target. `details.code` is `item_not_found` or `edge_type_not_found`.", + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkAtomicRollbackRefusal" + "$ref": "#/components/schemas/InsufficientStorageRefusal" } } }, @@ -14869,20 +17217,116 @@ "$ref": "#/components/headers/X-Error-Code" } } + } + } + } + }, + "/items/summaries": { + "get": { + "operationId": "listItemSummaries", + "tags": [ + "Items" + ], + "summary": "List item summaries", + "description": "Returns item names, types, versions, sources and creation times, newest first. Only the owner or a local command can use this operation. Trashed and system items are excluded.", + "security": [ + { + "ownerSession": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "Only return items of this exact type." + }, + "required": false, + "description": "Only return items of this exact type.", + "name": "type", + "in": "query" }, - "409": { - "description": "- `bulk_atomic_rollback`: with `atomic` true, an entry's item has moved or is taken. `details.code` names the cause, such as `version_conflict`, `link_taken`, `type_mismatch` (the natural key matched an item of another type) or `id_reused` (the entry's `id` belongs to an item it doesn't describe).", + { + "schema": { + "type": "string", + "maxLength": 500, + "description": "Only return names containing this text, ignoring case and surrounding whitespace." + }, + "required": false, + "description": "Only return names containing this text, ignoring case and surrounding whitespace.", + "name": "q", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50, + "description": "The maximum number of results to return." + }, + "required": false, + "description": "The maximum number of results to return.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "The `next_cursor` from the previous page. Leave it out to get the first page." + }, + "required": false, + "description": "The `next_cursor` from the previous page. Leave it out to get the first page.", + "name": "cursor", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Returns a page of item summaries.", "content": { "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/BulkAtomicRollbackRefusal" - }, + "example": { + "data": [ { - "$ref": "#/components/schemas/InstanceChangedRefusal" + "id": "01900000-0000-7000-8000-000000000001", + "name": "Shopping list", + "type": "core.note", + "version": 1, + "source": "notes", + "created_at": "2026-01-01T12:00:00.000Z" } - ] + ], + "next_cursor": null + }, + "schema": { + "$ref": "#/components/schemas/ItemSummaryPage" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "`validation_error`: a filter or cursor is invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorRefusal" } } }, @@ -14907,12 +17351,42 @@ } } }, - "413": { - "description": "`request_too_large`: the request body is larger than this instance accepts.", + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RequestTooLargeRefusal" + "$ref": "#/components/schemas/UnauthorizedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "403": { + "description": "`forbidden`: the request does not carry direct owner or local authority.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenRefusal" } } }, @@ -14923,6 +17397,15 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } @@ -15051,185 +17534,61 @@ } } } - }, - "parameters": [ - { - "name": "X-Marfa-Instance-Id", - "in": "header", - "required": false, - "schema": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - }, - "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." - } - ] + } } }, - "/items/stats": { + "/occurrences": { "get": { - "operationId": "getItemStats", + "operationId": "listOccurrences", "tags": [ "Items" ], - "summary": "Get item counts", - "description": "Returns item counts by state or type using the filters of `GET /items`. Counts every state unless you name one. The owner can use their browser session.", + "summary": "List event occurrences", + "description": "Returns the events that overlap a time window, with each recurring event expanded into one entry per occurrence. Marfa computes occurrences when you read them and doesn't store them.", "security": [ { "bearerAuth": [] - }, - { - "ownerSession": [] } ], "parameters": [ { "schema": { "type": "string", - "enum": [ - "state", - "type" - ], - "description": "Grouping axis. Defaults to `state`." - }, - "required": false, - "description": "Grouping axis. Defaults to `state`.", - "name": "by", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "Only return items of this type or a subtype. A wildcard such as `core.*` matches every type under that prefix." - }, - "required": false, - "description": "Only return items of this type or a subtype. A wildcard such as `core.*` matches every type under that prefix.", - "name": "type", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "Only count items in this lifecycle state. Without it, every state is counted, as with `any`." - }, - "required": false, - "description": "Only count items in this lifecycle state. Without it, every state is counted, as with `any`.", - "name": "state", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "Only return items stamped with this source." - }, - "required": false, - "description": "Only return items stamped with this source.", - "name": "source", - "in": "query" - }, - { - "schema": { - "type": "string", - "enum": [ - "library", - "feed", - "all" - ], - "description": "Only return items in this tier. Omit it or send `all` for both tiers." - }, - "required": false, - "description": "Only return items in this tier. Omit it or send `all` for both tiers.", - "name": "tier", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "Comma-separated tags. Only return items that carry all of them." - }, - "required": false, - "description": "Comma-separated tags. Only return items that carry all of them.", - "name": "tags", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "A filter expression. A term naming an edge type, `edge[]` or `backref[]`, matches by relationship and needs read on that edge type. `edge[]=` also works as a query parameter of its own." - }, - "required": false, - "description": "A filter expression. A term naming an edge type, `edge[]` or `backref[]`, matches by relationship and needs read on that edge type. `edge[]=` also works as a query parameter of its own.", - "name": "filter", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "description": "Only return items whose own time (`occurred_at`, else `created_at`) is after this time. For the modification time, use `updated_after`." - }, - "required": false, - "description": "Only return items whose own time (`occurred_at`, else `created_at`) is after this time. For the modification time, use `updated_after`.", - "name": "occurred_after", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "description": "Only return items whose own time (`occurred_at`, else `created_at`) is before this time." - }, - "required": false, - "description": "Only return items whose own time (`occurred_at`, else `created_at`) is before this time.", - "name": "occurred_before", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "description": "Only return items changed at or after this time. Send the latest `updated_at` you hold, and deduplicate by ID, as items can share an instant. Ordered by `updated_at`, then ID, ascending, so leave out `sort` and `direction`. Purges aren't reported." + "description": "Window start, ISO 8601. No zone means UTC; a date is its UTC midnight. An event overlaps if it starts before `to` and ends after this. Its end is `ends_at`, else start plus `duration`. A whole-day event fills whole days in its `timezone`, else UTC." }, - "required": false, - "description": "Only return items changed at or after this time. Send the latest `updated_at` you hold, and deduplicate by ID, as items can share an instant. Ordered by `updated_at`, then ID, ascending, so leave out `sort` and `direction`. Purges aren't reported.", - "name": "updated_after", + "required": true, + "description": "Window start, ISO 8601. No zone means UTC; a date is its UTC midnight. An event overlaps if it starts before `to` and ends after this. Its end is `ends_at`, else start plus `duration`. A whole-day event fills whole days in its `timezone`, else UTC.", + "name": "from", "in": "query" }, { "schema": { "type": "string", - "minLength": 1, - "description": "Only return items changed before this time. It doesn't change the ordering, so it works with any `sort`." + "description": "Window end, ISO 8601, read as `from` is. The window can span at most 400 days. An event with no length is in the window if it starts at or after `from` and before this time." }, - "required": false, - "description": "Only return items changed before this time. It doesn't change the ordering, so it works with any `sort`.", - "name": "updated_before", + "required": true, + "description": "Window end, ISO 8601, read as `from` is. The window can span at most 400 days. An event with no length is in the window if it starts at or after `from` and before this time.", + "name": "to", "in": "query" }, { "schema": { "type": "string", - "enum": [ - "system" - ], - "description": "`system` counts `system.*` items too, which are left out by default. A `type` filter in the `system.` namespace opts in on its own." + "description": "Only return events of this type, or of the event types a wildcard matches." }, "required": false, - "description": "`system` counts `system.*` items too, which are left out by default. A `type` filter in the `system.` namespace opts in on its own.", - "name": "include", + "description": "Only return events of this type, or of the event types a wildcard matches.", + "name": "type", "in": "query" } ], "responses": { "200": { - "description": "Returns an object that maps each state, or each type, to its count.", + "description": "Returns occurrences ordered by start time. A recurring event gives an entry per occurrence, with `series_id`. A stored exception replaces its occurrence and carries `replaces`. A rule Marfa can't read or fully apply is listed in `series_errors`, and `expansion_incomplete` is `true` if an expansion didn't finish.", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "type": "number" - } + "$ref": "#/components/schemas/OccurrencePage" } } }, @@ -15252,11 +17611,11 @@ } }, "400": { - "description": "- `validation_error`: a query parameter is unknown or invalid, or `by` is not `state` or `type`.\n- `unknown_type`: `type` is a concrete type that nothing registers.", + "description": "- `missing_required_field`: `from` or `to` is missing.\n- `unknown_type`: `type` isn't registered.\n- `validation_error`: a time is unreadable, `to` isn't after `from`, the window is over 400 days, `type` is malformed, or the window holds more than 5,000 occurrences. Then `details` has `max_occurrences` and `found`, and `expansion_incomplete` if a series also stopped expanding.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnknownTypeOrValidationErrorRefusal" + "$ref": "#/components/schemas/MissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" } } }, @@ -15312,11 +17671,11 @@ } }, "403": { - "description": "- `type_not_permitted`: your credential reaches no type, or `type` names a type you can't read with none readable under it.\n- `edge_permission_denied`: the filter has an `edge` or `backref` term for an edge type you can't read.", + "description": "- `type_not_permitted`: your credential reaches no type, or `type` is a registered type you can't read with none readable under it.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EdgePermissionDeniedOrTypeNotPermittedRefusal" + "$ref": "#/components/schemas/TypeNotPermittedRefusal" } } }, @@ -15463,89 +17822,145 @@ "$ref": "#/components/headers/X-Error-Code" } } - } - } - } - }, - "/items/summaries": { - "get": { - "operationId": "listItemSummaries", - "tags": [ - "Items" - ], - "summary": "List item summaries", - "description": "Returns item names, types, versions, sources and creation times, newest first. Only the owner or a local command can use this operation. Trashed and system items are excluded.", - "security": [ - { - "ownerSession": [] - } - ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "Only return items of this exact type." - }, - "required": false, - "description": "Only return items of this exact type.", - "name": "type", - "in": "query" + } + } + } + }, + "/items/tombstones": { + "post": { + "operationId": "settleTombstones", + "tags": [ + "Items" + ], + "summary": "Settle tombstones", + "description": "Settles tombstones, moving their `settled_at` later, never earlier, and returns them. A connector calls it after changing the vendor's copy to carry out a purge, so that change doesn't bring the item back as a new row.", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type the links are held in and the tombstones are kept under." + }, + "links": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "Link values to name. `type` must name a `link_field`. Links held by a subtype's items aren't included." + }, + "source": { + "type": "string", + "minLength": 1, + "description": "The source the `source_ids` are natural keys under." + }, + "source_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "The `source_id` values to name under `source`." + }, + "settled_at": { + "type": "string", + "description": "The time of the vendor's own change that carried out the purge, as an RFC 3339 time. A tombstone takes it only if it is later than the tombstone's current `settled_at`." + } + }, + "required": [ + "type", + "settled_at" + ], + "additionalProperties": false + } + } + } + }, + "responses": { + "200": { + "description": "Returns the named tombstones as they now stand. A key with no tombstone under `type` is left out.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tombstones": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tombstone" + }, + "description": "The named tombstones as they stand after the write, in the order named. A key with no tombstone under `type` is left out." + } + }, + "required": [ + "tombstones" + ] + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "- `missing_required_field`: `type` or `settled_at` is missing.\n- `validation_error`: `type` or `settled_at` is malformed, the body doesn't name exactly one of `links` or `source` with `source_ids`, or has more than 500 values, an empty value, an undeclared key, or `links` for a type with no `link_field`.\n- `unknown_type`: `type` isn't registered.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } }, - { - "schema": { - "type": "string", - "maxLength": 500, - "description": "Only return names containing this text, ignoring case and surrounding whitespace." - }, - "required": false, - "description": "Only return names containing this text, ignoring case and surrounding whitespace.", - "name": "q", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 50, - "description": "The maximum number of results to return." - }, - "required": false, - "description": "The maximum number of results to return.", - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "The `next_cursor` from the previous page. Leave it out to get the first page." - }, - "required": false, - "description": "The `next_cursor` from the previous page. Leave it out to get the first page.", - "name": "cursor", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Returns a page of item summaries.", + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", "content": { "application/json": { - "example": { - "data": [ - { - "id": "01900000-0000-7000-8000-000000000001", - "name": "Shopping list", - "type": "core.note", - "version": 1, - "source": "notes", - "created_at": "2026-01-01T12:00:00.000Z" - } - ], - "next_cursor": null - }, "schema": { - "$ref": "#/components/schemas/ItemSummaryPage" + "$ref": "#/components/schemas/UnauthorizedRefusal" } } }, @@ -15564,15 +17979,18 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" } } }, - "400": { - "description": "`validation_error`: a filter or cursor is invalid.", + "403": { + "description": "- `type_not_permitted`: you don't have write on `type`.\n- `forbidden`: `source` is not your credential's own or one of your key's `sources`. `details.source` names it.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorRefusal" + "$ref": "#/components/schemas/ForbiddenOrTypeNotPermittedRefusal" } } }, @@ -15597,12 +18015,12 @@ } } }, - "401": { - "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "409": { + "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedRefusal" + "$ref": "#/components/schemas/InstanceChangedRefusal" } } }, @@ -15627,12 +18045,12 @@ } } }, - "403": { - "description": "`forbidden`: the request does not carry direct owner or local authority.", + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForbiddenRefusal" + "$ref": "#/components/schemas/RequestTooLargeRefusal" } } }, @@ -15643,15 +18061,6 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } @@ -15780,61 +18189,87 @@ } } } - } + }, + "parameters": [ + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + } + ] } }, - "/occurrences": { + "/trash": { "get": { - "operationId": "listOccurrences", + "operationId": "listTrash", "tags": [ - "Items" + "Trash" ], - "summary": "List event occurrences", - "description": "Returns the events that overlap a time window, with each recurring event expanded into one entry per occurrence. Marfa computes occurrences when you read them and doesn't store them.", + "summary": "List the trash", + "description": "Returns a page of trash summaries, newest creation first, without item content. Only the owner or a local command can use this operation.", "security": [ { - "bearerAuth": [] + "ownerSession": [] } ], "parameters": [ { "schema": { "type": "string", - "description": "Window start, ISO 8601. An event overlaps the window if it starts before `to` and ends after this time. Its end is `ends_at`, else its start plus `duration`. A whole-day event fills whole days in its `timezone`, or in UTC if it has none." + "description": "Only return items of this exact type." }, - "required": true, - "description": "Window start, ISO 8601. An event overlaps the window if it starts before `to` and ends after this time. Its end is `ends_at`, else its start plus `duration`. A whole-day event fills whole days in its `timezone`, or in UTC if it has none.", - "name": "from", + "required": false, + "description": "Only return items of this exact type.", + "name": "type", "in": "query" }, { "schema": { "type": "string", - "description": "Window end, ISO 8601. The window can span at most 400 days. An event with no length is in the window if it starts at or after `from` and before this time." + "maxLength": 500, + "description": "Only return item names containing this text, ignoring case and surrounding whitespace." }, - "required": true, - "description": "Window end, ISO 8601. The window can span at most 400 days. An event with no length is in the window if it starts at or after `from` and before this time.", - "name": "to", + "required": false, + "description": "Only return item names containing this text, ignoring case and surrounding whitespace.", + "name": "q", "in": "query" }, { "schema": { "type": "string", - "description": "Only return events of this type, or of the event types a wildcard matches." + "description": "The `next_cursor` from the previous page. Leave it out to get the first page." }, "required": false, - "description": "Only return events of this type, or of the event types a wildcard matches.", - "name": "type", + "description": "The `next_cursor` from the previous page. Leave it out to get the first page.", + "name": "cursor", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 25, + "description": "The maximum number of results to return." + }, + "required": false, + "description": "The maximum number of results to return.", + "name": "limit", "in": "query" } ], "responses": { "200": { - "description": "Returns occurrences ordered by start time. A recurring event gives an entry per occurrence, with `series_id`. A stored exception replaces its occurrence and carries `replaces`. A rule Marfa can't read or fully apply is listed in `series_errors`, and `expansion_incomplete` is `true` if an expansion didn't finish.", + "description": "Returns trash summaries and the effective retention in days. Zero disables automatic expiry.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OccurrencePage" + "$ref": "#/components/schemas/TrashPage" } } }, @@ -15857,11 +18292,11 @@ } }, "400": { - "description": "- `missing_required_field`: `from` or `to` is missing.\n- `unknown_type`: `type` isn't registered.\n- `validation_error`: a time is unreadable, `to` isn't after `from`, the window is over 400 days, `type` is malformed, or the window holds more than 5,000 occurrences. Then `details` has `max_occurrences` and `found`, and `expansion_incomplete` if a series also stopped expanding.", + "description": "`validation_error`: invalid page or cursor.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" + "$ref": "#/components/schemas/ValidationErrorRefusal" } } }, @@ -15917,11 +18352,11 @@ } }, "403": { - "description": "- `type_not_permitted`: your credential reaches no type, or `type` is a registered type you can't read with none readable under it.", + "description": "`forbidden`: requires direct owner or local authority, and a recent sign-in for changes.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TypeNotPermittedRefusal" + "$ref": "#/components/schemas/ForbiddenRefusal" } } }, @@ -16068,115 +18503,121 @@ "$ref": "#/components/headers/X-Error-Code" } } - } - } - } - }, - "/items/tombstones": { - "post": { - "operationId": "settleTombstones", - "tags": [ - "Items" - ], - "summary": "Settle tombstones", - "description": "Settles tombstones, moving their `settled_at` later, never earlier, and returns them. A connector calls it after changing the vendor's copy to carry out a purge, so that change doesn't bring the item back as a new row.", - "security": [ - { - "bearerAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "The type the links are held in and the tombstones are kept under." - }, - "links": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "description": "Link values to name. `type` must name a `link_field`. Links held by a subtype's items aren't included." - }, - "source": { - "type": "string", - "minLength": 1, - "description": "The source the `source_ids` are natural keys under." - }, - "source_ids": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "description": "The `source_id` values to name under `source`." - }, - "settled_at": { - "type": "string", - "description": "The time of the vendor's own change that carried out the purge, as an RFC 3339 time. A tombstone takes it only if it is later than the tombstone's current `settled_at`." - } - }, - "required": [ - "type", - "settled_at" - ], - "additionalProperties": false - } - } - } - }, - "responses": { - "200": { - "description": "Returns the named tombstones as they now stand. A key with no tombstone under `type` is left out.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tombstones": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Tombstone" - }, - "description": "The named tombstones as they stand after the write, in the order named. A key with no tombstone under `type` is left out." - } - }, - "required": [ - "tombstones" - ] - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - } - } + } + } + } + }, + "/items/summaries/{id}/trash": { + "post": { + "operationId": "trashItemSummary", + "tags": [ + "Trash" + ], + "summary": "Trash an item by summary", + "description": "Moves the item and its cascading children to the trash. Returns only a summary. Requires the version from the listing.", + "security": [ + { + "ownerSession": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the item." + }, + "required": true, + "description": "The ID of the item.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "The item version from the summary listing. If it differs, nothing changes." + }, + "required": true, + "description": "The item version from the summary listing. If it differs, nothing changes.", + "name": "version", + "in": "query" + }, + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + } + ], + "responses": { + "200": { + "description": "The action completed. Returns only the item summary.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemSummary" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "- `invalid_id`: the item ID is invalid.\n- `validation_error`: the version is invalid or a live connection prevents the change.\n- `invalid_transition`: the item is no longer in the required state.\n- `edge_constraint_violation`: a cascading edge blocks the change.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EdgeConstraintViolationOrInvalidIdOrInvalidTransitionOrValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } }, - "400": { - "description": "- `missing_required_field`: `type` or `settled_at` is missing.\n- `validation_error`: `type` or `settled_at` is malformed, the body doesn't name exactly one of `links` or `source` with `source_ids`, or has more than 500 values, an empty value, an undeclared key, or `links` for a type with no `link_field`.\n- `unknown_type`: `type` isn't registered.", + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" + "$ref": "#/components/schemas/UnauthorizedRefusal" } } }, @@ -16201,12 +18642,12 @@ } } }, - "401": { - "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "403": { + "description": "`forbidden`: requires direct owner or local authority, and a recent sign-in for changes.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedRefusal" + "$ref": "#/components/schemas/ForbiddenRefusal" } } }, @@ -16231,12 +18672,12 @@ } } }, - "403": { - "description": "- `type_not_permitted`: you don't have write on `type`.\n- `forbidden`: `source` is not your credential's own or one of your key's `sources`. `details.source` names it.", + "404": { + "description": "`item_not_found`: the item is absent or not in the owner's item listing.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForbiddenOrTypeNotPermittedRefusal" + "$ref": "#/components/schemas/ItemNotFoundRefusal" } } }, @@ -16262,11 +18703,18 @@ } }, "409": { - "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "description": "`version_conflict`: the item changed. The current value is a summary, without content.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InstanceChangedRefusal" + "anyOf": [ + { + "$ref": "#/components/schemas/ItemSummaryConflict" + }, + { + "$ref": "#/components/schemas/InstanceChangedRefusal" + } + ] } } }, @@ -16435,29 +18883,17 @@ } } } - }, - "parameters": [ - { - "name": "X-Marfa-Instance-Id", - "in": "header", - "required": false, - "schema": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - }, - "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." - } - ] + } } }, - "/trash": { - "get": { - "operationId": "listTrash", + "/items/summaries/{id}/restore": { + "post": { + "operationId": "restoreItemSummary", "tags": [ "Trash" ], - "summary": "List the trash", - "description": "Returns a page of trash summaries, newest creation first, without item content. Only the owner or a local command can use this operation.", + "summary": "Restore an item by summary", + "description": "Restores the item and the children its trash took. Returns only a summary. Requires the version from the listing.", "security": [ { "ownerSession": [] @@ -16467,55 +18903,43 @@ { "schema": { "type": "string", - "description": "Only return items of this exact type." + "description": "The ID of the item." }, - "required": false, - "description": "Only return items of this exact type.", - "name": "type", - "in": "query" + "required": true, + "description": "The ID of the item.", + "name": "id", + "in": "path" }, { "schema": { - "type": "string", - "maxLength": 500, - "description": "Only return item names containing this text, ignoring case and surrounding whitespace." + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "The item version from the summary listing. If it differs, nothing changes." }, - "required": false, - "description": "Only return item names containing this text, ignoring case and surrounding whitespace.", - "name": "q", + "required": true, + "description": "The item version from the summary listing. If it differs, nothing changes.", + "name": "version", "in": "query" }, { - "schema": { - "type": "string", - "description": "The `next_cursor` from the previous page. Leave it out to get the first page." - }, + "name": "X-Marfa-Instance-Id", + "in": "header", "required": false, - "description": "The `next_cursor` from the previous page. Leave it out to get the first page.", - "name": "cursor", - "in": "query" - }, - { "schema": { - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 25, - "description": "The maximum number of results to return." + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }, - "required": false, - "description": "The maximum number of results to return.", - "name": "limit", - "in": "query" + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." } ], "responses": { "200": { - "description": "Returns trash summaries and the effective retention in days. Zero disables automatic expiry.", + "description": "The action completed. Returns only the item summary.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrashPage" + "$ref": "#/components/schemas/ItemSummary" } } }, @@ -16538,11 +18962,11 @@ } }, "400": { - "description": "`validation_error`: invalid page or cursor.", + "description": "- `invalid_id`: the item ID is invalid.\n- `validation_error`: the version is invalid or a live connection prevents the change.\n- `invalid_transition`: the item is no longer in the required state.\n- `edge_constraint_violation`: a cascading edge blocks the change.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorRefusal" + "$ref": "#/components/schemas/EdgeConstraintViolationOrInvalidIdOrInvalidTransitionOrValidationErrorRefusal" } } }, @@ -16627,6 +19051,94 @@ } } }, + "404": { + "description": "`item_not_found`: the item is absent or not in the owner's item listing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemNotFoundRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "409": { + "description": "`version_conflict`: the item changed. The current value is a summary, without content.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ItemSummaryConflict" + }, + { + "$ref": "#/components/schemas/InstanceChangedRefusal" + } + ] + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTooLargeRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, "429": { "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", "content": { @@ -16753,14 +19265,14 @@ } } }, - "/items/summaries/{id}/trash": { + "/items/summaries/{id}/purge": { "post": { - "operationId": "trashItemSummary", + "operationId": "purgeItemSummary", "tags": [ "Trash" ], - "summary": "Trash an item by summary", - "description": "Moves the item and its cascading children to the trash. Returns only a summary. Requires the version from the listing.", + "summary": "Purge an item by summary", + "description": "Purges one item, deleting it permanently, if it is still in the trash at the listed version. Returns only a summary.", "security": [ { "ownerSession": [] @@ -17132,14 +19644,14 @@ } } }, - "/items/summaries/{id}/restore": { + "/trash/empty": { "post": { - "operationId": "restoreItemSummary", + "operationId": "emptyTrash", "tags": [ "Trash" ], - "summary": "Restore an item by summary", - "description": "Restores the item and the children its trash took. Returns only a summary. Requires the version from the listing.", + "summary": "Empty the trash", + "description": "Empties one page of the trash: deletes up to 200 items permanently if their state and version still match. Returns partial results and a continuation cursor. Items trashed during the operation may remain.", "security": [ { "ownerSession": [] @@ -17149,23 +19661,11 @@ { "schema": { "type": "string", - "description": "The ID of the item." - }, - "required": true, - "description": "The ID of the item.", - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991, - "description": "The item version from the summary listing. If it differs, nothing changes." + "description": "The `next_cursor` from the previous page. Leave it out to get the first page." }, - "required": true, - "description": "The item version from the summary listing. If it differs, nothing changes.", - "name": "version", + "required": false, + "description": "The `next_cursor` from the previous page. Leave it out to get the first page.", + "name": "cursor", "in": "query" }, { @@ -17181,11 +19681,72 @@ ], "responses": { "200": { - "description": "The action completed. Returns only the item summary.", + "description": "Returns the committed purge count, per-item failures and the next batch cursor. Unknown write outcomes carry `details.write_outcome: unknown` and stop this batch; refresh before retrying. Other failures leave the item in place.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemSummary" + "type": "object", + "properties": { + "purged": { + "type": "integer", + "description": "The number of items this batch permanently deleted." + }, + "failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the item whose purge failed or could not be confirmed." + }, + "code": { + "type": "string", + "description": "The code describing the failure or uncertain outcome." + }, + "details": { + "type": "object", + "properties": { + "write_outcome": { + "type": "string", + "enum": [ + "unknown" + ], + "description": "The purge may have committed. Refresh before retrying." + } + }, + "required": [ + "write_outcome" + ], + "description": "Present when the write outcome cannot be confirmed." + }, + "message": { + "type": "string", + "description": "A summary of the failure without item content." + } + }, + "required": [ + "id", + "code", + "message" + ], + "description": "One failed or unconfirmed purge." + }, + "description": "Failed or unconfirmed purges." + }, + "next_cursor": { + "type": [ + "string", + "null" + ], + "description": "The cursor for the next batch, or null when this traversal ended or stopped on an unknown outcome." + } + }, + "required": [ + "purged", + "failures", + "next_cursor" + ] } } }, @@ -17208,11 +19769,11 @@ } }, "400": { - "description": "- `invalid_id`: the item ID is invalid.\n- `validation_error`: the version is invalid or a live connection prevents the change.\n- `invalid_transition`: the item is no longer in the required state.\n- `edge_constraint_violation`: a cascading edge blocks the change.", + "description": "`validation_error`: invalid cursor.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EdgeConstraintViolationOrInvalidIdOrInvalidTransitionOrValidationErrorRefusal" + "$ref": "#/components/schemas/ValidationErrorRefusal" } } }, @@ -17297,49 +19858,12 @@ } } }, - "404": { - "description": "`item_not_found`: the item is absent or not in the owner's item listing.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ItemNotFoundRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - } - } - }, "409": { - "description": "`version_conflict`: the item changed. The current value is a summary, without content.", + "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ItemSummaryConflict" - }, - { - "$ref": "#/components/schemas/InstanceChangedRefusal" - } - ] + "$ref": "#/components/schemas/InstanceChangedRefusal" } } }, @@ -17511,60 +20035,262 @@ } } }, - "/items/summaries/{id}/purge": { + "/items/bulk-actions": { "post": { - "operationId": "purgeItemSummary", + "operationId": "applyBulkAction", "tags": [ - "Trash" + "Bulk actions" ], - "summary": "Purge an item by summary", - "description": "Purges one item, deleting it permanently, if it is still in the trash at the listed version. Returns only a summary.", + "summary": "Apply a bulk action", + "description": "Applies one action to every item that matches a filter: change state, purge, update tags, tier, properties or own time. It matches only items you can write. With `dry_run: true` it returns the matched IDs; otherwise it queues a bulk-action job.", "security": [ { - "ownerSession": [] + "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The ID of the item." - }, - "required": true, - "description": "The ID of the item.", - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991, - "description": "The item version from the summary listing. If it differs, nothing changes." - }, - "required": true, - "description": "The item version from the summary listing. If it differs, nothing changes.", - "name": "version", - "in": "query" - }, - { - "name": "X-Marfa-Instance-Id", - "in": "header", - "required": false, - "schema": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - }, - "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "filter": { + "$ref": "#/components/schemas/BulkActionFilter" + }, + "dry_run": { + "type": "boolean" + }, + "max_items": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." + }, + "enable_fanout": { + "type": "boolean" + }, + "action": { + "type": "string", + "enum": [ + "transition" + ] + }, + "state": { + "type": "string", + "enum": [ + "active", + "archived", + "trashed" + ] + } + }, + "required": [ + "action", + "state" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "filter": { + "$ref": "#/components/schemas/BulkActionFilter" + }, + "dry_run": { + "type": "boolean" + }, + "max_items": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." + }, + "enable_fanout": { + "type": "boolean" + }, + "action": { + "type": "string", + "enum": [ + "purge" + ] + }, + "confirm": { + "type": "string", + "enum": [ + "PURGE" + ] + }, + "expected_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "The ids a dry run of this purge returned. Where given, the purge takes only rows that are both in this list and matched by the filter now: a row the filter has come to match since is left untouched, and a listed id the filter no longer matches is not purged. `matched` counts what the purge will take, and `max_items` caps that rather than what the filter reaches. An empty list is refused, since it names nothing to purge. Taken by `purge` alone." + } + }, + "required": [ + "action" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "filter": { + "$ref": "#/components/schemas/BulkActionFilter" + }, + "dry_run": { + "type": "boolean" + }, + "max_items": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." + }, + "enable_fanout": { + "type": "boolean" + }, + "action": { + "type": "string", + "enum": [ + "update_tags" + ] + }, + "add": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "remove": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "action" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "filter": { + "$ref": "#/components/schemas/BulkActionFilter" + }, + "dry_run": { + "type": "boolean" + }, + "max_items": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." + }, + "enable_fanout": { + "type": "boolean" + }, + "action": { + "type": "string", + "enum": [ + "update_tier" + ] + }, + "tier": { + "$ref": "#/components/schemas/Tier" + } + }, + "required": [ + "action", + "tier" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "filter": { + "$ref": "#/components/schemas/BulkActionFilter" + }, + "dry_run": { + "type": "boolean" + }, + "max_items": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." + }, + "enable_fanout": { + "type": "boolean" + }, + "action": { + "type": "string", + "enum": [ + "update_properties" + ] + }, + "patch": { + "type": "object", + "additionalProperties": {}, + "description": "The properties to lay over each item's properties. A whole number whose digits a 64-bit float doesn't keep is refused: send it as a string." + } + }, + "required": [ + "action", + "patch" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "filter": { + "$ref": "#/components/schemas/BulkActionFilter" + }, + "dry_run": { + "type": "boolean" + }, + "max_items": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." + }, + "enable_fanout": { + "type": "boolean" + }, + "action": { + "type": "string", + "enum": [ + "update_occurred_at" + ] + }, + "occurred_at": { + "type": "string" + } + }, + "required": [ + "action", + "occurred_at" + ], + "additionalProperties": false + } + ] + } + } } - ], + }, "responses": { "200": { - "description": "The action completed. Returns only the item summary.", + "description": "Dry run: returns `matched` and the matched `ids` without writing anything. A purge dry run also lists matches that aren't in the trash. The purge skips those.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemSummary" + "$ref": "#/components/schemas/BulkActionResult" } } }, @@ -17583,15 +20309,18 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, - "400": { - "description": "- `invalid_id`: the item ID is invalid.\n- `validation_error`: the version is invalid or a live connection prevents the change.\n- `invalid_transition`: the item is no longer in the required state.\n- `edge_constraint_violation`: a cascading edge blocks the change.", + "202": { + "description": "Returns the queued job. Poll `GET /items/bulk-actions/jobs/{id}` until `status` is `completed`, `failed` or `canceled`. The job acts for your credential as it stands. If your key is revoked or expires, your app's access is revoked, or a purge loses `items.purge`, the job ends `failed` and keeps its `result`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EdgeConstraintViolationOrInvalidIdOrInvalidTransitionOrValidationErrorRefusal" + "$ref": "#/components/schemas/BulkActionJob" } } }, @@ -17611,17 +20340,17 @@ "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, - "401": { - "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "400": { + "description": "- `validation_error`: the body is malformed or has an undeclared or empty field, or `update_tags` has no `add` or `remove`.\n- `missing_required_field`: a needed field is missing.\n- `invalid_properties`: `patch` holds a whole number a 64-bit float cannot keep.\n- `bulk_confirmation_required`: a purge without `confirm: \"PURGE\"`.\n- `bulk_cap_exceeded`: more items match than `max_items` allows.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedRefusal" + "$ref": "#/components/schemas/BulkCapExceededOrBulkConfirmationRequiredOrInvalidPropertiesOrMissingRequiredFieldOrValidationErrorRefusal" } } }, @@ -17643,15 +20372,18 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, - "403": { - "description": "`forbidden`: requires direct owner or local authority, and a recent sign-in for changes.", + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForbiddenRefusal" + "$ref": "#/components/schemas/UnauthorizedRefusal" } } }, @@ -17676,12 +20408,12 @@ } } }, - "404": { - "description": "`item_not_found`: the item is absent or not in the owner's item listing.", + "403": { + "description": "- `forbidden`: you don't have `items.purge` for a purge, or the instance's source filter changed while Marfa selected items. Repeat the request.\n- `type_not_permitted`: your credential reaches no type, or `filter.type` is a type you can't read.\n- `edge_permission_denied`: the filter names an edge type you can't read.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemNotFoundRefusal" + "$ref": "#/components/schemas/EdgePermissionDeniedOrForbiddenOrTypeNotPermittedRefusal" } } }, @@ -17707,16 +20439,16 @@ } }, "409": { - "description": "`version_conflict`: the item changed. The current value is a summary, without content.", + "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/ItemSummaryConflict" + "$ref": "#/components/schemas/InstanceChangedRefusal" }, { - "$ref": "#/components/schemas/InstanceChangedRefusal" + "$ref": "#/components/schemas/IdempotencyKeyInFlightRefusal" } ] } @@ -17740,6 +20472,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -17764,6 +20499,39 @@ } } }, + "422": { + "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, "429": { "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", "content": { @@ -17794,6 +20562,9 @@ }, "Retry-After": { "$ref": "#/components/headers/Retry-After" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -17824,6 +20595,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -17854,6 +20628,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -17884,35 +20661,23 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } } - } - } - }, - "/trash/empty": { - "post": { - "operationId": "emptyTrash", - "tags": [ - "Trash" - ], - "summary": "Empty the trash", - "description": "Empties one page of the trash: deletes up to 200 items permanently if their state and version still match. Returns partial results and a continuation cursor. Items trashed during the operation may remain.", - "security": [ - { - "ownerSession": [] - } - ], + }, "parameters": [ { + "name": "Idempotency-Key", + "in": "header", + "required": false, "schema": { "type": "string", - "description": "The `next_cursor` from the previous page. Leave it out to get the first page." + "maxLength": 255 }, - "required": false, - "description": "The `next_cursor` from the previous page. Leave it out to get the first page.", - "name": "cursor", - "in": "query" + "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." }, { "name": "X-Marfa-Instance-Id", @@ -17924,75 +20689,41 @@ }, "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." } + ] + } + }, + "/items/bulk-actions/jobs/{id}": { + "get": { + "operationId": "getBulkActionJob", + "tags": [ + "Bulk actions" + ], + "summary": "Get a bulk-action job", + "description": "Returns a bulk-action job's status and counts, and its `result` once it has finished. Only the credential that queued the job, or the holder of `instance.read`, can read it.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the bulk-action job." + }, + "required": true, + "description": "The ID of the bulk-action job.", + "name": "id", + "in": "path" + } ], "responses": { "200": { - "description": "Returns the committed purge count, per-item failures and the next batch cursor. Unknown write outcomes carry `details.write_outcome: unknown` and stop this batch; refresh before retrying. Other failures leave the item in place.", + "description": "Returns the job. Once it is terminal, `result.errors` lists each item left unchanged, with a code such as `invalid_transition` (a purge of an item not in the trash), `type_not_permitted` (a type you can no longer write), `item_not_found` (a type you can no longer read) or `invalid_properties`.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "purged": { - "type": "integer", - "description": "The number of items this batch permanently deleted." - }, - "failures": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The ID of the item whose purge failed or could not be confirmed." - }, - "code": { - "type": "string", - "description": "The code describing the failure or uncertain outcome." - }, - "details": { - "type": "object", - "properties": { - "write_outcome": { - "type": "string", - "enum": [ - "unknown" - ], - "description": "The purge may have committed. Refresh before retrying." - } - }, - "required": [ - "write_outcome" - ], - "description": "Present when the write outcome cannot be confirmed." - }, - "message": { - "type": "string", - "description": "A summary of the failure without item content." - } - }, - "required": [ - "id", - "code", - "message" - ], - "description": "One failed or unconfirmed purge." - }, - "description": "Failed or unconfirmed purges." - }, - "next_cursor": { - "type": [ - "string", - "null" - ], - "description": "The cursor for the next batch, or null when this traversal ended or stopped on an unknown outcome." - } - }, - "required": [ - "purged", - "failures", - "next_cursor" - ] + "$ref": "#/components/schemas/BulkActionJob" } } }, @@ -18015,7 +20746,7 @@ } }, "400": { - "description": "`validation_error`: invalid cursor.", + "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.", "content": { "application/json": { "schema": { @@ -18075,7 +20806,7 @@ } }, "403": { - "description": "`forbidden`: requires direct owner or local authority, and a recent sign-in for changes.", + "description": "- `forbidden`: another credential queued the job, and yours lacks `instance.read`.", "content": { "application/json": { "schema": { @@ -18104,12 +20835,12 @@ } } }, - "409": { - "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "404": { + "description": "- `bulk_job_not_found`: no job has this ID.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InstanceChangedRefusal" + "$ref": "#/components/schemas/BulkJobNotFoundRefusal" } } }, @@ -18134,27 +20865,6 @@ } } }, - "413": { - "description": "`request_too_large`: the request body is larger than this instance accepts.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestTooLargeRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - } - } - }, "429": { "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", "content": { @@ -18281,262 +20991,48 @@ } } }, - "/items/bulk-actions": { + "/items/bulk-actions/jobs/{id}/cancel": { "post": { - "operationId": "applyBulkAction", + "operationId": "cancelBulkActionJob", "tags": [ "Bulk actions" ], - "summary": "Apply a bulk action", - "description": "Applies one action to every item that matches a filter: change state, purge, update tags, tier, properties or own time. It matches only items you can write. With `dry_run: true` it returns the matched IDs; otherwise it queues a bulk-action job.", + "summary": "Cancel a bulk-action job", + "description": "Cancels a bulk-action job and returns it. Items the job already processed stay processed. A job that has already finished is left as it is.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "filter": { - "$ref": "#/components/schemas/BulkActionFilter" - }, - "dry_run": { - "type": "boolean" - }, - "max_items": { - "type": "integer", - "exclusiveMinimum": 0, - "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." - }, - "enable_fanout": { - "type": "boolean" - }, - "action": { - "type": "string", - "enum": [ - "transition" - ] - }, - "state": { - "type": "string", - "enum": [ - "active", - "archived", - "trashed" - ] - } - }, - "required": [ - "action", - "state" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "filter": { - "$ref": "#/components/schemas/BulkActionFilter" - }, - "dry_run": { - "type": "boolean" - }, - "max_items": { - "type": "integer", - "exclusiveMinimum": 0, - "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." - }, - "enable_fanout": { - "type": "boolean" - }, - "action": { - "type": "string", - "enum": [ - "purge" - ] - }, - "confirm": { - "type": "string", - "enum": [ - "PURGE" - ] - }, - "expected_ids": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1, - "description": "The ids a dry run of this purge returned. Where given, the purge takes only rows that are both in this list and matched by the filter now: a row the filter has come to match since is left untouched, and a listed id the filter no longer matches is not purged. `matched` counts what the purge will take, and `max_items` caps that rather than what the filter reaches. An empty list is refused, since it names nothing to purge. Taken by `purge` alone." - } - }, - "required": [ - "action" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "filter": { - "$ref": "#/components/schemas/BulkActionFilter" - }, - "dry_run": { - "type": "boolean" - }, - "max_items": { - "type": "integer", - "exclusiveMinimum": 0, - "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." - }, - "enable_fanout": { - "type": "boolean" - }, - "action": { - "type": "string", - "enum": [ - "update_tags" - ] - }, - "add": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 128 - } - }, - "remove": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "action" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "filter": { - "$ref": "#/components/schemas/BulkActionFilter" - }, - "dry_run": { - "type": "boolean" - }, - "max_items": { - "type": "integer", - "exclusiveMinimum": 0, - "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." - }, - "enable_fanout": { - "type": "boolean" - }, - "action": { - "type": "string", - "enum": [ - "update_tier" - ] - }, - "tier": { - "$ref": "#/components/schemas/Tier" - } - }, - "required": [ - "action", - "tier" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "filter": { - "$ref": "#/components/schemas/BulkActionFilter" - }, - "dry_run": { - "type": "boolean" - }, - "max_items": { - "type": "integer", - "exclusiveMinimum": 0, - "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." - }, - "enable_fanout": { - "type": "boolean" - }, - "action": { - "type": "string", - "enum": [ - "update_properties" - ] - }, - "patch": { - "type": "object", - "additionalProperties": {}, - "description": "The properties to lay over each item's properties. A whole number whose digits a 64-bit float doesn't keep is refused: send it as a string." - } - }, - "required": [ - "action", - "patch" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "filter": { - "$ref": "#/components/schemas/BulkActionFilter" - }, - "dry_run": { - "type": "boolean" - }, - "max_items": { - "type": "integer", - "exclusiveMinimum": 0, - "description": "The most items the action may match. Defaults to 10,000. A value above 50,000 counts as 50,000." - }, - "enable_fanout": { - "type": "boolean" - }, - "action": { - "type": "string", - "enum": [ - "update_occurred_at" - ] - }, - "occurred_at": { - "type": "string" - } - }, - "required": [ - "action", - "occurred_at" - ], - "additionalProperties": false - } - ] - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the bulk-action job." + }, + "required": true, + "description": "The ID of the bulk-action job.", + "name": "id", + "in": "path" + }, + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." } - }, + ], "responses": { "200": { - "description": "Dry run: returns `matched` and the matched `ids` without writing anything. A purge dry run also lists matches that aren't in the trash. The purge skips those.", + "description": "Returns the job: `canceled` if it was queued or running, otherwise its final state unchanged.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkActionResult" + "$ref": "#/components/schemas/BulkActionJob" } } }, @@ -18555,18 +21051,15 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, - "202": { - "description": "Returns the queued job. Poll `GET /items/bulk-actions/jobs/{id}` until `status` is `completed`, `failed` or `canceled`. The job acts for your credential as it stands. If your key is revoked or expires, your app's access is revoked, or a purge loses `items.purge`, the job ends `failed` and keeps its `result`.", + "400": { + "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkActionJob" + "$ref": "#/components/schemas/ValidationErrorRefusal" } } }, @@ -18586,17 +21079,17 @@ "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" } } }, - "400": { - "description": "- `validation_error`: the body is malformed or has an undeclared or empty field, or `update_tags` has no `add` or `remove`.\n- `missing_required_field`: a needed field is missing.\n- `invalid_properties`: `patch` holds a whole number a 64-bit float cannot keep.\n- `bulk_confirmation_required`: a purge without `confirm: \"PURGE\"`.\n- `bulk_cap_exceeded`: more items match than `max_items` allows.", + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkCapExceededOrBulkConfirmationRequiredOrInvalidPropertiesOrMissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/UnauthorizedRefusal" } } }, @@ -18618,18 +21111,15 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, - "401": { - "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "403": { + "description": "- `forbidden`: another credential queued the job, and yours lacks `instance.maintain`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedRefusal" + "$ref": "#/components/schemas/ForbiddenRefusal" } } }, @@ -18654,12 +21144,12 @@ } } }, - "403": { - "description": "- `forbidden`: you don't have `items.purge` for a purge, or the instance's source filter changed while Marfa selected items. Repeat the request.\n- `type_not_permitted`: your credential reaches no type, or `filter.type` is a type you can't read.\n- `edge_permission_denied`: the filter names an edge type you can't read.", + "404": { + "description": "- `bulk_job_not_found`: no job has this ID.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EdgePermissionDeniedOrForbiddenOrTypeNotPermittedRefusal" + "$ref": "#/components/schemas/BulkJobNotFoundRefusal" } } }, @@ -18689,14 +21179,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InstanceChangedRefusal" - }, - { - "$ref": "#/components/schemas/IdempotencyKeyInFlightRefusal" - } - ] + "$ref": "#/components/schemas/InstanceChangedRefusal" } } }, @@ -18718,9 +21201,6 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -18745,39 +21225,6 @@ } } }, - "422": { - "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - }, "429": { "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", "content": { @@ -18808,9 +21255,6 @@ }, "Retry-After": { "$ref": "#/components/headers/Retry-After" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -18841,9 +21285,6 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -18874,9 +21315,6 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -18907,45 +21345,20 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } } - }, - "parameters": [ - { - "name": "Idempotency-Key", - "in": "header", - "required": false, - "schema": { - "type": "string", - "maxLength": 255 - }, - "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." - }, - { - "name": "X-Marfa-Instance-Id", - "in": "header", - "required": false, - "schema": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - }, - "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." - } - ] + } } }, - "/items/bulk-actions/jobs/{id}": { + "/items/{id}/metadata": { "get": { - "operationId": "getBulkActionJob", + "operationId": "getItemMetadata", "tags": [ - "Bulk actions" + "Metadata" ], - "summary": "Get a bulk-action job", - "description": "Returns a bulk-action job's status and counts, and its `result` once it has finished. Only the credential that queued the job, or the holder of `instance.read`, can read it.", + "summary": "Get item metadata", + "description": "Returns an item's metadata: its tags and the extension namespaces you can read. To read metadata for many items, use `include=metadata` on `GET /items`.", "security": [ { "bearerAuth": [] @@ -18955,21 +21368,21 @@ { "schema": { "type": "string", - "description": "The ID of the bulk-action job." + "description": "The ID of the item." }, "required": true, - "description": "The ID of the bulk-action job.", + "description": "The ID of the item.", "name": "id", "in": "path" } ], "responses": { "200": { - "description": "Returns the job. Once it is terminal, `result.errors` lists each item left unchanged, with a code such as `invalid_transition` (a purge of an item not in the trash), `type_not_permitted` (a type you can no longer write), `item_not_found` (a type you can no longer read) or `invalid_properties`.", + "description": "Returns the item's metadata.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkActionJob" + "$ref": "#/components/schemas/MetadataResponse" } } }, @@ -18992,11 +21405,11 @@ } }, "400": { - "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.", + "description": "- `invalid_id`: the ID is not a valid item ID.\n- `validation_error`: the query has a parameter this endpoint doesn't take.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorRefusal" + "$ref": "#/components/schemas/InvalidIdOrValidationErrorRefusal" } } }, @@ -19052,11 +21465,11 @@ } }, "403": { - "description": "- `forbidden`: another credential queued the job, and yours lacks `instance.read`.", + "description": "- `type_not_permitted`: your credential reaches no type.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForbiddenRefusal" + "$ref": "#/components/schemas/TypeNotPermittedRefusal" } } }, @@ -19082,11 +21495,11 @@ } }, "404": { - "description": "- `bulk_job_not_found`: no job has this ID.", + "description": "- `item_not_found`: no item has this ID, the item is in the trash, or its type is one you can't read.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkJobNotFoundRefusal" + "$ref": "#/components/schemas/ItemNotFoundRefusal" } } }, @@ -19235,16 +21648,14 @@ } } } - } - }, - "/items/bulk-actions/jobs/{id}/cancel": { - "post": { - "operationId": "cancelBulkActionJob", + }, + "put": { + "operationId": "replaceItemMetadata", "tags": [ - "Bulk actions" + "Metadata" ], - "summary": "Cancel a bulk-action job", - "description": "Cancels a bulk-action job and returns it. Items the job already processed stay processed. A job that has already finished is left as it is.", + "summary": "Replace an item's tags", + "description": "Replaces the item's tags with the ones you send and returns its metadata. An empty list clears all tags.", "security": [ { "bearerAuth": [] @@ -19254,13 +21665,23 @@ { "schema": { "type": "string", - "description": "The ID of the bulk-action job." + "description": "The ID of the item." }, "required": true, - "description": "The ID of the bulk-action job.", + "description": "The ID of the item.", "name": "id", "in": "path" }, + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "maxLength": 255 + }, + "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." + }, { "name": "X-Marfa-Instance-Id", "in": "header", @@ -19272,13 +21693,36 @@ "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "default": [], + "description": "The tags the item will have: at most 100, each up to 128 characters. An empty list clears them." + } + }, + "additionalProperties": false + } + } + } + }, "responses": { "200": { - "description": "Returns the job: `canceled` if it was queued or running, otherwise its final state unchanged.", + "description": "Returns the item's metadata with the new tags.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkActionJob" + "$ref": "#/components/schemas/MetadataResponse" } } }, @@ -19297,15 +21741,18 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, "400": { - "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.", + "description": "- `validation_error`: `tags` is not a list of valid tags or has more than 100, or the body has a field this endpoint doesn't take.\n- `invalid_id`: the ID is not a valid item ID.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorRefusal" + "$ref": "#/components/schemas/InvalidIdOrMissingRequiredFieldOrValidationErrorRefusal" } } }, @@ -19327,6 +21774,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -19361,11 +21811,11 @@ } }, "403": { - "description": "- `forbidden`: another credential queued the job, and yours lacks `instance.maintain`.", + "description": "- `type_not_permitted`: you can read the item's type but don't have write on it, or your credential reaches no type. `details.grant` names the missing grant.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForbiddenRefusal" + "$ref": "#/components/schemas/TypeNotPermittedRefusal" } } }, @@ -19391,155 +21841,11 @@ } }, "404": { - "description": "- `bulk_job_not_found`: no job has this ID.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkJobNotFoundRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - } - } - }, - "409": { - "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceChangedRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - } - } - }, - "413": { - "description": "`request_too_large`: the request body is larger than this instance accepts.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestTooLargeRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - } - } - }, - "429": { - "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RateLimitedRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Retry-After": { - "$ref": "#/components/headers/Retry-After" - } - } - }, - "500": { - "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InternalErrorRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - } - } - }, - "503": { - "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "description": "- `item_not_found`: no item has this ID, or its type is one you can't read. If the item is in the trash and you can read its type, `details.trashed` is `true`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WriteContentionRefusal" + "$ref": "#/components/schemas/ItemNotFoundRefusal" } } }, @@ -19561,101 +21867,25 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - } - } - }, - "507": { - "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InsufficientStorageRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - } - } - } - } - } - }, - "/items/{id}/metadata": { - "get": { - "operationId": "getItemMetadata", - "tags": [ - "Metadata" - ], - "summary": "Get item metadata", - "description": "Returns an item's metadata: its tags and the extension namespaces you can read. To read metadata for many items, use `include=metadata` on `GET /items`.", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The ID of the item." - }, - "required": true, - "description": "The ID of the item.", - "name": "id", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Returns the item's metadata.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MetadataResponse" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, - "400": { - "description": "- `invalid_id`: the ID is not a valid item ID.\n- `validation_error`: the query has a parameter this endpoint doesn't take.", + "409": { + "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidIdOrValidationErrorRefusal" + "anyOf": [ + { + "$ref": "#/components/schemas/InstanceChangedRefusal" + }, + { + "$ref": "#/components/schemas/IdempotencyKeyInFlightRefusal" + } + ] } } }, @@ -19677,45 +21907,18 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - } - } - }, - "401": { - "description": "`unauthorized`: the request has no credential, or its credential is not valid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, - "403": { - "description": "- `type_not_permitted`: your credential reaches no type.", + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TypeNotPermittedRefusal" + "$ref": "#/components/schemas/RequestTooLargeRefusal" } } }, @@ -19726,26 +21929,17 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } } }, - "404": { - "description": "- `item_not_found`: no item has this ID, the item is in the trash, or its type is one you can't read.", + "422": { + "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemNotFoundRefusal" + "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" } } }, @@ -19767,6 +21961,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -19800,6 +21997,9 @@ }, "Retry-After": { "$ref": "#/components/headers/Retry-After" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -19830,6 +22030,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -19860,6 +22063,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -19890,18 +22096,23 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } } } - }, - "put": { - "operationId": "replaceItemMetadata", + } + }, + "/items/{id}/tags": { + "post": { + "operationId": "addItemTags", "tags": [ "Metadata" ], - "summary": "Replace an item's tags", - "description": "Replaces the item's tags with the ones you send and returns its metadata. An empty list clears all tags.", + "summary": "Add tags to an item", + "description": "Adds tags to the item and returns its metadata. A tag the item already has isn't added twice.", "security": [ { "bearerAuth": [] @@ -19918,6 +22129,17 @@ "name": "id", "in": "path" }, + { + "schema": { + "type": "integer", + "minimum": 1, + "description": "The item version you read. If it differs, no tag changes. Leave it out to change the tags of the item as it is." + }, + "required": false, + "description": "The item version you read. If it differs, no tag changes. Leave it out to change the tags of the item as it is.", + "name": "version", + "in": "query" + }, { "name": "Idempotency-Key", "in": "header", @@ -19953,10 +22175,13 @@ "minLength": 1, "maxLength": 128 }, - "default": [], - "description": "The tags the item will have: at most 100, each up to 128 characters. An empty list clears them." + "minItems": 1, + "description": "The tags to add: at least one, each up to 128 characters. The item can hold at most 100." } }, + "required": [ + "tags" + ], "additionalProperties": false } } @@ -19964,7 +22189,7 @@ }, "responses": { "200": { - "description": "Returns the item's metadata with the new tags.", + "description": "Returns the item's metadata with the tags added.", "content": { "application/json": { "schema": { @@ -19994,7 +22219,7 @@ } }, "400": { - "description": "- `validation_error`: `tags` is not a list of valid tags or has more than 100, or the body has a field this endpoint doesn't take.\n- `invalid_id`: the ID is not a valid item ID.", + "description": "- `validation_error`: `tags` is empty or not a list of valid tags or the item would hold more than 100, or the body has a field this endpoint doesn't take.\n- `missing_required_field`: `tags` is missing.\n- `invalid_id`: the ID is not a valid item ID.", "content": { "application/json": { "schema": { @@ -20120,11 +22345,14 @@ } }, "409": { - "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "description": "`version_conflict`: `version` differs from the item. `current` is the item now; no tag changed.", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/ItemStaleVersion" + }, { "$ref": "#/components/schemas/InstanceChangedRefusal" }, @@ -20351,14 +22579,14 @@ } } }, - "/items/{id}/tags": { - "post": { - "operationId": "addItemTags", + "/items/{id}/tags/{tag}": { + "delete": { + "operationId": "removeItemTag", "tags": [ "Metadata" ], - "summary": "Add tags to an item", - "description": "Adds tags to the item and returns its metadata. A tag the item already has isn't added twice.", + "summary": "Remove a tag from an item", + "description": "Removes one tag from the item and returns its metadata. Removing a tag the item doesn't have changes nothing and succeeds.", "security": [ { "bearerAuth": [] @@ -20375,6 +22603,27 @@ "name": "id", "in": "path" }, + { + "schema": { + "type": "string", + "description": "The tag to remove, URL-encoded." + }, + "required": true, + "description": "The tag to remove, URL-encoded.", + "name": "tag", + "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "description": "The item version you read. If it differs, no tag changes. Leave it out to change the tags of the item as it is." + }, + "required": false, + "description": "The item version you read. If it differs, no tag changes. Leave it out to change the tags of the item as it is.", + "name": "version", + "in": "query" + }, { "name": "Idempotency-Key", "in": "header", @@ -20396,35 +22645,9 @@ "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tags": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "minItems": 1, - "description": "The tags to add: at least one, each up to 128 characters. The item can hold at most 100." - } - }, - "required": [ - "tags" - ], - "additionalProperties": false - } - } - } - }, "responses": { "200": { - "description": "Returns the item's metadata with the tags added.", + "description": "Returns the item's metadata without the tag.", "content": { "application/json": { "schema": { @@ -20454,11 +22677,11 @@ } }, "400": { - "description": "- `validation_error`: `tags` is empty or not a list of valid tags or the item would hold more than 100, or the body has a field this endpoint doesn't take.\n- `missing_required_field`: `tags` is missing.\n- `invalid_id`: the ID is not a valid item ID.", + "description": "- `invalid_id`: the ID is not a valid item ID.\n- `validation_error`: the query has a parameter this endpoint doesn't take.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidIdOrMissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/InvalidIdOrValidationErrorRefusal" } } }, @@ -20580,455 +22803,14 @@ } }, "409": { - "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "description": "`version_conflict`: `version` differs from the item. `current` is the item now; no tag changed.", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/InstanceChangedRefusal" + "$ref": "#/components/schemas/ItemStaleVersion" }, - { - "$ref": "#/components/schemas/IdempotencyKeyInFlightRefusal" - } - ] - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - }, - "413": { - "description": "`request_too_large`: the request body is larger than this instance accepts.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestTooLargeRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - } - } - }, - "422": { - "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - }, - "429": { - "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RateLimitedRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Retry-After": { - "$ref": "#/components/headers/Retry-After" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - }, - "500": { - "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InternalErrorRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - }, - "503": { - "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WriteContentionRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - }, - "507": { - "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InsufficientStorageRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - } - } - } - }, - "/items/{id}/tags/{tag}": { - "delete": { - "operationId": "removeItemTag", - "tags": [ - "Metadata" - ], - "summary": "Remove a tag from an item", - "description": "Removes one tag from the item and returns its metadata. Removing a tag the item doesn't have changes nothing and succeeds.", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The ID of the item." - }, - "required": true, - "description": "The ID of the item.", - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "string", - "description": "The tag to remove, URL-encoded." - }, - "required": true, - "description": "The tag to remove, URL-encoded.", - "name": "tag", - "in": "path" - }, - { - "name": "Idempotency-Key", - "in": "header", - "required": false, - "schema": { - "type": "string", - "maxLength": 255 - }, - "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." - }, - { - "name": "X-Marfa-Instance-Id", - "in": "header", - "required": false, - "schema": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - }, - "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." - } - ], - "responses": { - "200": { - "description": "Returns the item's metadata without the tag.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MetadataResponse" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - }, - "400": { - "description": "- `invalid_id`: the ID is not a valid item ID.\n- `validation_error`: the query has a parameter this endpoint doesn't take.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidIdOrValidationErrorRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - }, - "401": { - "description": "`unauthorized`: the request has no credential, or its credential is not valid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - } - } - }, - "403": { - "description": "- `type_not_permitted`: you can read the item's type but don't have write on it, or your credential reaches no type. `details.grant` names the missing grant.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TypeNotPermittedRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - } - } - }, - "404": { - "description": "- `item_not_found`: no item has this ID, or its type is one you can't read. If the item is in the trash and you can read its type, `details.trashed` is `true`.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ItemNotFoundRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - }, - "409": { - "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", - "content": { - "application/json": { - "schema": { - "anyOf": [ { "$ref": "#/components/schemas/InstanceChangedRefusal" }, @@ -23082,11 +24864,10 @@ "schema": { "type": "integer", "minimum": 1, - "maximum": 500, - "description": "The maximum number of results to return." + "description": "The maximum number of results to return. Up to 500; a larger value is read as 500." }, "required": false, - "description": "The maximum number of results to return.", + "description": "The maximum number of results to return. Up to 500; a larger value is read as 500.", "name": "limit", "in": "query" }, @@ -25655,11 +27436,10 @@ "schema": { "type": "integer", "minimum": 1, - "maximum": 500, - "description": "The maximum number of results to return." + "description": "The maximum number of results to return. Up to 500; a larger value is read as 500." }, "required": false, - "description": "The maximum number of results to return.", + "description": "The maximum number of results to return. Up to 500; a larger value is read as 500.", "name": "limit", "in": "query" }, @@ -26037,11 +27817,10 @@ "schema": { "type": "integer", "minimum": 1, - "maximum": 500, - "description": "The maximum number of results to return." + "description": "The maximum number of results to return. Up to 500; a larger value is read as 500." }, "required": false, - "description": "The maximum number of results to return.", + "description": "The maximum number of results to return. Up to 500; a larger value is read as 500.", "name": "limit", "in": "query" }, @@ -26337,7 +28116,7 @@ "Search" ], "summary": "Search items", - "description": "Returns a page of the items you can read whose text properties or tags match `q`, ranked by BM25 relevance, with ties ordered by item ID.", + "description": "Returns a page of the items you can read whose text properties or tags match `q`, best match first unless you give a `sort`. Items that tie come in ID order.", "security": [ { "bearerAuth": [] @@ -26358,23 +28137,33 @@ { "schema": { "type": "string", - "description": "Only return items of this type or a subtype. A wildcard such as `core.*` matches every type under that prefix." + "description": "Only return items of these types and their subtypes: a comma-separated list of up to 10, such as `core.note,app.*`. A wildcard such as `core.*` matches every type under that prefix." }, "required": false, - "description": "Only return items of this type or a subtype. A wildcard such as `core.*` matches every type under that prefix.", + "description": "Only return items of these types and their subtypes: a comma-separated list of up to 10, such as `core.note,app.*`. A wildcard such as `core.*` matches every type under that prefix.", "name": "type", "in": "query" }, { "schema": { "type": "string", - "description": "Only return items in this lifecycle state. Without it, you get `active` items. `any` searches every state, but trashed items are never searchable." + "description": "Only return items in these lifecycle states: one state or a comma-separated list, such as `active,archived`. Without it, you get `active` items. `any` on its own searches every state. Trashed items are never searchable." }, "required": false, - "description": "Only return items in this lifecycle state. Without it, you get `active` items. `any` searches every state, but trashed items are never searchable.", + "description": "Only return items in these lifecycle states: one state or a comma-separated list, such as `active,archived`. Without it, you get `active` items. `any` on its own searches every state. Trashed items are never searchable.", "name": "state", "in": "query" }, + { + "schema": { + "type": "string", + "description": "Only return items stamped with this source." + }, + "required": false, + "description": "Only return items stamped with this source.", + "name": "source", + "in": "query" + }, { "schema": { "type": "string", @@ -26443,14 +28232,39 @@ "name": "filter", "in": "query" }, + { + "schema": { + "type": "string", + "pattern": "^(relevance|created_at|updated_at|occurred_at|properties\\.[a-z0-9_]+)$", + "description": "How to order the results: `relevance` (the default), `created_at`, `updated_at`, `occurred_at` or `properties.`, such as `properties.due_at`. Items without a value for a property come last." + }, + "required": false, + "description": "How to order the results: `relevance` (the default), `created_at`, `updated_at`, `occurred_at` or `properties.`, such as `properties.due_at`. Items without a value for a property come last.", + "name": "sort", + "in": "query" + }, + { + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "Sort direction. Defaults to `desc`: best match first under `relevance`, newest or largest first otherwise. Under `relevance`, `asc` puts the worst match first." + }, + "required": false, + "description": "Sort direction. Defaults to `desc`: best match first under `relevance`, newest or largest first otherwise. Under `relevance`, `asc` puts the worst match first.", + "name": "direction", + "in": "query" + }, { "schema": { "type": "string", "minLength": 1, - "description": "Only return items whose own time (`occurred_at`, else `created_at`) is after this RFC 3339 time." + "description": "Only return items whose own time (`occurred_at`, else `created_at`) is after this time. For the modification time, use `updated_after`." }, "required": false, - "description": "Only return items whose own time (`occurred_at`, else `created_at`) is after this RFC 3339 time.", + "description": "Only return items whose own time (`occurred_at`, else `created_at`) is after this time. For the modification time, use `updated_after`.", "name": "occurred_after", "in": "query" }, @@ -26458,12 +28272,34 @@ "schema": { "type": "string", "minLength": 1, - "description": "Only return items whose own time (`occurred_at`, else `created_at`) is before this RFC 3339 time." + "description": "Only return items whose own time (`occurred_at`, else `created_at`) is before this time." }, "required": false, - "description": "Only return items whose own time (`occurred_at`, else `created_at`) is before this RFC 3339 time.", + "description": "Only return items whose own time (`occurred_at`, else `created_at`) is before this time.", "name": "occurred_before", "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "description": "Only return items changed at or after this time. Items can share an instant, so the bound includes it. It doesn't change the ordering, so it works with any `sort`." + }, + "required": false, + "description": "Only return items changed at or after this time. Items can share an instant, so the bound includes it. It doesn't change the ordering, so it works with any `sort`.", + "name": "updated_after", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "description": "Only return items changed before this time. It doesn't change the ordering, so it works with any `sort`." + }, + "required": false, + "description": "Only return items changed before this time. It doesn't change the ordering, so it works with any `sort`.", + "name": "updated_before", + "in": "query" } ], "responses": { @@ -26495,7 +28331,7 @@ } }, "400": { - "description": "- `missing_required_field`: `q` is missing.\n- `validation_error`: a parameter is unknown or invalid, such as a `state` that isn't a lifecycle state or `any`, a time that isn't an instant, or a `cursor` from another search.\n- `unknown_type`: `type` is a concrete type that nothing registers.", + "description": "- `missing_required_field`: `q` is missing.\n- `validation_error`: a parameter is unknown or invalid, such as a `state` that isn't a lifecycle state or `any`, a time that isn't an instant, or a `cursor` from another search or sort.\n- `unknown_type`: `type` is a concrete type that nothing registers.", "content": { "application/json": { "schema": { @@ -27653,7 +29489,7 @@ "Types" ], "summary": "Replace a type", - "description": "Replaces a registered type's whole schema and returns it. A field you leave out is removed, but items keep their values for it. `metadata.types:write` alone can add optional fields and change `label`, `description`, `display_hints` or `version`.", + "description": "Replaces a type's whole schema and returns it. A field left out is removed; items keep their values. `metadata.types:write` alone can add optional fields and change labels, descriptions, `plural`, `display_hints` and `version`.", "security": [ { "bearerAuth": [] @@ -29302,7 +31138,7 @@ } }, "400": { - "description": "`invalid_schema`: `icon` isn't the name of an icon in the pinned Lucide release (`details.errors` names it).\n- `validation_error`: the query has a parameter this endpoint doesn't take.", + "description": "`invalid_schema`: `icon` isn't the ID of an icon in a pinned set (`details.errors` names it).\n- `validation_error`: the query has a parameter this endpoint doesn't take.", "content": { "application/json": { "schema": { @@ -36403,7 +38239,7 @@ } }, "400": { - "description": "- `validation_error`: a setting is malformed or undeclared, such as a `system.*` type or a `first_placement` outside the folder. `details.errors[0].path` or `details.unknown_body_fields` names it.\n- `missing_required_field`: `title` is missing.\n- `unknown_type`: a setting's type is unknown.\n- `invalid_properties`: `defaults.properties` has a whole number a 64-bit float can't keep.", + "description": "- `validation_error`: a setting is malformed or undeclared, such as a `system.*` type, a `..` placement or a `backref` filter. `details.errors[0].path` or `details.unknown_body_fields` names it.\n- `missing_required_field`: `title` is missing.\n- `unknown_type`: a setting's type is unknown.\n- `invalid_properties`: `defaults.properties` has a whole number a 64-bit float can't keep.", "content": { "application/json": { "schema": { @@ -38798,35 +40634,357 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + } + }, + "parameters": [ + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "maxLength": 255 + }, + "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." + }, + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + } + ] + } + }, + "/views/{id}/items": { + "get": { + "operationId": "listViewItems", + "tags": [ + "Views" + ], + "summary": "List a view's items", + "description": "Returns a page of the items a view lists, in the view's sort order. Items of types you can't read are left out. A view's grouping isn't applied.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the view." + }, + "required": true, + "description": "The ID of the view.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50, + "description": "The maximum number of results to return." + }, + "required": false, + "description": "The maximum number of results to return.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "The `next_cursor` from the previous page. Leave it out to get the first page." + }, + "required": false, + "description": "The `next_cursor` from the previous page. Leave it out to get the first page.", + "name": "cursor", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Returns a page of items.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewItemPage" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "- `validation_error`: `limit` is out of range, `cursor` came from another view or an earlier version of it, or the view holds a setting that can't be listed.\n- `invalid_id`: the ID is invalid.\n- `invalid_transition`: the view is revoked.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidIdOrInvalidTransitionOrValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "403": { + "description": "- `type_not_permitted`: your credential reaches no type.\n- `edge_permission_denied`: the view's filter names an edge type, or its `beneath` needs `parent-of`, that you can't read.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EdgePermissionDeniedOrTypeNotPermittedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "404": { + "description": "- `item_not_found`: no `system.view` you can read has this ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemNotFoundRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + } + } + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WriteContentionRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" } } - } - }, - "parameters": [ - { - "name": "Idempotency-Key", - "in": "header", - "required": false, - "schema": { - "type": "string", - "maxLength": 255 - }, - "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." }, - { - "name": "X-Marfa-Instance-Id", - "in": "header", - "required": false, - "schema": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientStorageRefusal" + } + } }, - "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } } - ] + } } }, "/views/{id}": { @@ -54530,12 +56688,216 @@ } } }, - "401": { - "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "403": { + "description": "Requires direct owner or local authority. Mutations require an owner sign-in within five minutes.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "404": { + "description": "No registered passkey of the owner has this ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PasskeyNotFoundRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "409": { + "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceChangedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTooLargeRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + } + } + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedRefusal" + "$ref": "#/components/schemas/WriteContentionRefusal" } } }, @@ -54560,12 +56922,12 @@ } } }, - "403": { - "description": "Requires direct owner or local authority. Mutations require an owner sign-in within five minutes.", + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForbiddenRefusal" + "$ref": "#/components/schemas/InsufficientStorageRefusal" } } }, @@ -54589,13 +56951,25 @@ "$ref": "#/components/headers/X-Error-Code" } } - }, - "404": { - "description": "No registered passkey of the owner has this ID.", + } + } + } + }, + "/": { + "get": { + "operationId": "getInstance", + "tags": [ + "Instance" + ], + "summary": "Get the instance", + "description": "Returns a description of the instance: its display name, `instance_id`, the build it runs, the release it was made as when it is one, the contract version it speaks and the features it serves. Needs no credential.", + "responses": { + "200": { + "description": "Returns the instance's description. A request whose `Accept` header prefers `text/html` gets an HTML page instead.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PasskeyNotFoundRefusal" + "$ref": "#/components/schemas/InstanceDescription" } } }, @@ -54605,27 +56979,15 @@ }, "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" } } }, - "409": { - "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "400": { + "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InstanceChangedRefusal" + "$ref": "#/components/schemas/ValidationErrorRefusal" } } }, @@ -54636,26 +56998,17 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } } }, - "413": { - "description": "`request_too_large`: the request body is larger than this instance accepts.", + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RequestTooLargeRefusal" + "$ref": "#/components/schemas/InternalErrorRefusal" } } }, @@ -54670,13 +57023,30 @@ "$ref": "#/components/headers/X-Error-Code" } } - }, - "429": { - "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + } + } + } + }, + "/overview": { + "get": { + "operationId": "getOverview", + "tags": [ + "Instance" + ], + "summary": "Get an overview", + "description": "Returns what the instance holds that your credential can reach: types with item counts, edge types, top tags, views and folders, and what your credential may do. Every list is capped. Needs no type access.", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Returns the overview. A credential that reaches nothing gets the instance and credential only.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RateLimitedRefusal" + "$ref": "#/components/schemas/Overview" } } }, @@ -54695,21 +57065,15 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Retry-After": { - "$ref": "#/components/headers/Retry-After" } } }, - "500": { - "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "400": { + "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InternalErrorRefusal" + "$ref": "#/components/schemas/ValidationErrorRefusal" } } }, @@ -54734,12 +57098,12 @@ } } }, - "503": { - "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WriteContentionRefusal" + "$ref": "#/components/schemas/UnauthorizedRefusal" } } }, @@ -54764,12 +57128,12 @@ } } }, - "507": { - "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InsufficientStorageRefusal" + "$ref": "#/components/schemas/RateLimitedRefusal" } } }, @@ -54791,27 +57155,18 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" } } - } - } - } - }, - "/": { - "get": { - "operationId": "getInstance", - "tags": [ - "Instance" - ], - "summary": "Get the instance", - "description": "Returns a description of the instance: its display name, `instance_id`, the build it runs, the release it was made as when it is one, the contract version it speaks and the features it serves. Needs no credential.", - "responses": { - "200": { - "description": "Returns the instance's description. A request whose `Accept` header prefers `text/html` gets an HTML page instead.", + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InstanceDescription" + "$ref": "#/components/schemas/InternalErrorRefusal" } } }, @@ -54821,15 +57176,27 @@ }, "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" } } }, - "400": { - "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.", + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorRefusal" + "$ref": "#/components/schemas/WriteContentionRefusal" } } }, @@ -54840,17 +57207,26 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } } }, - "500": { - "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InternalErrorRefusal" + "$ref": "#/components/schemas/InsufficientStorageRefusal" } } }, @@ -54861,6 +57237,15 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } @@ -56084,6 +58469,99 @@ ] } }, + "/health": { + "get": { + "operationId": "getHealth", + "tags": [ + "Health and maintenance" + ], + "summary": "Get the instance's health", + "description": "Returns whether the instance is serving and the state of each component it depends on. Needs no credential.", + "responses": { + "200": { + "description": "Returns the health. `status` is `ok` or `degraded`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Health" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + } + } + }, + "400": { + "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "503": { + "description": "Returns the health when `status` is `down`: a component refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Health" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + } + } + } + }, "/attention": { "get": { "operationId": "listAttention", @@ -56694,7 +59172,7 @@ "Health and maintenance" ], "summary": "List audit log entries", - "description": "Returns audit log entries, newest first. Marfa records changes, sign-ins and exports here, not other reads. Requires `audit.read`.", + "description": "Returns audit log entries, newest first. Marfa records changes, sign-ins, exports and the files MCP tools fetch from the web here, not other reads. Requires `audit.read`.", "security": [ { "bearerAuth": [] @@ -58330,20 +60808,20 @@ { "schema": { "type": "string", - "description": "Only export items of this type or a subtype. A wildcard such as `core.*` matches every type under that prefix." + "description": "Only export items of these types and their subtypes: a comma-separated list of up to 10, such as `core.note,app.*`. A wildcard such as `core.*` matches every type under that prefix." }, "required": false, - "description": "Only export items of this type or a subtype. A wildcard such as `core.*` matches every type under that prefix.", + "description": "Only export items of these types and their subtypes: a comma-separated list of up to 10, such as `core.note,app.*`. A wildcard such as `core.*` matches every type under that prefix.", "name": "type", "in": "query" }, { "schema": { "type": "string", - "description": "Only export items in this lifecycle state. Without it, you get every state but `trashed`. Send `any` to include trashed items too." + "description": "Only export items in these lifecycle states: one state or a comma-separated list, such as `active,archived`. Without it, you get every state but `trashed`. Send `any` on its own to include trashed items too." }, "required": false, - "description": "Only export items in this lifecycle state. Without it, you get every state but `trashed`. Send `any` to include trashed items too.", + "description": "Only export items in these lifecycle states: one state or a comma-separated list, such as `active,archived`. Without it, you get every state but `trashed`. Send `any` on its own to include trashed items too.", "name": "state", "in": "query" }, @@ -58782,7 +61260,7 @@ "additionalProperties": { "type": "number" }, - "description": "How many of the owner's type icons Marfa skipped for each reason: `already_set` when the instance already has an owner icon for the type, `unknown_icon` when the name is not an icon of the Lucide release Marfa pins." + "description": "How many of the owner's type icons Marfa skipped for each reason: `already_set` when the instance already has an owner icon for the type, `unknown_icon` when the icon isn't the ID of an icon in a set Marfa pins." } }, "required": [ @@ -58823,7 +61301,7 @@ } }, "400": { - "description": "- `validation_error`: the body isn't a valid archive, lacks one of its four files (`details.entries`), repeats an entry, is at another version, has an invalid row, or has a file over 64 MiB.\n- `invalid_properties`: a `core` or `system` item breaks its type, an event's schedule can't unfold, or `strict_mode` refuses a property.\n- `invalid_schema`: an archived type is invalid or has an unknown key.", + "description": "- `validation_error`: the body isn't a valid archive, lacks one of its four files (`details.entries`), repeats an entry, is at another version, has an invalid row, or has a file over 64 MiB.\n- `invalid_properties`: a `core` or `system` item breaks its type or its schedule can't unfold, or `strict_mode` refuses a property.\n- `invalid_schema`: an archived type is invalid or has an unknown key.", "content": { "application/json": { "schema": { @@ -59211,6 +61689,8 @@ "listItems", "updateItem", "deleteItem", + "previewItemTrash", + "trashItems", "restoreItem", "purgeItem", "transitionItem", @@ -59361,6 +61841,7 @@ "description": "Saved lists of items with their sort and grouping, held as `system.view` items.", "x-operation-order": [ "createView", + "listViewItems", "updateView", "deleteView" ] @@ -59455,6 +61936,7 @@ "description": "The instance itself: what it is and how it's configured.", "x-operation-order": [ "getInstance", + "getOverview", "getConfig", "updateConfig", "replaceConfig" @@ -59462,8 +61944,9 @@ }, { "name": "Health and maintenance", - "description": "How the instance is doing: what needs attention, its metrics and audit log, its housekeeping jobs and its retired platform types, which this build no longer ships.", + "description": "How the instance is doing: its health, what needs attention, its metrics and audit log, its housekeeping jobs and its retired platform types, which this build no longer ships.", "x-operation-order": [ + "getHealth", "listAttention", "getInstanceMetrics", "listAuditLog", diff --git a/scripts/sync-from-marfa.sh b/scripts/sync-from-marfa.sh index 7cdf9345..1ec56637 100755 --- a/scripts/sync-from-marfa.sh +++ b/scripts/sync-from-marfa.sh @@ -520,11 +520,12 @@ def write(name, lines): # The type catalog -FIELD_KEYS = {"type", "description", "required", "format", "enum_values", "items_type"} -TYPE_KEYS = {"id", "parent", "label", "description", "version", "fields", "display_hints", "merge_policy", "roles"} +FIELD_KEYS = {"type", "label", "description", "required", "format", "enum_values", "items_type", "items_fields", "status_values", "minimum", "maximum"} +MEMBER_KEYS = {"type", "label", "description", "required", "enum_values", "format", "minimum", "maximum"} +TYPE_KEYS = {"id", "parent", "label", "plural", "description", "version", "fields", "display_hints", "merge_policy", "roles"} EDGE_KEYS = { "id", "label", "reverse_label", "description", "cardinality", "source_type_constraints", "target_type_constraints", - "cascade_on_delete", "reverse_name", "written_at", "property_schema", "shipped", + "cascade_on_delete", "reverse_name", "written_at", "property_schema", "shipped", "acyclic", "flat", } STRATEGIES = {"last_writer_wins", "keep_both_copies"} CARDINALITIES = {"one-to-one", "one-to-many", "many-to-one", "many-to-many"} @@ -540,6 +541,13 @@ def kind(field): out += " of " + code(field["items_type"]) if "format" in field: out += f" ({code(field['format'])})" + low, high = field.get("minimum"), field.get("maximum") + if low is not None and high is not None: + out += f" ({low} to {high})" + elif low is not None: + out += f" (at least {low})" + elif high is not None: + out += f" (at most {high})" return out @@ -550,7 +558,13 @@ def with_values(description, values): text = description.rstrip() if text and text[-1] not in ".:;!?": text += "." - return (text + " " if text else "") + "Values: " + ", ".join(code(v) for v in values) + "." + # A status field's values are a word and the group it counts in. + shown = [code(v) if isinstance(v, str) else f"{code(v['value'])} in {code(v['group'])}" for v in values] + return (text + " " if text else "") + "Values: " + ", ".join(shown) + "." + + +def values_of(field): + return field.get("enum_values") or field.get("status_values") def check_fields(owner, fields, allowed): @@ -567,6 +581,24 @@ def check_fields(owner, fields, allowed): refuse(catalog_path, f"has an enum {owner}.{name} without values") if field["type"] == "array" and not field.get("items_type"): refuse(catalog_path, f"has an array {owner}.{name} without an items_type") + if "label" in field and not (isinstance(field["label"], str) and field["label"]): + refuse(catalog_path, f"has a field {owner}.{name} whose label is not a label") + if field["type"] == "status": + # A subtype may declare the status field it inherits without its words. + values = field.get("status_values", [{"value": "inherited", "group": "inherited"}]) + if not isinstance(values, list) or not values or not all( + isinstance(v, dict) and set(v) == {"value", "group"} and all(isinstance(x, str) and x for x in v.values()) for v in values + ): + refuse(catalog_path, f"has a status {owner}.{name} whose status_values are not a list of values and groups") + elif "status_values" in field: + refuse(catalog_path, f"has status_values on {owner}.{name}, which is not a status") + for bound in ("minimum", "maximum"): + if bound in field and (isinstance(field[bound], bool) or not isinstance(field[bound], (int, float))): + refuse(catalog_path, f"has a {bound} on {owner}.{name} that is not a number") + if "items_fields" in field: + if field["type"] != "array" or field.get("items_type") != "object": + refuse(catalog_path, f"has items_fields on {owner}.{name}, which is not an array of objects") + check_fields(f"{owner}.{name}[]", field["items_fields"], MEMBER_KEYS) def merge_policy_value(owner, policy): @@ -636,6 +668,8 @@ def render_types(types): def settings(t, parent): """The definition's keys other than its fields. A subtype gives only those its parent does not.""" rows = [] + if t.get("plural") and (parent is None or t["plural"] != parent.get("plural")): + rows.append([code("plural"), t["plural"]]) hints, parent_hints = hints_of(t), (hints_of(parent) if parent else None) or {} for key in HINT_KEYS: # A hint comes from the nearest type that sets it, so a subtype says only the ones it changes. @@ -650,12 +684,21 @@ def render_types(types): def fields_table(t, parent): """A subtype lists only the fields it adds or changes.""" - rows = [ - [code(n), kind(f), "Yes" if f.get("required") else "No", with_values(f["description"], f.get("enum_values"))] - for n, f in t["fields"].items() - if parent is None or parent["fields"].get(n) != f + rows = [] + for n, f in t["fields"].items(): + if parent is not None and parent["fields"].get(n) == f: + continue + rows.append((code(n), f)) + # The members of a list of entries follow their field, named as the field's entries are: emails[].value. + rows += [(code(f"{n}[].{m}"), mf) for m, mf in f.get("items_fields", {}).items()] + # The Label column appears only where a field in the table names one. + labelled = any("label" in f for _, f in rows) + cells = [ + [name, *([f.get("label", "")] if labelled else []), kind(f), "Yes" if f.get("required") else "No", with_values(f["description"], values_of(f))] + for name, f in rows ] - return [table(["Field", "Kind", "Required", "Description"], rows), ""] if rows else [] + header = ["Field", *(["Label"] if labelled else []), "Kind", "Required", "Description"] + return [table(header, cells), ""] if cells else [] def page(t): """A top-level type has a page of its own, whose header gives its label, description and @@ -762,10 +805,15 @@ def render_edges(edges, type_ids): lines = [f"## {e['id']}", "", safe(plain(e["description"])), ""] if e["written_at"] != "source": lines += [f"**written_at:** {code(e['written_at'])}", ""] + for flag in ("acyclic", "flat"): + if not isinstance(e.get(flag, False), bool): + refuse(catalog_path, f"has the edge type {e['id']} with an {flag} that is not true or false") + if e.get(flag): + lines += [f"**{flag}:** {code('true')}", ""] props = e.get("property_schema") or {} if props: check_fields(f"{e['id']} property", props, FIELD_KEYS) - rows = [[code(n), kind(f), with_values(f["description"], f.get("enum_values"))] for n, f in props.items()] + rows = [[code(n), kind(f), with_values(f["description"], values_of(f))] for n, f in props.items()] lines += [table(["Property", "Kind", "Description"], rows), ""] sections += lines write("edges.md", sections) @@ -889,7 +937,7 @@ ITEM_GROUPS = ( ("Identity", ("id", "type", "tier")), ("Content", ("properties", "edges", "extensions")), ("Origin", ("source", "source_id", "capture_latitude", "capture_longitude")), - ("State", ("state", "trashed_by_cascade", "trashed_with")), + ("State", ("state", "trashed_at", "trashed_by_cascade", "trashed_with")), ("Version", ("version", "schema_version")), ("Time", ("occurred_at", "created_at", "updated_at")), ) diff --git a/src/generated/GLOSSARY.md b/src/generated/GLOSSARY.md index 3ca95e96..26a3fd89 100644 --- a/src/generated/GLOSSARY.md +++ b/src/generated/GLOSSARY.md @@ -17,8 +17,8 @@ The words are grouped by topic. | **item** | One record. The unit everything else hangs off: it has a type, a state, properties, and a time it occurred at. A tier too, except on `system.*` items, where the dimension does not apply. | | **type** | What an item is, named in the namespace grammar, such as `core.note`, `user.recipe` or `.`. A type under `app.*`, `user.*` or a publisher root is registered at run time and enforced, and keeps the `version` its registration names; `core.*` and `system.*` are shipped by the platform and immutable, and `marfa.*` is reserved to it. | | **display hints** | What a type says about how to show its items: `title_field`, the field that names an item, `body_field`, the field that holds its text as Markdown, and `icon`. Each hint comes from the nearest type, the type itself first and then its parents in turn, that sets it. | -| **icon** | The picture a type is shown by: a kebab-case name from one pinned release of Lucide. A type's author sets it as `display_hints.icon`; the owner can choose another for any type, and Marfa keeps that choice beside the definition, so a connector that registers its types again does not undo it. | -| **state** | Where an item sits in its lifecycle: `active`, `archived`, `trashed`, `revoked`. Lists and searches default to `active`; `state=any` reads every state in one pass. An export is a copy rather than a listing and carries every state but `trashed`. A connector's **state document** is another thing, the JSON object it keeps on the instance under its source; name it in full. | +| **icon** | The picture a type is shown by: an Iconify icon ID, `:`, from one of the sets Marfa accepts, each pinned to one version: `lucide`, such as `lucide:calendar-days`, and `simple-icons` for brand marks, such as `simple-icons:github`. A type's author sets it as `display_hints.icon`; the owner can choose another for any type, and Marfa keeps that choice beside the definition, so a connector that registers its types again does not undo it. | +| **state** | Where an item sits in its lifecycle: `active`, `archived`, `trashed`, `revoked`. Lists and searches default to `active`; `state=any` reads every state in one pass, the bin included, on a listing and an export, but `GET /search` leaves the bin out because its index holds no row there. An export is a copy rather than a listing and carries every state but `trashed` when it names none. A connector's **state document** is another thing, the JSON object it keeps on the instance under its source; name it in full. | | **tier** | Which layer an item sits in. `feed` holds what arrives in volume and as it came, from connectors and capture, under the source's own type, unsorted until a person or an app moves it to the library or archives it. `library` holds what a person chose to keep, modeled in a core type or a child of one. An item **moves to the library** or **moves to the feed** by a change of its `tier` (`PATCH /items/{id}`): it is the same item, not a copy, and a connector's type must be readable as a core type, by `parent` or `compatible_with`, for the moved item to be read as one. Not a permission and not a lifecycle. The **namespace tier** (`core`, `system`, `app`, `user`, `marfa`, publisher) is a different axis and is not this word; say "namespace" for it. | | **link** | The value a row holds in the property its type names as `link_field`: the row's own id at the vendor that writes the type, by which a connector finds the row. One row's within the type, in every state, and kept as a tombstone when the row is purged. A type's own, never a parent's. The word keeps its ordinary sense of a URL, a blob's link or a `[[name]]` in a Markdown body, where the sentence says which. | | **tombstone** | What a purge leaves of a row under its type: its link and its natural key `(source, source_id)`, each with `purged_at` and a `settled_at`, the purge time or the later time of the vendor's own change a connector made in carrying the purge out, moved only later. An entry from the vendor naming the key is a new row only if the vendor changed it after `settled_at`. Kept, never swept, until a row holds the key again or the type goes: one small row per purged key. A link change drops the old link's; registering a type clears its identifier's. A trashed row leaves none. Not a trashed row, nor a revoked grant row. | @@ -26,15 +26,19 @@ The words are grouped by topic. ### Properties, metadata and edges -| Word | What it means | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **property** | A named value inside an item, governed by its type's schema. Never "field" for the same thing on the wire. | -| **tag** | A free-form label on an item. Flat, unversioned, and searched alongside the item's text. A release is named by a git tag, `v0.0.1`; call that a **release tag**. | -| **extension** | A namespaced object of metadata attached to an item by something other than the key that wrote the item. Read and written per namespace, and not to be confused with a blob: an extension is JSON. | -| **edge** | A directed relationship between two items, named by an **edge type**. Edges carry properties and a version of their own. | -| **reverse name** | The name an edge goes by read from its target, which an edge type may declare: `child-of` for `parent-of`. An edge type also says where it is **written at**, the end whose file writes it, or `none` where no file does. Never "inverse". | -| **label** | The heading a type or an edge type shows for itself. An edge type's label heads the source end of an edge, such as `Children` on `parent-of`, and every edge type has one. Distinct from the `id` and from a **reverse name**. | -| **reverse label** | The heading an edge type shows for the target end, such as `Parent` on `parent-of`. Optional at registration: where none is registered the **label** names both ends and a listing answers it as the reverse label. Distinct from a **reverse name**, which is a frontmatter key. | +| Word | What it means | +| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **property** | A named value inside an item, governed by its type's schema. Never "field" for the same thing on the wire. | +| **status field** | A type's one field of the `status` kind, whatever its name: `core.task` calls it `status`, `core.media` calls it `progress`. A `status_group` filter reads it. | +| **status word** | A value a type's status field takes, such as `in_review`, declared in the field's `status_values` with the **status group** it belongs to. A type's own: a subtype adds words to the ones it inherits and never declares one again. An app shows its own label for a word. Never "status value" or "state", which is the lifecycle. | +| **status group** | One of four fixed meanings every **status word** belongs to: `not_started`, `in_progress`, `completed` and `canceled`. The same on every type, so a filter (`status_group`), a view or an app can tell what is open or finished without knowing a type's words. | +| **tag** | A free-form label on an item. Flat, unversioned, and searched alongside the item's text. A release is named by a git tag, `v0.0.1`; call that a **release tag**. | +| **extension** | A namespaced object of metadata attached to an item by something other than the key that wrote the item. Read and written per namespace, and not to be confused with a blob: an extension is JSON. | +| **edge** | A directed relationship between two items, named by an **edge type**. Edges carry properties and a version of their own. | +| **reverse name** | The name an edge goes by read from its target, which an edge type may declare: `child-of` for `parent-of`. An edge type also says where it is **written at**, the end whose file writes it, or `none` where no file does. Never "inverse". | +| **label** | The heading a type, a field or an edge type shows for itself. An edge type's label heads the source end of an edge, such as `Children` on `parent-of`, and every edge type has one. A field without one reads by its name, `due_at` as `Due At`. Distinct from the `id` and from a **reverse name**. | +| **plural** | The name of a list of a type's items, such as `People` for `core.entity.person`. A type that names none reads with one made from its label. Never inherited. | +| **reverse label** | The heading an edge type shows for the target end, such as `Parent` on `parent-of`. Optional at registration: where none is registered the **label** names both ends and a listing answers it as the reverse label. Distinct from a **reverse name**, which is a frontmatter key. | ### Versions and events @@ -98,20 +102,20 @@ Access is read the same way wherever a person sees it: on the pages where they a | `metadata.types` | Custom data types | | `metadata.edge_types` | Custom relationship types | -| Power | The title | -| ------------------- | ----------------------------------------------------------- | -| `keys.mint` | Make, see, edit and revoke keys | -| `keys.manage` | See, edit, narrow and revoke every key | -| `grants.manage` | See and disconnect other apps | -| `config.manage` | See and change Marfa's settings | -| `instance.read` | See health, metrics, storage and job status | -| `instance.maintain` | Run housekeeping, reset platform types and cancel bulk jobs | -| `schema.write` | Replace or remove type definitions | -| `blobs.manage` | Read and add any file, and manage file storage | -| `webhooks.manage` | Manage webhooks that send events to other services | -| `connectors.manage` | See, manage and remove all connectors | -| `audit.read` | Read the activity log | -| `items.purge` | Delete items in the trash for good | +| Power | The title | +| ------------------- | -------------------------------------------------------------------- | +| `keys.mint` | Make, see, edit and revoke keys | +| `keys.manage` | See, edit, narrow and revoke every key | +| `grants.manage` | See and disconnect other apps | +| `config.manage` | See and change Marfa's settings | +| `instance.read` | See health, metrics, storage and job status | +| `instance.maintain` | Run housekeeping, remove retired platform types and cancel bulk jobs | +| `schema.write` | Replace or remove type definitions | +| `blobs.manage` | Read and add any file, and manage file storage | +| `webhooks.manage` | Manage webhooks that send events to other services | +| `connectors.manage` | See, manage and remove all connectors | +| `audit.read` | Read the activity log | +| `items.purge` | Delete items in the trash for good | ### The instance @@ -133,7 +137,7 @@ Access is read the same way wherever a person sees it: on the pages where they a | **reach** | The server's proof, for one credential, that the inventory, the direct reads and the event stream a working copy holds were all read under the same limits on what that credential may read. A copy stream gives it as `reach` in its markers, and a conditional read or a resumed stream sends it back as `X-Marfa-Reach`. It changes when what the credential may read changes, and then the server answers `reach_changed` and the working copy is built again. A key's reach, in the other sense, is what its permissions and maps let it read and write. | | **slice** | The declared subset of an instance one device holds: a type list and a tier, and any edge types it holds whole. What a working copy is a copy of. | | **pin** | To hold one row in a working copy by id, whatever the slice says of it. A pinned row is read at once, kept current by catch-up and read again by every hydration, and goes once its pin is off and the slice does not take it (where a write to it waits, at an answered move, the next catch-up that touches it or the next hydration), or when it is purged or its create refused. | -| **verdict** | What a device gives a queued write once the server answers it, or once it cannot be sent: one of `accepted`, `merged` (applied with changes), `conflicted` (written to a conflicted copy), `refused` (with a reason), `blocked` (held, for one of six reasons such as a refused credential or a write it waits on) and `dead` (given up after retries). | +| **verdict** | What a device gives a queued write once the server answers it, or once it cannot be sent: one of `accepted`, `merged` (applied with changes), `conflicted` (written to a conflicted copy), `refused` (with a reason), `blocked` (held, for one of seven reasons such as a refused credential or a write it waits on) and `dead` (given up after retries). | | **file cache** | The folder beside a device's store that holds the bytes of the files it fetched and of its queued uploads, named by hash. What a device can fetch again, other than an attachment's bytes, it may take away to fit the file budget, and keeps out of backups where the platform allows; the bytes of a queued upload stay in backups. | | **attachment** | A file item out of the bin that is the source of an `attached-to` edge whose target a working copy holds out of the bin. Its bytes stay in the file cache outside the file budget, and a device fetches them when asked (`marfa device blobs fetch-kept`, `fetchKept()` in the Node module and the Swift binding). Once its edges to items the copy holds go, its bytes are cached as any other. In a folder, a document's attachments are written with it: where an embed says, else beside it, unless the folder's query holds one on its own. | | **file budget** | The most bytes the file cache is to hold, other than the bytes of attachments: a byte count a caller sets, none while every file is kept, or by default the lesser of the larger of 2 GiB and a tenth of the volume, and the bytes held other than attachments' plus the volume's free bytes less a twentieth of the volume, never below 256 MiB, and 2 GiB where the volume cannot be read. Bytes a queued upload names, an attachment's bytes, and bytes whose path or file a caller in the same process is still waiting for, are never taken to fit it. | @@ -184,20 +188,20 @@ These name things that were removed. None of them belongs in a tracked file, in A permission is named by what it permits, never by a noun for the whole. The mapping is fixed here so the derived names are as settled as the three the vocabulary decided. -| Permission | What it permits | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `schema.write` | Removing type and edge type definitions that already exist, and replacing them, only those the credential's type or edge map grants write on. Registering one is separately fenced by `metadata.types:write` and `metadata.edge_types:write`. The one act both permissions open is a type's own evolution through `PUT /types/{id}`: adding an optional field no stored row holds a value under, and changing its label, description, display hints and version. Every other replacement, a field's removal among them, and every deletion takes `schema.write`. It also permits changing or clearing the owner's icon for a type, with write on that type in the key's map, the same as to replace the type. | -| `keys.mint` | Minting keys within the caller's current access. It also permits listing, changing and revoking keys within that access. An app may delegate only what its grant permits. | -| `items.purge` | Destroying a trashed row irrecoverably, through the single-row operation and the bulk one alike. | -| `webhooks.manage` | Reading, registering, changing and removing the outbound subscriptions that send an instance's events out, and redelivering what they sent. Reading takes the same permission as registering. | -| `config.manage` | Reading and replacing the instance configuration at `/config`: the enforcement levers and the cleanup-job retention overrides. **Not `config.write`**: a caller refused it cannot read the config either. | -| `audit.read` | Reading the audit log. There is no `audit.write` beside it: the server writes the log, a caller never does. | -| `grants.manage` | Listing and revoking other apps' access. Deliberately not `keys.mint`: a key is this app's own credential, a grant is another app's, and folding them together would let an app trusted to rotate a key revoke every other app the owner has authorized. | -| `instance.read` | Reading detailed health, metrics, housekeeping status, retired platform types, blob storage diagnostics and bulk job status. It grants no content read access. | -| `instance.maintain` | Running housekeeping, resetting platform type definitions and cancelling bulk jobs. It grants no content write access. | -| `connectors.manage` | Reading and administering connector registrations, their runs and their endpoints, deleting registrations and clearing a registration's retained state. Heartbeats, holds, run reports, reading and writing retained state, agreements and received deliveries belong to the connector's own key alone. | -| `blobs.manage` | Managing blob bytes and copies across the instance, without granting item content access. | -| `keys.manage` | Listing and revoking keys across the instance, and changing their metadata or narrowing access. It does not grant unrestricted minting or widening; changing enforcement overrides also requires `config.manage`. | +| Permission | What it permits | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `schema.write` | Removing type and edge type definitions that already exist, and replacing them, only those the credential's type or edge map grants write on. Registering one is separately fenced by `metadata.types:write` and `metadata.edge_types:write`. The one act both permissions open is a type's own evolution through `PUT /types/{id}`: adding an optional field no stored row holds a value under, and changing its label, plural, description, display hints and version, and a kept field's label and description. Every other replacement, a field's removal among them, and every deletion takes `schema.write`. It also permits changing or clearing the owner's icon for a type, with write on that type in the key's map, the same as to replace the type. | +| `keys.mint` | Minting keys within the caller's current access. It also permits listing, changing and revoking keys within that access. An app may delegate only what its grant permits. | +| `items.purge` | Destroying a trashed row irrecoverably, through the single-row operation and the bulk one alike. | +| `webhooks.manage` | Reading, registering, changing and removing the outbound subscriptions that send an instance's events out, and redelivering what they sent. Reading takes the same permission as registering. | +| `config.manage` | Reading and replacing the instance configuration at `/config`: the enforcement levers and the cleanup-job retention overrides. **Not `config.write`**: a caller refused it cannot read the config either. | +| `audit.read` | Reading the audit log. There is no `audit.write` beside it: the server writes the log, a caller never does. | +| `grants.manage` | Listing and revoking other apps' access. Deliberately not `keys.mint`: a key is this app's own credential, a grant is another app's, and folding them together would let an app trusted to rotate a key revoke every other app the owner has authorized. | +| `instance.read` | Reading detailed health, metrics, housekeeping status, retired platform types, blob storage diagnostics and bulk job status. It grants no content read access. | +| `instance.maintain` | Running housekeeping, removing retired platform types and cancelling bulk jobs. It grants no content write access. | +| `connectors.manage` | Reading and administering connector registrations, their runs and their endpoints, deleting registrations and clearing a registration's retained state. Heartbeats, holds, run reports, reading and writing retained state, agreements and received deliveries belong to the connector's own key alone. | +| `blobs.manage` | Managing blob bytes and copies across the instance, without granting item content access. | +| `keys.manage` | Listing and revoking keys across the instance, and changing their metadata or narrowing access. It does not grant unrestricted minting or widening; changing enforcement overrides also requires `config.manage`. | These twelve permissions name existing operations. They can be granted to ordinary keys and approved OAuth apps. Content maps remain independent, so holding every named permission does not grant item read or write access. No permission grants private machine authority, owner recovery, access to the direct `/owner` endpoint or archive restore. @@ -226,6 +230,6 @@ Its sibling `updated_before` closes the same window at the top and is **exclusiv - `type_not_permitted`: the type is not one this credential may reach here. A `403`, decided by the namespace and the credential's permission map rather than by the identifier. It consults no registry, so it is not a statement that the type exists. On an operation that looks up the item an id names, it means the credential may read the item's type and not write it, or reaches no type at all, and it is never the answer for an item whose type the credential may not read. The folder settings and view operations are the exception: they refuse a key without write on `system.folder` or `system.view` before looking any row up. Folder status reads and reports use folder read access. On the blob operations it refuses a credential reaching no type before the hash is read, and an upload from one that writes no type; a blob it may not read answers `blob_not_found`, as an unknown hash does. - `item_not_found`: no item this credential may read has this id, or, on the folder operations, no folder: a readable item that is not a `system.folder` answers it there too, as one that is not a `system.view` does on the view operations. An item of a type it may not read answers exactly as one that does not exist, so the answer is not a statement that no such row exists. A key learns its own reach from `GET /keys/current`, and a signed-in app's token from the scope list on its token response, never from a refusal. - `api_key_not_found`: no key within this credential's reach has this id. A key it could not have minted answers exactly as one that does not exist, as `item_not_found` does for an item it may not read. -- A **malformed** identifier is neither: it never reached a lookup, so it answers the generic `validation_error` with the field named. +- A **malformed** identifier is neither: it never reached a lookup, so it answers `400 invalid_id`. - `instance_changed`: a request named, in `X-Marfa-Instance-Id`, an instance other than the one at the address, so it reached a different instance from the one its sender's copy came from. A `409`, judged before anything about the request but its credential, and never recorded under an `Idempotency-Key`. Not `reach_changed`, which says the reach a credential holds on the same instance has changed. - `edge_cycle`: an edge that would close a cycle. A **self-loop** is refused on every edge type; a longer cycle is detected only on the cycle-prone types, `parent-of` and `supersedes`, where the graph is not acyclic by construction. diff --git a/src/generated/catalog/builtin-catalog.json b/src/generated/catalog/builtin-catalog.json index b904169d..331a7a94 100644 --- a/src/generated/catalog/builtin-catalog.json +++ b/src/generated/catalog/builtin-catalog.json @@ -8,6 +8,7 @@ "fields": { "url": { "type": "url", + "label": "URL", "description": "The saved URL" }, "body": { @@ -24,6 +25,7 @@ }, "source_url": { "type": "url", + "label": "Source URL", "description": "Where the content was sourced from" }, "source_title": { @@ -36,10 +38,12 @@ }, "published_at": { "type": "datetime", + "label": "Published", "description": "When the original content was published" }, "image_url": { "type": "url", + "label": "Image URL", "description": "Preview image" }, "language": { @@ -55,7 +59,7 @@ "display_hints": { "title_field": "title", "body_field": "body", - "icon": "bookmark" + "icon": "lucide:bookmark" }, "merge_policy": { "fields": { @@ -63,7 +67,8 @@ "notes": "keep_both_copies" }, "default": "last_writer_wins" - } + }, + "plural": "Bookmarks" }, { "id": "core.collection", @@ -84,14 +89,15 @@ "display_hints": { "title_field": "title", "body_field": "description", - "icon": "layers" + "icon": "lucide:layers" }, "merge_policy": { "default": "last_writer_wins" }, "roles": [ "container" - ] + ], + "plural": "Collections" }, { "id": "core.entity", @@ -106,26 +112,83 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "description": { "type": "string", "description": "What the entity is" }, - "email": { - "type": "email", - "description": "Contact email" - }, - "phone": { - "type": "string", - "description": "Contact phone (E.164 recommended)" - }, - "place": { - "type": "string", - "description": "Location (human-readable address)" + "emails": { + "type": "array", + "description": "Email addresses. The first entry is the main one, and an entry's label is free text, such as work or home.", + "items_type": "object", + "items_fields": { + "value": { + "type": "email", + "label": "Email", + "description": "The email address", + "required": true + }, + "label": { + "type": "string", + "description": "What the entry is for, such as work or home" + } + } + }, + "phones": { + "type": "array", + "label": "Phone Numbers", + "description": "Phone numbers. The first entry is the main one, and an entry's label is free text, such as work or home.", + "items_type": "object", + "items_fields": { + "value": { + "type": "string", + "label": "Number", + "description": "The phone number, in E.164 form where known", + "required": true, + "format": "phone" + }, + "label": { + "type": "string", + "description": "What the entry is for, such as work or home" + } + } + }, + "addresses": { + "type": "array", + "description": "Postal addresses. The first entry is the main one, and an entry's label is free text, such as work or home.", + "items_type": "object", + "items_fields": { + "label": { + "type": "string", + "description": "What the entry is for, such as work or home" + }, + "street_address": { + "type": "string", + "description": "Street line" + }, + "locality": { + "type": "string", + "description": "City or town" + }, + "region": { + "type": "string", + "description": "State, province, or county" + }, + "postal_code": { + "type": "string", + "description": "Postcode or ZIP code" + }, + "country": { + "type": "string", + "description": "Country name or code" + } + } }, "image_url": { "type": "url", + "label": "Image URL", "description": "Logo or brand image" }, "legal_name": { @@ -143,16 +206,18 @@ }, "display_hints": { "title_field": "name", - "icon": "shapes" + "icon": "lucide:shapes" }, "merge_policy": { "default": "last_writer_wins" - } + }, + "plural": "Entities" }, { "id": "core.entity.person", "parent": "core.entity", "label": "Person", + "plural": "People", "description": "Contact information for an individual. Inherits all core.entity fields.", "version": 0, "fields": { @@ -163,26 +228,83 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "description": { "type": "string", "description": "What the entity is" }, - "email": { - "type": "email", - "description": "Contact email" - }, - "phone": { - "type": "string", - "description": "Contact phone (E.164 recommended)" - }, - "place": { - "type": "string", - "description": "Location (human-readable address)" + "emails": { + "type": "array", + "description": "Email addresses. The first entry is the main one, and an entry's label is free text, such as work or home.", + "items_type": "object", + "items_fields": { + "value": { + "type": "email", + "label": "Email", + "description": "The email address", + "required": true + }, + "label": { + "type": "string", + "description": "What the entry is for, such as work or home" + } + } + }, + "phones": { + "type": "array", + "label": "Phone Numbers", + "description": "Phone numbers. The first entry is the main one, and an entry's label is free text, such as work or home.", + "items_type": "object", + "items_fields": { + "value": { + "type": "string", + "label": "Number", + "description": "The phone number, in E.164 form where known", + "required": true, + "format": "phone" + }, + "label": { + "type": "string", + "description": "What the entry is for, such as work or home" + } + } + }, + "addresses": { + "type": "array", + "description": "Postal addresses. The first entry is the main one, and an entry's label is free text, such as work or home.", + "items_type": "object", + "items_fields": { + "label": { + "type": "string", + "description": "What the entry is for, such as work or home" + }, + "street_address": { + "type": "string", + "description": "Street line" + }, + "locality": { + "type": "string", + "description": "City or town" + }, + "region": { + "type": "string", + "description": "State, province, or county" + }, + "postal_code": { + "type": "string", + "description": "Postcode or ZIP code" + }, + "country": { + "type": "string", + "description": "Country name or code" + } + } }, "image_url": { "type": "url", + "label": "Image URL", "description": "Logo or brand image" }, "legal_name": { @@ -243,7 +365,7 @@ } }, "display_hints": { - "icon": "user" + "icon": "lucide:user" }, "merge_policy": { "default": "last_writer_wins" @@ -263,26 +385,83 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "description": { "type": "string", "description": "What the entity is" }, - "email": { - "type": "email", - "description": "Contact email" - }, - "phone": { - "type": "string", - "description": "Contact phone (E.164 recommended)" - }, - "place": { - "type": "string", - "description": "Location (human-readable address)" + "emails": { + "type": "array", + "description": "Email addresses. The first entry is the main one, and an entry's label is free text, such as work or home.", + "items_type": "object", + "items_fields": { + "value": { + "type": "email", + "label": "Email", + "description": "The email address", + "required": true + }, + "label": { + "type": "string", + "description": "What the entry is for, such as work or home" + } + } + }, + "phones": { + "type": "array", + "label": "Phone Numbers", + "description": "Phone numbers. The first entry is the main one, and an entry's label is free text, such as work or home.", + "items_type": "object", + "items_fields": { + "value": { + "type": "string", + "label": "Number", + "description": "The phone number, in E.164 form where known", + "required": true, + "format": "phone" + }, + "label": { + "type": "string", + "description": "What the entry is for, such as work or home" + } + } + }, + "addresses": { + "type": "array", + "description": "Postal addresses. The first entry is the main one, and an entry's label is free text, such as work or home.", + "items_type": "object", + "items_fields": { + "label": { + "type": "string", + "description": "What the entry is for, such as work or home" + }, + "street_address": { + "type": "string", + "description": "Street line" + }, + "locality": { + "type": "string", + "description": "City or town" + }, + "region": { + "type": "string", + "description": "State, province, or county" + }, + "postal_code": { + "type": "string", + "description": "Postcode or ZIP code" + }, + "country": { + "type": "string", + "description": "Country name or code" + } + } }, "image_url": { "type": "url", + "label": "Image URL", "description": "Logo or brand image" }, "legal_name": { @@ -320,6 +499,7 @@ }, "timezone": { "type": "string", + "label": "Time Zone", "description": "IANA timezone" }, "latitude": { @@ -336,11 +516,12 @@ } }, "display_hints": { - "icon": "map-pin" + "icon": "lucide:map-pin" }, "merge_policy": { "default": "last_writer_wins" - } + }, + "plural": "Places" }, { "id": "core.event", @@ -359,10 +540,12 @@ }, "starts_at": { "type": "datetime", + "label": "Starts", "description": "The instant the event starts, ISO 8601. An instant carries no zone of its own however it is written; the timezone field is what anchors the event's wall-clock hour." }, "ends_at": { "type": "datetime", + "label": "Ends", "description": "The instant the event ends, ISO 8601." }, "duration": { @@ -383,6 +566,7 @@ }, "url": { "type": "url", + "label": "URL", "description": "Event link" }, "precision": { @@ -396,8 +580,13 @@ ] }, "status": { - "type": "string", - "description": "Recommended values: tentative, confirmed, canceled, rescheduled" + "type": "enum", + "description": "Whether the event will happen. A tentative event might not, a confirmed one will and a canceled one will not. Moving an event changes its time, not its status.", + "enum_values": [ + "tentative", + "confirmed", + "canceled" + ] }, "notes": { "type": "string", @@ -414,28 +603,57 @@ }, "timezone": { "type": "string", + "label": "Time Zone", "description": "IANA time zone the event's schedule keeps its wall-clock hour in, e.g. Europe/Berlin. Stored times stay instants; a recurring series expands in this zone so occurrences keep their local hour across a daylight-saving transition. Absent means the rule advances in UTC. This is the start zone: an event that ends somewhere else states that in end_timezone. A write is refused unless the zone database resolves it as a named zone." }, "end_timezone": { "type": "string", + "label": "End Time Zone", "description": "IANA time zone the event ends in, when that differs from timezone — a flight lands in a zone it did not depart from, and both ends are wall-clock facts a person reads off a ticket. Absent means the event ends in the zone it started in. Recurrence expands in timezone only; this field never anchors a rule. A write is refused unless the zone database resolves it as a named zone." }, + "attendees": { + "type": "array", + "description": "The people invited, one entry each, in the order written. An entry names a person by name, email or both, and holds no item ID: an app finds the person behind an entry by matching its email to the email addresses a person holds. response is the reply, from the participation status of RFC 5545; needs_action means no reply yet.", + "items_type": "object", + "items_fields": { + "name": { + "type": "string", + "description": "The attendee's name as the source gives it" + }, + "email": { + "type": "email", + "description": "The attendee's email address" + }, + "response": { + "type": "enum", + "description": "The attendee's reply to the invitation", + "enum_values": [ + "needs_action", + "accepted", + "declined", + "tentative" + ] + } + } + }, "original_starts_at": { "type": "datetime", + "label": "Original Start", "description": "For an event that replaces one occurrence of a series, the start instant of the occurrence it replaces. Its series is named by a parent-of edge." } }, "display_hints": { "title_field": "title", "body_field": "description", - "icon": "calendar" + "icon": "lucide:calendar" }, "merge_policy": { "fields": { "notes": "keep_both_copies" }, "default": "last_writer_wins" - } + }, + "plural": "Events" }, { "id": "core.file", @@ -445,11 +663,13 @@ "fields": { "blob_ref": { "type": "string", + "label": "Blob Reference", "description": "Reference to the binary content (sha256:)", "required": true }, "mime_type": { "type": "string", + "label": "MIME Type", "description": "MIME type", "required": true }, @@ -463,10 +683,12 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "source_url": { "type": "url", + "label": "Source URL", "description": "Where the file was sourced from" }, "author": { @@ -492,31 +714,36 @@ }, "size_bytes": { "type": "integer", + "label": "Size", "description": "Size in bytes of the referenced blob (set by the server from the bytes the instance holds; absent where whoever named blob_ref never proved it holds them)" } }, "display_hints": { "title_field": "title", - "icon": "file" + "icon": "lucide:file" }, "merge_policy": { "default": "last_writer_wins" - } + }, + "plural": "Files" }, { "id": "core.file.audio", "parent": "core.file", "label": "Audio", + "plural": "Audio", "description": "Recordings, music files, voice memos. Inherits all core.file fields.", "version": 0, "fields": { "blob_ref": { "type": "string", + "label": "Blob Reference", "description": "Reference to the binary content (sha256:)", "required": true }, "mime_type": { "type": "string", + "label": "MIME Type", "description": "MIME type", "required": true }, @@ -530,10 +757,12 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "source_url": { "type": "url", + "label": "Source URL", "description": "Where the file was sourced from" }, "author": { @@ -559,6 +788,7 @@ }, "size_bytes": { "type": "integer", + "label": "Size", "description": "Size in bytes of the referenced blob (set by the server from the bytes the instance holds; absent where whoever named blob_ref never proved it holds them)" }, "duration": { @@ -567,7 +797,7 @@ } }, "display_hints": { - "icon": "file-music" + "icon": "lucide:file-music" }, "merge_policy": { "default": "last_writer_wins" @@ -582,11 +812,13 @@ "fields": { "blob_ref": { "type": "string", + "label": "Blob Reference", "description": "Reference to the binary content (sha256:)", "required": true }, "mime_type": { "type": "string", + "label": "MIME Type", "description": "MIME type", "required": true }, @@ -600,10 +832,12 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "source_url": { "type": "url", + "label": "Source URL", "description": "Where the file was sourced from" }, "author": { @@ -629,6 +863,7 @@ }, "size_bytes": { "type": "integer", + "label": "Size", "description": "Size in bytes of the referenced blob (set by the server from the bytes the instance holds; absent where whoever named blob_ref never proved it holds them)" }, "width": { @@ -657,11 +892,12 @@ } }, "display_hints": { - "icon": "file-image" + "icon": "lucide:file-image" }, "merge_policy": { "default": "last_writer_wins" - } + }, + "plural": "Images" }, { "id": "core.file.video", @@ -672,11 +908,13 @@ "fields": { "blob_ref": { "type": "string", + "label": "Blob Reference", "description": "Reference to the binary content (sha256:)", "required": true }, "mime_type": { "type": "string", + "label": "MIME Type", "description": "MIME type", "required": true }, @@ -690,10 +928,12 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "source_url": { "type": "url", + "label": "Source URL", "description": "Where the file was sourced from" }, "author": { @@ -719,6 +959,7 @@ }, "size_bytes": { "type": "integer", + "label": "Size", "description": "Size in bytes of the referenced blob (set by the server from the bytes the instance holds; absent where whoever named blob_ref never proved it holds them)" }, "width": { @@ -747,11 +988,12 @@ } }, "display_hints": { - "icon": "file-video-camera" + "icon": "lucide:file-video-camera" }, "merge_policy": { "default": "last_writer_wins" - } + }, + "plural": "Videos" }, { "id": "core.file.webpage", @@ -762,11 +1004,13 @@ "fields": { "blob_ref": { "type": "string", + "label": "Blob Reference", "description": "Reference to the binary content (sha256:)", "required": true }, "mime_type": { "type": "string", + "label": "MIME Type", "description": "MIME type", "required": true }, @@ -780,10 +1024,12 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "source_url": { "type": "url", + "label": "Source URL", "description": "Where the file was sourced from" }, "author": { @@ -809,16 +1055,18 @@ }, "size_bytes": { "type": "integer", + "label": "Size", "description": "Size in bytes of the referenced blob (set by the server from the bytes the instance holds; absent where whoever named blob_ref never proved it holds them)" } }, "display_hints": { "title_field": "title", - "icon": "globe" + "icon": "lucide:globe" }, "merge_policy": { "default": "last_writer_wins" - } + }, + "plural": "Web Pages" }, { "id": "core.highlight", @@ -871,18 +1119,20 @@ "display_hints": { "title_field": "text", "body_field": "note", - "icon": "highlighter" + "icon": "lucide:highlighter" }, "merge_policy": { "fields": { "note": "keep_both_copies" }, "default": "last_writer_wins" - } + }, + "plural": "Highlights" }, { "id": "core.media", "label": "Media", + "plural": "Media", "description": "Content produced by someone that the user engages with — a film, podcast, book, article, song, show.", "version": 0, "fields": { @@ -901,6 +1151,7 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "description": { @@ -913,10 +1164,12 @@ }, "published_at": { "type": "datetime", + "label": "Published", "description": "When originally published or released" }, "image_url": { "type": "url", + "label": "Image URL", "description": "Cover art, poster, or thumbnail" }, "language": { @@ -924,6 +1177,34 @@ "description": "BCP 47 language code", "format": "bcp47" }, + "progress": { + "type": "status", + "description": "Where the owner is with the work: want to read, watch, listen to or play it, started, finished, or gave up", + "status_values": [ + { + "value": "planned", + "group": "not_started" + }, + { + "value": "started", + "group": "in_progress" + }, + { + "value": "finished", + "group": "completed" + }, + { + "value": "abandoned", + "group": "canceled" + } + ] + }, + "rating": { + "type": "integer", + "description": "The owner's overall view of the work, from 1 (lowest) to 5 (highest)", + "minimum": 1, + "maximum": 5 + }, "notes": { "type": "string", "description": "Personal annotations" @@ -936,7 +1217,7 @@ "display_hints": { "title_field": "title", "body_field": "body", - "icon": "library-big" + "icon": "lucide:library-big" }, "merge_policy": { "fields": { @@ -968,6 +1249,7 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "description": { @@ -980,10 +1262,12 @@ }, "published_at": { "type": "datetime", + "label": "Published", "description": "When originally published or released" }, "image_url": { "type": "url", + "label": "Image URL", "description": "Cover art, poster, or thumbnail" }, "language": { @@ -991,6 +1275,34 @@ "description": "BCP 47 language code", "format": "bcp47" }, + "progress": { + "type": "status", + "description": "Where the owner is with the work: want to read, watch, listen to or play it, started, finished, or gave up", + "status_values": [ + { + "value": "planned", + "group": "not_started" + }, + { + "value": "started", + "group": "in_progress" + }, + { + "value": "finished", + "group": "completed" + }, + { + "value": "abandoned", + "group": "canceled" + } + ] + }, + "rating": { + "type": "integer", + "description": "The owner's overall view of the work, from 1 (lowest) to 5 (highest)", + "minimum": 1, + "maximum": 5 + }, "notes": { "type": "string", "description": "Personal annotations" @@ -1005,11 +1317,12 @@ }, "num_tracks": { "type": "integer", + "label": "Tracks", "description": "Total track count" } }, "display_hints": { - "icon": "disc-3" + "icon": "lucide:disc-3" }, "merge_policy": { "fields": { @@ -1020,7 +1333,8 @@ }, "roles": [ "container" - ] + ], + "plural": "Albums" }, { "id": "core.media.article", @@ -1044,6 +1358,7 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "description": { @@ -1056,10 +1371,12 @@ }, "published_at": { "type": "datetime", + "label": "Published", "description": "When originally published or released" }, "image_url": { "type": "url", + "label": "Image URL", "description": "Cover art, poster, or thumbnail" }, "language": { @@ -1067,6 +1384,34 @@ "description": "BCP 47 language code", "format": "bcp47" }, + "progress": { + "type": "status", + "description": "Where the owner is with the work: want to read, watch, listen to or play it, started, finished, or gave up", + "status_values": [ + { + "value": "planned", + "group": "not_started" + }, + { + "value": "started", + "group": "in_progress" + }, + { + "value": "finished", + "group": "completed" + }, + { + "value": "abandoned", + "group": "canceled" + } + ] + }, + "rating": { + "type": "integer", + "description": "The owner's overall view of the work, from 1 (lowest) to 5 (highest)", + "minimum": 1, + "maximum": 5 + }, "notes": { "type": "string", "description": "Personal annotations" @@ -1075,17 +1420,23 @@ "type": "thumbnail", "description": "A small preview of the work, supplied by the writer" }, + "doi": { + "type": "string", + "label": "DOI", + "description": "Digital Object Identifier (ISO 26324)" + }, "section": { "type": "string", "description": "Section of the publication" }, "word_count": { "type": "integer", + "label": "Words", "description": "Word count" } }, "display_hints": { - "icon": "newspaper" + "icon": "lucide:newspaper" }, "merge_policy": { "fields": { @@ -1093,7 +1444,8 @@ "notes": "keep_both_copies" }, "default": "last_writer_wins" - } + }, + "plural": "Articles" }, { "id": "core.media.book", @@ -1117,6 +1469,7 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "description": { @@ -1129,10 +1482,12 @@ }, "published_at": { "type": "datetime", + "label": "Published", "description": "When originally published or released" }, "image_url": { "type": "url", + "label": "Image URL", "description": "Cover art, poster, or thumbnail" }, "language": { @@ -1140,6 +1495,34 @@ "description": "BCP 47 language code", "format": "bcp47" }, + "progress": { + "type": "status", + "description": "Where the owner is with the work: want to read, watch, listen to or play it, started, finished, or gave up", + "status_values": [ + { + "value": "planned", + "group": "not_started" + }, + { + "value": "started", + "group": "in_progress" + }, + { + "value": "finished", + "group": "completed" + }, + { + "value": "abandoned", + "group": "canceled" + } + ] + }, + "rating": { + "type": "integer", + "description": "The owner's overall view of the work, from 1 (lowest) to 5 (highest)", + "minimum": 1, + "maximum": 5 + }, "notes": { "type": "string", "description": "Personal annotations" @@ -1150,10 +1533,12 @@ }, "isbn": { "type": "string", + "label": "ISBN", "description": "International Standard Book Number (ISO 2108)" }, "page_count": { "type": "integer", + "label": "Pages", "description": "Number of pages" }, "edition": { @@ -1162,7 +1547,7 @@ } }, "display_hints": { - "icon": "book-open" + "icon": "lucide:book-open" }, "merge_policy": { "fields": { @@ -1170,7 +1555,8 @@ "notes": "keep_both_copies" }, "default": "last_writer_wins" - } + }, + "plural": "Books" }, { "id": "core.media.episode", @@ -1194,6 +1580,7 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "description": { @@ -1206,10 +1593,12 @@ }, "published_at": { "type": "datetime", + "label": "Published", "description": "When originally published or released" }, "image_url": { "type": "url", + "label": "Image URL", "description": "Cover art, poster, or thumbnail" }, "language": { @@ -1217,6 +1606,34 @@ "description": "BCP 47 language code", "format": "bcp47" }, + "progress": { + "type": "status", + "description": "Where the owner is with the work: want to read, watch, listen to or play it, started, finished, or gave up", + "status_values": [ + { + "value": "planned", + "group": "not_started" + }, + { + "value": "started", + "group": "in_progress" + }, + { + "value": "finished", + "group": "completed" + }, + { + "value": "abandoned", + "group": "canceled" + } + ] + }, + "rating": { + "type": "integer", + "description": "The owner's overall view of the work, from 1 (lowest) to 5 (highest)", + "minimum": 1, + "maximum": 5 + }, "notes": { "type": "string", "description": "Personal annotations" @@ -1250,15 +1667,17 @@ }, "media_url": { "type": "url", + "label": "Media URL", "description": "Direct address of the media file itself: an episode's audio or video enclosure, a track's audio file, a film's stream. Distinct from url, which is the web page about the work." }, "mime_type": { "type": "string", + "label": "MIME Type", "description": "MIME type of the resource at media_url, such as audio/mpeg or video/mp4. Describes what media_url points at, not what this item is." } }, "display_hints": { - "icon": "circle-play" + "icon": "lucide:circle-play" }, "merge_policy": { "fields": { @@ -1266,7 +1685,8 @@ "notes": "keep_both_copies" }, "default": "last_writer_wins" - } + }, + "plural": "Episodes" }, { "id": "core.media.film", @@ -1290,6 +1710,7 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "description": { @@ -1302,10 +1723,12 @@ }, "published_at": { "type": "datetime", + "label": "Published", "description": "When originally published or released" }, "image_url": { "type": "url", + "label": "Image URL", "description": "Cover art, poster, or thumbnail" }, "language": { @@ -1313,6 +1736,34 @@ "description": "BCP 47 language code", "format": "bcp47" }, + "progress": { + "type": "status", + "description": "Where the owner is with the work: want to read, watch, listen to or play it, started, finished, or gave up", + "status_values": [ + { + "value": "planned", + "group": "not_started" + }, + { + "value": "started", + "group": "in_progress" + }, + { + "value": "finished", + "group": "completed" + }, + { + "value": "abandoned", + "group": "canceled" + } + ] + }, + "rating": { + "type": "integer", + "description": "The owner's overall view of the work, from 1 (lowest) to 5 (highest)", + "minimum": 1, + "maximum": 5 + }, "notes": { "type": "string", "description": "Personal annotations" @@ -1335,15 +1786,17 @@ }, "media_url": { "type": "url", + "label": "Media URL", "description": "Direct address of the media file itself: an episode's audio or video enclosure, a track's audio file, a film's stream. Distinct from url, which is the web page about the work." }, "mime_type": { "type": "string", + "label": "MIME Type", "description": "MIME type of the resource at media_url, such as audio/mpeg or video/mp4. Describes what media_url points at, not what this item is." } }, "display_hints": { - "icon": "film" + "icon": "lucide:film" }, "merge_policy": { "fields": { @@ -1351,12 +1804,128 @@ "notes": "keep_both_copies" }, "default": "last_writer_wins" - } + }, + "plural": "Films" + }, + { + "id": "core.media.game", + "parent": "core.media", + "label": "Game", + "description": "A video, board or card game. Inherits all core.media fields.", + "version": 0, + "fields": { + "title": { + "type": "string", + "description": "Name of the work", + "required": true + }, + "body": { + "type": "string", + "description": "Text content or description" + }, + "author": { + "type": "string", + "description": "Who created the work" + }, + "url": { + "type": "url", + "label": "URL", + "description": "Web address" + }, + "description": { + "type": "string", + "description": "Summary or blurb" + }, + "publisher": { + "type": "string", + "description": "Who published the work" + }, + "published_at": { + "type": "datetime", + "label": "Published", + "description": "When originally published or released" + }, + "image_url": { + "type": "url", + "label": "Image URL", + "description": "Cover art, poster, or thumbnail" + }, + "language": { + "type": "string", + "description": "BCP 47 language code", + "format": "bcp47" + }, + "progress": { + "type": "status", + "description": "Where the owner is with the work: want to read, watch, listen to or play it, started, finished, or gave up", + "status_values": [ + { + "value": "planned", + "group": "not_started" + }, + { + "value": "started", + "group": "in_progress" + }, + { + "value": "finished", + "group": "completed" + }, + { + "value": "abandoned", + "group": "canceled" + } + ] + }, + "rating": { + "type": "integer", + "description": "The owner's overall view of the work, from 1 (lowest) to 5 (highest)", + "minimum": 1, + "maximum": 5 + }, + "notes": { + "type": "string", + "description": "Personal annotations" + }, + "thumbnail": { + "type": "thumbnail", + "description": "A small preview of the work, supplied by the writer" + }, + "platforms": { + "type": "array", + "description": "What the game is played on, such as Nintendo Switch, PC or Tabletop", + "items_type": "string" + }, + "min_players": { + "type": "integer", + "label": "Minimum Players", + "description": "Fewest players the game supports", + "minimum": 1 + }, + "max_players": { + "type": "integer", + "label": "Maximum Players", + "description": "Most players the game supports", + "minimum": 1 + } + }, + "display_hints": { + "icon": "lucide:gamepad-2" + }, + "merge_policy": { + "fields": { + "body": "keep_both_copies", + "notes": "keep_both_copies" + }, + "default": "last_writer_wins" + }, + "plural": "Games" }, { "id": "core.media.series", "parent": "core.media", "label": "Series", + "plural": "Series", "description": "Any ongoing media container: a TV show, a podcast, a radio serial, a video series. Inherits all core.media fields. Episodes join it through the in-collection edge; containment is never part of a type name.", "version": 0, "fields": { @@ -1375,6 +1944,7 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "description": { @@ -1387,10 +1957,12 @@ }, "published_at": { "type": "datetime", + "label": "Published", "description": "When originally published or released" }, "image_url": { "type": "url", + "label": "Image URL", "description": "Cover art, poster, or thumbnail" }, "language": { @@ -1398,6 +1970,34 @@ "description": "BCP 47 language code", "format": "bcp47" }, + "progress": { + "type": "status", + "description": "Where the owner is with the work: want to read, watch, listen to or play it, started, finished, or gave up", + "status_values": [ + { + "value": "planned", + "group": "not_started" + }, + { + "value": "started", + "group": "in_progress" + }, + { + "value": "finished", + "group": "completed" + }, + { + "value": "abandoned", + "group": "canceled" + } + ] + }, + "rating": { + "type": "integer", + "description": "The owner's overall view of the work, from 1 (lowest) to 5 (highest)", + "minimum": 1, + "maximum": 5 + }, "notes": { "type": "string", "description": "Personal annotations" @@ -1428,7 +2028,7 @@ } }, "display_hints": { - "icon": "tv" + "icon": "lucide:tv" }, "merge_policy": { "fields": { @@ -1463,6 +2063,7 @@ }, "url": { "type": "url", + "label": "URL", "description": "Web address" }, "description": { @@ -1475,10 +2076,12 @@ }, "published_at": { "type": "datetime", + "label": "Published", "description": "When originally published or released" }, "image_url": { "type": "url", + "label": "Image URL", "description": "Cover art, poster, or thumbnail" }, "language": { @@ -1486,6 +2089,34 @@ "description": "BCP 47 language code", "format": "bcp47" }, + "progress": { + "type": "status", + "description": "Where the owner is with the work: want to read, watch, listen to or play it, started, finished, or gave up", + "status_values": [ + { + "value": "planned", + "group": "not_started" + }, + { + "value": "started", + "group": "in_progress" + }, + { + "value": "finished", + "group": "completed" + }, + { + "value": "abandoned", + "group": "canceled" + } + ] + }, + "rating": { + "type": "integer", + "description": "The owner's overall view of the work, from 1 (lowest) to 5 (highest)", + "minimum": 1, + "maximum": 5 + }, "notes": { "type": "string", "description": "Personal annotations" @@ -1500,6 +2131,7 @@ }, "isrc": { "type": "string", + "label": "ISRC", "description": "International Standard Recording Code (ISO 3901)" }, "album": { @@ -1512,15 +2144,17 @@ }, "media_url": { "type": "url", + "label": "Media URL", "description": "Direct address of the media file itself: an episode's audio or video enclosure, a track's audio file, a film's stream. Distinct from url, which is the web page about the work." }, "mime_type": { "type": "string", + "label": "MIME Type", "description": "MIME type of the resource at media_url, such as audio/mpeg or video/mp4. Describes what media_url points at, not what this item is." } }, "display_hints": { - "icon": "audio-lines" + "icon": "lucide:audio-lines" }, "merge_policy": { "fields": { @@ -1528,7 +2162,8 @@ "notes": "keep_both_copies" }, "default": "last_writer_wins" - } + }, + "plural": "Songs" }, { "id": "core.message", @@ -1553,14 +2188,15 @@ }, "display_hints": { "body_field": "body", - "icon": "message-square" + "icon": "lucide:message-square" }, "merge_policy": { "fields": { "body": "keep_both_copies" }, "default": "last_writer_wins" - } + }, + "plural": "Messages" }, { "id": "core.note", @@ -1589,7 +2225,7 @@ "display_hints": { "title_field": "title", "body_field": "body", - "icon": "sticky-note" + "icon": "lucide:sticky-note" }, "merge_policy": { "fields": { @@ -1597,7 +2233,8 @@ "notes": "keep_both_copies" }, "default": "last_writer_wins" - } + }, + "plural": "Notes" }, { "id": "core.task", @@ -1620,19 +2257,40 @@ }, "due_at": { "type": "datetime", + "label": "Due", "description": "Deadline" }, "starts_at": { "type": "datetime", + "label": "Starts", "description": "When to start" }, "completed_at": { "type": "datetime", + "label": "Completed", "description": "When completed" }, "status": { - "type": "string", - "description": "Recommended values: pending, in_progress, completed, canceled" + "type": "status", + "description": "Where the task stands", + "status_values": [ + { + "value": "not_started", + "group": "not_started" + }, + { + "value": "in_progress", + "group": "in_progress" + }, + { + "value": "completed", + "group": "completed" + }, + { + "value": "canceled", + "group": "canceled" + } + ] }, "priority": { "type": "enum", @@ -1660,17 +2318,38 @@ }, "url": { "type": "url", + "label": "URL", "description": "Related link" }, + "assignees": { + "type": "array", + "description": "The people the task is assigned to, one entry each, in the order written. An entry names a person by name, email or both, and holds no item ID: an app finds the person behind an entry by matching its email to the email addresses a person holds. It has the members of an attendee of an event, without a response.", + "items_type": "object", + "items_fields": { + "name": { + "type": "string", + "description": "The assignee's name as the source gives it, such as a login" + }, + "email": { + "type": "email", + "description": "The assignee's email address" + } + } + }, "notes": { "type": "string", "description": "Personal annotations" + }, + "recurrence": { + "type": "array", + "description": "RFC 5545 recurrence property lines (RRULE, RDATE, EXDATE) for a task that repeats. The series starts at due_at, or at starts_at when the task has no due_at, and advances in UTC because a task has no timezone. The server keeps and checks the rule but does not unfold it: GET /occurrences answers events only. A write is refused when a line cannot be read or is not applied (EXRULE, an RDATE period), when it carries more than one RRULE, more than 40,000 characters in all or more than 1,000 added and removed dates, when the rule names no date that exists after the start, when it produces no occurrence within the bound a read unfolds a rule to, or when the task has neither due_at nor starts_at.", + "items_type": "string" } }, "display_hints": { "title_field": "title", "body_field": "body", - "icon": "square-check-big" + "icon": "lucide:square-check-big" }, "merge_policy": { "fields": { @@ -1678,7 +2357,8 @@ "notes": "keep_both_copies" }, "default": "last_writer_wins" - } + }, + "plural": "Tasks" }, { "id": "system.connection", @@ -1729,7 +2409,8 @@ "type": "datetime", "description": "When the grant was revoked, if any" } - } + }, + "plural": "Connections" }, { "id": "system.folder", @@ -1744,7 +2425,7 @@ }, "query": { "type": "object", - "description": "Which items the folder holds. `types`: type identifiers, each with its subtypes; empty or absent holds every type the folder's key reads. `tier`: `library`, `feed`, or `all` for both tiers; absent is `library`. `state`: `active`, `archived` or both; absent is both. `filter`: an expression in the listing grammar's `filter`. `beneath`: an item id; the item and everything under it by `parent-of`." + "description": "Which items the folder holds. `types`: type identifiers, each with its subtypes; empty or absent holds every type the folder's key reads. `tier`: `library`, `feed`, or `all` for both tiers; absent is `library`. `state`: `active`, `archived` or both; absent is both. `filter`: an expression in the listing grammar's `filter`, with no `backref` condition. `beneath`: an item id; the item and everything under it by `parent-of`." }, "defaults": { "type": "object", @@ -1762,7 +2443,7 @@ }, "first_placement": { "type": "object", - "description": "Where a new item of a type made elsewhere first appears: a map from type identifier to a directory relative to the folder's root. Placement only; it never decides membership." + "description": "Where a new item of a type made elsewhere first appears: a map from type identifier to a directory relative to the folder's root, with no `..` name. Placement only; it never decides membership." }, "removal_threshold": { "type": "object", @@ -1772,7 +2453,8 @@ "type": "datetime", "description": "When the folder was revoked" } - } + }, + "plural": "Folders" }, { "id": "system.view", @@ -1798,7 +2480,8 @@ "type": "object", "description": "How the view groups the items it lists: `by` (a field, or `edge[]` or `backref[]`), and optionally `bucket` (`day`, `week`, `month` or `year`, for a date field), `order` (the values whose groups come first) and `hidden` (the values whose groups are left out)." } - } + }, + "plural": "Views" } ], "edge_types": [ @@ -1817,7 +2500,9 @@ "cascade_on_delete": "orphan", "written_at": "source", "property_schema": {}, - "shipped": true + "shipped": true, + "acyclic": false, + "flat": false }, { "id": "attached-to", @@ -1835,24 +2520,9 @@ "reverse_name": "has-attachment", "written_at": "source", "property_schema": {}, - "shipped": true - }, - { - "id": "authored-by", - "label": "Authored by", - "reverse_label": "Author of", - "description": "Source was authored by the target (usually a core.entity.person).", - "cardinality": "many-to-many", - "source_type_constraints": [ - "*" - ], - "target_type_constraints": [ - "*" - ], - "cascade_on_delete": "orphan", - "written_at": "source", - "property_schema": {}, - "shipped": true + "shipped": true, + "acyclic": false, + "flat": false }, { "id": "derived-from", @@ -1869,7 +2539,9 @@ "cascade_on_delete": "orphan", "written_at": "source", "property_schema": {}, - "shipped": true + "shipped": true, + "acyclic": false, + "flat": false }, { "id": "in-collection", @@ -1891,7 +2563,9 @@ "description": "Ordering within the collection (1-based)." } }, - "shipped": true + "shipped": true, + "acyclic": false, + "flat": true }, { "id": "in-folder", @@ -1910,10 +2584,12 @@ "property_schema": { "path": { "type": "string", - "description": "The file's path relative to the folder's root, with `/` between names; required, and it may not climb out of the folder. The edge takes no other property." + "description": "The file's path relative to the folder's root, with `/` between names; required, with no `..` name. The edge takes no other property." } }, - "shipped": true + "shipped": true, + "acyclic": false, + "flat": false }, { "id": "in-thread", @@ -1935,7 +2611,9 @@ "description": "Ordering within the thread (1-based)." } }, - "shipped": true + "shipped": true, + "acyclic": false, + "flat": false }, { "id": "parent-of", @@ -1953,7 +2631,9 @@ "reverse_name": "child-of", "written_at": "target", "property_schema": {}, - "shipped": true + "shipped": true, + "acyclic": true, + "flat": false }, { "id": "references", @@ -1970,7 +2650,9 @@ "cascade_on_delete": "orphan", "written_at": "source", "property_schema": {}, - "shipped": true + "shipped": true, + "acyclic": false, + "flat": false }, { "id": "supersedes", @@ -1987,7 +2669,9 @@ "cascade_on_delete": "orphan", "written_at": "source", "property_schema": {}, - "shipped": true + "shipped": true, + "acyclic": true, + "flat": false } ], "field_types": [ @@ -2002,8 +2686,27 @@ "enum", "array", "object", - "thumbnail" + "thumbnail", + "status" + ], + "entry_member_types": [ + "string", + "email", + "url", + "enum", + "boolean", + "integer", + "number", + "date", + "datetime" ], + "format_field_types": { + "url": "url", + "email": "email", + "datetime": "datetime", + "date": "date", + "thumbnail": "thumbnail" + }, "reserved_type_roots": [ "app", "audit", @@ -2038,6 +2741,7 @@ "source_id", "state", "tier", + "trashed_at", "trashed_by_cascade", "trashed_with", "type", diff --git a/src/generated/catalog/core.bookmark.md b/src/generated/catalog/core.bookmark.md index f988fa2f..84ee8f99 100644 --- a/src/generated/catalog/core.bookmark.md +++ b/src/generated/catalog/core.bookmark.md @@ -1,24 +1,25 @@ ## Fields -| Field | Kind | Required | Description | -| -------------- | ------------------ | -------- | ------------------------------------------------- | -| `url` | `url` | No | The saved URL | -| `body` | `string` | No | Captured text (a highlight, excerpt, or clipping) | -| `title` | `string` | No | Title of the saved content | -| `description` | `string` | No | Summary | -| `source_url` | `url` | No | Where the content was sourced from | -| `source_title` | `string` | No | Title of the source | -| `author` | `string` | No | Who created the original content | -| `published_at` | `datetime` | No | When the original content was published | -| `image_url` | `url` | No | Preview image | -| `language` | `string` (`bcp47`) | No | BCP 47 language code | -| `notes` | `string` | No | Personal annotations | +| Field | Label | Kind | Required | Description | +| -------------- | ---------- | ------------------ | -------- | ------------------------------------------------- | +| `url` | URL | `url` | No | The saved URL | +| `body` | | `string` | No | Captured text (a highlight, excerpt, or clipping) | +| `title` | | `string` | No | Title of the saved content | +| `description` | | `string` | No | Summary | +| `source_url` | Source URL | `url` | No | Where the content was sourced from | +| `source_title` | | `string` | No | Title of the source | +| `author` | | `string` | No | Who created the original content | +| `published_at` | Published | `datetime` | No | When the original content was published | +| `image_url` | Image URL | `url` | No | Preview image | +| `language` | | `string` (`bcp47`) | No | BCP 47 language code | +| `notes` | | `string` | No | Personal annotations | ## Settings | Setting | Value | | ------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| `plural` | Bookmarks | | [`title_field`](/catalog/your-own-types#display-hints) | `title` | | [`body_field`](/catalog/your-own-types#display-hints) | `body` | -| [`icon`](/catalog/your-own-types#display-hints) | `bookmark` | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:bookmark` | | [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `body` and `notes`; `last_writer_wins` for every other field | diff --git a/src/generated/catalog/core.collection.md b/src/generated/catalog/core.collection.md index 0ec5b494..e150168a 100644 --- a/src/generated/catalog/core.collection.md +++ b/src/generated/catalog/core.collection.md @@ -9,8 +9,9 @@ | Setting | Value | | ------------------------------------------------------ | ---------------------------------- | +| `plural` | Collections | | [`title_field`](/catalog/your-own-types#display-hints) | `title` | | [`body_field`](/catalog/your-own-types#display-hints) | `description` | -| [`icon`](/catalog/your-own-types#display-hints) | `layers` | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:layers` | | [`merge_policy`](/catalog/your-own-types#merge-policy) | `last_writer_wins` for every field | | [`roles`](/catalog/your-own-types#roles) | `container` | diff --git a/src/generated/catalog/core.entity.md b/src/generated/catalog/core.entity.md index 6bce5883..b173cc6e 100644 --- a/src/generated/catalog/core.entity.md +++ b/src/generated/catalog/core.entity.md @@ -1,24 +1,35 @@ ## Fields -| Field | Kind | Required | Description | -| ------------- | -------- | -------- | --------------------------------- | -| `name` | `string` | Yes | Display name | -| `url` | `url` | No | Web address | -| `description` | `string` | No | What the entity is | -| `email` | `email` | No | Contact email | -| `phone` | `string` | No | Contact phone (E.164 recommended) | -| `place` | `string` | No | Location (human-readable address) | -| `image_url` | `url` | No | Logo or brand image | -| `legal_name` | `string` | No | Official registered name | -| `founded` | `date` | No | Founding date (ISO 8601) | -| `notes` | `string` | No | Personal annotations | +| Field | Label | Kind | Required | Description | +| ---------------------------- | ------------- | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------- | +| `name` | | `string` | Yes | Display name | +| `url` | URL | `url` | No | Web address | +| `description` | | `string` | No | What the entity is | +| `emails` | | `array` of `object` | No | Email addresses. The first entry is the main one, and an entry's label is free text, such as work or home. | +| `emails[].value` | Email | `email` | Yes | The email address | +| `emails[].label` | | `string` | No | What the entry is for, such as work or home | +| `phones` | Phone Numbers | `array` of `object` | No | Phone numbers. The first entry is the main one, and an entry's label is free text, such as work or home. | +| `phones[].value` | Number | `string` (`phone`) | Yes | The phone number, in E.164 form where known | +| `phones[].label` | | `string` | No | What the entry is for, such as work or home | +| `addresses` | | `array` of `object` | No | Postal addresses. The first entry is the main one, and an entry's label is free text, such as work or home. | +| `addresses[].label` | | `string` | No | What the entry is for, such as work or home | +| `addresses[].street_address` | | `string` | No | Street line | +| `addresses[].locality` | | `string` | No | City or town | +| `addresses[].region` | | `string` | No | State, province, or county | +| `addresses[].postal_code` | | `string` | No | Postcode or ZIP code | +| `addresses[].country` | | `string` | No | Country name or code | +| `image_url` | Image URL | `url` | No | Logo or brand image | +| `legal_name` | | `string` | No | Official registered name | +| `founded` | | `date` | No | Founding date (ISO 8601) | +| `notes` | | `string` | No | Personal annotations | ## Settings | Setting | Value | | ------------------------------------------------------ | ---------------------------------- | +| `plural` | Entities | | [`title_field`](/catalog/your-own-types#display-hints) | `name` | -| [`icon`](/catalog/your-own-types#display-hints) | `shapes` | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:shapes` | | [`merge_policy`](/catalog/your-own-types#merge-policy) | `last_writer_wins` for every field | ## Subtypes @@ -27,9 +38,10 @@ Contact information for an individual. Inherits all core.entity fields. -| Setting | Value | -| ----------------------------------------------- | ------ | -| [`icon`](/catalog/your-own-types#display-hints) | `user` | +| Setting | Value | +| ----------------------------------------------- | ------------- | +| `plural` | People | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:user` | | Field | Kind | Required | Description | | -------------- | -------- | -------- | ---------------------------------- | @@ -49,18 +61,19 @@ Contact information for an individual. Inherits all core.entity fields. A location or venue. Inherits all core.entity fields. -| Setting | Value | -| ----------------------------------------------- | --------- | -| [`icon`](/catalog/your-own-types#display-hints) | `map-pin` | +| Setting | Value | +| ----------------------------------------------- | ---------------- | +| `plural` | Places | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:map-pin` | -| Field | Kind | Required | Description | -| ---------------- | -------------------- | -------- | ---------------------------------- | -| `street_address` | `string` | No | Street line | -| `locality` | `string` | No | City or town | -| `region` | `string` | No | State, province, or county | -| `postal_code` | `string` | No | Postcode or ZIP code | -| `country` | `string` (`iso3166`) | No | ISO 3166-1 alpha-2 country code | -| `timezone` | `string` | No | IANA timezone | -| `latitude` | `number` | No | Subject latitude | -| `longitude` | `number` | No | Subject longitude | -| `altitude` | `number` | No | Altitude in meters above sea level | +| Field | Label | Kind | Required | Description | +| ---------------- | --------- | -------------------- | -------- | ---------------------------------- | +| `street_address` | | `string` | No | Street line | +| `locality` | | `string` | No | City or town | +| `region` | | `string` | No | State, province, or county | +| `postal_code` | | `string` | No | Postcode or ZIP code | +| `country` | | `string` (`iso3166`) | No | ISO 3166-1 alpha-2 country code | +| `timezone` | Time Zone | `string` | No | IANA timezone | +| `latitude` | | `number` | No | Subject latitude | +| `longitude` | | `number` | No | Subject longitude | +| `altitude` | | `number` | No | Altitude in meters above sea level | diff --git a/src/generated/catalog/core.event.md b/src/generated/catalog/core.event.md index 4a5aef43..bfc7283c 100644 --- a/src/generated/catalog/core.event.md +++ b/src/generated/catalog/core.event.md @@ -1,30 +1,35 @@ ## Fields -| Field | Kind | Required | Description | -| -------------------- | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `title` | `string` | Yes | Event name | -| `description` | `string` | No | Event details | -| `starts_at` | `datetime` | No | The instant the event starts, ISO 8601. An instant carries no zone of its own however it is written; the timezone field is what anchors the event's wall-clock hour. | -| `ends_at` | `datetime` | No | The instant the event ends, ISO 8601. | -| `duration` | `number` | No | Duration in seconds; the length of each occurrence of a series that states no ends_at. A write is refused unless it is from 0 to a century. | -| `place` | `string` | No | Location | -| `latitude` | `number` | No | Venue latitude | -| `longitude` | `number` | No | Venue longitude | -| `url` | `url` | No | Event link | -| `precision` | `enum` | No | How much of the start instant is actually known, for an event dated from memory or from a source that gave only a year. Narrows an instant that exists; it does not say the event has no instant, which is what all_day says. Values: `year`, `month`, `day`, `time`. | -| `status` | `string` | No | Recommended values: tentative, confirmed, canceled, rescheduled | -| `notes` | `string` | No | Personal annotations | -| `recurrence` | `array` of `string` | No | RFC 5545 recurrence property lines (RRULE, RDATE, EXDATE). Present on the series itself; occurrences are computed from it at read time rather than stored, starting with starts_at itself. A write is refused when a line cannot be read or is not applied (EXRULE, an RDATE period), when it carries more than one RRULE, more than 40,000 characters in all or more than 1,000 added and removed dates, when the rule names no date that exists after the start, or when it produces no occurrence within the bound a read unfolds a rule to. | -| `all_day` | `boolean` | No | True when the event occupies whole days rather than a span of time. A whole day has no instant, so a reader must take the calendar date from starts_at read in timezone and never re-derive it in the reader's own zone, which is how an all-day event ends up on the wrong day for anyone further west. Absent or false means the event has real start and end instants. | -| `timezone` | `string` | No | IANA time zone the event's schedule keeps its wall-clock hour in, e.g. Europe/Berlin. Stored times stay instants; a recurring series expands in this zone so occurrences keep their local hour across a daylight-saving transition. Absent means the rule advances in UTC. This is the start zone: an event that ends somewhere else states that in end_timezone. A write is refused unless the zone database resolves it as a named zone. | -| `end_timezone` | `string` | No | IANA time zone the event ends in, when that differs from timezone: a flight lands in a zone it did not depart from, and both ends are wall-clock facts a person reads off a ticket. Absent means the event ends in the zone it started in. Recurrence expands in timezone only; this field never anchors a rule. A write is refused unless the zone database resolves it as a named zone. | -| `original_starts_at` | `datetime` | No | For an event that replaces one occurrence of a series, the start instant of the occurrence it replaces. Its series is named by a parent-of edge. | +| Field | Label | Kind | Required | Description | +| ---------------------- | -------------- | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` | | `string` | Yes | Event name | +| `description` | | `string` | No | Event details | +| `starts_at` | Starts | `datetime` | No | The instant the event starts, ISO 8601. An instant carries no zone of its own however it is written; the timezone field is what anchors the event's wall-clock hour. | +| `ends_at` | Ends | `datetime` | No | The instant the event ends, ISO 8601. | +| `duration` | | `number` | No | Duration in seconds; the length of each occurrence of a series that states no ends_at. A write is refused unless it is from 0 to a century. | +| `place` | | `string` | No | Location | +| `latitude` | | `number` | No | Venue latitude | +| `longitude` | | `number` | No | Venue longitude | +| `url` | URL | `url` | No | Event link | +| `precision` | | `enum` | No | How much of the start instant is actually known, for an event dated from memory or from a source that gave only a year. Narrows an instant that exists; it does not say the event has no instant, which is what all_day says. Values: `year`, `month`, `day`, `time`. | +| `status` | | `enum` | No | Whether the event will happen. A tentative event might not, a confirmed one will and a canceled one will not. Moving an event changes its time, not its status. Values: `tentative`, `confirmed`, `canceled`. | +| `notes` | | `string` | No | Personal annotations | +| `recurrence` | | `array` of `string` | No | RFC 5545 recurrence property lines (RRULE, RDATE, EXDATE). Present on the series itself; occurrences are computed from it at read time rather than stored, starting with starts_at itself. A write is refused when a line cannot be read or is not applied (EXRULE, an RDATE period), when it carries more than one RRULE, more than 40,000 characters in all or more than 1,000 added and removed dates, when the rule names no date that exists after the start, or when it produces no occurrence within the bound a read unfolds a rule to. | +| `all_day` | | `boolean` | No | True when the event occupies whole days rather than a span of time. A whole day has no instant, so a reader must take the calendar date from starts_at read in timezone and never re-derive it in the reader's own zone, which is how an all-day event ends up on the wrong day for anyone further west. Absent or false means the event has real start and end instants. | +| `timezone` | Time Zone | `string` | No | IANA time zone the event's schedule keeps its wall-clock hour in, e.g. Europe/Berlin. Stored times stay instants; a recurring series expands in this zone so occurrences keep their local hour across a daylight-saving transition. Absent means the rule advances in UTC. This is the start zone: an event that ends somewhere else states that in end_timezone. A write is refused unless the zone database resolves it as a named zone. | +| `end_timezone` | End Time Zone | `string` | No | IANA time zone the event ends in, when that differs from timezone: a flight lands in a zone it did not depart from, and both ends are wall-clock facts a person reads off a ticket. Absent means the event ends in the zone it started in. Recurrence expands in timezone only; this field never anchors a rule. A write is refused unless the zone database resolves it as a named zone. | +| `attendees` | | `array` of `object` | No | The people invited, one entry each, in the order written. An entry names a person by name, email or both, and holds no item ID: an app finds the person behind an entry by matching its email to the email addresses a person holds. response is the reply, from the participation status of RFC 5545; needs_action means no reply yet. | +| `attendees[].name` | | `string` | No | The attendee's name as the source gives it | +| `attendees[].email` | | `email` | No | The attendee's email address | +| `attendees[].response` | | `enum` | No | The attendee's reply to the invitation. Values: `needs_action`, `accepted`, `declined`, `tentative`. | +| `original_starts_at` | Original Start | `datetime` | No | For an event that replaces one occurrence of a series, the start instant of the occurrence it replaces. Its series is named by a parent-of edge. | ## Settings | Setting | Value | | ------------------------------------------------------ | ------------------------------------------------------------------------ | +| `plural` | Events | | [`title_field`](/catalog/your-own-types#display-hints) | `title` | | [`body_field`](/catalog/your-own-types#display-hints) | `description` | -| [`icon`](/catalog/your-own-types#display-hints) | `calendar` | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:calendar` | | [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `notes`; `last_writer_wins` for every other field | diff --git a/src/generated/catalog/core.file.md b/src/generated/catalog/core.file.md index 91bab5a7..bc2c7cbc 100644 --- a/src/generated/catalog/core.file.md +++ b/src/generated/catalog/core.file.md @@ -1,26 +1,27 @@ ## Fields -| Field | Kind | Required | Description | -| ---------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `blob_ref` | `string` | Yes | Reference to the binary content (sha256:\) | -| `mime_type` | `string` | Yes | MIME type | -| `title` | `string` | No | Filename or title | -| `description` | `string` | No | What the file contains | -| `url` | `url` | No | Web address | -| `source_url` | `url` | No | Where the file was sourced from | -| `author` | `string` | No | Who created the file | -| `language` | `string` (`bcp47`) | No | BCP 47 language code | -| `notes` | `string` | No | Personal annotations | -| `extracted_text` | `string` | No | Machine-extracted text content of the referenced blob (server enrichment: document text or image OCR) | -| `executable` | `boolean` | No | Whether the file may be run as a program; absent means it may not | -| `size_bytes` | `integer` | No | Size in bytes of the referenced blob (set by the server from the bytes the instance holds; absent where whoever named blob_ref never proved it holds them) | +| Field | Label | Kind | Required | Description | +| ---------------- | -------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `blob_ref` | Blob Reference | `string` | Yes | Reference to the binary content (sha256:\) | +| `mime_type` | MIME Type | `string` | Yes | MIME type | +| `title` | | `string` | No | Filename or title | +| `description` | | `string` | No | What the file contains | +| `url` | URL | `url` | No | Web address | +| `source_url` | Source URL | `url` | No | Where the file was sourced from | +| `author` | | `string` | No | Who created the file | +| `language` | | `string` (`bcp47`) | No | BCP 47 language code | +| `notes` | | `string` | No | Personal annotations | +| `extracted_text` | | `string` | No | Machine-extracted text content of the referenced blob (server enrichment: document text or image OCR) | +| `executable` | | `boolean` | No | Whether the file may be run as a program; absent means it may not | +| `size_bytes` | Size | `integer` | No | Size in bytes of the referenced blob (set by the server from the bytes the instance holds; absent where whoever named blob_ref never proved it holds them) | ## Settings | Setting | Value | | ------------------------------------------------------ | ---------------------------------- | +| `plural` | Files | | [`title_field`](/catalog/your-own-types#display-hints) | `title` | -| [`icon`](/catalog/your-own-types#display-hints) | `file` | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:file` | | [`merge_policy`](/catalog/your-own-types#merge-policy) | `last_writer_wins` for every field | ## Subtypes @@ -29,9 +30,10 @@ Recordings, music files, voice memos. Inherits all core.file fields. -| Setting | Value | -| ----------------------------------------------- | ------------ | -| [`icon`](/catalog/your-own-types#display-hints) | `file-music` | +| Setting | Value | +| ----------------------------------------------- | ------------------- | +| `plural` | Audio | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:file-music` | | Field | Kind | Required | Description | | ---------- | ------------------ | -------- | ----------------------------------------------------------------------------------------------- | @@ -42,9 +44,10 @@ Recordings, music files, voice memos. Inherits all core.file fields. Photos, screenshots, diagrams. Inherits all core.file fields. -| Setting | Value | -| ----------------------------------------------- | ------------ | -| [`icon`](/catalog/your-own-types#display-hints) | `file-image` | +| Setting | Value | +| ----------------------------------------------- | ------------------- | +| `plural` | Images | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:file-image` | | Field | Kind | Required | Description | | ----------- | ----------- | -------- | ---------------------------------------------------------------------------------------------- | @@ -59,9 +62,10 @@ Photos, screenshots, diagrams. Inherits all core.file fields. Video files. Inherits all core.file fields. -| Setting | Value | -| ----------------------------------------------- | ------------------- | -| [`icon`](/catalog/your-own-types#display-hints) | `file-video-camera` | +| Setting | Value | +| ----------------------------------------------- | -------------------------- | +| `plural` | Videos | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:file-video-camera` | | Field | Kind | Required | Description | | ----------- | ------------------ | -------- | ----------------------------------------------------------------------------------------------- | @@ -77,6 +81,7 @@ Video files. Inherits all core.file fields. An HTML page held as a file, such as a report an agent wrote. Inherits all core.file fields. -| Setting | Value | -| ----------------------------------------------- | ------- | -| [`icon`](/catalog/your-own-types#display-hints) | `globe` | +| Setting | Value | +| ----------------------------------------------- | -------------- | +| `plural` | Web Pages | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:globe` | diff --git a/src/generated/catalog/core.highlight.md b/src/generated/catalog/core.highlight.md index c39f966b..45912452 100644 --- a/src/generated/catalog/core.highlight.md +++ b/src/generated/catalog/core.highlight.md @@ -13,7 +13,8 @@ | Setting | Value | | ------------------------------------------------------ | ----------------------------------------------------------------------- | +| `plural` | Highlights | | [`title_field`](/catalog/your-own-types#display-hints) | `text` | | [`body_field`](/catalog/your-own-types#display-hints) | `note` | -| [`icon`](/catalog/your-own-types#display-hints) | `highlighter` | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:highlighter` | | [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `note`; `last_writer_wins` for every other field | diff --git a/src/generated/catalog/core.media.md b/src/generated/catalog/core.media.md index a24f7b33..dc173ef1 100644 --- a/src/generated/catalog/core.media.md +++ b/src/generated/catalog/core.media.md @@ -1,26 +1,29 @@ ## Fields -| Field | Kind | Required | Description | -| -------------- | ------------------ | -------- | --------------------------------------------------- | -| `title` | `string` | Yes | Name of the work | -| `body` | `string` | No | Text content or description | -| `author` | `string` | No | Who created the work | -| `url` | `url` | No | Web address | -| `description` | `string` | No | Summary or blurb | -| `publisher` | `string` | No | Who published the work | -| `published_at` | `datetime` | No | When originally published or released | -| `image_url` | `url` | No | Cover art, poster, or thumbnail | -| `language` | `string` (`bcp47`) | No | BCP 47 language code | -| `notes` | `string` | No | Personal annotations | -| `thumbnail` | `thumbnail` | No | A small preview of the work, supplied by the writer | +| Field | Label | Kind | Required | Description | +| -------------- | --------- | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` | | `string` | Yes | Name of the work | +| `body` | | `string` | No | Text content or description | +| `author` | | `string` | No | Who created the work | +| `url` | URL | `url` | No | Web address | +| `description` | | `string` | No | Summary or blurb | +| `publisher` | | `string` | No | Who published the work | +| `published_at` | Published | `datetime` | No | When originally published or released | +| `image_url` | Image URL | `url` | No | Cover art, poster, or thumbnail | +| `language` | | `string` (`bcp47`) | No | BCP 47 language code | +| `progress` | | `status` | No | Where the owner is with the work: want to read, watch, listen to or play it, started, finished, or gave up. Values: `planned` in `not_started`, `started` in `in_progress`, `finished` in `completed`, `abandoned` in `canceled`. | +| `rating` | | `integer` (1 to 5) | No | The owner's overall view of the work, from 1 (lowest) to 5 (highest) | +| `notes` | | `string` | No | Personal annotations | +| `thumbnail` | | `thumbnail` | No | A small preview of the work, supplied by the writer | ## Settings | Setting | Value | | ------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| `plural` | Media | | [`title_field`](/catalog/your-own-types#display-hints) | `title` | | [`body_field`](/catalog/your-own-types#display-hints) | `body` | -| [`icon`](/catalog/your-own-types#display-hints) | `library-big` | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:library-big` | | [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `body` and `notes`; `last_writer_wins` for every other field | ## Subtypes @@ -29,76 +32,97 @@ An album. Inherits all core.media fields. -| Setting | Value | -| ----------------------------------------------- | ----------- | -| [`icon`](/catalog/your-own-types#display-hints) | `disc-3` | -| [`roles`](/catalog/your-own-types#roles) | `container` | +| Setting | Value | +| ----------------------------------------------- | --------------- | +| `plural` | Albums | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:disc-3` | +| [`roles`](/catalog/your-own-types#roles) | `container` | -| Field | Kind | Required | Description | -| -------------- | --------- | -------- | ------------------------------------- | -| `release_type` | `string` | No | Recommended values: album, ep, single | -| `num_tracks` | `integer` | No | Total track count | +| Field | Label | Kind | Required | Description | +| -------------- | ------ | --------- | -------- | ------------------------------------- | +| `release_type` | | `string` | No | Recommended values: album, ep, single | +| `num_tracks` | Tracks | `integer` | No | Total track count | ### core.media.article [#core-media-article] An article. Inherits all core.media fields. -| Setting | Value | -| ----------------------------------------------- | ----------- | -| [`icon`](/catalog/your-own-types#display-hints) | `newspaper` | +| Setting | Value | +| ----------------------------------------------- | ------------------ | +| `plural` | Articles | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:newspaper` | -| Field | Kind | Required | Description | -| ------------ | --------- | -------- | -------------------------- | -| `body` | `string` | No | The article text | -| `section` | `string` | No | Section of the publication | -| `word_count` | `integer` | No | Word count | +| Field | Label | Kind | Required | Description | +| ------------ | ----- | --------- | -------- | ------------------------------------- | +| `body` | | `string` | No | The article text | +| `doi` | DOI | `string` | No | Digital Object Identifier (ISO 26324) | +| `section` | | `string` | No | Section of the publication | +| `word_count` | Words | `integer` | No | Word count | ### core.media.book [#core-media-book] A book. Inherits all core.media fields. -| Setting | Value | -| ----------------------------------------------- | ----------- | -| [`icon`](/catalog/your-own-types#display-hints) | `book-open` | +| Setting | Value | +| ----------------------------------------------- | ------------------ | +| `plural` | Books | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:book-open` | -| Field | Kind | Required | Description | -| ------------ | --------- | -------- | --------------------------------------------- | -| `isbn` | `string` | No | International Standard Book Number (ISO 2108) | -| `page_count` | `integer` | No | Number of pages | -| `edition` | `string` | No | Edition designation | +| Field | Label | Kind | Required | Description | +| ------------ | ----- | --------- | -------- | --------------------------------------------- | +| `isbn` | ISBN | `string` | No | International Standard Book Number (ISO 2108) | +| `page_count` | Pages | `integer` | No | Number of pages | +| `edition` | | `string` | No | Edition designation | ### core.media.episode [#core-media-episode] Any member of an ongoing series: a TV episode, a podcast episode, one part of a serial. Inherits all core.media fields. Joins its series through the in-collection edge, so one episode can belong to several series and outlives any of them. -| Setting | Value | -| ----------------------------------------------- | ------------- | -| [`icon`](/catalog/your-own-types#display-hints) | `circle-play` | +| Setting | Value | +| ----------------------------------------------- | -------------------- | +| `plural` | Episodes | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:circle-play` | -| Field | Kind | Required | Description | -| ---------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `medium` | `enum` | No | What the episode is made of. Values: `tv`, `podcast`, `radio`, `video`, `mixed`. | -| `season_number` | `integer` | No | Which season, where the series has them | -| `episode_number` | `integer` | No | Position within the season, or within the series when unseasoned | -| `duration` | `number` | No | Runtime in seconds | -| `media_url` | `url` | No | Direct address of the media file itself: an episode's audio or video enclosure, a track's audio file, a film's stream. Distinct from url, which is the web page about the work. | -| `mime_type` | `string` | No | MIME type of the resource at media_url, such as audio/mpeg or video/mp4. Describes what media_url points at, not what this item is. | +| Field | Label | Kind | Required | Description | +| ---------------- | --------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `medium` | | `enum` | No | What the episode is made of. Values: `tv`, `podcast`, `radio`, `video`, `mixed`. | +| `season_number` | | `integer` | No | Which season, where the series has them | +| `episode_number` | | `integer` | No | Position within the season, or within the series when unseasoned | +| `duration` | | `number` | No | Runtime in seconds | +| `media_url` | Media URL | `url` | No | Direct address of the media file itself: an episode's audio or video enclosure, a track's audio file, a film's stream. Distinct from url, which is the web page about the work. | +| `mime_type` | MIME Type | `string` | No | MIME type of the resource at media_url, such as audio/mpeg or video/mp4. Describes what media_url points at, not what this item is. | ### core.media.film [#core-media-film] A film. Inherits all core.media fields. -| Setting | Value | -| ----------------------------------------------- | ------ | -| [`icon`](/catalog/your-own-types#display-hints) | `film` | +| Setting | Value | +| ----------------------------------------------- | ------------- | +| `plural` | Films | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:film` | + +| Field | Label | Kind | Required | Description | +| ---------------- | --------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `duration` | | `number` | No | Runtime in seconds | +| `director` | | `string` | No | Primary director | +| `content_rating` | | `string` | No | Age or content classification | +| `media_url` | Media URL | `url` | No | Direct address of the media file itself: an episode's audio or video enclosure, a track's audio file, a film's stream. Distinct from url, which is the web page about the work. | +| `mime_type` | MIME Type | `string` | No | MIME type of the resource at media_url, such as audio/mpeg or video/mp4. Describes what media_url points at, not what this item is. | + +### core.media.game [#core-media-game] -| Field | Kind | Required | Description | -| ---------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `duration` | `number` | No | Runtime in seconds | -| `director` | `string` | No | Primary director | -| `content_rating` | `string` | No | Age or content classification | -| `media_url` | `url` | No | Direct address of the media file itself: an episode's audio or video enclosure, a track's audio file, a film's stream. Distinct from url, which is the web page about the work. | -| `mime_type` | `string` | No | MIME type of the resource at media_url, such as audio/mpeg or video/mp4. Describes what media_url points at, not what this item is. | +A video, board or card game. Inherits all core.media fields. + +| Setting | Value | +| ----------------------------------------------- | ------------------ | +| `plural` | Games | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:gamepad-2` | + +| Field | Label | Kind | Required | Description | +| ------------- | --------------- | ---------------------- | -------- | ------------------------------------------------------------------- | +| `platforms` | | `array` of `string` | No | What the game is played on, such as Nintendo Switch, PC or Tabletop | +| `min_players` | Minimum Players | `integer` (at least 1) | No | Fewest players the game supports | +| `max_players` | Maximum Players | `integer` (at least 1) | No | Most players the game supports | ### core.media.series [#core-media-series] @@ -106,7 +130,8 @@ Any ongoing media container: a TV show, a podcast, a radio serial, a video serie | Setting | Value | | ----------------------------------------------- | ----------- | -| [`icon`](/catalog/your-own-types#display-hints) | `tv` | +| `plural` | Series | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:tv` | | [`roles`](/catalog/your-own-types#roles) | `container` | | Field | Kind | Required | Description | @@ -118,15 +143,16 @@ Any ongoing media container: a TV show, a podcast, a radio serial, a video serie A song. Inherits all core.media fields. -| Setting | Value | -| ----------------------------------------------- | ------------- | -| [`icon`](/catalog/your-own-types#display-hints) | `audio-lines` | - -| Field | Kind | Required | Description | -| -------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `duration` | `number` | No | Track length in seconds | -| `isrc` | `string` | No | International Standard Recording Code (ISO 3901) | -| `album` | `string` | No | Containing album name | -| `track_number` | `integer` | No | Position within the album | -| `media_url` | `url` | No | Direct address of the media file itself: an episode's audio or video enclosure, a track's audio file, a film's stream. Distinct from url, which is the web page about the work. | -| `mime_type` | `string` | No | MIME type of the resource at media_url, such as audio/mpeg or video/mp4. Describes what media_url points at, not what this item is. | +| Setting | Value | +| ----------------------------------------------- | -------------------- | +| `plural` | Songs | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:audio-lines` | + +| Field | Label | Kind | Required | Description | +| -------------- | --------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `duration` | | `number` | No | Track length in seconds | +| `isrc` | ISRC | `string` | No | International Standard Recording Code (ISO 3901) | +| `album` | | `string` | No | Containing album name | +| `track_number` | | `integer` | No | Position within the album | +| `media_url` | Media URL | `url` | No | Direct address of the media file itself: an episode's audio or video enclosure, a track's audio file, a film's stream. Distinct from url, which is the web page about the work. | +| `mime_type` | MIME Type | `string` | No | MIME type of the resource at media_url, such as audio/mpeg or video/mp4. Describes what media_url points at, not what this item is. | diff --git a/src/generated/catalog/core.message.md b/src/generated/catalog/core.message.md index 7b6908d4..326df044 100644 --- a/src/generated/catalog/core.message.md +++ b/src/generated/catalog/core.message.md @@ -10,6 +10,7 @@ | Setting | Value | | ------------------------------------------------------ | ----------------------------------------------------------------------- | +| `plural` | Messages | | [`body_field`](/catalog/your-own-types#display-hints) | `body` | -| [`icon`](/catalog/your-own-types#display-hints) | `message-square` | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:message-square` | | [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `body`; `last_writer_wins` for every other field | diff --git a/src/generated/catalog/core.note.md b/src/generated/catalog/core.note.md index b08cf4f4..4b9dfa18 100644 --- a/src/generated/catalog/core.note.md +++ b/src/generated/catalog/core.note.md @@ -11,7 +11,8 @@ | Setting | Value | | ------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| `plural` | Notes | | [`title_field`](/catalog/your-own-types#display-hints) | `title` | | [`body_field`](/catalog/your-own-types#display-hints) | `body` | -| [`icon`](/catalog/your-own-types#display-hints) | `sticky-note` | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:sticky-note` | | [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `body` and `notes`; `last_writer_wins` for every other field | diff --git a/src/generated/catalog/core.task.md b/src/generated/catalog/core.task.md index dd83f73a..50d8ad8e 100644 --- a/src/generated/catalog/core.task.md +++ b/src/generated/catalog/core.task.md @@ -1,25 +1,30 @@ ## Fields -| Field | Kind | Required | Description | -| -------------- | ---------- | -------- | ------------------------------------------------------------- | -| `title` | `string` | Yes | What needs doing | -| `description` | `string` | No | Brief context | -| `body` | `string` | No | Detailed description | -| `due_at` | `datetime` | No | Deadline | -| `starts_at` | `datetime` | No | When to start | -| `completed_at` | `datetime` | No | When completed | -| `status` | `string` | No | Recommended values: pending, in_progress, completed, canceled | -| `priority` | `enum` | No | Task priority. Values: `low`, `medium`, `high`, `urgent`. | -| `place` | `string` | No | Location | -| `precision` | `enum` | No | Temporal precision. Values: `year`, `month`, `day`, `time`. | -| `url` | `url` | No | Related link | -| `notes` | `string` | No | Personal annotations | +| Field | Label | Kind | Required | Description | +| ------------------- | --------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `title` | | `string` | Yes | What needs doing | +| `description` | | `string` | No | Brief context | +| `body` | | `string` | No | Detailed description | +| `due_at` | Due | `datetime` | No | Deadline | +| `starts_at` | Starts | `datetime` | No | When to start | +| `completed_at` | Completed | `datetime` | No | When completed | +| `status` | | `status` | No | Where the task stands. Values: `not_started` in `not_started`, `in_progress` in `in_progress`, `completed` in `completed`, `canceled` in `canceled`. | +| `priority` | | `enum` | No | Task priority. Values: `low`, `medium`, `high`, `urgent`. | +| `place` | | `string` | No | Location | +| `precision` | | `enum` | No | Temporal precision. Values: `year`, `month`, `day`, `time`. | +| `url` | URL | `url` | No | Related link | +| `assignees` | | `array` of `object` | No | The people the task is assigned to, one entry each, in the order written. An entry names a person by name, email or both, and holds no item ID: an app finds the person behind an entry by matching its email to the email addresses a person holds. It has the members of an attendee of an event, without a response. | +| `assignees[].name` | | `string` | No | The assignee's name as the source gives it, such as a login | +| `assignees[].email` | | `email` | No | The assignee's email address | +| `notes` | | `string` | No | Personal annotations | +| `recurrence` | | `array` of `string` | No | RFC 5545 recurrence property lines (RRULE, RDATE, EXDATE) for a task that repeats. The series starts at due_at, or at starts_at when the task has no due_at, and advances in UTC because a task has no timezone. The server keeps and checks the rule but does not unfold it: GET /occurrences answers events only. A write is refused when a line cannot be read or is not applied (EXRULE, an RDATE period), when it carries more than one RRULE, more than 40,000 characters in all or more than 1,000 added and removed dates, when the rule names no date that exists after the start, when it produces no occurrence within the bound a read unfolds a rule to, or when the task has neither due_at nor starts_at. | ## Settings | Setting | Value | | ------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| `plural` | Tasks | | [`title_field`](/catalog/your-own-types#display-hints) | `title` | | [`body_field`](/catalog/your-own-types#display-hints) | `body` | -| [`icon`](/catalog/your-own-types#display-hints) | `square-check-big` | +| [`icon`](/catalog/your-own-types#display-hints) | `lucide:square-check-big` | | [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `body` and `notes`; `last_writer_wins` for every other field | diff --git a/src/generated/catalog/edge-types.md b/src/generated/catalog/edge-types.md index 96b30665..9fcd1d7d 100644 --- a/src/generated/catalog/edge-types.md +++ b/src/generated/catalog/edge-types.md @@ -2,7 +2,6 @@ | --------------- | ------------- | --------------- | ---------------- | -------------- | --------- | ---------------------------------- | | `about` | About | Subject of | None | `many-to-many` | `orphan` | Any type | | `attached-to` | Attached to | Attachments | `has-attachment` | `many-to-many` | `orphan` | Any type | -| `authored-by` | Authored by | Author of | None | `many-to-many` | `orphan` | Any type | | `derived-from` | Derived from | Basis for | None | `many-to-many` | `orphan` | Any type | | `in-collection` | In collection | Contents | None | `many-to-many` | `orphan` | Any type with the role `container` | | `in-folder` | In folder | Folder contents | None | `many-to-many` | `orphan` | `system.folder` | diff --git a/src/generated/catalog/edges.md b/src/generated/catalog/edges.md index e8cc9df0..67988a7a 100644 --- a/src/generated/catalog/edges.md +++ b/src/generated/catalog/edges.md @@ -6,10 +6,6 @@ Source item is about the target item. Topical connection between items across ty Source is a file attached to the target. Many-to-many: a file may be attached to multiple hosts; a host may have multiple attachments. Deleting the host orphans the attachment (it may still be attached to other items). The attachment's own file writes the edge, saying what it was made for; where the attachment cannot carry frontmatter, such as an image, the host's file writes it under the reverse name, `has-attachment`. -## authored-by - -Source was authored by the target (usually a core.entity.person). - ## derived-from Source was produced from the target (imports, AI generation, transformations). Both items remain valid in parallel. @@ -18,6 +14,8 @@ Source was produced from the target (imports, AI generation, transformations). B Source item is a member of the target container. The target may be any type declaring the `container` role, such as a collection, an ongoing series, an album or a publisher's own container type, rather than a fixed list of type names, so a connector writing its own vocabulary can express containment without the platform naming its types first. An item may belong to any number of containers, and a container holds any number of members. A container may not itself be a member: containment here is flat, and `parent-of` is what hierarchy is for. Deleting the container leaves its members in place. Carries position for ordering within the container. +**flat:** `true` + | Property | Kind | Description | | ---------- | -------- | ----------------------------------------- | | `position` | `number` | Ordering within the collection (1-based). | @@ -28,9 +26,9 @@ Where an item's file sits in a folder: the source is the item, the target the fo **written_at:** `none` -| Property | Kind | Description | -| -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `path` | `string` | The file's path relative to the folder's root, with `/` between names; required, and it may not climb out of the folder. The edge takes no other property. | +| Property | Kind | Description | +| -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `path` | `string` | The file's path relative to the folder's root, with `/` between names; required, with no `..` name. The edge takes no other property. | ## in-thread @@ -46,6 +44,8 @@ Source is the parent of the target. Target has at most one parent. Deleting the **written_at:** `target` +**acyclic:** `true` + ## references Source explicitly references the target. Distinct from `about` (topical) and `derived-from` (provenance). Covers wikilink-style mentions and the highlight-on-a-book case. @@ -53,3 +53,5 @@ Source explicitly references the target. Distinct from `about` (topical) and `de ## supersedes Source replaces the target as the current version. Linear chain; no cycles. Deleting the head does not promote the predecessor (orphan). + +**acyclic:** `true` diff --git a/src/generated/catalog/item-fields.md b/src/generated/catalog/item-fields.md index db989d26..4c25c024 100644 --- a/src/generated/catalog/item-fields.md +++ b/src/generated/catalog/item-fields.md @@ -25,11 +25,12 @@ ### State -| Field | Kind | Description | -| -------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `state` | `enum` | The item's lifecycle state. Values: `active`, `archived`, `trashed`, `revoked`. | -| `trashed_by_cascade` | `boolean` | Always `true` where present: the item went to the trash with another item, through a cascading edge such as `parent-of`. Present while it stays in the trash. Absent on an item trashed on its own. | -| `trashed_with` | `string` | The ID of the item whose trash took this one into the trash, beside `trashed_by_cascade`, whatever became of that item since. Present only if you can read that item's type. | +| Field | Kind | Description | +| -------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `state` | `enum` | The item's lifecycle state. Values: `active`, `archived`, `trashed`, `revoked`. | +| `trashed_at` | `string` or `null` | When the item went to the trash, in UTC, or when a `system.*` item was revoked. A later write to the item leaves it as it is, and a restore clears it. `null` for an item outside the trash, and for one an archive restored to the trash without a time. | +| `trashed_by_cascade` | `boolean` | Always `true` where present: the item went to the trash with another item, through a cascading edge such as `parent-of`. Present while it stays in the trash. Absent on an item trashed on its own. | +| `trashed_with` | `string` | The ID of the item whose trash took this one into the trash, beside `trashed_by_cascade`, whatever became of that item since. Present only if you can read that item's type. | ### Version diff --git a/src/generated/catalog/reserved-field-names.md b/src/generated/catalog/reserved-field-names.md index 1e26b83a..e81ff3ec 100644 --- a/src/generated/catalog/reserved-field-names.md +++ b/src/generated/catalog/reserved-field-names.md @@ -1 +1 @@ -`capture_latitude`, `capture_longitude`, `created_at`, `id`, `occurred_at`, `properties`, `schema_version`, `source`, `source_id`, `state`, `tier`, `trashed_by_cascade`, `trashed_with`, `type`, `updated_at`, `version` +`capture_latitude`, `capture_longitude`, `created_at`, `id`, `occurred_at`, `properties`, `schema_version`, `source`, `source_id`, `state`, `tier`, `trashed_at`, `trashed_by_cascade`, `trashed_with`, `type`, `updated_at`, `version` diff --git a/src/generated/catalog/system.connection.md b/src/generated/catalog/system.connection.md index a12cf32f..b962b744 100644 --- a/src/generated/catalog/system.connection.md +++ b/src/generated/catalog/system.connection.md @@ -10,3 +10,9 @@ | `granted_at` | `datetime` | Yes | When the grant was approved | | `last_used_at` | `datetime` | No | Most recent successful use of any token issued under this grant | | `revoked_at` | `datetime` | No | When the grant was revoked, if any | + +## Settings + +| Setting | Value | +| -------- | ----------- | +| `plural` | Connections | diff --git a/src/generated/catalog/system.folder.md b/src/generated/catalog/system.folder.md index 7fd2b734..3b14eccc 100644 --- a/src/generated/catalog/system.folder.md +++ b/src/generated/catalog/system.folder.md @@ -1,12 +1,18 @@ ## Fields -| Field | Kind | Required | Description | -| ------------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `title` | `string` | Yes | The folder's name | -| `query` | `object` | No | Which items the folder holds. `types`: type identifiers, each with its subtypes; empty or absent holds every type the folder's key reads. `tier`: `library`, `feed`, or `all` for both tiers; absent is `library`. `state`: `active`, `archived` or both; absent is both. `filter`: an expression in the listing grammar's `filter`. `beneath`: an item id; the item and everything under it by `parent-of`. | -| `defaults` | `object` | No | What a new file takes where its frontmatter leaves a blank, never applied to an edit: `type`, `tier`, `properties`, `tags`, and `edges` as a map from edge type to item ids, each edge running from the new file to the item named except `parent-of`, which runs from the item named so the new file is its child; a folder whose query follows an edge gives every new file that edge. | -| `include` | `array` of `string` | No | Paths the folder takes, as gitignore patterns relative to its root; empty or absent takes every path. A dot-led path is taken only where a line names a dot-led name on its way, and no line reaches what the built-in lists name | -| `ignore` | `array` of `string` | No | Paths the folder leaves alone, as gitignore patterns relative to its root, winning over `include`; the built-in lists, of files a machine or an editor writes for itself and of secrets, apply whatever either list says | -| `first_placement` | `object` | No | Where a new item of a type made elsewhere first appears: a map from type identifier to a directory relative to the folder's root. Placement only; it never decides membership. | -| `removal_threshold` | `object` | No | When a removal pauses for confirmation: more than `files` files and more than `fraction` of the folder at once. Absent members are 10 and 0.25. | -| `revoked_at` | `datetime` | No | When the folder was revoked | +| Field | Kind | Required | Description | +| ------------------- | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` | `string` | Yes | The folder's name | +| `query` | `object` | No | Which items the folder holds. `types`: type identifiers, each with its subtypes; empty or absent holds every type the folder's key reads. `tier`: `library`, `feed`, or `all` for both tiers; absent is `library`. `state`: `active`, `archived` or both; absent is both. `filter`: an expression in the listing grammar's `filter`, with no `backref` condition. `beneath`: an item id; the item and everything under it by `parent-of`. | +| `defaults` | `object` | No | What a new file takes where its frontmatter leaves a blank, never applied to an edit: `type`, `tier`, `properties`, `tags`, and `edges` as a map from edge type to item ids, each edge running from the new file to the item named except `parent-of`, which runs from the item named so the new file is its child; a folder whose query follows an edge gives every new file that edge. | +| `include` | `array` of `string` | No | Paths the folder takes, as gitignore patterns relative to its root; empty or absent takes every path. A dot-led path is taken only where a line names a dot-led name on its way, and no line reaches what the built-in lists name | +| `ignore` | `array` of `string` | No | Paths the folder leaves alone, as gitignore patterns relative to its root, winning over `include`; the built-in lists, of files a machine or an editor writes for itself and of secrets, apply whatever either list says | +| `first_placement` | `object` | No | Where a new item of a type made elsewhere first appears: a map from type identifier to a directory relative to the folder's root, with no `..` name. Placement only; it never decides membership. | +| `removal_threshold` | `object` | No | When a removal pauses for confirmation: more than `files` files and more than `fraction` of the folder at once. Absent members are 10 and 0.25. | +| `revoked_at` | `datetime` | No | When the folder was revoked | + +## Settings + +| Setting | Value | +| -------- | ------- | +| `plural` | Folders | diff --git a/src/generated/catalog/system.view.md b/src/generated/catalog/system.view.md index 4683d20d..8e123667 100644 --- a/src/generated/catalog/system.view.md +++ b/src/generated/catalog/system.view.md @@ -6,3 +6,9 @@ | `query` | `object` | No | Which items the view lists. `types`: type identifiers, each with its subtypes; empty or absent lists every type the reading key reads. `tier`: `library`, `feed` or `all`; absent is `all`. `state`: `active`, `archived` or both; absent is both. `filter`: an expression in the listing grammar's `filter`. `beneath`: an item id; the item and everything under it by `parent-of`. | | `sort` | `array` of `object` | No | The keys the view sorts by, first key first. Each is a `by` field and a `direction`, `asc` or `desc`. | | `group` | `object` | No | How the view groups the items it lists: `by` (a field, or `edge[]` or `backref[]`), and optionally `bucket` (`day`, `week`, `month` or `year`, for a date field), `order` (the values whose groups come first) and `hidden` (the values whose groups are left out). | + +## Settings + +| Setting | Value | +| -------- | ----- | +| `plural` | Views | diff --git a/src/generated/catalog/types.md b/src/generated/catalog/types.md index fc368517..d83dfea1 100644 --- a/src/generated/catalog/types.md +++ b/src/generated/catalog/types.md @@ -1,17 +1,17 @@ ## Core types -| Type | Description | Subtypes | -| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`core.bookmark`](/catalog/bookmark) | Content you captured from elsewhere: a saved URL, a highlight, an excerpt, a clipped paragraph. | | -| [`core.collection`](/catalog/collection) | Items of any type that you gather together. Members join it through the in-collection edge, and it can hold any item but another container. | | -| [`core.entity`](/catalog/entity) | Any named entity: a person, a company, a band, a team, a charity, a brand, a school or a place. `core.entity.person` and `core.entity.place` narrow it. | [`person`](/catalog/entity#core-entity-person), [`place`](/catalog/entity#core-entity-place) | -| [`core.event`](/catalog/event) | Something that happens at a time. | | -| [`core.file`](/catalog/file) | A file or binary reference: the generic fallback for any file that has no more specific type. | [`audio`](/catalog/file#core-file-audio), [`image`](/catalog/file#core-file-image), [`video`](/catalog/file#core-file-video), [`webpage`](/catalog/file#core-file-webpage) | -| [`core.highlight`](/catalog/highlight) | A user's engagement with content: the highlighted passage plus optional annotation. The canonical relationship (what was highlighted) is carried by a references edge; the moment of highlighting is the item's own `occurred_at`. | | -| [`core.media`](/catalog/media) | Content produced by someone that the user engages with: a film, podcast, book, article, song, show. | [`album`](/catalog/media#core-media-album), [`article`](/catalog/media#core-media-article), [`book`](/catalog/media#core-media-book), [`episode`](/catalog/media#core-media-episode), [`film`](/catalog/media#core-media-film), [`series`](/catalog/media#core-media-series), [`song`](/catalog/media#core-media-song) | -| [`core.message`](/catalog/message) | A light, cross-platform message. Threading is edge-based via in-thread; reply trees via parent-of. Tool-specific richness lives in app namespaces. | | -| [`core.note`](/catalog/note) | Text content you created. | | -| [`core.task`](/catalog/task) | Something to be done. | | +| Type | Description | Subtypes | +| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`core.bookmark`](/catalog/bookmark) | Content you captured from elsewhere: a saved URL, a highlight, an excerpt, a clipped paragraph. | | +| [`core.collection`](/catalog/collection) | Items of any type that you gather together. Members join it through the in-collection edge, and it can hold any item but another container. | | +| [`core.entity`](/catalog/entity) | Any named entity: a person, a company, a band, a team, a charity, a brand, a school or a place. `core.entity.person` and `core.entity.place` narrow it. | [`person`](/catalog/entity#core-entity-person), [`place`](/catalog/entity#core-entity-place) | +| [`core.event`](/catalog/event) | Something that happens at a time. | | +| [`core.file`](/catalog/file) | A file or binary reference: the generic fallback for any file that has no more specific type. | [`audio`](/catalog/file#core-file-audio), [`image`](/catalog/file#core-file-image), [`video`](/catalog/file#core-file-video), [`webpage`](/catalog/file#core-file-webpage) | +| [`core.highlight`](/catalog/highlight) | A user's engagement with content: the highlighted passage plus optional annotation. The canonical relationship (what was highlighted) is carried by a references edge; the moment of highlighting is the item's own `occurred_at`. | | +| [`core.media`](/catalog/media) | Content produced by someone that the user engages with: a film, podcast, book, article, song, show. | [`album`](/catalog/media#core-media-album), [`article`](/catalog/media#core-media-article), [`book`](/catalog/media#core-media-book), [`episode`](/catalog/media#core-media-episode), [`film`](/catalog/media#core-media-film), [`game`](/catalog/media#core-media-game), [`series`](/catalog/media#core-media-series), [`song`](/catalog/media#core-media-song) | +| [`core.message`](/catalog/message) | A light, cross-platform message. Threading is edge-based via in-thread; reply trees via parent-of. Tool-specific richness lives in app namespaces. | | +| [`core.note`](/catalog/note) | Text content you created. | | +| [`core.task`](/catalog/task) | Something to be done. | | ## System types diff --git a/src/generated/cli-commands.json b/src/generated/cli-commands.json index a25f85e5..6702e541 100644 --- a/src/generated/cli-commands.json +++ b/src/generated/cli-commands.json @@ -4,6 +4,7 @@ "": {"options": [{"short": "-V", "flag": "--version"}], "subcommands": true}, "status": {"options": []}, "metrics": {"options": []}, + "overview": {"options": []}, "whoami": {"options": []}, "login": {"options": [{"flag": "--scope", "value": true}, {"flag": "--no-browser"}, {"flag": "--print-token"}, {"flag": "--client-id", "value": true}, {"flag": "--name", "value": true}]}, "logout": {"options": []}, @@ -42,15 +43,15 @@ "items trash purge": {"options": [{"flag": "--version", "value": true}], "positional": true}, "items trash empty": {"options": [{"flag": "--cursor", "value": true}, {"flag": "--confirm", "value": true}], "positional": true}, "items get": {"options": [{"flag": "--include", "value": true}], "positional": true}, - "items create": {"options": [{"flag": "--type", "value": true}, {"flag": "--properties", "value": true}, {"flag": "--prop", "value": true}, {"flag": "--tag", "value": true}, {"flag": "--tier", "value": true}, {"flag": "--state", "value": true}, {"flag": "--occurred-at", "value": true}, {"flag": "--source", "value": true}, {"flag": "--source-id", "value": true}, {"flag": "--id", "value": true}, {"flag": "--version", "value": true}, {"flag": "--edges", "value": true}, {"flag": "--idempotency-key", "value": true}], "positional": true}, + "items create": {"options": [{"flag": "--type", "value": true}, {"flag": "--properties", "value": true}, {"flag": "--prop", "value": true}, {"flag": "--tag", "value": true}, {"flag": "--tier", "value": true}, {"flag": "--state", "value": true}, {"flag": "--occurred-at", "value": true}, {"flag": "--source", "value": true}, {"flag": "--source-id", "value": true}, {"flag": "--id", "value": true}, {"flag": "--create-only"}, {"flag": "--version", "value": true}, {"flag": "--edges", "value": true}, {"flag": "--idempotency-key", "value": true}], "positional": true}, "items update": {"options": [{"flag": "--version", "value": true}, {"flag": "--properties", "value": true}, {"flag": "--prop", "value": true}, {"flag": "--replace"}, {"flag": "--type", "value": true}, {"flag": "--retype"}, {"flag": "--tier", "value": true}, {"flag": "--occurred-at", "value": true}, {"flag": "--source-id", "value": true}, {"flag": "--edges", "value": true}, {"flag": "--conflict", "value": true}, {"flag": "--idempotency-key", "value": true}], "positional": true}, - "items delete": {"options": [{"flag": "--idempotency-key", "value": true}], "positional": true}, + "items delete": {"options": [{"flag": "--dry-run"}, {"flag": "--digest", "value": true}, {"flag": "--idempotency-key", "value": true}]}, "items restore": {"options": [{"flag": "--idempotency-key", "value": true}], "positional": true}, "items transition": {"options": [{"flag": "--state", "value": true}, {"flag": "--idempotency-key", "value": true}], "positional": true}, "items purge": {"options": [{"flag": "--idempotency-key", "value": true}], "positional": true}, "items versions": {"options": [{"flag": "--limit", "value": true}, {"flag": "--cursor", "value": true}], "positional": true}, - "items tag": {"options": [{"flag": "--idempotency-key", "value": true}], "positional": true}, - "items untag": {"options": [{"flag": "--idempotency-key", "value": true}], "positional": true}, + "items tag": {"options": [{"flag": "--version", "value": true}, {"flag": "--idempotency-key", "value": true}], "positional": true}, + "items untag": {"options": [{"flag": "--version", "value": true}, {"flag": "--idempotency-key", "value": true}], "positional": true}, "items edges": {"options": [{"flag": "--type", "value": true}, {"flag": "--limit", "value": true}, {"flag": "--cursor", "value": true}], "positional": true}, "items backrefs": {"options": [{"flag": "--type", "value": true}, {"flag": "--limit", "value": true}, {"flag": "--cursor", "value": true}], "positional": true}, "items add": {"options": [{"flag": "--mime-type", "value": true}, {"flag": "--title", "value": true}, {"flag": "--type", "value": true}, {"flag": "--tag", "value": true}, {"flag": "--tier", "value": true}], "positional": true}, @@ -94,7 +95,7 @@ "types move": {"options": [{"flag": "--dry-run"}, {"flag": "--properties", "value": true}], "positional": true}, "types retired": {"options": []}, "types delete-retired": {"options": [], "positional": true}, - "search": {"options": [{"flag": "--type", "value": true}, {"flag": "--state", "value": true}, {"flag": "--tier", "value": true}, {"flag": "--tag", "value": true}, {"flag": "--filter", "value": true}, {"flag": "--occurred-after", "value": true}, {"flag": "--occurred-before", "value": true}, {"flag": "--include", "value": true}, {"flag": "--limit", "value": true}, {"flag": "--cursor", "value": true}], "positional": true}, + "search": {"options": [{"flag": "--type", "value": true}, {"flag": "--state", "value": true}, {"flag": "--source", "value": true}, {"flag": "--tier", "value": true}, {"flag": "--tag", "value": true}, {"flag": "--filter", "value": true}, {"flag": "--occurred-after", "value": true}, {"flag": "--occurred-before", "value": true}, {"flag": "--updated-after", "value": true}, {"flag": "--updated-before", "value": true}, {"flag": "--sort", "value": true}, {"flag": "--direction", "value": true}, {"flag": "--include", "value": true}, {"flag": "--limit", "value": true}, {"flag": "--cursor", "value": true}], "positional": true}, "metadata": {"options": [], "subcommands": true}, "metadata get": {"options": [], "positional": true}, "metadata replace": {"options": [{"flag": "--tag", "value": true}, {"flag": "--idempotency-key", "value": true}], "positional": true}, @@ -131,6 +132,7 @@ "views change": {"options": [{"flag": "--version", "value": true}, {"flag": "--file", "value": true}, {"flag": "--body", "value": true}, {"flag": "--title", "value": true}, {"flag": "--query", "value": true}, {"flag": "--sort", "value": true}, {"flag": "--group", "value": true}, {"flag": "--idempotency-key", "value": true}], "positional": true}, "views delete": {"options": [{"flag": "--idempotency-key", "value": true}], "positional": true}, "views get": {"options": [], "positional": true}, + "views items": {"options": [{"flag": "--limit", "value": true}, {"flag": "--cursor", "value": true}], "positional": true}, "webhooks": {"options": [], "subcommands": true}, "webhooks create": {"options": [{"flag": "--to", "value": true}, {"flag": "--event", "value": true}, {"flag": "--type-filter", "value": true}, {"flag": "--secret", "value": true}], "positional": true}, "webhooks list": {"options": []}, @@ -173,16 +175,18 @@ "connectors agreements list": {"options": [{"flag": "--waiting", "value": true}, {"flag": "--limit", "value": true}, {"flag": "--cursor", "value": true}], "positional": true}, "operations": {"options": []}, "device": {"options": [{"flag": "--db", "value": true}, {"flag": "--reader"}], "subcommands": true}, - "device hydrate": {"options": [{"flag": "--types", "value": true}, {"flag": "--tier", "value": true}, {"flag": "--edge-type", "value": true}], "positional": true}, + "device hydrate": {"options": [{"flag": "--types", "value": true}, {"flag": "--tier", "value": true}, {"flag": "--edge-type", "value": true}, {"flag": "--in-place-only"}], "positional": true}, "device pin": {"options": [], "positional": true}, "device unpin": {"options": [], "positional": true}, "device catch-up": {"options": []}, "device follow": {"options": [{"flag": "--for", "value": true}]}, "device watch": {"options": [{"flag": "--for", "value": true}]}, "device changes": {"options": [{"flag": "--for", "value": true}]}, + "device changed": {"options": [{"flag": "--since", "value": true}, {"flag": "--most", "value": true}]}, "device items": {"options": [], "subcommands": true}, "device items list": {"options": [{"flag": "--type", "value": true}, {"flag": "--state", "value": true}, {"flag": "--all-states"}, {"flag": "--tier", "value": true}, {"flag": "--tag", "value": true}, {"flag": "--occurred-after", "value": true}, {"flag": "--occurred-before", "value": true}, {"flag": "--filter", "value": true}, {"flag": "--beneath", "value": true}, {"flag": "--source", "value": true}, {"flag": "--updated-after", "value": true}, {"flag": "--updated-before", "value": true}, {"flag": "--sort", "value": true}, {"flag": "--direction", "value": true}, {"flag": "--limit", "value": true}, {"flag": "--offset", "value": true}, {"flag": "--view", "value": true}]}, "device items get": {"options": [], "positional": true}, + "device items bulk-get": {"options": [{"flag": "--include-bin"}]}, "device items create": {"options": [{"flag": "--type", "value": true}, {"flag": "--properties", "value": true}, {"flag": "--tag", "value": true}, {"flag": "--tier", "value": true}, {"flag": "--state", "value": true}, {"flag": "--capture-latitude", "value": true}, {"flag": "--capture-longitude", "value": true}, {"flag": "--edges", "value": true}, {"flag": "--source", "value": true}, {"flag": "--source-id", "value": true}, {"flag": "--occurred-at", "value": true}, {"flag": "--id", "value": true}, {"flag": "--version", "value": true}], "positional": true}, "device items update": {"options": [{"flag": "--properties", "value": true}, {"flag": "--version", "value": true}, {"flag": "--source-id", "value": true}, {"flag": "--as-read"}, {"flag": "--type", "value": true}, {"flag": "--tier", "value": true}, {"flag": "--replace"}, {"flag": "--occurred-at", "value": true}, {"flag": "--clear-occurred-at"}], "positional": true}, "device items delete": {"options": [], "positional": true}, @@ -193,6 +197,9 @@ "device items add": {"options": [{"flag": "--mime-type", "value": true}, {"flag": "--title", "value": true}, {"flag": "--type", "value": true}, {"flag": "--tag", "value": true}, {"flag": "--tier", "value": true}], "positional": true}, "device items attach": {"options": [{"flag": "--mime-type", "value": true}, {"flag": "--title", "value": true}, {"flag": "--type", "value": true}, {"flag": "--tier", "value": true}], "positional": true}, "device items links": {"options": [], "positional": true}, + "device items bulk-links": {"options": []}, + "device items link-parts": {"options": [{"flag": "--file", "value": true}], "positional": true}, + "device items link-candidates": {"options": [{"flag": "--item", "value": true}]}, "device items embed": {"options": [], "positional": true}, "device items thumbnail": {"options": [{"flag": "--out", "value": true}], "positional": true}, "device folders": {"options": [], "subcommands": true}, @@ -223,9 +230,12 @@ "device edge-types served": {"options": []}, "device edge-types get": {"options": [], "positional": true}, "device edges": {"options": [], "subcommands": true}, + "device edges get": {"options": [], "positional": true}, + "device edges bulk-get": {"options": []}, "device edges list": {"options": [], "positional": true}, "device edges to": {"options": [], "positional": true}, "device edges create": {"options": [{"flag": "--source", "value": true}, {"flag": "--target", "value": true}, {"flag": "--type", "value": true}, {"flag": "--properties", "value": true}, {"flag": "--id", "value": true}], "positional": true}, + "device edges check": {"options": [{"flag": "--source", "value": true}, {"flag": "--target", "value": true}, {"flag": "--type", "value": true}, {"flag": "--properties", "value": true}, {"flag": "--id", "value": true}], "positional": true}, "device edges update": {"options": [{"flag": "--properties", "value": true}, {"flag": "--version", "value": true}, {"flag": "--source", "value": true}, {"flag": "--target", "value": true}], "positional": true}, "device edges delete": {"options": [], "positional": true}, "device tags": {"options": [], "subcommands": true}, diff --git a/src/generated/cli-download.md b/src/generated/cli-download.md index 2cd6d376..1bd3b0a6 100644 --- a/src/generated/cli-download.md +++ b/src/generated/cli-download.md @@ -1,5 +1,5 @@ ```bash -curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.45/marfa-0.0.45-darwin-arm64.tar.gz | tar -xz +curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.49/marfa-0.0.49-darwin-arm64.tar.gz | tar -xz sudo mkdir -p /usr/local/bin sudo mv marfa /usr/local/bin/ marfa --version diff --git a/src/generated/contract-index.json b/src/generated/contract-index.json index 84b39b87..e17a082b 100644 --- a/src/generated/contract-index.json +++ b/src/generated/contract-index.json @@ -406,7 +406,7 @@ "device/pin-gone-completes": "7c2acc85ea007e3f", "device/unpin-says": "9e765bdd92559e7e", "device/unpin-lets-go": "8bb0306ebb9a007f", - "device/unpin-edges": "67e58e7e3838cf13", + "device/unpin-edges": "846dc92e404b2df5", "device/unpin-waiting": "d85faf350155bdd3", "device/unpin-answered": "a12aeacdda716ffe", "device/unpin-hydration": "37a085f6da3bc861", @@ -431,6 +431,7 @@ "device/instance-named-one-write": "2fe22ae678d5aff7", "device/instance-changed-one-write": "336fb0f00e394f18", "device/instance-changed-holds-report": "ccfe5b45d7246410", + "device/queue-other-instance-held": "97220d790d7b857a", "device/late-answer": "8424582da6b3bd42", "device/proof-lost": "1618592c3910ce22", "device/reading-handle-reads": "b636171f4c8c9f5a", @@ -481,6 +482,8 @@ "device/hydrate-page-walk": "1fa660839033f68f", "device/hydrate-pins-batched": "6b3a0c15f38af506", "device/hydrate-repeated-cursor": "17324c90fa8bdf95", + "device/hydrate-accepts-gzip": "b3a7a89e0796be9c", + "device/hydrate-reads-gzip": "cc31e1da0a0e9d00", "device/hydrate-replays": "6ddc6395664cb986", "device/hydrate-live-marker": "eacb9491ddbc5e87", "device/hydrate-replaces": "20ec8939407baf43", @@ -495,7 +498,7 @@ "device/hydrate-restart-instance": "dfbeff9f8e29f090", "device/hydrate-restart-reach": "f3e0dd2213a42542", "device/hydrate-restart-catalog": "41784240e2ecf297", - "device/hydrate-icon-only-catalog": "ab083165d6eb421a", + "device/hydrate-icon-only-catalog": "b97ab072ab3cd113", "device/hydrate-restart-head-aged-out": "38484d0729f6f4d1", "device/hydrate-restart-head-past-log": "e221b8f38d478ec6", "device/hydrate-restart-cursor": "20a194db4a75a7fd", @@ -507,6 +510,10 @@ "device/refill-restart-clears": "621e4a708b1c3f41", "device/refill-restored-clears": "fb342e558fd1d6d0", "device/refill-reported": "c7985aee19aa7b34", + "device/refill-in-place-only": "1d53fef5aa052ec9", + "device/refill-in-place-only-keeps": "d53f22600e6111df", + "device/refill-in-place-only-server-ends": "e9cbcadabf20e3e9", + "device/refill-in-place-only-fills": "09888043419b8866", "device/hydrate-report": "9615b24f448e8365", "device/hydrate-every-type": "4dc335f0247e9623", "device/hydrate-every-type-beside-system": "e1ccc2177034ff00", @@ -563,9 +570,9 @@ "device/version-local-create": "f0e22934115dec3f", "device/version-waiting-edit": "f70ae85d4199acd1", "device/version-from-answer": "dbca39eba3d7021c", - "device/conflict-blocked": "3ff4506ae9af7ee3", - "device/conflict-unresolved": "a43d5249348cc663", - "device/conflict-version-kept": "30d93e3f70e13dd3", + "device/conflict-blocked": "b8a3904ff9cf4359", + "device/conflict-unresolved": "175a83673b32cf2b", + "device/conflict-version-kept": "beded7c952d986d4", "device/create-tags-queued": "bd77687bf6dbde10", "device/create-fields-one-write": "474f1e2e1d7bb9ef", "device/create-fields-shown": "9653273e7bbc814a", @@ -596,7 +603,12 @@ "device/bytes-absent-item-whole": "2bdb67bea3682862", "device/list-default-active": "cff559c719433f63", "device/search-default-active": "dce47daa9448201a", - "device/read-named-state": "64f430d81c034ae3", + "device/read-named-state": "7d54523d3856bf0e", + "device/read-states-beside-all": "7a624c9ee7bea7d7", + "device/read-type-list": "748c32b42b9c5a76", + "device/read-type-list-max": "44f856db466f2e56", + "device/read-list-empty": "8106a0f70aeb3faf", + "device/read-limit-after-filter": "bf64ff5067517d98", "device/get-archived": "7d3c856a33d97515", "device/get-trashed": "c2862d09a0bbfa30", "device/search-no-bin": "88cb5a5848873827", @@ -623,12 +635,31 @@ "device/filter-case-ascii": "e66773f14c88f46f", "device/filter-numeric-bound": "65c2358a2fcdc14f", "device/filter-system-number": "8205902f1f434078", + "device/filter-status-group": "0845fe0b9087a658", "device/filter-property-kind": "a222a8aaee4b2d46", "device/filter-nested-number": "a84eb5e398adf143", "device/filter-edge-held": "1c4d1ab150101e44", - "device/beneath": "2d5526ce5298e8a0", + "device/beneath": "65aa771c41cb16f8", + "device/view-items-parity": "db11a72acc6675dd", "device/beneath-references": "ad51bd86572a871c", "device/beneath-cycle": "a1cbbae198cb7cda", + "device/bulk-get-order": "de6eb203c73c207f", + "device/bulk-get-same": "0dcdc7cbda392adb", + "device/bulk-get-bin": "ed5b748216280795", + "device/bulk-get-partial": "a97bfd7e7617fa5c", + "device/edge-get": "3698a309844f80ef", + "device/edge-get-unheld": "a2b638a6d6c32ba1", + "device/edge-bulk-get": "56b0f3d7f9b95e71", + "device/bulk-links-same": "848cc3193fa7a31b", + "device/bulk-links-left-out": "851129cc55d979ba", + "device/changed-cursor": "c8fd9db32833e787", + "device/changed-names": "a8ae267e724f5c65", + "device/changed-edge-ends": "9f1a4b567733df10", + "device/changed-removed": "ee97efff531b2b66", + "device/changed-everything": "7f48fd86e321feef", + "device/changed-most": "1b6ebb3b71922f71", + "device/changed-partial-refused": "f4a9ab5d0eac0090", + "device/changed-malformed": "48458d72d56dd62e", "device/edges-to": "fd97926f7fdbeac0", "device/edges-whole-either-end": "254dd3725d188a73", "device/edge-source-unheld": "2ca506b23e75ed85", @@ -725,7 +756,7 @@ "device/attachment-miss-kept": "919dbdcedac790e9", "device/attachment-not-in-copy": "d909a6d321db767a", "device/attachment-hydrating": "9f9d32ff611ee23c", - "device/command-contract-mismatch": "b534efa338b9e954", + "device/command-contract-mismatch": "6083df61c7770908", "device/command-mismatch-prints-nothing": "5cd8bfe422400b58", "device/command-unnamed-success": "0ea330a5a934e790", "device/command-contract-twice": "73f32c5b1f1162e5", @@ -902,7 +933,9 @@ "device/command-reader-not-a-store": "a53ffa0f805e547a", "device/reader-missing-table": "7b31a8f7612a79ae", "device/added-table": "06d5941f3c96b639", - "device/wrong-schema": "1471f0d32c7b29b7", + "device/reader-missing-column": "5789fb72258aa5b2", + "device/added-column": "517898ca3b7f9274", + "device/wrong-schema": "5e31a1bd8d143388", "device/wrong-schema-says": "2d5673c280a0de5c", "device/wrong-schema-names-kind": "29f0d20d00f88c4b", "device/wrong-schema-no-delete": "3cb3cfa4cc6ef74c", @@ -942,9 +975,9 @@ "device/catalog-replaced-catch-up": "0377b6e5cf959ede", "device/catalog-replaced-follow": "90f5be33d116a6a7", "device/catalog-offline": "a6f7a18fec847f81", - "device/catalog-type-read": "f11969b0fdb676fa", + "device/catalog-type-read": "d67e1930d07f8fb5", "device/catalog-icon": "8afa9a64443795f5", - "device/catalog-edge-type-read": "c24c69e44f6f6983", + "device/catalog-edge-type-read": "b7b6923f4a37ef00", "device/catalog-read-consistent": "ca25ce0516de3858", "device/catalog-replace-whole": "cec5668a64745365", "device/catalog-damaged-properties": "7e49d7e2f0a899c7", @@ -956,6 +989,21 @@ "device/catalog-changed-told": "f49c94e775a2b3de", "device/catch-up-catalog-told": "550712baa365440c", "device/catch-up-failed-catalog-told": "576bdd57c3bd5a63", + "device/item-name": "a6f1c612e9e5e405", + "device/item-name-none": "3f6a61044e2f03e9", + "device/item-preview-field": "080332cd90416a63", + "device/item-preview-after-name": "a8ef43eed0af786d", + "device/item-preview-fence": "1dbbff7ad40ef953", + "device/item-preview-lines": "d5d28ef984b00b3a", + "device/item-preview-prefixes": "c15b050e7a9323e7", + "device/item-preview-escape": "e2784cd6a51137df", + "device/item-preview-code": "21d379c3ff294632", + "device/item-preview-images": "eff4c1a448a13688", + "device/item-preview-links": "a2c42bd1c9331aa9", + "device/item-preview-emphasis": "ca6d62975e79c821", + "device/item-preview-unclosed": "3f70da45d822090d", + "device/item-preview-text": "9928b071fd61ec87", + "device/item-preview-none": "fc3730cdca0c9e2f", "device/resume-presents-reach": "6bd2a00e840207a7", "device/live-marker-required": "d3c7abd2c0659c3a", "device/proof-invalid": "f9b4103ff6fe6419", @@ -964,7 +1012,7 @@ "device/undecodable-answer": "523a673561ba6730", "device/undecodable-answer-keeps-copy": "17793215996eea12", "device/undecodable-event-keeps-cursor": "03692cd765e80c0e", - "device/reach-changed": "2e9c22e9c32ee09c", + "device/reach-changed": "4c735910fc59347d", "device/listed-membership": "7c5e18ffc76bd490", "device/uncertified-keeps-copy": "745ab260f59a85ba", "device/credential-ended": "e7a16f038b1bae10", @@ -980,10 +1028,15 @@ "device/number-exact-folder-flagged": "1bc25c70f8059113", "device/number-exact-folder-kept": "f4abbcf9e7a313b9", "device/number-digits": "e7d56d1cce146bd6", - "device/url-trimmed": "28313af0bd24a6d5", + "device/url-trimmed": "afda56888fcbc266", "device/url-idna-unassigned": "2a9edf20a556e53d", + "device/range-checked": "2b6683726ac5a2c2", + "device/currency-checked": "52c13eeda4482e3d", + "device/entries-checked": "8562319b19a19cc9", "device/unknown-type-write": "69342d8d253b5913", - "device/declaration-refused": "46d894c681563663", + "device/declaration-refused": "2e03f775f49a389b", + "device/declaration-field-refused": "4c1142ce5f916f93", + "device/declared-format-kind": "f05057bd06b2b5d7", "device/declaration-refused-whole": "a4978e4678068722", "device/declaration-replaces": "57e55a887f8f8027", "device/declaration-dropped-queued": "a3270ee709f9a88c", @@ -1064,24 +1117,21 @@ "device/view-write-no-server": "b0dfa79cdefb7a2d", "device/folder-system-tier": "fe0985226e4628f0", "device/folder-read": "900a26cb73352eb2", - "device/folder-read-backref": "8cbf7e03c4e9a16c", "device/folder-list": "1ed5678db1ad2247", "device/folder-list-beneath": "02481ef61da7ce0c", "device/folder-unanswerable": "e7dff644f4a94810", "device/folder-unanswerable-named": "9c3e4508b86ed9b4", - "device/folder-list-backref": "d908823586667f6f", "device/folder-list-revoked": "8496d4bec50d8bc9", "device/folder-list-not-folder": "55a11fdc126a9833", "device/folder-not-held": "3d4b3f7be0e474a6", "device/folder-write-at-once": "64c6aeb727ecee62", "device/folder-write-unqueued": "3dfab684165ae1aa", "device/folder-write-offline": "0d1976d74dd2c55d", - "device/folder-write-backref": "60707d097526a869", - "device/folder-write-grammar": "a9fc7736059e641c", + "device/folder-write-backref": "de12c92515dd06ad", + "device/folder-write-grammar": "44d65c48afac8d5b", "device/folder-write-defaults": "423ddd64a7fc285c", "device/folder-change-as-stands": "62146255cec7b8f5", "device/folder-write-held": "b9a65474152359c5", - "device/folder-write-accepted": "0108742459d69587", "device/file-size": "e7958d7ad16e6c63", "device/purge-not-held": "fd17b38042eb2310", "device/purge-not-in-bin": "0eeb0790daeaf6ec", @@ -1097,9 +1147,18 @@ "device/purge-accepted": "1901d824077b0aa6", "device/purge-event-after": "c226568d3aab01a9", "device/purge-unanswered": "411d7eca845ee778", - "device/bin-read": "f2d7a57d12bf494d", + "device/bin-read": "49f696d46543bd83", + "device/bin-read-order": "b18bc0908ace4bfe", "device/bin-unheld": "bba0768fbea3e18c", - "device/bin-offline": "f38960ffa32af7df", + "device/bin-offline": "0325805ba70ac285", + "device/bin-held-delete": "4959772d1a9c09f4", + "device/bin-held-event": "766c9bf00ad0a5da", + "device/bin-local": "eeb48debe02f45e5", + "device/bin-local-order": "0378593d512e6ed3", + "device/bin-local-stamp": "20122c094400c390", + "device/bin-local-restore": "d98c2e1ce2f58e06", + "device/bin-local-server-time": "01de68225a0f7001", + "device/bin-restored-one-row": "674e0100499badb4", "device/pin-trashed": "843dea122062674b", "device/pin-trashed-held": "14395f8c426d49ec", "device/pin-trashed-unread": "fec8546068320eee", @@ -1116,7 +1175,7 @@ "device/body-link-id-first": "581c5f15ca8ad5f0", "device/body-link-id-asked": "decb1c4533283a31", "device/body-link-by-title": "c40271b24c57884c", - "device/body-link-forms": "7046140b117dc422", + "device/body-link-forms": "17453b174eb02c62", "device/body-link-text": "0d9ab74d36675cb4", "device/body-link-self": "352f4621004d9e82", "device/body-link-queued-create": "f74ac4a98475725f", @@ -1158,6 +1217,25 @@ "device/attach-title": "6860714a4ff44ca9", "device/attach-title-given": "097d7504eca4afea", "device/attach-embed-shared": "65fc343c3349d31f", + "device/link-parts": "e6ebdc76d344d3e8", + "device/link-parts-blank": "c0b90cd36f6553ec", + "device/link-parts-positions": "659cadc733c4263a", + "device/link-parts-split": "93504196ec51c7ad", + "device/link-parts-text": "20e8aa8616fcfb49", + "device/link-bounds": "7caff4cbb0ca5873", + "device/link-bounds-pairs": "8cb6531985915483", + "device/link-bounds-code": "4714b511c1d3da97", + "device/link-backslash": "2722f141d11cee57", + "device/link-parts-no-copy": "4e2d45bbc21815a0", + "device/link-candidates": "08e6c7837b725ba9", + "device/link-candidates-whole": "c7b20d8978515aff", + "device/link-candidates-ambiguous": "1e93c0039f013e0d", + "device/link-candidates-bound": "15f817bc08f0d9bc", + "device/link-candidates-id": "e5f4234066400b74", + "device/link-candidates-unresolved": "7abac516fbfef39e", + "device/link-candidates-bin": "43cab729a66b267e", + "device/link-candidates-offline": "59983e7c9a8f8cbe", + "device/link-candidates-host": "82df3178a6d46e33", "device/id-format": "744ae7d27eadabfb", "device/edge-id-format": "c86549f1915c7410", "device/read-back-invalid-id": "f998d8083f21a1cf", @@ -1171,7 +1249,8 @@ "device/catalog-served": "27dd9df7849ddb83", "device/catalog-served-unheld": "6bdeebac194cc4a7", "device/catalog-no-server": "3daf71830e1256d8", - "device/type-grammar": "d2c9cc1be07336c4", + "device/type-grammar": "400a6df9615f5490", + "device/type-unknown": "19e5d37e8d4507ae", "device/docs-site": "ce4fafcf465e8404", "device/docs-paths": "db544b2755dbce92", "device/docs-no-credential": "b74eaa64f4911a3a", @@ -1263,8 +1342,9 @@ "edges/list-updated-before": "2596377484cf5360", "edges/list-order": "a60901f445aa9b08", "edges/list-paginate": "6fd2ca8fb7384b93", - "edges/list-limit": "8f7f7c6e7142f5da", - "edges/list-limit-refused": "c4a7bf52ce967ca8", + "edges/list-limit": "235bab8cf7d54a7d", + "edges/list-limit-capped": "1dddf997067acdb0", + "edges/list-limit-refused": "6e27b4a3866333a6", "edges/list-unknown-key": "57fc9fd992c05390", "edges/list-no-credential": "0bc709442a5c5e77", "edges/list-no-type": "a78de0ce98db60a1", @@ -1379,19 +1459,21 @@ "edges/cycle-hidden": "122864cf58e1ee57", "edges/types-in-folder": "32e71bf1364134cf", "edges/folder-target": "ca6c8f05e2218863", - "edges/folder-path": "bb470eca2115892b", + "edges/folder-path": "91e38459c3496d41", "edges/folder-other-property": "50ee036b01827755", - "edges/folder-path-climb-back": "214de98fd044e272", "edges/folder-update": "4595b67ce05bc6d7", "edges/folder-bulk": "16cc6a8a63f8bc50", - "edges/folder-inline": "f59514c86729ce75", + "edges/folder-inline": "e5607d408c8a191b", "edges/folder-revoked": "f4e3aa82c90082a9", "edges/folder-revoked-keeps": "a966fa8e996ecd70", "edges/folder-place": "b4c7a00e6469018b", "edges/folder-blind": "bff04a27c6aed303", - "edges/collection-target": "9f825815a5cf2672", - "edges/collection-no-nesting": "11e27fb49df9d0d0", - "edges/collection-no-nesting-others": "c26b7617cbec51ad", + "edges/collection-target": "ec788a908c9f4a09", + "edges/collection-no-nesting": "e419a9e20309b763", + "edges/collection-no-nesting-others": "12a2ef4298e8f041", + "edges/retype-endpoint": "bb0bab060ed8a991", + "edges/retype-nesting": "19617dd389a6f10d", + "edges/retype-replaced-edges": "d25c63df233a9b97", "edges/cascade-parent-of": "5f8cce4a86abc3c7", "edges/cascade-custom": "e304ddf3e3bc2b45", "edges/orphan-shipped": "0c045bebf46730a5", @@ -1405,9 +1487,9 @@ "edges/cascade-hidden-children": "90973c4ee9e2c463", "edges/cascade-hidden-unnamed": "35f0e4c08b96c098", "edges/types-register-order": "e5c0f45119d4a46a", - "edges/types-shipped": "79988de7197915a8", - "edges/types-shipped-cardinality": "2b4eb6e7e679e5c5", - "edges/types-shipped-cascade": "707c1e9ba09bee5a", + "edges/types-shipped": "8bc5fbe608f23035", + "edges/types-shipped-cardinality": "43bf9ffaeded35f7", + "edges/types-shipped-cascade": "d69dcb0ef6a151f6", "edges/types-in-collection": "1d6a402d2f275c44", "edges/types-shipped-labels": "69c31b92d2922c9a", "edges/types-reverse-names": "629a27c077ca849c", @@ -1415,6 +1497,8 @@ "edges/types-list": "e7bc7a6396d9168c", "edges/types-list-whole": "2f5bde6a3e50a9ea", "edges/types-shipped-flag": "ecb017770ab62690", + "edges/types-acyclic": "12117aaf59cc8d34", + "edges/types-flat": "f537daaed2cd9412", "edges/types-register": "3bb7e1a4e3db631f", "edges/types-label": "094e91a48bc54780", "edges/types-label-kept": "104e75505336095b", @@ -1422,6 +1506,7 @@ "edges/types-usable": "30c85dbb883c253b", "edges/types-register-permission": "ff73583f9ce0f3c6", "edges/types-register-map": "de39b05e2ad14195", + "edges/types-register-map-scope": "15874399d1f23827", "edges/types-register-shipped": "e8e0abcab935a141", "edges/types-register-duplicate": "0e05d299a078c233", "edges/types-register-duplicate-kept": "608c9b2c00e455c4", @@ -1469,12 +1554,12 @@ "edges/properties-move": "8960aabfe9981104", "edges/properties-bulk-entry": "741b7040346ad01e", "edges/properties-bulk-atomic": "3581e6aa2a961eb4", - "edges/properties-inline": "a14442174ab8788c", + "edges/properties-inline": "830f7b3e58f8b189", "edges/properties-inline-none": "9775b2a56f90a403", "edges/properties-shipped": "a71be4db0b81a166", "edges/properties-null-optional": "e03677a403aa4b46", "edges/properties-null-required": "e6c70eb60a92f3a4", - "edges/properties-format": "6648093efa1117e3", + "edges/properties-format": "a2a775c9aec3049f", "edges/types-register-format": "2072fa598ca7dc33", "edges/types-register-format-fit": "96cf5f9dd9b69fdf", "edges/properties-stored": "167773e736ac87cb", @@ -1841,7 +1926,7 @@ "events/webhook-starts-after-creation": "8da5a611586dc90d", "events/schedule-fails-visibly": "ff0fdafad940bb72", "events/webhook-bulk-backlog": "cbb026822f035f02", - "events/audit-records-write": "cd78c40d5c3f0c9c", + "events/audit-records-write": "9dcc0bbf5293e49b", "events/audit-server-writes": "5007dc026ca2e32c", "events/audit-readable": "b81df74a34cd63ff", "events/audit-insert-fails": "81db9f1e8409ffad", @@ -1930,7 +2015,7 @@ "folders/settings-refused-bytes": "b172dd78edb4391e", "folders/settings-refusal-report": "49c04373f4e70370", "folders/settings-refusal-words": "ce451a14166feee8", - "folders/settings-refusal-no-repeat": "bccb376073dc6fab", + "folders/settings-refusal-no-repeat": "a8ab09228f7c0cb1", "folders/settings-transient-retry": "834fbff1e734b54d", "folders/settings-transient-report": "24ef2eecef15f99b", "folders/settings-conflict-advice": "115960c362335b1a", @@ -1948,6 +2033,7 @@ "folders/invalid-document-flagged": "1da7df6ada513e53", "folders/invalid-document-scan-continues": "e38c66944f9aec24", "folders/invalid-document-retried": "c16e5161b640744f", + "folders/refused-line-retried": "b8194bd41063edc4", "folders/first-sync-waits": "23460f9f8d7216be", "folders/first-sync-no-effects": "229432d4bfd6894f", "folders/first-sync-no-effects-writes-unsent": "8cb646606cfe9622", @@ -2006,7 +2092,7 @@ "folders/query-tier-departure-kept": "d5bd50be7e40ecea", "folders/query-tier-never-trashes": "a99cec0be6c4b907", "folders/search-default-states": "dc247ecc7f055f54", - "folders/search-refuse-unsupported": "ea71e0edf6943842", + "folders/search-refuse-unsupported": "a9a3a6592e88a519", "folders/search-edit-refused": "b345c63b4c1a7302", "folders/query-settings-file-refusal": "31bb1d69568a98cd", "folders/query-cli-flag": "f6e0bfbc0b85f8cf", @@ -2179,7 +2265,7 @@ "folders/file-item-doc-name-emptied": "0bcc3bc1b41f6b6b", "folders/file-item-doc-name-emptied-scan": "ba27c103d7f00b3a", "folders/file-item-doc-name-moved-out": "01a5b34c61b80a16", - "folders/file-item-doc-name-replaced": "9a21a995762241fb", + "folders/file-item-doc-name-replaced": "c38258ce92fdc717", "folders/version-line": "52e3d6e0d162afe3", "folders/version-stale-merge": "ebce032284cf0c02", "folders/version-absent-merge": "5b9d1faec2ac4dbd", @@ -2539,7 +2625,7 @@ "folders/unmatched-file-updated": "a8ba7aa2309cebf5", "folders/replaced-binding-unpinned": "636e63266359c693", "folders/embedded-archive-kept": "a3913cc22995ef23", - "folders/binary-created": "2084a92fa1220915", + "folders/binary-created": "1ba66a4db2170b29", "folders/binary-webpage": "2a53e4720ed619ca", "folders/binary-edited": "84021a258e7007bc", "folders/binary-title-preserved": "eebe4a7ca0612bac", @@ -2700,6 +2786,10 @@ "folders/autostart-launchd-agent": "0d7ae87bb25b4841", "folders/autostart-systemd-unit": "755877a0cad35bbe", "folders/autostart-runs-watch": "547d172cee0a6583", + "folders/autostart-launchd-output": "544bac73f5ccd8fb", + "folders/autostart-launchd-output-private": "06f4fed0cf8be547", + "folders/autostart-systemd-output": "7915446ddc16b578", + "folders/autostart-status-launchd-output": "35e5bf3d83640d31", "folders/autostart-registry": "1e25f62fcdd38e5d", "folders/autostart-no-credential": "aaf5c241646e0d7f", "folders/autostart-key-file": "01755011effbc2b1", @@ -2735,12 +2825,17 @@ "folders/autostart-status-credential": "40d121aa3b9dbb58", "folders/autostart-status-earlier-build": "d3af9574cd842f23", "folders/watch-terminate": "130cd706ad70007a", - "folders/watch-terminate-telemetry": "d4b4af912c3d5a18", + "folders/watch-terminate-telemetry": "71fb31ca7d20174c", "folders/watch-utility-priority": "5cf2c4b0f300b8e0", "folders/watch-log": "146f93a7fb545226", "folders/watch-log-private": "d59d0621a6dc09e9", "folders/watch-log-start-full": "7770f015395918c1", "folders/watch-log-running-full": "1e79157fc63987dd", + "folders/watch-launchd-log-start-full": "e0fdb3fbda3eb4d6", + "folders/watch-log-stopped": "345be2035274e5bc", + "folders/watch-log-stopped-sender": "73e6ea7710b1131c", + "folders/watch-log-stopped-in-pass": "404c4753f864a828", + "folders/watch-log-stopped-moved": "59371d0dda61c03a", "folders/take-in-file": "c2adeaaafcb3f40f", "folders/take-in-file-taken-count": "1257fa2dfd916afd", "folders/take-in-edited-source-kept": "72bf45d2c52e9e3c", @@ -3337,10 +3432,29 @@ "instance/contract-document": "461d7c026e559987", "instance/archive-version": "0d2e49556b22707d", "instance/contract-header": "82b9f1794891bc71", + "instance/gzip-json": "76213d8ee6218c48", + "instance/gzip-needs-authorization": "76de0ae8895b73a7", + "instance/event-stream-uncompressed": "d671dcf22fa36539", "instance/document-groups": "8ba21430d7f3e4c1", "instance/document-tags": "7fff09111d9f99c0", "instance/document-operation-order": "d2561c3eee81f7a8", "instance/document-paths-order": "e679e9c2428a8180", + "instance/overview-credential-only": "a0fccca2b79b0d91", + "instance/overview-fields": "8250c8a1b3fa55a5", + "instance/overview-no-reach": "d458be02d3360e40", + "instance/overview-omit-empty": "dcb3ea071d44881d", + "instance/overview-types": "6c85fdba4ef78580", + "instance/overview-total": "7108292991ddf3d6", + "instance/overview-types-order": "c9fd6ad0eb47d969", + "instance/overview-unreadable": "0079c47ad8ea79e5", + "instance/overview-tags": "dd5dc5d45f17095c", + "instance/overview-edge-types": "0aa97e020efd10a9", + "instance/overview-views-folders": "9ba5a31508472432", + "instance/overview-key": "1c6b055f11c76ab2", + "instance/overview-app": "9ae972a20f514023", + "instance/overview-list-caps": "517c7bf8f3ed43ca", + "instance/overview-truncated": "4d699900c29f7fce", + "instance/overview-text-cap": "ed316945d16ac00c", "instance/replica-under-prefix": "3fb23200932bf5cc", "instance/replica-restores-own": "87c0601ce15e1834", "instance/replica-not-older": "b725fb3c9297b18d", @@ -3368,7 +3482,7 @@ "instance/health-contention-degraded": "348fcfdb827c8f1e", "instance/health-timeout-degraded": "7ac3719dc8091e0f", "instance/health-disk-down": "f817589934ba2901", - "instance/health-disk-degraded": "3b52c30850aca053", + "instance/health-disk-degraded": "9272d3e907708fc3", "instance/health-disk-unknown": "1e772adc2c4afeec", "instance/health-write-reuse": "1719501ffff5c220", "instance/health-write-once": "f9b4986291754a29", @@ -3454,7 +3568,7 @@ "instance/config-patch-merges": "660577eb3c5a6c05", "instance/config-patch-clears": "c2d3a697d0e2faf6", "instance/config-patch-refused": "9250b9eb60c0daa8", - "instance/config-audit": "75d926bfaf9fd790", + "instance/config-audit": "0598a15eabbe7143", "instance/config-refused-no-audit": "a75858588f503016", "instance/config-query-key": "3e852ff0cd143f00", "instance/config-query-key-order": "e734016969c68d40", @@ -3505,11 +3619,11 @@ "items/create-defaults": "e6a85d81f383bc90", "items/create-client-id": "5b8258e6ad7ec5d7", "items/create-id-format": "d17132d2ad639458", - "items/create-repeat": "ab235a5a0dcc4441", + "items/create-repeat": "1da27deae639ce5a", "items/create-repeat-silent": "aae589a2f0ff4d9f", - "items/create-id-other-type": "4291a925de8c62a0", + "items/create-id-other-type": "fecafa624028e0ae", "items/create-id-unreadable": "8db1666f94c8621f", - "items/create-id-and-natural-key": "073fac20ead29809", + "items/create-id-and-natural-key": "95ff78f33175f8b7", "items/create-undeclared-key": "412f9cfe05fdcc16", "items/no-device-field": "6ff890b99a5dd4eb", "items/no-device-filter": "71c558b3f0782d27", @@ -3518,15 +3632,25 @@ "items/source-unclaimed": "410b44e578aa804b", "items/source-allow-list": "ba299cd622ce270a", "items/source-fixed": "7e8ddc31b0b9d195", - "items/natural-key-upsert": "61233775035e192a", + "items/natural-key-upsert": "976271cff00d8d24", "items/natural-key-merge": "603e809ed51d4b60", "items/natural-key-tags": "9b4f42085d9b438f", - "items/natural-key-concurrent": "d626c1d759190666", - "items/natural-key-other-type": "60190c081a6a3c45", - "items/natural-key-new": "0be40c4e3f2b4c38", - "items/natural-key-id-mismatch": "cd8074caa1c74362", + "items/natural-key-concurrent": "f3c07883140b3791", + "items/natural-key-other-type": "7d342eae09df3d20", + "items/natural-key-new": "98a0a6efda9954ab", + "items/natural-key-id-mismatch": "0d7ff168e70a9ab5", "items/natural-key-unreadable": "0ff8b9568a9ecd81", - "items/natural-key-trashed": "d2e6e06c2fd947b5", + "items/natural-key-trashed": "108752951d445b7a", + "items/create-only-source": "4f4662bd4eb81c45", + "items/create-only-id": "63620292b9a28d69", + "items/create-only-no-write": "a27dbd808c0c946f", + "items/create-only-natural-key-first": "1454be7c45379fd9", + "items/create-only-new": "a736f17160d18d28", + "items/create-only-concurrent": "7b4b63a012f1474c", + "items/create-only-source-unreadable": "34ab1306f9418685", + "items/create-only-id-unreadable": "fef88984458020a9", + "items/create-only-replay": "c2be313812e1c2b3", + "items/create-only-mode": "e1b25e8eb83b0c7e", "items/occurred-at-utc": "dfeccfba1b2f2559", "items/occurred-at-stale-compare": "361a08fffb350e2d", "items/occurred-at-zoneless": "c84b7e3106a4c91d", @@ -3592,6 +3716,8 @@ "items/update-type-mismatch": "eefbda2a705ec7e9", "items/update-undeclared-key": "7f12bc6ad15db1b3", "items/update-inline-edges": "d85ae82b8b6ad61b", + "items/update-inline-edges-keep": "98d4994da84ce729", + "items/update-inline-edges-quiet": "c76afdd7de1c8d41", "items/retype": "b09695a792a41a79", "items/retype-validated": "282dc4f39ca464d8", "items/retype-unknown": "3ecee1a479c132f7", @@ -3607,7 +3733,7 @@ "items/write-race-key-narrowed-entry": "4d4e055ea3f61cb5", "items/write-race-key-revoked": "4b0d88d84dbdb7ad", "items/write-race-key-revoked-entry": "cda8803de0e7b5d1", - "items/idempotency-operations": "968cf2b38a619ab8", + "items/idempotency-operations": "8bde4bc7733a8d41", "items/idempotency-replay": "b4a5251badf68216", "items/idempotency-once": "42bae408261e141d", "items/idempotency-reused": "b866942bd4645957", @@ -3641,6 +3767,9 @@ "items/delete": "5b732fd145d66b1f", "items/bin-hidden": "375105af1c528d05", "items/bin-listed": "1688cba594353f74", + "items/trashed-at": "2c04e1bc70de6e9b", + "items/trashed-at-null": "518e7a9ef5b3141f", + "items/trashed-at-fresh": "dfc4c40e87d79bb7", "items/archived-hidden": "a0360cb97d3fdd3a", "items/archived-readable": "52f38d7a8c2472e0", "items/write-in-bin": "504c775d848a56ee", @@ -3655,6 +3784,17 @@ "items/trash-blocked-bulk": "7046ffbd511e7c5d", "items/trash-connection": "3d66eb528d629ca2", "items/trash-connection-hidden": "ad0cf9c9dd7f5f48", + "items/trash-preview": "b93537c3656daf83", + "items/trash-preview-count": "aeb653fc0f8a66fd", + "items/trash-preview-digest": "cca42801fc5a5dfb", + "items/trash-several": "0c5ca5a3d4095ea6", + "items/trash-several-refused": "351e06ade67838b6", + "items/trash-digest-match": "1a0af79aa304db60", + "items/trash-digest-changed": "638d18dcb9542a7c", + "items/trash-digest-malformed": "efe0be0a3b6571ce", + "items/trash-preview-writes-nothing": "6fa31f1b7ae4d3c7", + "items/trash-preview-hidden": "aa4018d7eace23d0", + "items/trash-preview-refusals": "6d5d2261fce0d6d6", "items/cascade-mark": "a61475efa9d7f4c7", "items/cascade-mark-names": "fd2b2968e8b53427", "items/cascade-mark-none": "32c954edfcb9f6d4", @@ -3690,7 +3830,7 @@ "items/bulk-skipped": "40254cb7be760987", "items/bulk-create-only": "ca7974f30532e923", "items/bulk-create-only-id": "936d69fbcf6ec2d2", - "items/bulk-skipped-id": "d2b30d57c9efecb2", + "items/bulk-skipped-id": "ff8f562c68611af8", "items/bulk-trashed": "039da9f04f87f753", "items/bulk-trashed-unwritable": "3d4f64db15691474", "items/bulk-trashed-other-type": "a3278820707fa6e3", @@ -3752,6 +3892,8 @@ "items/tags-add": "eefbde3855c0efc6", "items/tags-replace": "a732a3493b412d2d", "items/tags-remove": "4329e8b0a6307d47", + "items/tags-version-stale": "cde06818e9f160bf", + "items/tags-version-current": "0a2befe369dab54e", "items/tags-read": "5bfa09e0706d7e85", "items/tags-not-list": "ec3b54e1c3c93b66", "items/metadata-undeclared-key": "71608a71c83a094b", @@ -3786,9 +3928,9 @@ "items/folder-revoked": "ccca7a2d52653fe3", "items/folder-setting-checked": "0789b08079df2ad3", "items/folder-setting-unknown-type": "c97984085aafae69", - "items/folder-setting-invalid": "182f0d68efcc6109", + "items/folder-setting-invalid": "9f5286f95821321c", "items/folder-setting-undeclared": "160e58b9a813e063", - "items/folder-placement-inside": "a990d69b9b7eabab", + "items/folder-setting-followable": "53fb131d6095ed7b", "items/link-taken": "e955ce5cce5d43f3", "items/link-every-write": "b30d9836442b970d", "items/link-empty": "3f88bba62a153eaf", @@ -3880,7 +4022,7 @@ "keys-and-oauth/update-narrow-only": "98345011333f6d4a", "keys-and-oauth/update-narrow-only-unchanged": "80051bbc9b9cfe42", "keys-and-oauth/update-narrow-only-applies": "5ce0a93e67eb099b", - "keys-and-oauth/update-narrow-only-levers": "eb06f275bb0c85aa", + "keys-and-oauth/update-narrow-only-levers": "7e68d65536c39063", "keys-and-oauth/key-unknown": "cc5aa0d3acd83314", "keys-and-oauth/key-malformed-id": "45676aecf585759c", "keys-and-oauth/revoke-answer": "8b230250f4155ac0", @@ -3919,7 +4061,7 @@ "keys-and-oauth/expired-unchanged": "6ce97bd5ef2d9cfc", "keys-and-oauth/expiry-inherited": "f9beba72e9f6a6bf", "keys-and-oauth/expiry-mint-ceiling": "47b30215f39392ef", - "keys-and-oauth/expiry-update-ceiling": "02026e99137786f6", + "keys-and-oauth/expiry-update-ceiling": "776e782b0ea47fc9", "keys-and-oauth/expiry-manage-narrows": "4dc55ec3b738bd73", "keys-and-oauth/expiry-app-key-narrows": "b15a4469a20d5f0a", "keys-and-oauth/expiry-owner-free": "a400c5602fef7d1a", @@ -3975,6 +4117,9 @@ "keys-and-oauth/discovery-grants": "b3bde25e8f32bec0", "keys-and-oauth/jwks": "2ea2b105c9a263a8", "keys-and-oauth/protected-resource": "6e1d4d63a3257a7f", + "keys-and-oauth/protected-resource-mcp": "5dbe01646c269a47", + "keys-and-oauth/protected-resource-mcp-scopes": "3129fcddcb601c3e", + "keys-and-oauth/discovery-metadata-documents": "ad670aa1962d537e", "keys-and-oauth/no-backchannel-advertised": "1db9f81a53f3209e", "keys-and-oauth/register-answer": "154a78c26332f534", "keys-and-oauth/register-auth-methods": "74493b867e107ebe", @@ -3997,15 +4142,17 @@ "keys-and-oauth/register-granted-kept": "45ba2033c9fb57d6", "keys-and-oauth/register-offered-kept": "398f18856c823f5c", "keys-and-oauth/no-backchannel-registered": "6a8a759e874f9714", + "keys-and-oauth/metadata-document-private-host": "40e3da700de21a07", "keys-and-oauth/consent-screen": "d27892df284d763b", "keys-and-oauth/consent-accepted": "1f6c7122f504a25c", "keys-and-oauth/consent-declined": "f68b1619e4191282", - "keys-and-oauth/consent-nothing-denies": "ac8a0ee0736f491a", - "keys-and-oauth/consent-ticked": "3acc2dab9abfa695", + "keys-and-oauth/consent-nothing-denies": "63b7adeb4545fd66", + "keys-and-oauth/consent-ticked": "0e2449dd844cedc0", "keys-and-oauth/consent-standing": "44b74fa33945a1f6", "keys-and-oauth/consent-wider": "bd1e6b4845387025", "keys-and-oauth/consent-prompt": "62d7669cda1e33dd", "keys-and-oauth/authorize-narrowed": "99d695052c65f3fc", + "keys-and-oauth/authorize-type-registered": "3f5f2267f6dbeb14", "keys-and-oauth/authorize-narrowed-types": "b7e13fa6ac437614", "keys-and-oauth/authorize-narrowed-refresh": "1d38d9b1c59a5156", "keys-and-oauth/authorize-nothing-grantable": "84f26fa217ed274b", @@ -4020,13 +4167,17 @@ "keys-and-oauth/consent-recent-held": "26fba6553ebae9e6", "keys-and-oauth/consent-page-confirm": "573d1168c0cf0267", "keys-and-oauth/consent-page-reads": "30c161aa43200808", + "keys-and-oauth/approval-full-access": "4a064abdc6ba4623", + "keys-and-oauth/approval-full-grants-request": "7522d4865bf8fb2c", + "keys-and-oauth/approval-full-never-wider": "1afddc3e46a3519e", + "keys-and-oauth/approval-access-unstated": "4be6e052660f2d43", "keys-and-oauth/approval-one-control": "acb6d5cb6c99480d", "keys-and-oauth/approval-edit-words": "d108465dfda36ac0", "keys-and-oauth/approval-levels-asked": "1e13de9608489631", "keys-and-oauth/approval-only-asked": "29be86f83de99797", "keys-and-oauth/approval-open-ended": "6abfcaff6aac61d3", - "keys-and-oauth/approval-powers-off": "31e2c858f3b34f42", - "keys-and-oauth/approval-powers-marked": "b1a72d2a5eded535", + "keys-and-oauth/approval-powers-off": "654cd136a2fe6e97", + "keys-and-oauth/approval-powers-marked": "5c36b9ea66bd7a1f", "keys-and-oauth/approval-powers-words": "136193c47e4eaf08", "keys-and-oauth/manage-powers-marked": "100241feb952e0b4", "keys-and-oauth/approval-held-shown": "28cee7e209888407", @@ -4067,7 +4218,16 @@ "keys-and-oauth/refresh-replay-chain": "6a8c88f3bbb4669d", "keys-and-oauth/refresh-unknown": "b6bc78573c01dc0b", "keys-and-oauth/resource-this-server": "bc95c4f634c81211", - "keys-and-oauth/resource-other-server": "c0cc1e9c193259a3", + "keys-and-oauth/resource-case": "7713d7b5995dc625", + "keys-and-oauth/resource-mcp": "f2ac66ddff218325", + "keys-and-oauth/resource-other-server": "58521f30b394ba5a", + "keys-and-oauth/resource-one-audience": "4e14842c4bcf8bf3", + "keys-and-oauth/resource-one-value": "dc743e72f43c0d6e", + "keys-and-oauth/resource-authorize-refused": "bd65a165df6469f5", + "keys-and-oauth/resource-authorize-unregistered": "1176b49810c057d9", + "keys-and-oauth/mcp-challenge-credential": "88c0e531b6ea1408", + "keys-and-oauth/mcp-challenge-scope": "9314a51033004bac", + "keys-and-oauth/mcp-challenge-unregistered": "f8705ee51842ccbc", "keys-and-oauth/revoke-answer-ok": "ddede142f4e9d749", "keys-and-oauth/revoke-access-token": "b546d6fe89c87774", "keys-and-oauth/revoke-refresh-token": "aaefe2fba61c3416", @@ -4103,7 +4263,7 @@ "keys-and-oauth/page-origin-allowed": "800c5168c1a71d13", "keys-and-oauth/page-origin-refused": "b4deab476de550b7", "keys-and-oauth/origin-no-credentials": "2d4d254327309a17", - "keys-and-oauth/discovery-every-origin": "4b7561a3bbce5b93", + "keys-and-oauth/discovery-every-origin": "afacb37452f379f4", "keys-and-oauth/register-every-origin": "8590643edcd237ca", "keys-and-oauth/register-preflight": "391bdef785aabf00", "keys-and-oauth/page-origin-serialized": "0095acfa2d0d044c", @@ -4118,7 +4278,7 @@ "keys-and-oauth/device-pending": "746c52329194ad61", "keys-and-oauth/device-slow-down": "bbea12d7ec67534e", "keys-and-oauth/device-denied": "4c4d5dc5f52decf6", - "keys-and-oauth/device-nothing-denies": "831e978dbb44226d", + "keys-and-oauth/device-nothing-denies": "0747fab3a4d2e7ef", "keys-and-oauth/device-expired": "3dc71e8f6878d2a8", "keys-and-oauth/device-approved": "0e09538cc6f7542f", "keys-and-oauth/device-grant-revoked": "91df5a66c247a7fd", @@ -4184,7 +4344,7 @@ "keys-and-oauth/reach-line-one-type": "fff13ba7d449029f", "keys-and-oauth/sign-ins-list": "c1655c22074e5299", "keys-and-oauth/sign-ins-fields": "d8bbf6fd2852da33", - "keys-and-oauth/sign-ins-app-expiry": "3f825400f22c769a", + "keys-and-oauth/sign-ins-app-expiry": "1b3688f3ac9960d3", "keys-and-oauth/sign-ins-order": "9cf3f34f2c1c64d0", "keys-and-oauth/sign-ins-browser-name": "d90724f13c80e806", "keys-and-oauth/sign-ins-app-name": "62a3903f95207cb7", @@ -4268,17 +4428,71 @@ "keys-and-oauth/audit-rollback-withdrawal-event": "b3a514b6a41d2421", "keys-and-oauth/audit-unknown-outcome": "761aaae595777891" }, + "mcp": { + "mcp/foreign-origin": "765a452339474b7d", + "mcp/revision-2026-07-28": "4361d96592c8553d", + "mcp/revision-2025-11-25": "8bd3521489db3ba2", + "mcp/no-listen": "115b4239eabf1065", + "mcp/tools-for-writer": "c460684ac1dd70a0", + "mcp/tools-for-reader": "486df11ce74045ff", + "mcp/tools-for-no-type": "6da405caf565faaf", + "mcp/tools-for-schema": "7e32c581c8638ec3", + "mcp/tools-for-admin": "8023c2075403d9e1", + "mcp/tools-withheld": "f7ad1bc6b390fb3f", + "mcp/open-world-hint": "d0966a737cf45476", + "mcp/unknown-argument": "05cead76349dd415", + "mcp/write-audited": "cd48fb42f8a257b1", + "mcp/get-budget": "859cd54921cd00c3", + "mcp/get-cut": "6f39eb4ee26df6b0", + "mcp/get-field-part": "ff26853bcc8b10b5", + "mcp/get-not-returned": "fc5d97c8ee131ae0", + "mcp/update-stale-answer": "1b4b8bb9ab7a5b81", + "mcp/update-stale-fields": "c4804bb3183efe05", + "mcp/update-stale-unchanged": "d7504da482de6795", + "mcp/update-changed-mid-call": "81c25948f912b133", + "mcp/delete-preview": "8b838683a6a2209b", + "mcp/delete-confirmed": "e07c1417d08e5d41", + "mcp/delete-total": "d6cbd3c39b3139a6", + "mcp/delete-plan-changed": "d6705e47b9f38701", + "mcp/delete-plan-changed-after-check": "af5142941a9c0745", + "mcp/delete-other-credential": "8560620d49cc5a09", + "mcp/delete-expired": "3dcc49bb2d9fcee4", + "mcp/move-preview": "096262495ba144b5", + "mcp/move-plan-changed": "7f8a3836ff999f2e", + "mcp/revoke-preview": "f4a771919830005b", + "mcp/revoke-confirmed": "9bc5f1b8383695aa", + "mcp/revoke-other-credential": "35ef5ecac5977a92", + "mcp/webhook-preview": "41e64504c3699429", + "mcp/webhook-confirmed": "a6f2a958f7ba0ce5", + "mcp/webhook-changed": "5680b5a0152aa472", + "mcp/webhook-made-once": "3a4b1799084320b8", + "mcp/webhook-other-credential": "8356d5f5c7a19cd7", + "mcp/webhook-one-step": "f0c8da9ca5bc15a3", + "mcp/file-saved": "aa57cba89e2cbe78", + "mcp/file-not-public": "8f4bc1f6f8f93606", + "mcp/file-redirect": "f3a5ec6010f822d5", + "mcp/file-too-large": "ce621318feb1a9bb", + "mcp/file-status": "399231d90955ef02", + "mcp/file-attach-repeat": "9262feb8eceebc55", + "mcp/file-repeat-other-bytes": "9d1b00df76c8bdbc", + "mcp/file-audited": "c3261280dc7dd17a", + "mcp/file-audit-query": "3dea04faff4e4254", + "mcp/file-busy": "196a834f6ca2b248", + "mcp/file-rate-limited": "e4859bebdaafc48f", + "mcp/file-disabled": "4705e29e4d652a9a" + }, "occurrences": { "occurrences/window-expands": "771ba159737c97cd", "occurrences/order": "e409ede97d18f0ce", "occurrences/answer-keys": "a164fa49d95aa4b8", "occurrences/window-required": "9744b50189a0f6b7", "occurrences/window-ordered": "e9093f4443ac1213", - "occurrences/window-instants": "26a6fd8a21420bd0", - "occurrences/window-unreadable": "6d9738eee8bb998b", + "occurrences/window-instants": "922e7be1517ee14b", + "occurrences/window-unreadable": "0cd5cd92ff67053c", "occurrences/no-credential": "87490f9198b7c214", "occurrences/type-map": "8fee076e2e07ed18", "occurrences/type-names": "1c0acf7215435eae", + "occurrences/no-tasks": "11702a909568a5d1", "occurrences/type-names-move": "fd11fafca2a9c079", "occurrences/type-names-slot": "334c51a31f4aa6f4", "occurrences/source-filter": "5d281bd69f8e4c09", @@ -4333,17 +4547,19 @@ "occurrences/unfold-budget-unnamed": "179e3681c9d73e61", "occurrences/unfold-incomplete": "e96456ab4690cf4b", "occurrences/unfold-rest": "0310f9381ef4484d", - "occurrences/schedule-unreadable": "e545659765d1c724", - "occurrences/schedule-exrule": "7909d45072d7579e", - "occurrences/schedule-period": "ea8334cd5fc90872", - "occurrences/schedule-part": "b2ef8ddaa0ac69a6", - "occurrences/schedule-no-date": "780e55f4598b5ecc", - "occurrences/schedule-no-unfold": "ee3e0fd2fe3c6b72", + "occurrences/schedule-unreadable": "8dffcc74752bbad5", + "occurrences/schedule-exrule": "c6880b82f0cfb111", + "occurrences/schedule-period": "24dfa93733e84f1c", + "occurrences/schedule-part": "c060704565e461ee", + "occurrences/schedule-no-date": "b2a93fd333396fef", + "occurrences/schedule-no-unfold": "97e48859cf3997d1", "occurrences/schedule-no-start": "869c12f53754bba9", - "occurrences/schedule-second-rule": "4811e1eb9e959bbe", - "occurrences/schedule-dates": "ac3f74f1bbd53a68", - "occurrences/schedule-text": "c88ce04e351ebd99", + "occurrences/schedule-task-start": "3860829c674118ec", + "occurrences/schedule-second-rule": "2a64814cf1c6979b", + "occurrences/schedule-dates": "f44e4ccca68273f3", + "occurrences/schedule-text": "41a99eaad20b49e5", "occurrences/schedule-text-taken": "7efcdfb963ca0024", + "occurrences/schedule-task-taken": "655072b7d464e36a", "occurrences/schedule-zone": "16afc870c894c4ac", "occurrences/schedule-end-zone": "52bd777e5634e2d3", "occurrences/schedule-duration-low": "2be8677e014d271f", @@ -4351,10 +4567,11 @@ "occurrences/schedule-allday-hours": "29ecf2763d39c62b", "occurrences/schedule-allday-days": "2610917ffdcad561", "occurrences/schedule-taken": "b83f9f0c84370f94", - "occurrences/schedule-update": "adc8610a900c07e6", - "occurrences/schedule-bulk": "16b43b58a990b3c4", - "occurrences/schedule-bulk-action": "cc54fdb29b4adfa9", - "occurrences/schedule-restore": "18c444b2086158f4" + "occurrences/schedule-update": "d0dceeac071f33de", + "occurrences/schedule-conflicted-copy": "6f70dadbe4809422", + "occurrences/schedule-bulk": "2da8c0436014261e", + "occurrences/schedule-bulk-action": "0d02f3f5e471c0eb", + "occurrences/schedule-restore": "e6a92bba4f06b800" }, "queue-and-verdicts": { "queue-and-verdicts/json-depth-limit": "6fb352fc7a010ce0", @@ -4463,9 +4680,9 @@ "queue-and-verdicts/refusal-same-in-queue": "d1e5d602c6bfd510", "queue-and-verdicts/refusal-part-absent": "976b460e4d687a55", "queue-and-verdicts/refusal-drain-made": "5404dd52ccd3ebed", - "queue-and-verdicts/blocked-reasons-five": "188037b16304aabd", + "queue-and-verdicts/blocked-reasons-five": "2b074cb4cdfc40b2", "queue-and-verdicts/blocked-not-counted": "591c29d26eba28d3", - "queue-and-verdicts/blocked-passed-over": "d3a20835cb75cae4", + "queue-and-verdicts/blocked-passed-over": "5dc13b097907e589", "queue-and-verdicts/blocked-retried-itself": "2f0aed453d9387a8", "queue-and-verdicts/blocked-keeps-row": "1a59c41036791b99", "queue-and-verdicts/ceiling-five": "10383ce29ecce6cd", @@ -4534,9 +4751,9 @@ "queue-and-verdicts/release-takes-dependants": "24f08404533bb90e", "queue-and-verdicts/release-sent-refused": "64420d0e370784f8", "queue-and-verdicts/release-lays-over": "99fc2b18ad9c50e4", - "queue-and-verdicts/withdraw-two-reasons": "b0ed47449c5a31b7", + "queue-and-verdicts/withdraw-two-reasons": "7aa6b0a7e8015394", "queue-and-verdicts/withdraw-unknown": "101d0fbda6c97bc2", - "queue-and-verdicts/withdraw-takes-out": "2622414fa89452cd", + "queue-and-verdicts/withdraw-takes-out": "a3551f45ef2e6010", "queue-and-verdicts/withdraw-puts-back": "9ea1f7ae83411e05", "queue-and-verdicts/withdraw-read-first": "f3db5cabfb9cf530", "queue-and-verdicts/withdraw-refuses-held": "fabf78c907ff2c9e", @@ -4560,12 +4777,12 @@ "queue-and-verdicts/discard-unlanded-not-read": "abdb41ac001386b0", "queue-and-verdicts/discard-unlanded-pin-read": "cf57bb9a09116809", "queue-and-verdicts/rehydration-keeps-queue": "b8807e3d6e5bca6b", - "queue-and-verdicts/other-instance-sends-nothing": "eeeca9e338c118a1", + "queue-and-verdicts/other-instance-sends-nothing": "7a3995f040dae126", "queue-and-verdicts/unconfirmed-sends-nothing": "b413488406c774d5", "queue-and-verdicts/instance-named": "1b55bcfab648b173", "queue-and-verdicts/instance-changed-expires": "423ed7e0303e1e2a", - "queue-and-verdicts/instance-changed-sends-nothing": "8c9001bb601d8867", - "queue-and-verdicts/instance-changed-keeps-write": "1c66dedefcfe2f91", + "queue-and-verdicts/instance-changed-sends-nothing": "80e1e4389733ff1d", + "queue-and-verdicts/instance-changed-keeps-write": "a97b81a0c7852af9", "queue-and-verdicts/instance-changed-afresh": "7a24eb37797f87b7", "queue-and-verdicts/waiting-over-answer": "3e10b99fc6de738a", "queue-and-verdicts/waiting-over-event": "c01c9cd4b26a4a96", @@ -4644,7 +4861,9 @@ "queue-and-verdicts/check-refuses-invalid": "3b94b0ab49cfe707", "queue-and-verdicts/check-as-server": "ecb69944d8d6641a", "queue-and-verdicts/check-array-items": "2d409e09cb135ff3", - "queue-and-verdicts/check-inherited-nearest": "bad301ccbff78c23", + "queue-and-verdicts/check-status-word": "b57b560078b385a2", + "queue-and-verdicts/check-status-held": "4f4ca4f76c4058a6", + "queue-and-verdicts/check-inherited-nearest": "93ef3f638635c1c9", "queue-and-verdicts/check-accepts": "9d4f504611c5deda", "queue-and-verdicts/check-current-result": "8fdaa3d9a0a215b8", "queue-and-verdicts/check-stale-supplied": "847ace8c52ea0a59", @@ -4663,18 +4882,25 @@ "queue-and-verdicts/edge-edit-no-change": "de545ce28670461b", "queue-and-verdicts/edge-move-shown": "ca002db265f12567", "queue-and-verdicts/edge-move-refused-back": "b9b4191d30b0cf49", - "queue-and-verdicts/edge-source-move-expires": "313bf64218e3de03", - "queue-and-verdicts/edge-source-move-carries-on": "b00f6ea2d32b31d5", - "queue-and-verdicts/edge-source-move-reads-owed": "c96aaa7efce0f577", - "queue-and-verdicts/edge-source-move-one-expiry": "852acd8a1d74f53c", - "queue-and-verdicts/edge-source-move-report-kept": "adff2ce6dc4f6751", - "queue-and-verdicts/edge-source-move-error-wins": "a41f9c94ac59a6e2", - "queue-and-verdicts/edge-source-move-refused-keeps-copy": "05482bfa0ebd1391", - "queue-and-verdicts/reach-change-ends-drain": "5f2da87431dc96d0", + "queue-and-verdicts/edge-source-move-expires": "dc26df47d8e0b4cf", + "queue-and-verdicts/edge-source-move-carries-on": "3306a20dcde714aa", + "queue-and-verdicts/edge-source-move-reads-owed": "b3502276103ea5de", + "queue-and-verdicts/edge-source-move-one-expiry": "aec6cd033b749967", + "queue-and-verdicts/edge-source-move-report-kept": "2b46f4c096e60e71", + "queue-and-verdicts/edge-source-move-error-wins": "c5d48d3d1fac991f", + "queue-and-verdicts/edge-source-move-refused-keeps-copy": "4a7d5ac886a5bba9", + "queue-and-verdicts/reach-change-ends-drain": "9f26a7f4255c717f", "queue-and-verdicts/edge-move-waits-new-end": "4f5cac39fbf813f4", "queue-and-verdicts/edge-move-names-landed": "ff09fe254d58edff", - "queue-and-verdicts/edge-move-always-refused": "9593b0e85374e768", - "queue-and-verdicts/edge-self-loop": "3e53cc9e30a7ef5b" + "queue-and-verdicts/edge-move-always-refused": "17d73ec47b02d8b1", + "queue-and-verdicts/edge-self-loop": "3e53cc9e30a7ef5b", + "queue-and-verdicts/edge-endpoint-refused": "f1fb13b8d04ddb85", + "queue-and-verdicts/edge-nesting-refused": "879959fb68c2749a", + "queue-and-verdicts/edge-end-in-bin-refused": "6f53393b80be12e2", + "queue-and-verdicts/edge-cycle-refused": "eaf4521baf36e072", + "queue-and-verdicts/edge-judged-held": "2935ef5682be03b3", + "queue-and-verdicts/retype-edge-refused": "a4fc35e478cb3009", + "queue-and-verdicts/edge-check": "3170912200e82684" }, "reach": { "reach/copy-bootstrap": "b11094f8a5162ac7", @@ -4683,13 +4909,13 @@ "reach/copy-resume-unpaired": "92c441fad774996e", "reach/copy-cursor-refused": "53d6b9dea54add7b", "reach/copy-reach-refused": "639427239343d101", - "reach/copy-reach-ordinary": "eebcae075ad2b7f3", + "reach/copy-reach-ordinary": "a3ab2150d14385a4", "reach/copy-no-type": "f67fcbca8ff1bfea", "reach/copy-refusal-order": "96f7e676ed749c5d", "reach/stream-no-header": "b2dc8ee2bb00d631", "reach/marker-first": "d56ab9e7e0f2f316", "reach/marker-no-id": "5fa7e2661078bcd4", - "reach/marker-body": "e17d4d069b7e36c4", + "reach/marker-body": "2d9ed8ba4b37b48f", "reach/marker-event-type": "9d6f018002c56bb1", "reach/marker-cursor": "d1a87485613e6b97", "reach/marker-reach": "d0c993ea3f645f18", @@ -4779,8 +5005,10 @@ "search-and-filters/empty-narrowing-bulk-before-action": "5ab0afd2824ec1cd", "search-and-filters/type-subtree": "3c26a97f8b68a861", "search-and-filters/type-wildcard": "c1045b79e0de9056", + "search-and-filters/type-list": "fa1b5fac4c7d6b9d", + "search-and-filters/type-list-max": "2cef053a106f9ba1", "search-and-filters/type-grammar-refused": "e42867b62cc06940", - "search-and-filters/type-unknown": "9161e398c9c3c673", + "search-and-filters/type-unknown": "08ea6ba6d0516460", "search-and-filters/type-bulk-unregistered": "080e58da5783629d", "search-and-filters/type-unreadable": "e359fcbb28f35659", "search-and-filters/type-unreadable-grant": "cf70d6db2a30d1cd", @@ -4792,12 +5020,14 @@ "search-and-filters/type-map-none": "deec425ca20cd957", "search-and-filters/type-map-narrows": "f3b119f854f6b734", "search-and-filters/state-named": "f049f96ad15eb6ab", + "search-and-filters/state-list": "0f64ca3199e52aad", + "search-and-filters/state-any-alone": "59391ebf85dcf1ce", + "search-and-filters/state-empty": "4dbe8408f4161a19", "search-and-filters/state-search-default": "9b14d871ba8c11de", "search-and-filters/state-search-any": "87e404a93bbdf664", "search-and-filters/search-bin": "c12a0d3865a54904", - "search-and-filters/state-invalid": "19169c74a99c74ca", - "search-and-filters/source-narrows": "fb60252bd5c9089c", - "search-and-filters/source-not-in-search": "94092ff91320acd9", + "search-and-filters/state-invalid": "61be6ccbd3f027da", + "search-and-filters/source-narrows": "2aa0a60fc2ccfe54", "search-and-filters/tags-every": "b449519e35778d8b", "search-and-filters/tier-narrows": "56473c3e09854c98", "search-and-filters/tier-both": "3e9dc5aba5602940", @@ -4806,21 +5036,23 @@ "search-and-filters/own-time-exclusive": "89cc3519f67a2985", "search-and-filters/own-time-sort": "44c9f8c5638fae36", "search-and-filters/own-time-created-sort": "fc09a6f8ab1018d9", - "search-and-filters/bound-not-instant": "c531ad753a7d71b1", - "search-and-filters/updated-after": "8e5ed5badc6c0d86", + "search-and-filters/bound-not-instant": "123b9d0235462400", + "search-and-filters/updated-after": "fb18cf8f6299d48c", "search-and-filters/updated-after-order": "db74cfe3b057ae2e", - "search-and-filters/updated-before": "455f1006106f127e", + "search-and-filters/updated-before": "92cc84bad362056e", + "search-and-filters/search-updated-instant": "1e44be3462aa3b78", + "search-and-filters/search-updated-after-order": "544f81c74bf2c3a7", "search-and-filters/include-edges": "e28211b8791a739d", "search-and-filters/system-named-type": "a329db251ac4cd9e", "search-and-filters/filter-question-mark": "ad349cb413e9c392", "search-and-filters/tag-count-question-mark": "3e49065eb093085c", - "search-and-filters/filter-max-length": "2873032aca7ee3a8", - "search-and-filters/filter-over-length": "4dec5e702202c284", + "search-and-filters/filter-max-length": "a0030b24ecfa34b9", + "search-and-filters/filter-over-length": "df2223f3959a3144", "search-and-filters/filter-max-conditions": "46e9e4e0c4f41517", "search-and-filters/filter-over-conditions": "340f0b9d5998d8f7", "search-and-filters/filter-mixed-logic": "6f4c62931d1dd4d5", "search-and-filters/filter-or-shorthand": "04bd91fc2638ff90", - "search-and-filters/shorthand-backslash": "76dd212fd55bf14f", + "search-and-filters/shorthand-backslash": "374f5395da8864fe", "search-and-filters/shorthand-quote": "cd047bf8281246bb", "search-and-filters/filter-number-range": "875360ae1841ee71", "search-and-filters/filter-numeric": "4cf4f10e2beeffd7", @@ -4830,18 +5062,32 @@ "search-and-filters/filter-null-hint": "51b96836bd92b458", "search-and-filters/filter-not-exists": "7d829eebfcb8f7e6", "search-and-filters/filter-null-quoted": "5579b2c876192378", + "search-and-filters/filter-string-escapes": "2f3e64fbab4b018e", + "search-and-filters/filter-string-backslash": "81f0231b30ccf14b", "search-and-filters/filter-time-instant": "63beeb19611fa97b", "search-and-filters/filter-time-refused": "891e4faf35d6b371", "search-and-filters/filter-time-text": "ccc139fd75e01f73", + "search-and-filters/filter-entry-member": "532de9be43d83a33", + "search-and-filters/filter-entry-member-none": "88d556766042e538", + "search-and-filters/filter-entry-member-refused": "a5e5efa507dfd69f", + "search-and-filters/status-group": "ab667848941d17d6", + "search-and-filters/status-group-neq": "9592e4705335d97b", + "search-and-filters/status-group-none": "8f5f35a667856ed7", + "search-and-filters/status-group-exists": "4ce0e5699d7baf22", + "search-and-filters/status-group-refused": "5860550c806b3db8", + "search-and-filters/bin-order": "1606f62f97ce3e7c", + "search-and-filters/bin-order-sort": "f75b9d315465f010", "search-and-filters/sort-property": "2f830b204e7ed17f", "search-and-filters/sort-property-ascending": "272cf62e22c282cc", "search-and-filters/sort-property-descending": "316af88ce88e78c2", "search-and-filters/sort-property-missing": "86f9d5329fd35a56", "search-and-filters/sort-property-ties": "deaca01a1fcc932d", "search-and-filters/sort-property-cursor": "cf065e6aa7166822", - "search-and-filters/limit-listing": "5153a350d6be810c", - "search-and-filters/limit-listing-refused": "d6d9421bc73d310e", + "search-and-filters/limit-listing": "59e74db398053251", + "search-and-filters/limit-listing-capped": "68c37a5311b32753", + "search-and-filters/limit-listing-refused": "4d1f86e02e25d6f3", "search-and-filters/limit-search": "814688ff31cde095", + "search-and-filters/limit-after-filter": "0f0fda6378c53fef", "search-and-filters/limit-search-refused": "0f32b4a21659f1ea", "search-and-filters/cursor-continues": "91a6a810abf6912f", "search-and-filters/cursor-last-page": "b3ac10af94a5da8e", @@ -4854,7 +5100,16 @@ "search-and-filters/search-score": "c25cbe92dc7b0dbe", "search-and-filters/search-cursor": "7d1a539e5090886e", "search-and-filters/search-cursor-refused": "a98352168e3de9b9", + "search-and-filters/search-cursor-parameters": "e5f4ea967baa9c53", + "search-and-filters/search-cursor-default-order": "83dbf5a7007ee7d9", + "search-and-filters/search-cursor-sorted": "773ce06605a21203", "search-and-filters/search-depth": "63ec657500e11810", + "search-and-filters/search-sort-relevance": "f90b2964552f1cdf", + "search-and-filters/search-sort-relevance-ascending": "7d1c64a953adad5a", + "search-and-filters/search-sort-column": "af0ebd78fe2d63b6", + "search-and-filters/search-sort-property": "b94d1936b454288b", + "search-and-filters/search-sort-ties": "34377dec75171e55", + "search-and-filters/search-sort-invalid": "2a729b38c3f1ef0e", "search-and-filters/search-stem": "0b67f8f0df1d8676", "search-and-filters/search-fold-case": "4b410c04ea8562a0", "search-and-filters/search-fold-diacritics": "13fd6a8e6a648fef", @@ -4876,8 +5131,7 @@ "search-and-filters/index-not-string": "33d7f1a786109e49", "search-and-filters/index-unregistered": "2dd0a003c851c5ca", "search-and-filters/index-type-change": "af5efe6ffcde8a13", - "search-and-filters/rank-bm25": "49fde53bd285c6ac", - "search-and-filters/rank-ties": "9ddfbbcaa5da9fe0", + "search-and-filters/rank-bm25": "9ce766bdc94fd43e", "search-and-filters/excerpt-words": "512294c4ae7ab978", "search-and-filters/excerpt-column": "ea2abcf0136ab607", "search-and-filters/excerpt-mark": "46244ec28e801c34", @@ -4954,7 +5208,7 @@ "search-and-filters/archive-types": "339f0b45f661cff5", "search-and-filters/archive-type-icons": "d71ad9b744cd1e6a", "search-and-filters/restore-type-icons": "fff29826b9a5f04c", - "search-and-filters/restore-type-icons-skipped": "4af5ee51c6e1bfff", + "search-and-filters/restore-type-icons-skipped": "70d95263f20baf47", "search-and-filters/restore-type-icons-refused": "dd7e42ec0439ed7c", "search-and-filters/archive-history": "f999cd7825a69cd2", "search-and-filters/archive-history-reach": "bea3add2daa638c4", @@ -5033,7 +5287,7 @@ "search-and-filters/restore-malformed-held": "911a7b935f2f493c", "search-and-filters/restore-history-exact": "e7bcf507ed3adabc", "search-and-filters/restore-history-shape-free": "6a5142d31ce78893", - "search-and-filters/restore-rows-shape-free": "58654e4fb03a07de", + "search-and-filters/restore-rows-shape-free": "fcdf2292eb75a3f3", "search-and-filters/restore-rows-properties-kept": "a63196eb6314e0ce", "search-and-filters/restore-schema-version-kept": "04125f268bec7dd5", "search-and-filters/restore-rows-orphan": "9dd10fe56c347e2d", @@ -5072,7 +5326,7 @@ "search-and-filters/restore-writers-wait": "0fd4450fda93d7b2", "search-and-filters/restore-bytes-kept": "23dbf1d87cc3a145", "search-and-filters/owner-summaries-authority": "cfac7e2451551eb2", - "search-and-filters/owner-summaries-projection": "3c2eaa8f88770098", + "search-and-filters/owner-summaries-projection": "33d4a16a1269c14c", "search-and-filters/owner-summaries-selection": "d1d446d635579dbc", "search-and-filters/owner-summaries-search": "93d5aea7e206b7bc", "search-and-filters/owner-summaries-order": "572c43f5c1f1436d", @@ -5212,54 +5466,93 @@ "types/register-effective": "e42992524027bd85", "types/register-permission": "70855edd05b3e109", "types/register-reach": "00b2a3819a69f8a1", + "types/register-reach-scope": "5b49082718f963bc", "types/register-duplicate": "f465077baae8f4d1", "types/register-no-fields": "ca8ac6443da281d5", "types/register-no-id": "170644197721afe4", - "types/field-shadows-item": "e6c33103f3feb450", + "types/field-shadows-item": "2974c615603c2c11", "types/field-builtin-names": "343bca5c500abf9e", "types/refused-unchanged": "b42748592f33dd91", "types/write-race": "ee1ca0e917d6534a", "types/label": "a4b8bed623392661", "types/label-kept": "c22ecb8e0ecef6f4", + "types/plural": "1e77b950400a16da", + "types/plural-kept": "d29458b5e0e42b5a", + "types/field-label": "d235b387b9b3e8db", + "types/list-declared-label": "bc2289fe88b17fee", + "types/list-declared-plural": "30bd3438b4e82242", + "types/label-refused": "9c05d2f3385dfd7f", "types/read": "64cce0c74507385c", "types/list-declared-policy": "896dd22f3a40bc9d", "types/read-display-hint": "30b182cf9029ba98", "types/list-declared-hints": "4b6a9dfbb8ebaf2c", + "types/list-own-roles": "88e7bf841253d220", "types/body-markdown": "141eeedca599b296", - "types/icon-name": "95ed6580ac10bfd0", + "types/icon-name": "9da712ffcc31eca8", "types/read-missing": "8af3608faad8ba83", "types/registry-open": "6035a80609528c95", "types/parent": "428f972befd06d6f", "types/parent-required": "52004093723dd6dd", "types/parent-unknown": "d29b7c1f6464dd85", "types/parent-chain": "21f92869658a6e47", - "types/child-adds": "43b40f0732d1b9a3", - "types/child-shape": "672029f0cd861917", + "types/child-adds": "31e86d56b1c05cca", + "types/child-shape": "92d9fc3cd73df055", + "types/child-range": "d462410a8f51b651", + "types/child-range-refused": "5ca78466433d5ac9", "types/listing-subtree": "09d1c1404b519887", - "types/compatible-with": "c4d6065929e29fa1", + "types/compatible-with": "686f6c18b7df0225", + "types/compatible-with-range": "7b225ce50b042007", + "types/compatible-with-currency": "d60e058fbb2d2a54", + "types/compatible-with-items-fields": "639ca9470e51686d", "types/compatible-with-refused": "5e13ad050091e8ff", "types/compatible-with-preserved": "aad48a3c912965b4", "types/custom-lifecycle": "7d7376591002eb2b", "types/merge-policy": "25831575b00f0059", "types/merge-policy-refused": "e4417faff5ba9518", - "types/field-grammar": "287a27bc6df953dc", - "types/unknown-key": "723b7673cd6320ba", + "types/field-grammar": "44363bba20ec8365", + "types/unknown-key": "01152e86aef24ec6", + "types/range": "e51595bb3b08fecd", + "types/range-refused": "bb690d44d051724d", + "types/items-fields": "798b0a5e8b0a9daa", + "types/items-fields-refused": "12478588c53d6544", "types/format-collapses": "75a0aa46430e14fe", "types/format-own-type": "f38e4a81403c218e", "types/format-refused": "266165a6b98e8ff7", - "types/format-string-only": "22c3a8cd73ee23d3", - "types/format-string-only-refused": "47f4bcaea2e397cc", + "types/format-string-only": "0c4839f0ebb3f4b5", + "types/format-string-only-refused": "ac9647103757e7ad", "types/datetime-value": "fd07bee460e3d65e", "types/datetime-refused": "02b692f30451cc2f", - "types/url-value": "512fddb8c5b3e4ff", + "types/url-value": "7ecf73c0fd424c66", "types/url-punycode": "1137cb9a93c3b47c", "types/url-idna": "0eccb0c75d7f7bf3", "types/url-host-refused": "1a5c9bf6f3bc551b", - "types/url-stored": "dd3950dd59da8624", - "types/array-items": "8001e3dc5fa40455", + "types/url-stored": "941c83c875fb6a5e", + "types/array-items": "135455398d3fb389", "types/array-items-refused": "12f194f18218820c", "types/array-items-unnamed": "86a4fe72b9e76609", + "types/range-value-refused": "e22f9f1bd365eec7", + "types/currency-value": "24090d0c53150725", + "types/phone-value": "8cc4557fcdac16d8", + "types/currency-value-refused": "508f4c3560daa199", + "types/array-entries": "6db9f8d3852f6826", + "types/array-entries-refused": "dc1e07523ed8e91c", + "types/object-stored": "368610e53fcaa640", "types/searchable-false": "1cd484d756268588", + "types/status-groups": "06a871a01b456c07", + "types/status-refused": "c28b679c9fe4a5d1", + "types/status-one": "d00be8bf29b2da0e", + "types/status-one-chain": "127384e20d33619d", + "types/status-inherit": "5cbea5bf8929b1ff", + "types/status-word-redeclared": "1f91df6c07b91148", + "types/status-parent-wordless": "7a901564df446334", + "types/list-declared-status": "1c9612d921ed8db3", + "types/status-write-refused": "c878db4b8196b4e0", + "types/status-word-held": "82fe1f06527c3310", + "types/shipped-progress": "2be5f9efd80a117d", + "types/shipped-attendees": "cc094e45022b80f1", + "types/shipped-assignees": "5b7187f27cd92337", + "types/shipped-rating": "2ebd645d68022591", + "types/shipped-game": "022fd526c4725689", "types/shipped-thumbnail": "9fba0d11c206d4a1", "types/thumbnail-one": "75405cd50e2f3a72", "types/thumbnail-name": "4c6c32a74875d2b4", @@ -5280,7 +5573,7 @@ "types/field-removed": "1c5b8a9f7d89b3b7", "types/field-removed-writable": "53c3023548f2f28e", "types/subtype-members": "7ab7cc92e5cc02fa", - "types/evolve-free": "061195ad726fda74", + "types/evolve-free": "ef979e1a5f754df0", "types/evolve-held-name": "d959cf9f392b0a01", "types/evolve-schema-change": "a6477105ff9ae0b1", "types/evolve-refusal-details": "9281ce52046616bb", @@ -5336,13 +5629,23 @@ "types/source-filter": "4cb80605ef5eb54c", "types/source-filter-missing-source": "ffaa4dc35611173f", "types/source-filter-by-id": "6617d24512e658f0", - "types/shipped": "1f92bfb8fd3017a3", + "types/shipped": "aea3efaa0869440a", "types/shipped-system": "aa1b148e069da6e4", "types/message": "b7749ce97bb970c1", "types/collection": "9b963e89e951391b", "types/body-optional": "6dcf4daee3e74bd6", + "types/article-doi": "9fcabf6a4aedcee0", "types/highlight": "14a1b11a8d1a648f", "types/entity": "699d3bbbab2cdc19", + "types/entity-contact-lists": "d9d5601b0b18e599", + "types/core-labels": "38caf4c485d2b53b", + "types/entity-main-entry": "4640dd7db1f26aa9", + "types/entity-contact-upgrade": "14833a15f488f83d", + "types/entity-contact-upgrade-empty": "c1426d43b70e5381", + "types/entity-contact-upgrade-kept": "fc1d4360fca4fcca", + "types/entity-contact-upgrade-held": "01fb630fae425f27", + "types/entity-contact-upgrade-version": "1b78a02da272ec20", + "types/entity-contact-upgrade-event": "20ba574dbfb6c59b", "types/connection": "f244e49517636a4f", "types/platform-drift": "218564892a31e97e", "types/platform-remove": "95b3179fbb64b6e7", @@ -5351,7 +5654,8 @@ "types/platform-remove-missing": "04e8cd32852b7725", "types/platform-operator-only": "f2df61370e202b21", "types/normalize-authenticated": "ca834dd1005ac669", - "types/normalize-answer": "f2339c98461d0ce6", + "types/normalize-answer": "e36b79a006069fe7", + "types/normalize-declared-only": "6e214d7e97bcaa33", "types/normalize-absent": "a05b32ea25c26809", "types/normalize-defaults": "5ffaec92f1631ed7", "types/normalize-declared-refinements": "59f4ccdb93bf17d5", @@ -5359,20 +5663,22 @@ "types/normalize-no-write": "c6861da0c2c16e85", "types/normalize-no-audit": "8741f16698fba13d", "types/normalize-cli": "096eeb37e47c8155", - "types/icon-set": "953c694f5888defe", + "types/icon-set": "68e89c4208d6cf58", "types/icon-clear": "d11609c1d7cdee83", "types/icon-read": "8b5b8daada233290", "types/icon-kept": "02f926b089c3d43c", "types/icon-write-back": "5ceddcc1d064f2bd", "types/hint-write-back": "19d0b63eb6c20d08", - "types/icon-refused": "386f3cd4406b8ec8", + "types/label-write-back": "df4556afbfd3be0e", + "types/icon-refused": "8127dd05cbda643e", "types/icon-missing-type": "acb8d8a749fa2d7d", "types/icon-who": "02ea99aeaf49ab48", "types/icon-who-refused": "072df4f49ad439e3", "types/icon-deleted": "b433a7c923351c6b", "types/icon-audit": "b9b01878358698ba", - "types/item-name-fields": "75fb65501a6a7dad", - "types/item-name-text": "19ba94664f4e5de8", + "types/item-name-fields": "3a91e5052f356e81", + "types/item-name-text": "a4930d3d354de9a5", + "types/item-name-markdown": "69e11f2dc0b7054f", "types/item-name-empty": "84c62a6e3746e515", "types/delete-owner": "72c3a238a5995f6e", "types/delete-owner-recent": "040a0433bb364909", @@ -5501,6 +5807,8 @@ "versions/auto-refused-no-copy": "237ceffcc05ab3fa", "versions/auto-race": "caf030e287f2bce5", "versions/copy-written": "1902814907f38a50", + "versions/copy-judged": "eff21bc604710eff", + "versions/copy-refused": "564978828641c0b1", "versions/copy-version": "e067d566cc735b3d", "versions/copy-source": "07a588aaf8706238", "versions/copy-tags": "f0627a61a7c89124", @@ -5554,7 +5862,28 @@ "views/not-found": "53f43b426f7ef6e2", "views/parent-deleted": "05f5861c8a7adfac", "views/delete": "141a7a4357b639d1", - "views/delete-announced": "dcbedf915038670d", + "views/delete-announced": "e16b9e75b3d1c6d2", + "views/items": "e7f0ac44636655ca", + "views/items-query": "0d91cfd81c9e444a", + "views/items-system": "17d2443319d78a33", + "views/items-beneath": "846fbfb7741d7f51", + "views/items-beneath-state": "caf79b480e8250a1", + "views/items-beneath-missing": "08b012205b71faf6", + "views/items-order": "4c61fa7cc32709a2", + "views/items-order-title": "1d5e54cec5a980ec", + "views/items-order-missing": "9b3696c8894cd267", + "views/items-order-ties": "df41a4eba90ca03e", + "views/items-order-default-ties": "a02f208dc601eccd", + "views/items-limit": "1c0ea0f309030abd", + "views/items-limit-range": "9752e4c7f296cc8d", + "views/items-cursor": "e37df2486b67fa89", + "views/items-cursor-foreign": "03223ed174c53abc", + "views/items-readable": "e229ba4259889327", + "views/items-beneath-readable": "69fa31238cceea2e", + "views/items-edge-read": "bbe1bbbe271b2b3c", + "views/items-no-type": "5b472b410618f079", + "views/items-not-found": "46dea33d098e8500", + "views/items-revoked": "5481a822a0bf7b31", "views/grant": "a9eab891de3269d9", "views/read": "263f3ff144383c04", "views/read-hidden": "f505cc50dc77f05d" diff --git a/src/generated/error-codes.md b/src/generated/error-codes.md index dfe3d101..4a89eeab 100644 --- a/src/generated/error-codes.md +++ b/src/generated/error-codes.md @@ -5,7 +5,7 @@ | `bulk_confirmation_required` | 400 | A destructive bulk action was sent without its confirmation literal. | | `edge_constraint_violation` | 400 | An edge write breaks a constraint of its edge type or of the link graph, such as cardinality, endpoint types or a duplicate. | | `edge_cycle` | 400 | An edge would close a cycle: a self-loop on any edge type, or a loop on an edge type that must stay acyclic. | -| `inheritance_violation` | 400 | A type changes the shape of a field it inherits, or a parent gains a field that a child declares with another shape. | +| `inheritance_violation` | 400 | A type changes the shape of a field it inherits, or a change to a parent clashes with a field a child declares. | | `invalid_client` | 400 | The device sign-in page names a client that is not registered. | | `invalid_id` | 400 | An item, edge or folder id is not a well-formed identifier. | | `invalid_properties` | 400 | An item's properties break the schema of its type, or an edge's break the property schema of its edge type. | @@ -50,10 +50,12 @@ | `id_reused` | 409 | A caller-minted id already names a different item or edge. | | `idempotency_key_in_flight` | 409 | An `Idempotency-Key` is held by another request, or kept changing hands. Retry. | | `instance_changed` | 409 | A request names another instance in `X-Marfa-Instance-Id`. Nothing is written. Rebuild the working copy from the instance at this address. | +| `item_exists` | 409 | A create that asked not to overwrite names a source and source id, or an id, that an item already holds. Nothing is written. `details.existing_id` names the item. | | `link_taken` | 409 | A write would give an item a link that another item of its type holds. | | `owner_exists` | 409 | The instance already has an owner. | | `reach_changed` | 409 | A conditional copy read or copy stream carries a proof for a reach that has since changed. Rebuild the working copy. | | `source_id_conflict` | 409 | A change to `source_id` names a natural key that another item already holds under the same source. | +| `trash_plan_changed` | 409 | A trash names a `digest` that no longer matches what it would move to the trash. Nothing is trashed; preview it again. | | `type_already_exists` | 409 | A type is already registered under this identifier. | | `type_chain_unresolvable` | 409 | The stored parent chain of a type is circular or too deep to resolve. `PUT /types/{id}` still accepts a corrected schema. | | `type_has_subtypes` | 409 | Another type declares this type as its parent. Forcing the delete does not override this. | diff --git a/src/generated/marfa-release.json b/src/generated/marfa-release.json index 0068a382..8f0b795a 100644 --- a/src/generated/marfa-release.json +++ b/src/generated/marfa-release.json @@ -1 +1 @@ -{"version":"v0.0.45"} +{"version":"v0.0.49"} diff --git a/src/generated/permissions.md b/src/generated/permissions.md index 130dc08d..6ba8eac8 100644 --- a/src/generated/permissions.md +++ b/src/generated/permissions.md @@ -5,7 +5,7 @@ | `config.manage` | Reading and replacing the instance configuration at `/config`: the enforcement levers and the cleanup-job retention overrides. | | `connectors.manage` | Reading and administering connector registrations, their runs and their endpoints, deleting registrations and clearing a registration's retained state. | | `grants.manage` | Listing and revoking other apps' access. | -| `instance.maintain` | Running housekeeping, resetting platform type definitions and cancelling bulk jobs. | +| `instance.maintain` | Running housekeeping, removing retired platform types and cancelling bulk jobs. | | `instance.read` | Reading detailed health, metrics, housekeeping status, retired platform types, blob storage diagnostics and bulk job status. | | `items.purge` | Destroying a trashed row irrecoverably, through the single-row operation and the bulk one alike. | | `keys.manage` | Listing and revoking keys across the instance, and changing their metadata or narrowing access. | diff --git a/src/generated/server-image.md b/src/generated/server-image.md index 9b05f510..df4f6fe9 100644 --- a/src/generated/server-image.md +++ b/src/generated/server-image.md @@ -1,5 +1,5 @@ ```bash -docker pull ghcr.io/withmarfa/marfa-server:0.0.45 +docker pull ghcr.io/withmarfa/marfa-server:0.0.49 docker run --name marfa -d --restart unless-stopped -p 127.0.0.1:8600:8600 \ - -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.45 + -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.49 ``` diff --git a/src/generated/settings.md b/src/generated/settings.md index 54b88fff..bf9292e4 100644 --- a/src/generated/settings.md +++ b/src/generated/settings.md @@ -96,6 +96,9 @@ | `MARFA_BULK_ACTION_POLL_BACKOFF_MULTIPLIER` | `2` | Decimal number, 1 to 100 | The factor by which the bulk-action worker's wait grows after each look that finds no work. | | `MARFA_SSE_MAX_VIEWERS` | `0` | Whole number, 0 or more | The most event-stream viewers one server instance serves at once. At 0 there is no limit. | | `MARFA_WEBHOOK_ALLOW_PRIVATE_ADDRESSES` | `false` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether outbound webhooks may reach loopback, private and other non-public addresses. | +| `MARFA_MCP_FETCH_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether MCP tools fetch a file from a web address, or a file a chat attached, to save it. When off, a tool asked to fetch one is refused. | +| `MARFA_MCP_FETCH_MAX_BYTES` | `52428800` bytes | Whole number of bytes, 1 or more | The largest file an MCP tool fetches from a web address to save. A larger file is refused and nothing is stored. | +| `MARFA_MCP_FETCH_TIMEOUT_MS` | `60000` ms | Whole number of milliseconds, 1 to 2147483647 | How long an MCP tool's fetch of a file from a web address may take, from connecting to the last byte. | | `MARFA_ERROR_REPORTING_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Send error alerts and exception reports to configured receivers. `/config` overrides it; receiver addresses and credentials stay machine settings. | | `MARFA_HEARTBEAT_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Send scheduled requests to the configured heartbeat receiver. `/config` overrides it; its address stays a machine setting. | | `ERROR_WEBHOOK_URL` | None | Absolute `http` or `https` URL | The URL the server posts a notice to when a request fails with a 500 error. When blank, no notice is sent. | diff --git a/src/lib/catalog.test.ts b/src/lib/catalog.test.ts index 75b87859..23f20566 100644 --- a/src/lib/catalog.test.ts +++ b/src/lib/catalog.test.ts @@ -24,6 +24,8 @@ interface Field { format?: string; enum_values?: string[]; items_type?: string; + items_fields?: Record; + status_values?: { value: string; group: string }[]; } interface CatalogType { @@ -85,7 +87,7 @@ function isUrl(value: string): boolean { } /** Why a value does not fit a field's kind, or undefined if it does. */ -export function valueError(field: Pick, value: unknown): string | undefined { +export function valueError(field: Pick, value: unknown): string | undefined { switch (field.type) { case 'string': if (typeof value !== 'string') return 'is not a string'; @@ -111,6 +113,10 @@ export function valueError(field: Pick entry.value === value) + ? undefined + : `is not one of ${(field.status_values ?? []).map((entry) => entry.value).join(', ')}`; case 'array': { if (!Array.isArray(value)) return 'is not a list'; for (const [index, item] of value.entries()) { @@ -399,7 +405,7 @@ describe('a system type page', () => { describe('the generated type tables', () => { const files = readdirSync(GENERATED).filter((file) => /^(core|system)\.[a-z]+\.md$/.test(file)); - /** The rows of each table that has a Required column, as cell lists. */ + /** The rows of each table that has a Required column, as cell lists with the Required cell last. */ function requiredRows(text: string): string[][] { const rows: string[][] = []; let required = -1; @@ -410,7 +416,7 @@ describe('the generated type tables', () => { } const cells = line.split(/(? cell.trim()); if (cells[0] === 'Field') required = cells.indexOf('Required'); - else if (required >= 0 && !/^-+$/.test(cells[0])) rows.push(cells); + else if (required >= 0 && !/^-+$/.test(cells[0])) rows.push([cells[0], cells[required]]); } return rows; } @@ -421,7 +427,7 @@ describe('the generated type tables', () => { for (const file of files) { for (const cells of requiredRows(readFileSync(path.join(GENERATED, file), 'utf8'))) { seen += 1; - expect(['Yes', 'No'], `${file}: ${cells[0]}`).toContain(cells[2]); + expect(['Yes', 'No'], `${file}: ${cells[0]}`).toContain(cells[1]); } } expect(seen).toBeGreaterThan(30); @@ -431,9 +437,14 @@ describe('the generated type tables', () => { for (const type of TOP_LEVEL) { const text = readFileSync(path.join(GENERATED, `${type.id}.md`), 'utf8'); const own = text.slice(0, text.includes('## Subtypes') ? text.indexOf('## Subtypes') : undefined); - const shown = Object.fromEntries(requiredRows(own).map((cells) => [cells[0].replace(/`/g, ''), cells[2]])); + const shown = Object.fromEntries(requiredRows(own).map((cells) => [cells[0].replace(/`/g, ''), cells[1]])); expect(shown, type.id).toEqual( - Object.fromEntries(Object.entries(type.fields).map(([name, field]) => [name, field.required ? 'Yes' : 'No'])), + Object.fromEntries( + Object.entries(type.fields).flatMap(([name, field]) => [ + [name, field.required ? 'Yes' : 'No'], + ...Object.entries(field.items_fields ?? {}).map(([member, entry]) => [`${name}[].${member}`, entry.required ? 'Yes' : 'No']), + ]), + ), ); } }); @@ -506,7 +517,7 @@ describe('the example check', () => { const cases: [string, unknown, string][] = [ ['a type nothing ships', { ...sample, type: 'core.recipe' }, 'is not a shipped type'], ['a missing required field', { type: sample.type, properties: { body: 'Some text' } }, 'title is required by core.media.article'], - ['a field the type lacks', { ...sample, properties: { ...sample.properties, rating: 5 } }, 'rating is not a field of core.media.article'], + ['a field the type lacks', { ...sample, properties: { ...sample.properties, colour: 'red' } }, 'colour is not a field of core.media.article'], ['a string where a whole number goes', { ...sample, properties: { ...sample.properties, word_count: '120' } }, 'word_count is not a whole number'], ['a fraction where a whole number goes', { ...sample, properties: { ...sample.properties, word_count: 1.5 } }, 'word_count is not a whole number'], ['a date and time with no zone', { ...sample, properties: { ...sample.properties, published_at: '2026-05-12T08:00:00' } }, 'published_at is not a date'],