Vancetope — Document Kinds with Items
Specifies the
itemspayload for documents that carry an ordered list of small records — currently the privilegedkind: list. Defines a single canonical model that maps to three on-disk formats (markdown, JSON, YAML) and round-trips losslessly. See also: web-ui | recipes | DocumentDocument / DocumentHeader
1. Purpose
Documents in Vancetope have a kind header (see DocumentHeader.kind). For some kinds — list is the first — the actual content consists of an ordered sequence of small, uniform items. The Web-UI should offer the following for such Documents:
- Specialized editors/views (CRUD, drag-and-drop, multi-select), in addition to the existing raw editor.
- Format-agnostic operation — the same editor serves Markdown, JSON, and YAML documents.
For this to work, the server and client need a common item schema and a deterministic round-trip convention between the three formats and a typed item model.
What this spec defines:
- The canonical item schema (fields, required/optional, forward compatibility).
- The on-disk form per format (md, json, yaml).
- Round-trip guarantees for reading/writing.
- A stub for
kind: treeas a logical extension — not in v1.
What it does not define:
- UI layout, keyboard shortcuts, drag-and-drop library — see
web-ui.md. - Storage within the DB Document itself — items continue to reside in the
inlineTextbody, using the same persistence as all Documents.
2. Items Model
A Document with kind: list is flat — a single sequence of items without nesting. Each item is an object with:
| Field | Type | Required | Meaning |
|---|---|---|---|
text |
string |
yes | Display text of the item, single or multi-line allowed. |
Forward compatibility: Future fields (done, tags, id, dueAt, …) are possible without breaking the schema — parsers accept unknown fields and write them back unchanged (lossless round-trip). The initial v1 implementation writes only text; existing additional fields are preserved when reading and outputted when writing.
Canonical Form (JSON):
{
"$meta": { "kind": "list" },
"items": [
{ "text": "first item" },
{ "text": "second item" }
]
}
items is a List<Item>; an empty Document (items: []) is valid.
Header Convention per Format (read by the server HeaderStrategy path, which mirrors kind to DocumentDocument.kind):
| Format | Header | Body |
|---|---|---|
| Markdown | --- frontmatter with kind: list (see §3.1) |
Bullet list after the closing fence |
| JSON | Top-level object "$meta": { "kind": "list" } |
Remaining top-level keys (items, etc.) alongside $meta |
| YAML | Top-level mapping with $meta: { kind: list } as the first key |
items etc. at the same level alongside $meta |
Background: server-side JsonHeaderStrategy and YamlHeaderStrategy recognize kind exclusively in the canonical $meta form — otherwise, kind is not mirrored and Kind filters break. JSON and YAML are exactly symmetrical in their header convention.
3. On-Disk Formats
3.1 Markdown — kind: list
The on-disk form is a pure bullet list in the body, after the usual front-matter block:
---
kind: list
---
- first item
- second item
Reading Rules:
- The front-matter parser reads
kind: listfrom the---fences (seeMarkdownHeaderStrategy); for the items, only the body after the closing fence matters. - A line starting with
-(dash + space) or*is an item; everything after the marker istext(trimmed). - Multi-line items: any subsequent line indented with at least 2 spaces and not starting with a bullet itself becomes part of the
textof the previous item (with\nas separator). - Nesting through deeper indentation (
- sub-item) is not allowed inkind: list. If a parser encounters such lines, it treats them as a continuation oftextor ignores them — it never creates sub-items. (This will bekind: tree, see §5.) - Blank lines between items are ignored but not reproduced when writing.
Writing Rules (Round-trip):
- Front-matter is preserved unchanged (server writing path leaves the header pre-block untouched).
- Items are outputted as
- {text}, one line per item, in order. - Multi-line
textis outputted with a two-space continuation indent. - Items carrying additional fields other than
textare not serializable in Markdown — they would be lost on round-trip. Practical consequence: Users of typed editors with Markdown items are limited totext-only. Those who need more (done,tags, …) switch to JSON/YAML.
3.2 JSON
{
"$meta": { "kind": "list" },
"items": [
{ "text": "first item" },
{ "text": "second item" }
]
}
Reading Rules:
- The parser accepts the object form.
kindis read from$meta.kind; legacy top-levelkindis accepted as a fallback (backward compatibility). - Optional:
itemsasstring[](compact shorthand) — each string is then promoted to{ "text": "..." }. - Unknown top-level keys (other than
$meta,items) are preserved and written back as top-level (passthrough). Unknown item keys likewise. - Order of top-level keys: canonically
$metafirst, thenitems, then passthrough.
Writing Rules:
- 2-space indent, one item per line (or pretty-printed over multiple lines if the item has additional fields).
- Trailing newline at EOF.
3.3 YAML
$meta:
kind: list
items:
- text: first item
- text: second item
Reading Rules:
- Top-level mapping with
$metaas a reserved key, carryingkind(and possibly other scalar keys); remaining top-level keys (items, passthrough) form the body. - Optional:
itemsas a pure string sequence (- first item) — then the sametext-promote as for JSON. - Unknown keys are preserved (passthrough).
Writing Rules:
- Single-document, 2-space indent,
$metaas the first top-level key, block-style sequences (- text: …). - Trailing newline at EOF.
4. Server Path
No new endpoint, no server-side parser for items. The List editor loads the Document via the existing GET /documents/{id} (with inlineText in the body), parses in the browser, edits locally, and writes the newly serialized body back with PUT /documents/{id} (or the existing update path). Rationale:
- A duplicate parser path (server + client) would be redundant if the editor already needs a typed item model in the browser.
- Forward-compatible fields are passed through by the client parser via an
extramap — on save, the entire body is re-serialized; untouched fields are preserved. - Both editors (Raw + List) write via the same Document update path — no separate conflict/ETag handling.
On the server side, only the existing HeaderStrategy pipeline remains (front-matter parser for kind-mirror, see MarkdownHeaderStrategy / JsonHeaderStrategy / YamlHeaderStrategy) — this is independent of the item body and already in place.
If CLI/Mobile later need the same item logic, the parser will be added there separately — YAGNI for v1.
5. Web-UI
5.1 Editor Activation
The Document editor already knows the kind value per item in the list (see DocumentSummary.kind). When the user opens a Document:
kind === 'list'+ Format ∈ {md, json, yaml} → the List editor is offered in addition to the Raw editor. Tab switch at the top (List/Raw), defaultList.- Otherwise: only Raw editor as before.
When switching Raw → List, the client parses the current body in-memory (no server round-trip); when switching List → Raw, the item model is serialized back and written into the Raw editor. The Save button writes the serialized body via the existing Document update path (PUT /documents/{id}) — no new endpoint.
An in-flight switch between tabs will prompt a confirmation “unsaved changes” if the currently visible editor has unsaved changes.
5.2 Feature Set v1
| Feature | v1 | Note |
|---|---|---|
| Read-only render | ✓ | first incarnation |
| Add / edit / delete | ✓ | inline edit per item |
| Reorder (drag-drop) | ✓ | Library: vue-draggable-plus |
| Multi-select | ✓ | Shift-/Ctrl-Click, then Bulk-Delete |
| Indent / outdent | ✗ | belongs to kind: tree |
| Done-Toggle | ✗ | when done field arrives |
| Tags per Item | ✗ | when tags field arrives |
5.3 Components
<ListView>— read-only render, a<ul>with per-item row.<ListEditor>— wraps<ListView>+ drag-handle, edit mode, multi-select.- Uses DaisyUI primitives for buttons (
btn-ghost btn-sm), inputs (input input-bordered) — no custom classes outsidesrc/components/.
5.4 Keyboard Shortcuts (target)
Enteron an item row → new row after it, cursor in edit.Backspaceon an empty row → delete row, focus to previous item.Escin edit mode → Cancel.Cmd/Ctrl-Click→ Multi-Select toggle.Shift-Click→ Range-Select.
6. kind: tree (future, not in v1)
As soon as nesting is needed, kind: tree will be introduced as a second item variant. Schema sketch:
$meta:
kind: tree
items:
- text: parent
children:
- text: child
children: []
- Same
ItemsServicepipeline, separateTreeItemrecord type withchildren: List<TreeItem>. - Markdown form uses indent nesting (
- sub-item), which is explicitly forbidden forkind: list. - Editor gets
Tab/Shift+Tabfor indent/outdent. - A Tree cannot be loaded as a List and vice versa — editor selection strictly follows the
kindvalue.
The goal of this separation: lists and trees have different UI affordances (drag-and-drop is 1D for lists, 2D for trees); a common editor with a mode switch would be unnecessarily complex.
7. kind: checklist (separate Spec)
Third item variant: flat item sequence with status per item (open, done, in_progress, blocked, review, needs_info, deferred, delegated, waiting) plus an optional priority field. Markdown form uses the GFM checkbox syntax extended to single-character status (- [<char>] text). A separate Kind instead of an optional field on list, so the editor always has a status column and MD round-trip is unambiguous per Kind.
Full specification: doc-kind-checklist.md.
Relationship to this spec:
- Item model, top-level schema (
$meta/items/extra), format mapping conventions (JSON/YAML/MD), server path (no server parser, client round-trip viaPUT /documents/{id}), and UI component family (<ListView>/<ListEditor>analogous to<ChecklistView>/<ChecklistEditor>) are shared. - Delta:
statusfield per item (required with default),priorityfield (optional), character mapping in the MD bullet box, status dropdown + aggregate header in the editor.
A potential combination with nesting (kind: tree-checklist) is outlined in the Open Points section of the Checklist spec, not in v1.
8. Open Points
- Multi-line item texts in Markdown round-trip — continuation indent works but is not everyone’s stylistic preference. Alternative: enforce single-line, discard additional lines. Decision pending until the first real use case.
- Sort order when writing — keys within an item:
textfirst, thenextrain insertion order. Sufficient for now; if LLM output round-trip quality suffers, this will be regulated separately inrecipes.md§ Tooling. - DB index on
kind— already exists viaDocumentDocument.kind. Items themselves do not land in separate Mongo columns; the item sequence remains exclusively in theinlineTextbody. If bulk queries (“all items withdone: falseacross all list-Documents”) are needed, that is a separate spec point.