diff --git a/content/docs/(overview)/glossary.mdx b/content/docs/(overview)/glossary.mdx
index a6e16203..2d060240 100644
--- a/content/docs/(overview)/glossary.mdx
+++ b/content/docs/(overview)/glossary.mdx
@@ -20,7 +20,7 @@ Marfa uses each word below in one sense only, in its code, its API and these doc
| [**reverse name**](/properties-metadata-and-edges#edges) | An edge's name read from its target: `child-of` for `parent-of`. |
| [**label**](/properties-metadata-and-edges#edges) | The heading a type or an edge type shows for itself. An edge type's label heads the source's end of an edge, such as `Children` for `parent-of`. |
| [**reverse label**](/properties-metadata-and-edges#edges) | The heading an edge type shows for the target's end, such as `Parent`. Where an edge type names none, its label names both ends. |
-| [**collection**](/catalog/edges) | A flat container of items. An item joins one through an `in-collection` edge to an item whose type has the `container` role, such as an album. An item can be in many, a container can't itself be a member, and deleting the container leaves its members. |
+| [**collection**](/catalog/edges) | A flat container of items. An item joins one through an `in-collection` edge to an item whose type has the `container` role, such as a [`core.collection`](/catalog/collection) or an album. An item can be in many, a container can't itself be a member, and deleting the container leaves its members. |
| [**view**](/build/save-a-view) | A saved query with one sort and one grouping, kept on the instance as a `system.view` and shared by every app. How an app draws the items is the app's own. |
| [**file**](/files) | An item that names its bytes. |
| [**blob**](/files) | A file's bytes, stored once under a hash of their content. |
diff --git a/content/docs/catalog/(types)/(core)/collection.mdx b/content/docs/catalog/(types)/(core)/collection.mdx
new file mode 100644
index 00000000..fb0c5425
--- /dev/null
+++ b/content/docs/catalog/(types)/(core)/collection.mdx
@@ -0,0 +1,32 @@
+---
+title: Collection
+description: "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."
+identifier: core.collection
+---
+
+src/generated/catalog/core.collection.md
+
+## Example
+
+```json
+{
+ "type": "core.collection",
+ "properties": {
+ "title": "Kitchen renovation",
+ "description": "Quotes, sketches and the tasks still to book."
+ }
+}
+```
+
+An item joins the collection through an [`in-collection`](/catalog/edges#in-collection) edge from the item to the collection, with an optional `position`:
+
+```json
+{
+ "source_id": "01a12313-ad0e-71cd-99b4-7a176c5a9f18",
+ "target_id": "01a12314-0b52-7c41-8e0f-2d6f1a3b9c07",
+ "edge_type": "in-collection",
+ "properties": { "position": 1 }
+}
+```
+
+A container, such as another collection or an album, can't be a member. An item can be in any number of collections, and deleting a collection leaves its members in place.
diff --git a/content/docs/catalog/(types)/(core)/meta.json b/content/docs/catalog/(types)/(core)/meta.json
index 29341cd0..af69d297 100644
--- a/content/docs/catalog/(types)/(core)/meta.json
+++ b/content/docs/catalog/(types)/(core)/meta.json
@@ -3,6 +3,7 @@
"defaultOpen": false,
"pages": [
"bookmark",
+ "collection",
"entity",
"event",
"file",
diff --git a/src/generated/GLOSSARY.md b/src/generated/GLOSSARY.md
index 7c676fa0..bee225a7 100644
--- a/src/generated/GLOSSARY.md
+++ b/src/generated/GLOSSARY.md
@@ -141,7 +141,7 @@ Access is read the same way wherever a person sees it: on the pages where they a
| **placement** | Where an item's file sits in a folder: its `in-folder` edge to the folder's `system.folder`, whose `path` names the file from the folder's root. It never decides what a folder holds, and it is never written in a file. |
| **registry** | The folder registry: one file on a machine listing its folders, each by its directory and the `system.folder` it follows, so each folder can find the others and a file moved or copied between two of them is told apart. It is never a folder's store, and `MARFA_FOLDER_REGISTRY` names another file in its place. The type registry is the server's catalog of types (`types.md`). |
| **embed** | A file an item's body shows inside it, `` or `![[file]]`: that file item's `attached-to` edge to the item, in a folder's Markdown file and in a body written through a working copy alike. In a folder, the file's `has-attachment` line lists only what the body does not embed, and an embedded file is put where the link says, any other attachment beside the document. It names a file where a file answers to it or its extension names a MIME type; anything else, `![[A note]]` say, is an embed of a note, text in the body and neither a link nor an edge. |
-| **collection** | A container and its members: an item whose type declares the `container` role, such as an album or a series, and the items joined to it by an `in-collection` edge, each with an optional `position`. Flat: a container is never a member, `parent-of` is what hierarchy is for, and deleting the container leaves its members. Not a folder and not a view. |
+| **collection** | A container and its members: an item whose type declares the `container` role, such as `core.collection`, an album or a series, and the items joined to it by an `in-collection` edge, each with an optional `position`. Flat: a container is never a member, `parent-of` is what hierarchy is for, and deleting the container leaves its members. Not a folder and not a view. |
| **frontmatter** | The block of `key: value` lines a Markdown file in a folder opens with between `---` fences. Its `type`, `tier`, `tags`, `state` and `occurred_at` lines are the item's own fields; `marfa_id` and `marfa_version` name the item and the version the file was written from; a line named for an edge type or a reverse name is an edge's, written in the one file at the end the type names and naming its targets as `"[[name]]"` or `"[[]]"`; every other line is a property. The file's body is the property its type's `body_field` names. Never "header". |
### Connectors
diff --git a/src/generated/catalog/builtin-catalog.json b/src/generated/catalog/builtin-catalog.json
index d03ba4e0..6f120498 100644
--- a/src/generated/catalog/builtin-catalog.json
+++ b/src/generated/catalog/builtin-catalog.json
@@ -64,6 +64,33 @@
"default": "last_writer_wins"
}
},
+ {
+ "id": "core.collection",
+ "label": "Collection",
+ "description": "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.",
+ "version": 0,
+ "fields": {
+ "title": {
+ "type": "string",
+ "description": "Name of the collection",
+ "required": true
+ },
+ "description": {
+ "type": "string",
+ "description": "What the collection gathers"
+ }
+ },
+ "display_hints": {
+ "title_field": "title",
+ "body_field": "description"
+ },
+ "merge_policy": {
+ "default": "last_writer_wins"
+ },
+ "roles": [
+ "container"
+ ]
+ },
{
"id": "core.entity",
"label": "Entity",
diff --git a/src/generated/catalog/core.collection.md b/src/generated/catalog/core.collection.md
new file mode 100644
index 00000000..f9638f4e
--- /dev/null
+++ b/src/generated/catalog/core.collection.md
@@ -0,0 +1,15 @@
+## Fields
+
+| Field | Kind | Required | Description |
+| ------------- | -------- | -------- | --------------------------- |
+| `title` | `string` | Yes | Name of the collection |
+| `description` | `string` | No | What the collection gathers |
+
+## Settings
+
+| Setting | Value |
+| ------------------------------------------------------ | ---------------------------------- |
+| [`title_field`](/catalog/your-own-types#fields) | `title` |
+| [`body_field`](/catalog/your-own-types#fields) | `description` |
+| [`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/types.md b/src/generated/catalog/types.md
index 3af02f8c..f5e6cfe4 100644
--- a/src/generated/catalog/types.md
+++ b/src/generated/catalog/types.md
@@ -1,16 +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.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) |
-| [`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) |
+| [`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. | |
## System types
diff --git a/src/generated/contract-index.json b/src/generated/contract-index.json
index 1a8fbbd9..9507eb66 100644
--- a/src/generated/contract-index.json
+++ b/src/generated/contract-index.json
@@ -1385,7 +1385,7 @@
"edges/folder-revoked-keeps": "a966fa8e996ecd70",
"edges/folder-place": "b4c7a00e6469018b",
"edges/folder-blind": "bff04a27c6aed303",
- "edges/collection-target": "b25dcc6ec5dff1ba",
+ "edges/collection-target": "9f825815a5cf2672",
"edges/collection-no-nesting": "11e27fb49df9d0d0",
"edges/collection-no-nesting-others": "c26b7617cbec51ad",
"edges/cascade-parent-of": "5f8cce4a86abc3c7",
@@ -5294,9 +5294,10 @@
"types/source-filter": "4cb80605ef5eb54c",
"types/source-filter-missing-source": "ffaa4dc35611173f",
"types/source-filter-by-id": "6617d24512e658f0",
- "types/shipped": "a3d618d635c669ae",
+ "types/shipped": "d329bfef050dc7d6",
"types/shipped-system": "aa1b148e069da6e4",
"types/message": "b7749ce97bb970c1",
+ "types/collection": "9b963e89e951391b",
"types/body-optional": "6dcf4daee3e74bd6",
"types/highlight": "14a1b11a8d1a648f",
"types/entity": "699d3bbbab2cdc19",
diff --git a/src/lib/docs-map.test.ts b/src/lib/docs-map.test.ts
index 702250dc..efd245e2 100644
--- a/src/lib/docs-map.test.ts
+++ b/src/lib/docs-map.test.ts
@@ -48,7 +48,7 @@ describe('buildDocsMap', () => {
'- Items]',
'- Types]',
' - Core types',
- ...['Bookmark', 'Entity', 'Event', 'File', 'Highlight', 'Media', 'Message', 'Note', 'Task'].map((title) => ` - ${title}]`),
+ ...['Bookmark', 'Collection', 'Entity', 'Event', 'File', 'Highlight', 'Media', 'Message', 'Note', 'Task'].map((title) => ` - ${title}]`),
' - System types',
...['Connection', 'Folder', 'View'].map((title) => ` - ${title}]`),
' - Type rules]',