Vancetope — Document Kind records

Specifies the records payload for documents that carry a flat, ordered table — a fixed schema in the metadata plus one record per item, with each record holding values for the schema’s fields. Direct extension of kind: list: same flat structure, same bullet-per-item form in Markdown, but per item a map of schema fields instead of a single text string. See also: doc-kind-items | web-ui


1. Purpose

kind: list is a flat item list with only a text field. kind: records is the natural extension for this: many uniform data records, each record has multiple named fields, the field set is declared once in the document header and applies to all records.

Use cases:

  • CRUD-like lists with multiple columns (people, places, inventory, lookup tables).
  • Input forms for Recipes/Tools where the fields are fixed.
  • Structured note-pads in the style of “one line per entry, columns describe the entry”.

Distinctions:

  • list: one text per item, no schema, no columns. Records is the direct extension — same codec style, same on-disk form, just a richer item model.
  • records: flat, fixed field set, one value per field per record. Table-like. Deliberately no hierarchy — schema invariant applies equally to all records; sub-records would break the global schema concept and are unreliable with current LLM output quality.
  • sheet: a separate kind, to be specified later, for free 2D sheets without schema constraints.
  • dataset/tree-of-records (hypothetical): Hierarchy over record groups is additively possible, but not part of this spec — it would get its own kind.

What this spec defines:

  • Schema declaration in the header (comma-separated in md, array in json/yaml).
  • Per-record field values (positional in md, named in json/yaml).
  • Resilience rules: missing fields → empty, unknown fields → pass-through.
  • On-disk format for md/json/yaml.
  • Web-UI activation (Tab Records in addition to the Raw editor).

What it does not define:

  • Field types (string/number/date/…). v1 is all string. Type system is a separate spec step.
  • Server-side indexing per field. Records live in the inlineText body, not in separate Mongo columns.
  • Validation rules (required, length, regex). Later extension.
  • Schema editing via the UI (add/rename/delete columns) — see §6.

Naming: The kind name is records (plural), consistent with existing KIND_CREATE_OPTIONS in DocumentApp.vue and the list in instructions/web-ui.md. Singular record would follow the same convention as list/tree/sheet/graph — if desired, this is a simple rename explicitly left open here (see §7).


2. Schema and Records Model

2.1 Schema

The schema declares the order and the names of the fields that each record has. It lives as a top-level field in the document, not within the records.

Allowed forms:

Where Form Example
Markdown Front-Matter Comma-separated string (one line) schema: name, email, role
JSON Array of strings "schema": ["name", "email", "role"]
YAML Block or Flow Sequence of strings schema: [name, email, role]

Field name rules:

  • Whitespace around field names is trimmed when reading.
  • Case-sensitive (Namename).
  • An empty string as a field name is not allowed; the parser ignores empty entries (e.g., trailing comma name, email, → 2 fields).
  • Duplicate field names are not allowed; in case of conflict, the parser takes the first occurrence and ignores the rest with a codec warning (no throw — resilience).
  • Recommended form: kebab-case or camelCase. Special characters other than -, _, . are accepted but stylistically undesirable.

Requirement: The schema field must be present and non-empty. If it is missing:

  • Markdown: kind: records without schema: → the codec throws RecordsCodecError("Missing schema in front-matter").
  • JSON/YAML: no top-level schema (or empty array) → RecordsCodecError("Missing or empty schema").

The UI catches this and displays a clear error message in the Records tab; the Raw editor remains usable.

2.2 Record

A record is a Map<FieldName, Value>. v1: all values are strings; null/undefined are normalized to an empty string when read.

Field Type Required Meaning
<schemaField> string no Value for the field; if missing, it is considered empty.

Resilience rules (reading):

  • If a schema field is missing in a record (md: fewer values than schema fields; json/yaml: key missing in object), the value is treated as "" (empty string) — not as an error.
  • If a record has more values than schema fields:
    • Markdown: excess values are moved into an implicit extra._overflow: string[] and re-emitted at the end of the record line when writing (round-trip stable).
    • JSON/YAML: additional object keys not in the schema end up in a per-record extra: Record<string, unknown> (pass-through as with list/tree).
  • Values that are not strings (numbers, booleans, null in json/yaml): normalized to strings via String(value) when reading; null"".

Forward compatibility: As with list/tree, records have an extra field that losslessly rotates unknown keys/excess values.

Canonical form (JSON):

{
  "$meta": { "kind": "records" },
  "schema": ["name", "email", "role"],
  "items": [
    { "name": "Alice", "email": "alice@ex.com", "role": "admin" },
    { "name": "Bob",   "email": "bob@ex.com",   "role": "user" },
    { "name": "Eve",   "email": "eve@ex.com",   "role": "viewer" }
  ]
}

An empty records document (items: []) is valid — it is an empty table with a defined schema.

Header convention per format: identical to doc-kind-items — both JSON and YAML carry kind in a $meta mapping at the top level. schema deliberately remains outside of $meta — as an array, it is not a scalar and would be filtered by JsonHeaderStrategy/YamlHeaderStrategy anyway. The typed array form is easier to handle in the editor than a flat comma string.


3. On-Disk Formats

3.1 Markdown

---
kind: records
schema: name, email, role
---
- Alice, alice@ex.com, admin
- Bob, bob@ex.com, user
- "Eve, M.D.", eve@ex.com, "viewer, read-only"

One bullet line per record. Values are positionally assigned to the schema — the first value is name, the second email, the third role.

Reading rules:

  • Front-matter like list/tree (--- fences). schema: is mandatory; all other front-matter keys go into doc.extra (pass-through).
  • Body: Bullet lines (\s*[-*]\s+) are records. Each bullet line is parsed CSV-light:
    • Values are comma-separated.
    • A value in "…" is a quoted value: a comma inside is part of the value, "" inside the quote is a literal ". Example: "Eve, M.D.".
    • Whitespace around separating commas is trimmed; whitespace within a quoted value is preserved.
    • Empty value (two consecutive commas, Alice, , admin) → "".
  • Continuation lines (multi-line values): not supported in v1. To use values with actual newlines, use JSON/YAML. Markdown records remain single-line per bullet (resilience: subsequent lines without a bullet are ignored, not merged).
  • Skip indent: kind: records is flat. Indented bullets are normalized to top-level (like kind: list).

Writing rules:

  • Pre-order, one record per bullet line.
  • Value encoding per field:
    • If the value contains no special characters (, or " or leading/trailing whitespace): written unquoted, trimmed.
    • Otherwise: quoted ("…"), " inside escaped as "".
    • Values with newlines cannot be written in Markdown — the writer throws RecordsCodecError("Value contains newline; not representable in markdown form"). Resilience applies only when reading existing data; when writing, the writer must honestly abort, otherwise the round-trip would be broken.
  • Trailing newline at EOF.
  • extra._overflow values are appended to the end of the bullet line (comma-separated).

3.2 JSON

{
  "$meta": { "kind": "records" },
  "schema": ["name", "email", "role"],
  "items": [
    { "name": "Alice", "email": "alice@ex.com", "role": "admin" },
    { "name": "Bob",   "email": "bob@ex.com",   "role": "user" }
  ]
}

Reading rules:

  • kind from $meta.kind. Backward compatibility: legacy top-level kind is accepted as a fallback.
  • Top-level object with schema, items. Other top-level keys (except $meta) → doc.extra.
  • items is an array of objects. Each object has 0..n schema field keys; all remaining keys go into the record’s extra.
  • String coercion: non-string values (number/boolean/null/array/object) are converted to strings via String(v), with the exception null"" and undefined"".

Writing rules:

  • 2-space indent.
  • Order of top-level keys: $meta, schema, items, then extra pass-through.
  • Order of record keys: first schema fields in schema order, then extra keys in insertion order.
  • Fields that are empty in the record ("") are written as "" strings (not omitted) — the round-trip should make it clear that the field is known but empty. Fields that are in the schema but were in the record’s extra (should not happen) are promoted to the schema value.

3.3 YAML

$meta:
  kind: records
schema: [name, email, role]
items:
  - name: Alice
    email: alice@ex.com
    role: admin
  - name: Bob
    email: bob@ex.com
    role: user

Single document: top-level mapping with $meta: { kind: records } as the first key, followed by schema and items at the same level. Block-style sequences for items, flow-style for the schema array.

Reading rules:

  • kind is read from $meta.kind; schema and items are top-level keys next to $meta.
  • Schema block and flow forms are equally accepted; canonically written as flow.

Writing rules:

  • Single document, 2-space indent, $meta as the first top-level key.
  • Schema canonically as a flow sequence ([...]) — compact and suitable for the Markdown CSV string.
  • Records canonically as a block mapping (key/value per line).

4. Server Path

Like list/tree/mindmap: no dedicated endpoint, no server-side records parser. The editor loads the document via GET /documents/{id}, parses in the browser, writes the serialized body back with PUT /documents/{id}. The HeaderStrategy path automatically mirrors kind: records to DocumentDocument.kind.

schema remains transparent to the server — it is just a front-matter key (md) or a top-level JSON/YAML key that passes through the HeaderStrategy path unchanged.


5. Web-UI

5.1 Editor Activation

  • kind === 'records' + format ∈ {md, json, yaml} → Tabs Records (Default) / Raw.
  • Otherwise: Raw editor only.

When switching Records → Raw, the parsed model is serialized back, the Raw editor shows the canonical form. When switching Raw → Records, the codec re-parses the current body. Round-trip is idempotent — no oscillation via the watch loop.

5.2 Feature Set v1

Feature v1 Note
Read-only render (table) Columns from schema, one row per record
Edit-per-cell (inline) Click → edit, Enter → commit + next row, Tab → next cell
Add row ”+ Entry” at the end, focuses the first cell
Delete row ✕-icon per row
Reorder (drag-drop) rows Drag handle like with list, vue-draggable-plus
Multi-select over rows + Bulk-delete Cmd/Ctrl-Click + Shift-Click like with list
Edit schema (columns) ✓v2 Header editor: rename, add, delete, move — see §5.6
Validation per field v2
Sort / Filter v2

5.3 Keyboard Shortcuts

Key Action
Enter Commit current cell, jump to the same column of the next row (or create new row if at the end)
Tab / Shift+Tab Next / previous cell (across rows, left→right→next row)
Esc Cancel current edits
Backspace (empty) If all cells in the row are empty and Backspace is pressed in an empty cell: delete row, focus on the previous row/column
Cmd/Ctrl-Click Multi-select toggle per row
Shift-Click Range-select per row

5.4 Components

  • <RecordsView> — Top-level container. Receives :doc: RecordsDocument, emits update:doc. Manages:
    • Local schema array (mutable copy + re-emit after mutation).
    • Local items array (mutable copy + re-emit after mutation).
    • Inline edit state (which cell is currently being edited).
    • Multi-selection set (Set of row indices).
    • Schema mode flag (see §5.6).
    • Drag handler via vue-draggable-plus.
  • Schema header displays field names as column titles — read-only in display mode, fully editable in schema mode.
  • Uses VButton for actions, pure Tailwind layout classes for the grid. No DaisyUI classes outside the library.

5.5 Visual Conventions

  • Grid layout: CSS Grid with grid-template-columns: 1.25rem repeat(<n>, minmax(8rem, 1fr)) auto; — drag handle, n columns, action slot (delete row in display mode, “+ column” button in schema mode).
  • Schema header row: sticky top, font-mono for field names, with contrasting background tint in schema mode (hsl(var(--p) / 0.06)).
  • Empty cell: italic-gray placeholder (), clickable like a filled cell.
  • Active cell: border highlight, shadow analogous to the List editor.
  • Body in schema mode: opacity: 0.55 + pointer-events: none — rows remain visible but are inert to cell edit, drag, and row delete.
  • Bulk action bar: sticky at the bottom, identical to the List editor ({count} selected · Clear · Delete selected). Hidden in schema mode.

5.6 Schema Editor (v2)

Toggle-based mode where the schema header row becomes the editable column bar. Accessible via the Edit schema button in the top toolbar. Implemented in RecordsView.vue v2.

Operations (all direct-apply, no separate commit step; each mutation immediately emits a fresh RecordsDocument):

| Operation | UI | Effect on Records


Vancetope — Document Kind records

Specifies the records payload for documents that carry a flat, ordered table — a fixed schema in the metadata plus one record per item, with each record holding values for the schema’s fields. Direct extension of kind: list: same flat structure, same bullet-per-item form in Markdown, but per item a map of schema fields instead of a single text string. See also: doc-kind-items | web-ui


1. Purpose

kind: list is a flat item list with only a text field. kind: records is the natural extension for this: many uniform data records, each record has multiple named fields, the field set is declared once in the document header and applies to all records.

Use cases:

  • CRUD-like lists with multiple columns (people, places, inventory, lookup tables).
  • Input forms for Recipes/Tools where the fields are fixed.
  • Structured note-pads in the style of “one line per entry, columns describe the entry”.

Distinctions:

  • list: one text per item, no schema, no columns. Records is the direct extension — same codec style, same on-disk form, just a richer item model.
  • records: flat, fixed field set, one value per field per record. Table-like. Deliberately no hierarchy — schema invariant applies equally to all records; sub-records would break the global schema concept and are unreliable with current LLM output quality.
  • sheet: a separate kind, to be specified later, for free 2D sheets without schema constraints.
  • dataset/tree-of-records (hypothetical): Hierarchy over record groups is additively possible, but not part of this spec — it would get its own kind.

What this spec defines:

  • Schema declaration in the header (comma-separated in md, array in json/yaml).
  • Per-record field values (positional in md, named in json/yaml).
  • Resilience rules: missing fields → empty, unknown fields → pass-through.
  • On-disk format for md/json/yaml.
  • Web-UI activation (Tab Records in addition to the Raw editor).

What it does not define:

  • Field types (string/number/date/…). v1 is all string. Type system is a separate spec step.
  • Server-side indexing per field. Records live in the inlineText body, not in separate Mongo columns.
  • Validation rules (required, length, regex). Later extension.
  • Schema editing via the UI (add/rename/delete columns) — see §6.

Naming: The kind name is records (plural), consistent with existing KIND_CREATE_OPTIONS in DocumentApp.vue and the list in instructions/web-ui.md. Singular record would follow the same convention as list/tree/sheet/graph — if desired, this is a simple rename explicitly left open here (see §7).


2. Schema and Records Model

2.1 Schema

The schema declares the order and the names of the fields that each record has. It lives as a top-level field in the document, not within the records.

Allowed forms:

Where Form Example
Markdown Front-Matter Comma-separated string (one line) schema: name, email, role
JSON Array of strings "schema": ["name", "email", "role"]
YAML Block or Flow Sequence of strings schema: [name, email, role]

Field name rules:

  • Whitespace around field names is trimmed when reading.
  • Case-sensitive (Namename).
  • An empty string as a field name is not allowed; the parser ignores empty entries (e.g., trailing comma name, email, → 2 fields).
  • Duplicate field names are not allowed; in case of conflict, the parser takes the first occurrence and ignores the rest with a codec warning (no throw — resilience).
  • Recommended form: kebab-case or camelCase. Special characters other than -, _, . are accepted but stylistically undesirable.

Requirement: The schema field must be present and non-empty. If it is missing:

  • Markdown: kind: records without schema: → the codec throws RecordsCodecError("Missing schema in front-matter").
  • JSON/YAML: no top-level schema (or empty array) → RecordsCodecError("Missing or empty schema").

The UI catches this and displays a clear error message in the Records tab; the Raw editor remains usable.

2.2 Record

A record is a Map<FieldName, Value>. v1: all values are strings; null/undefined are normalized to an empty string when read.

Field Type Required Meaning
<schemaField> string no Value for the field; if missing, it is considered empty.

Resilience rules (reading):

  • If a schema field is missing in a record (md: fewer values than schema fields; json/yaml: key missing in object), the value is treated as "" (empty string) — not as an error.
  • If a record has more values than schema fields:
    • Markdown: excess values are moved into an implicit extra._overflow: string[] and re-emitted at the end of the record line when writing (round-trip stable).
    • JSON/YAML: additional object keys not in the schema end up in a per-record extra: Record<string, unknown> (pass-through as with list/tree).
  • Values that are not strings (numbers, booleans, null in json/yaml): normalized to strings via String(value) when reading; null"".

Forward compatibility: As with list/tree, records have an extra field that losslessly rotates unknown keys/excess values.

Canonical form (JSON):

{
  "$meta": { "kind": "records" },
  "schema": ["name", "email", "role"],
  "items": [
    { "name": "Alice", "email": "alice@ex.com", "role": "admin" },
    { "name": "Bob",   "email": "bob@ex.com",   "role": "user" },
    { "name": "Eve",   "email": "eve@ex.com",   "role": "viewer" }
  ]
}

An empty records document (items: []) is valid — it is an empty table with a defined schema.

Header convention per format: identical to doc-kind-items — both JSON and YAML carry kind in a $meta mapping at the top level. schema deliberately remains outside of $meta — as an array, it is not a scalar and would be filtered by JsonHeaderStrategy/YamlHeaderStrategy anyway. The typed array form is easier to handle in the editor than a flat comma string.


3. On-Disk Formats

3.1 Markdown

---
kind: records
schema: name, email, role
---
- Alice, alice@ex.com, admin
- Bob, bob@ex.com, user
- "Eve, M.D.", eve@ex.com, "viewer, read-only"

One bullet line per record. Values are positionally assigned to the schema — the first value is name, the second email, the third role.

Reading rules:

  • Front-matter like list/tree (--- fences). schema: is mandatory; all other front-matter keys go into doc.extra (pass-through).
  • Body: Bullet lines (\s*[-*]\s+) are records. Each bullet line is parsed CSV-light:
    • Values are comma-separated.
    • A value in "…" is a quoted value: a comma inside is part of the value, "" inside the quote is a literal ". Example: "Eve, M.D.".
    • Whitespace around separating commas is trimmed; whitespace within a quoted value is preserved.
    • Empty value (two consecutive commas, Alice, , admin) → "".
  • Continuation lines (multi-line values): not supported in v1. To use values with actual newlines, use JSON/YAML. Markdown records remain single-line per bullet (resilience: subsequent lines without a bullet are ignored, not merged).
  • Skip indent: kind: records is flat. Indented bullets are normalized to top-level (like kind: list).

Writing rules:

  • Pre-order, one record per bullet line.
  • Value encoding per field:
    • If the value contains no special characters (, or " or leading/trailing whitespace): written unquoted, trimmed.
    • Otherwise: quoted ("…"), " inside escaped as "".
    • Values with newlines cannot be written in Markdown — the writer throws RecordsCodecError("Value contains newline; not representable in markdown form"). Resilience applies only when reading existing data; when writing, the writer must honestly abort, otherwise the round-trip would be broken.
  • Trailing newline at EOF.
  • extra._overflow values are appended to the end of the bullet line (comma-separated).

3.2 JSON

{
  "$meta": { "kind": "records" },
  "schema": ["name", "email", "role"],
  "items": [
    { "name": "Alice", "email": "alice@ex.com", "role": "admin" },
    { "name": "Bob",   "email": "bob@ex.com",   "role": "user" }
  ]
}

Reading rules:

  • kind from $meta.kind. Backward compatibility: legacy top-level kind is accepted as a fallback.
  • Top-level object with schema, items. Other top-level keys (except $meta) → doc.extra.
  • items is an array of objects. Each object has 0..n schema field keys; all remaining keys go into the record’s extra.
  • String coercion: non-string values (number/boolean/null/array/object) are converted to strings via String(v), with the exception null"" and undefined"".

Writing rules:

  • 2-space indent.
  • Order of top-level keys: $meta, schema, items, then extra pass-through.
  • Order of record keys: first schema fields in schema order, then extra keys in insertion order.
  • Fields that are empty in the record ("") are written as "" strings (not omitted) — the round-trip should make it clear that the field is known but empty. Fields that are in the schema but were in the record’s extra (should not happen) are promoted to the schema value.

3.3 YAML

$meta:
  kind: records
schema: [name, email, role]
items:
  - name: Alice
    email: alice@ex.com
    role: admin
  - name: Bob
    email: bob@ex.com
    role: user

Single document: top-level mapping with $meta: { kind: records } as the first key, followed by schema and items at the same level. Block-style sequences for items, flow-style for the schema array.

Reading rules:

  • kind is read from $meta.kind; schema and items are top-level keys next to $meta.
  • Schema block and flow forms are equally accepted; canonically written as flow.

Writing rules:

  • Single document, 2-space indent, $meta as the first top-level key.
  • Schema canonically as a flow sequence ([...]) — compact and suitable for the Markdown CSV string.
  • Records canonically as a block mapping (key/value per line).

4. Server Path

Like list/tree/mindmap: no dedicated endpoint, no server-side records parser. The editor loads the document via GET /documents/{id}, parses in the browser, writes the serialized body back with PUT /documents/{id}. The HeaderStrategy path automatically mirrors kind: records to DocumentDocument.kind.

schema remains transparent to the server — it is just a front-matter key (md) or a top-level JSON/YAML key that passes through the HeaderStrategy path unchanged.


5. Web-UI

5.1 Editor Activation

  • kind === 'records' + format ∈ {md, json, yaml} → Tabs Records (Default) / Raw.
  • Otherwise: Raw editor only.

When switching Records → Raw, the parsed model is serialized back, the Raw editor shows the canonical form. When switching Raw → Records, the codec re-parses the current body. Round-trip is idempotent — no oscillation via the watch loop.

5.2 Feature Set v1

Feature v1 Note
Read-only render (table) Columns from schema, one row per record
Edit-per-cell (inline) Click → edit, Enter → commit + next row, Tab → next cell
Add row ”+ Entry” at the end, focuses the first cell
Delete row ✕-icon per row
Reorder (drag-drop) rows Drag handle like with list, vue-draggable-plus
Multi-select over rows + Bulk-delete Cmd/Ctrl-Click + Shift-Click like with list
Edit schema (columns) ✓v2 Header editor: rename, add, delete, move — see §5.6
Validation per field v2
Sort / Filter v2

5.3 Keyboard Shortcuts

Key Action
Enter Commit current cell, jump to the same column of the next row (or create new row if at the end)
Tab / Shift+Tab Next / previous cell (across rows, left→right→next row)
Esc Cancel current edits
Backspace (empty) If all cells in the row are empty and Backspace is pressed in an empty cell: delete row, focus on the previous row/column
Cmd/Ctrl-Click Multi-select toggle per row
Shift-Click Range-select per row

5.4 Components

  • <RecordsView> — Top-level container. Receives :doc: RecordsDocument, emits update:doc. Manages:
    • Local schema array (mutable copy + re-emit after mutation).
    • Local items array (mutable copy + re-emit after mutation).
    • Inline edit state (which cell is currently being edited).
    • Multi-selection set (Set of row indices).
    • Schema mode flag (see §5.6).
    • Drag handler via vue-draggable-plus.
  • Schema header displays field names as column titles — read-only in display mode, fully editable in schema mode.
  • Uses VButton for actions, pure Tailwind layout classes for the grid. No DaisyUI classes outside the library.

5.5 Visual Conventions

  • Grid layout: CSS Grid with grid-template-columns: 1.25rem repeat(<n>, minmax(8rem, 1fr)) auto; — drag handle, n columns, action slot (delete row in display mode, “+ column” button in schema mode).
  • Schema header row: sticky top, font-mono for field names, with contrasting background tint in schema mode (hsl(var(--p) / 0.06)).
  • Empty cell: italic-gray placeholder (), clickable like a filled cell.
  • Active cell: border highlight, shadow analogous to the List editor.
  • Body in schema mode: opacity: 0.55 + pointer-events: none — rows remain visible but are inert to cell edit, drag, and row delete.
  • Bulk action bar: sticky at the bottom, identical to the List editor ({count} selected · Clear · Delete selected). Hidden in schema mode.

5.6 Schema Editor (v2)

Toggle-based mode where the schema header row becomes the editable column bar. Accessible via the Edit schema button in the top toolbar. Implemented in RecordsView.vue v2.

Operations (all direct-apply, no separate commit step; each mutation immediately emits a fresh RecordsDocument):

| Operation | UI | Effect on Records

Vancetope — Document Kind records

Specifies the records payload for documents that carry a flat, ordered table — a fixed schema in the metadata plus one record per item, with each record holding values for the schema’s fields. Direct extension of kind: list: same flat structure, same bullet-per-item form in Markdown, but per item a map of schema fields instead of a single text string. See also: doc-kind-items | web-ui


1. Purpose

kind: list is a flat item list with only a text field. kind: records is the natural extension for this: many uniform data records, each record has multiple named fields, the field set is declared once in the document header and applies to all records.

Use cases:

  • CRUD-like lists with multiple columns (people, places, inventory, lookup tables).
  • Input forms for Recipes/Tools where the fields are fixed.
  • Structured note-pads in the style of “one line per entry, columns describe the entry”.

Distinctions:

  • list: one text per item, no schema, no columns. Records is the direct extension — same codec style, same on-disk form, just a richer item model.
  • records: flat, fixed field set, one value per field per record. Table-like. Deliberately no hierarchy — schema invariant applies equally to all records; sub-records would break the global schema concept and are unreliable with current LLM output quality.
  • sheet: a separate kind, to be specified later, for free 2D sheets without schema constraints.
  • dataset/tree-of-records (hypothetical): Hierarchy over record groups is additively possible, but not part of this spec — it would get its own kind.

What this spec defines:

  • Schema declaration in the header (comma-separated in md, array in json/yaml).
  • Per-record field values (positional in md, named in json/yaml).
  • Resilience rules: missing fields → empty, unknown fields → pass-through.
  • On-disk format for md/json/yaml.
  • Web-UI activation (Tab Records in addition to the Raw editor).

What it does not define:

  • Field types (string/number/date/…). v1 is all string. Type system is a separate spec step.
  • Server-side indexing per field. Records live in the inlineText body, not in separate Mongo columns.
  • Validation rules (required, length, regex). Later extension.
  • Schema editing via the UI (add/rename/delete columns) — see §6.

Naming: The kind name is records (plural), consistent with existing KIND_CREATE_OPTIONS in DocumentApp.vue and the list in instructions/web-ui.md. Singular record would follow the same convention as list/tree/sheet/graph — if desired, this is a simple rename explicitly left open here (see §7).


2. Schema and Records Model

2.1 Schema

The schema declares the order and the names of the fields that each record has. It lives as a top-level field in the document, not within the records.

Allowed forms:

Where Form Example
Markdown Front-Matter Comma-separated string (one line) schema: name, email, role
JSON Array of strings "schema": ["name", "email", "role"]
YAML Block or Flow Sequence of strings schema: [name, email, role]

Field name rules:

  • Whitespace around field names is trimmed when reading.
  • Case-sensitive (Namename).
  • An empty string as a field name is not allowed; the parser ignores empty entries (e.g., trailing comma name, email, → 2 fields).
  • Duplicate field names are not allowed; in case of conflict, the parser takes the first occurrence and ignores the rest with a codec warning (no throw — resilience).
  • Recommended form: kebab-case or camelCase. Special characters other than -, _, . are accepted but stylistically undesirable.

Requirement: The schema field must be present and non-empty. If it is missing:

  • Markdown: kind: records without schema: → the codec throws RecordsCodecError("Missing schema in front-matter").
  • JSON/YAML: no top-level schema (or empty array) → RecordsCodecError("Missing or empty schema").

The UI catches this and displays a clear error message in the Records tab; the Raw editor remains usable.

2.2 Record

A record is a Map<FieldName, Value>. v1: all values are strings; null/undefined are normalized to an empty string when read.

Field Type Required Meaning
<schemaField> string no Value for the field; if missing, it is considered empty.

Resilience rules (reading):

  • If a schema field is missing in a record (md: fewer values than schema fields; json/yaml: key missing in object), the value is treated as "" (empty string) — not as an error.
  • If a record has more values than schema fields:
    • Markdown: excess values are moved into an implicit extra._overflow: string[] and re-emitted at the end of the record line when writing (round-trip stable).
    • JSON/YAML: additional object keys not in the schema end up in a per-record extra: Record<string, unknown> (pass-through as with list/tree).
  • Values that are not strings (numbers, booleans, null in json/yaml): normalized to strings via String(value) when reading; null"".

Forward compatibility: As with list/tree, records have an extra field that losslessly rotates unknown keys/excess values.

Canonical form (JSON):

{
  "$meta": { "kind": "records" },
  "schema": ["name", "email", "role"],
  "items": [
    { "name": "Alice", "email": "alice@ex.com", "role": "admin" },
    { "name": "Bob",   "email": "bob@ex.com",   "role": "user" }
  ]
}

An empty records document (items: []) is valid — it is an empty table with a defined schema.

Header convention per format: identical to doc-kind-items — both JSON and YAML carry kind in a $meta mapping at the top level. schema deliberately remains outside of $meta — as an array, it is not a scalar and would be filtered by JsonHeaderStrategy/YamlHeaderStrategy anyway. The typed array form is easier to handle in the editor than a flat comma string.


3. On-Disk Formats

3.1 Markdown

---
kind: records
schema: name, email, role
---
- Alice, alice@ex.com, admin
- Bob, bob@ex.com, user
- "Eve, M.D.", eve@ex.com, "viewer, read-only"

One bullet line per record. Values are positionally assigned to the schema — the first value is name, the second email, the third role.

Reading rules:

  • Front-matter like list/tree (--- fences). schema: is mandatory; all other front-matter keys go into doc.extra (pass-through).
  • Body: Bullet lines (\s*[-*]\s+) are records. Each bullet line is parsed CSV-light:
    • Values are comma-separated.
    • A value in "…" is a quoted value: a comma inside is part of the value, "" inside the quote is a literal ". Example: "Eve, M.D.".
    • Whitespace around separating commas is trimmed; whitespace within a quoted value is preserved.
    • Empty value (two consecutive commas, Alice, , admin) → "".
  • Continuation lines (multi-line values): not supported in v1. To use values with actual newlines, use JSON/YAML. Markdown records remain single-line per bullet (resilience: subsequent lines without a bullet are ignored, not merged).
  • Skip indent: kind: records is flat. Indented bullets are normalized to top-level (like kind: list).

Writing rules:

  • Pre-order, one record per bullet line.
  • Value encoding per field:
    • If the value contains no special characters (, or " or leading/trailing whitespace): written unquoted, trimmed.
    • Otherwise: quoted ("…"), " inside escaped as "".
    • Values with newlines cannot be written in Markdown — the writer throws RecordsCodecError("Value contains newline; not representable in markdown form"). Resilience applies only when reading existing data; when writing, the writer must honestly abort, otherwise the round-trip would be broken.
  • Trailing newline at EOF.
  • extra._overflow values are appended to the end of the bullet line (comma-separated).

3.2 JSON

{
  "$meta": { "kind": "records" },
  "schema": ["name", "email", "role"],
  "items": [
    { "name": "Alice", "email": "alice@ex.com", "role": "admin" },
    { "name": "Bob",   "email": "bob@ex.com",   "role": "user" }
  ]
}

Reading rules:

  • kind from $meta.kind. Backward compatibility: legacy top-level kind is accepted as a fallback.
  • Top-level object with schema, items. Other top-level keys (except $meta) → doc.extra.
  • items is an array of objects. Each object has 0..n schema field keys; all remaining keys go into the record’s extra.
  • String coercion: non-string values (number/boolean/null/array/object) are converted to strings via String(v), with the exception null"" and undefined"".

Writing rules:

  • 2-space indent.
  • Order of top-level keys: $meta, schema, items, then extra pass-through.
  • Order of record keys: first schema fields in schema order, then extra keys in insertion order.
  • Fields that are empty in the record ("") are written as "" strings (not omitted) — the round-trip should make it clear that the field is known but empty. Fields that are in the schema but were in the record’s extra (should not happen) are promoted to the schema value.

3.3 YAML

$meta:
  kind: records
schema: [name, email, role]
items:
  - name: Alice
    email: alice@ex.com
    role: admin
  - name: Bob
    email: bob@ex.com
    role: user

Single document: top-level mapping with $meta: { kind: records } as the first key, followed by schema and items at the same level. Block-style sequences for items, flow-style for the schema array.

Reading rules:

  • kind is read from $meta.kind; schema and items are top-level keys next to $meta.
  • Schema block and flow forms are equally accepted; canonically written as flow.

Writing rules:

  • Single document, 2-space indent, $meta as the first top-level key.
  • Schema canonically as a flow sequence ([...]) — compact and suitable for the Markdown CSV string.
  • Records canonically as a block mapping (key/value per line).

4. Server Path

Like list/tree/mindmap: no dedicated endpoint, no server-side records parser. The editor loads the document via GET /documents/{id}, parses in the browser, writes the serialized body back with PUT /documents/{id}. The HeaderStrategy path automatically mirrors kind: records to DocumentDocument.kind.

schema remains transparent to the server — it is just a front-matter key (md) or a top-level JSON/YAML key that passes through the HeaderStrategy path unchanged.


5. Web-UI

5.1 Editor Activation

  • kind === 'records' + format ∈ {md, json, yaml} → Tabs Records (Default) / Raw.
  • Otherwise: Raw editor only.

When switching Records → Raw, the parsed model is serialized back, the Raw editor shows the canonical form. When switching Raw → Records, the codec re-parses the current body. Round-trip is idempotent — no oscillation via the watch loop.

5.2 Feature Set v1

Feature v1 Note
Read-only render (table) Columns from schema, one row per record
Edit-per-cell (inline) Click → edit, Enter → commit + next row, Tab → next cell
Add row ”+ Entry” at the end, focuses the first cell
Delete row ✕-icon per row
Reorder (drag-drop) rows Drag handle like with list, vue-draggable-plus
Multi-select over rows + Bulk-delete Cmd/Ctrl-Click + Shift-Click like with list
Edit schema (columns) ✓v2 Header editor: rename, add, delete, move — see §5.6
Validation per field v2
Sort / Filter v2

5.3 Keyboard Shortcuts

Key Action
Enter Commit current cell, jump to the same column of the next row (or create new row if at the end)
Tab / Shift+Tab Next / previous cell (across rows, left→right→next row)
Esc Cancel current edits
Backspace (empty) If all cells in the row are empty and Backspace is pressed in an empty cell: delete row, focus on the previous row/column
Cmd/Ctrl-Click Multi-select toggle per row
Shift-Click Range-select per row

5.4 Components

  • <RecordsView> — Top-level container. Receives :doc: RecordsDocument, emits update:doc. Manages:
    • Local schema array (mutable copy + re-emit after mutation).
    • Local items array (mutable copy + re-emit after mutation).
    • Inline edit state (which cell is currently being edited).
    • Multi-selection set (Set of row indices).
    • Schema mode flag (see §5.6).
    • Drag handler via vue-draggable-plus.
  • Schema header displays field names as column titles — read-only in display mode, fully editable in schema mode.
  • Uses VButton for actions, pure Tailwind layout classes for the grid. No DaisyUI classes outside the library.

5.5 Visual Conventions

  • Grid layout: CSS Grid with grid-template-columns: 1.25rem repeat(<n>, minmax(8rem, 1fr)) auto; — drag handle, n columns, action slot (delete row in display mode, “+ column” button in schema mode).
  • Schema header row: sticky top, font-mono for field names, with contrasting background tint in schema mode (hsl(var(--p) / 0.06)).
  • Empty cell: italic-gray placeholder (), clickable like a filled cell.
  • Active cell: border highlight, shadow analogous to the List editor.
  • Body in schema mode: opacity: 0.55 + pointer-events: none — rows remain visible but are inert to cell edit, drag, and row delete.
  • Bulk action bar: sticky at the bottom, identical to the List editor ({count} selected · Clear · Delete selected). Hidden in schema mode.

5.6 Schema Editor (v2)

Toggle-based mode where the schema header row becomes the editable column bar. Accessible via the Edit schema button in the top toolbar. Implemented in RecordsView.vue v2.

Operations (all direct-apply, no separate commit step; each mutation immediately emits a fresh RecordsDocument):

Operation UI Effect on Records