Vancetope — Document Kind timeline
Periods and points in time on a self-declared axis — geological eras in millions of years, a sequence of events in minutes, project phases, a biography. With parallel lanes, nesting (era ⊃ period ⊃ epoch), and explicitly drawn uncertainty. Read-only in v1; YAML/JSON, no Markdown. Resides in the Calendar Addon (
vance-addon-brain-calendar), but shares only codec conventions withcalendar, not the data model. See also: doc-kind-calendar (Appointments), doc-kind-diagram (Mermaid, incl.gantt/timeline), app-calendar, web-ui §7.
1. Purpose
Use cases: geological and historical epochs, reconstruction of an event sequence
(crime scene, incident post-mortem, accident), project phases over quarters, resume,
process stages, the chronology of a narrative. The primary use case, as with
calendar, is LLM output — the Worker reads sources and stores the extracted
chronology as a Timeline Document; secondarily, the user maintains it in the Source tab.
The fundamental difference from all existing solutions is the axis. A calendar has an implicit axis — the Gregorian calendar — and therefore cannot represent “201.4 million years before present” (no ISO-8601 instant) nor parallel actors at minute resolution (the monthly grid has no Y-axis). A timeline declares its axis: either a pure number line with a freely chosen unit or absolute timestamps. Both project onto the same number line; only the labeling of the ruler differs. This is precisely why deep time and minute reconstruction fit into one kind — they lie on opposite sides outside the reach of a calendar.
1.1 Delimitations
| Choose … | If |
|---|---|
timeline |
Spans and moments on a declared axis. Parallel strands. Nesting. Everything outside calendar range: millions of years, “hours after alarm”, BC. |
calendar |
Appointments a person keeps: meetings, deadlines, standups, vacation. Anything you would put on Google Calendar. |
diagram (Mermaid) |
A drawing — flowchart, sequence, state machine. Mermaid’s own timeline type is not a replacement (see §1.2). |
records |
Tabular data without a primary time axis. |
tree / mindmap |
Hierarchy without a time axis. |
Deciding question: Does the distance between two entries mean something?
Yes → timeline. No → diagram or list.
1.2 Why diagram is not sufficient
diagram (Mermaid) is the obvious but insufficient excuse:
- Mermaid’s
timelinetype has no proportional axis — entries are equally spaced, regardless of whether three days or thirty million years lie between them. This eliminates the one statement a timeline makes. - Mermaid’s
ganttcannot handle negative years or any unit other than date/time. - Neither supports uncertainty or nesting.
diagram.sourceis opaque text. An agent cannot modify an entry individually, query anything, andkind_validatehas nothing to check.
1.3 Vancetope does not do project management
A timeline draws time. No task dependencies, no resource allocation,
no progress in percent, no critical path. This adheres to the delimitation
drawn by doc-kind-calendar.md §1 and vision.md §7: if you need a Gantt chart
in that sense, export to MS Project / Linear / GitHub Projects.
Consequence in the model, intentionally so: there is no dependsOn. Causality in a
sequence of events (“because X, then Y”) belongs in notes, not in rendered arrows — with
arrows, it would be a Gantt, and the boundary would be removed through the back door. parent is
containment (the Upper Jurassic is within the Jurassic), not “after”.
1.4 Design Principles
One axis per document. A document that mixes 201.4 and 2026-03-04T21:40
has no defensible order. The mode is therefore a
document declaration, against which every position is read. Mixing is a
modeling error, not a feature. Two subjects on different scales are
two documents.
One entry type, not two. Period and event differ in
exactly one field — to — just as a calendar event with end differs from one without.
Two arrays (periods[] + events[]) would be two schemas for the same thing and
two rendering paths that would drift apart.
Nesting as a flat list plus parent. As with
Canvas Grouping: one update path instead of one per level, and
depth remains a matter of content rather than document structure. Nested
YAML objects would have required separate parser and write code for each level.
Uncertainty is content, not abbreviation. “Last seen between 21:40 and 22:05”
and “201.4 ± 0.2 Ma” are the substance of a reconstruction or a
geological table. A model that cannot express this forces the author
into notes — and the drawing then claims a hard edge where there is none.
It claims a precision that no one established. This is the one
thing that cannot be retrofitted without touching every existing document,
which is why it is in the schema from the start.
Read-only in v1. The view renders (with zoom/pan and detail panel), the
Source tab edits. Consistent with mindmap, slides, calendar.
Positions are strings. The codec stores them verbatim, so the file
comes back as it was typed (201.40 remains 201.40). They are resolved
only against the axis — see §4.
1.5 What this spec does not define
- Editing by drag (moving bars, pulling edges). v2.
- An app level (
app: timeline) for multiple superimposed timelines, analogous tocalendar→app: calendar. Will come when the need arises. - Filtering by tags or lanes in the view.
- Granular tools (
timeline_add_entry). v1 writes the entire body. - Import/Export (no foreign formats; there is no standard that combines deep time, lanes, and uncertainty).
- Zoom synchronization between multiple timelines.
2. Data Model
2.1 Top-Level
| Field | Type | Required | Meaning |
|---|---|---|---|
kind |
string = "timeline" |
yes | Dispatcher recognition. |
title |
string |
no | Heading above the ruler. The Mongo title is metadata and invisible to a reader seeing an embedded timeline — therefore the body may have its own. |
axis |
object |
no¹ | Axis declaration, see §2.2. |
lanes |
array | object |
no | Lanes in rendering order, see §2.3. |
entries |
array<Entry> |
yes | Flat list of periods and points, see §2.4. |
extra |
object |
no | Pass-through for unknown top-level keys, round-trip stable. |
¹ If axis is missing, a forward-running number line without a unit applies. This is a
fallback, not a recommendation — timeline_create explicitly requires axis.mode (§6).
2.2 Axis
| Field | Type | Default | Meaning |
|---|---|---|---|
mode |
enum |
numeric |
numeric = bare numbers with unit. datetime = ISO-8601. |
unit |
string |
– | Only numeric: Suffix of tick labels. Free — Ma, ka, yr BP, min, days. Deliberately not an Enum: the set of units people write on a timeline is open; an Enum would make every new one a code change. |
direction |
enum |
forward |
Only numeric: forward = larger number later. ago = larger number earlier. |
from |
string |
– | Optional left boundary of the visible window. Missing → the view fits the entries. |
to |
string |
– | Optional right boundary. |
label |
string |
– | Optional line below the ruler, e.g., “Millions of years before present”. |
extra |
object |
– | Pass-through. |
mode and direction are read leniently: an unknown value falls back to
numeric or forward respectively, instead of causing the document to fail. A typo
should render something the author can see and correct; it is named by
kind_validate (§5).
2.3 Lane
Three accepted spellings, equivalent, order = rendering order:
lanes: [taeter, opfer, zeuge] # only Ids
lanes: # canonical
- id: strat
title: Stratigraphy
color: blue
lanes: # Map, like _app.yaml of the Calendar app
design: { title: Design, color: blue }
backend: { title: Backend }
The map form is deliberately included: it is the form that
app-calendar §3 uses for lanes, and a model that has
seen them reproduces them here.
| Field | Type | Required | Meaning |
|---|---|---|---|
id |
string |
yes | Referenced by entry.lane. |
title |
string |
no | Display name; fallback is the id. |
color |
string |
no | Palette name or CSS color; entries in this lane without their own color inherit it. |
Lanes are pure display order — no Scope, no rights, nothing cascades
along a lane (same rule as for sections in
app-binder and Session Groups).
Declaring a lane buys two things: its position, and a lane that exists while empty. The latter is not cosmetic — “for the witness, there is no record on this night” is a statement, and a lane derived from entries could not make it. A lane named only by an entry still renders — after all declared ones, in order of first appearance.
2.4 Entry
| Field | Type | Required | Meaning |
|---|---|---|---|
id |
string |
no¹ | Stable identifier. Only needed as a target for parent. |
title |
string |
yes | Display label. |
from |
string |
yes | Start position, read against the axis. |
to |
string |
no | End position. Present = Period (bar), missing = Point (marker). |
fromEarliest |
string |
no | Earliest possible start. |
fromLatest |
string |
no | Latest possible start. |
toEarliest |
string |
no | Earliest possible end. |
toLatest |
string |
no | Latest possible end. |
lane |
string |
no | Lane ID; missing → unnamed default lane. |
parent |
string |
no | ID of the entry this one is nested within. |
color |
string |
no | Palette name or CSS color. |
tags |
array<string> |
no | Free filter tags. |
notes |
string |
no | Multi-line description — the evidence, the source, the justification. |
extra |
object |
no | Pass-through. |
¹ Required field in the sense of “must be present in the end” — the codec fills missing
IDs during parsing with a UUID, so that parent references and selection in the
view have something to attach to.
Aliases on the read side: at and start are read as from, end and
until as to. Models constantly use these words — at: simply reads better
for a point — and the alternative to accepting them is an entry that
silently disappears. When writing, the codec normalizes to the
canonical names, so a document converges after a save.
2.5 Validation (Codec)
The codec is permissive, as with calendar:
- Entries without
titleor without a start position are silently dropped. A defective entry must not cost the reader the other forty. - Unknown keys land in
extra. - Positions are not checked against the axis — the codec stores strings.
- An unknown
mode/directionfalls back instead of failing.
This trade-off is only defensible if something indicates what has disappeared. That
is kind_validate (§5) — and the view, which displays a note about unplaceable
entries (§7).
3. Format Mapping
YAML canonical, JSON dual, Markdown not. An axis declaration plus lanes
plus four uncertainty boundaries per entry do not survive a Markdown table — the same
reasoning as for calendar §3, even clearer here. MD-Body → KindCodecException.
3.1 YAML (canonical)
$meta:
kind: timeline
title: Mesozoic
axis:
mode: numeric
unit: Ma
direction: ago
label: Millions of years before present
lanes:
- id: strat
title: Stratigraphy
- id: fauna
title: Fauna
color: green
entries:
- id: trias
title: Triassic
from: 251.9
to: 201.4
lane: strat
- id: jura
title: Jurassic
from: 201.4
to: 143.1
lane: strat
- id: oberjura
title: Upper Jurassic
from: 161.5
to: 143.1
parent: jura
lane: strat
- id: tr-j
title: Triassic-Jurassic Extinction Event
from: 201.4
fromEarliest: 201.6
fromLatest: 201.2
lane: fauna
color: red
3.2 Numbers remain numbers
A position that is a pure number is written unquoted
(from: 201.4, not from: '201.4'), so that the file looks as a
human would write it. Two pitfalls behind this, both addressed in the codec:
- Integers must not become floating-point numbers. SnakeYAML tags a
BigDecimalas a Float and emits1969.0for an integer — a handwrittenfrom: 5would be rewritten on every save. Therefore, integral literals are serialized asLong/BigInteger, decimal asBigDecimal(ScalarCoercion.numberOrString). - The YAML tag resolver consumes dates. Unquoted
1969-07-20becomes ajava.util.Date,201.4becomes aDouble. Without re-coercion, the position would not be a string and the entry would be dropped as positionless. The helperScalarCoercionis therefore shared with the Calendar codec — two copies would mean two interpretations for the same YAML line, depending on which kind reads it.
For authors, however: quote ISO dates in YAML. The codec handles it, other tools do not.
4. The Axis — Projection and Semantics
TimelineScale (Java) and timelinePosition() (TypeScript) map a position to
a number, where “earlier” equals “smaller”. This is the only calculation
for which this kind exists; everything else in the renderer is rectangles.
4.1 mode: numeric
The number as written. The unit lives in axis.unit and never in the value —
from: 201.4 Ma is unreadable and the entry will not be drawn.
direction: ago negates the projection: a larger number is earlier. To be used
for any “before…” scale (geology, archaeology, “days before incident”). The
consequence that must be understood correctly: a period then runs from the larger
to the smaller number.
axis: { mode: numeric, unit: Ma, direction: ago }
entries:
- title: Jurassic
from: 201.4 # earlier — the LARGER number
to: 143.1 # later
from: 143.1, to: 201.4 there means that the Jurassic ends before it begins. This is
by far the most common error with this kind, and therefore a separate
validator rule with its own error message (§5).
Without direction: ago, the alternative would have been to write negative numbers
(from: -201.4). Rejected: no one writes geological eras that way, and the
tick labels would have to hide the minus again.
4.2 mode: datetime
Epoch seconds from an ISO-8601 value. Deliberately more permissive than java.time:
| Form | Example | Meaning |
|---|---|---|
| Full Instant | 2026-03-04T21:40:30+01:00 |
as written |
| Without Offset | 2026-03-04T21:40 |
positioned as UTC (see below) |
| Date Only | 2026-03-04 |
midnight |
| Year Only | 1969 |
Beginning of the year — 1969 sorts before 1969-07-20 |
| Negative Year | -0044-03-15 |
BC |
Year-only is not a convenience, but the normal case for a historical
timeline (“History of Printing”). Negative years are why
gantt fails as a replacement.
Time zones: a value without an offset is positioned as UTC, and the ruler
labels itself with the same numbers. A reader in another zone therefore sees
the time the author typed, not a shifted one. For a
reconstruction, this is the right choice: “21:40” is a statement about the
night of the crime, not an instant that needs conversion. (The calendar decides
this the other way around — there, an appointment is an instant in the reader’s life.)
An impossible date (2026-02-31, 2026-13-01) is unreadable, not
rolled. Both implementations check this explicitly, because both LocalDateTime
and Date.UTC would otherwise silently continue to March 3rd, and the entry would be
drawn a few days off.
4.3 Error Handling
position() returns null instead of throwing. An unreadable position means
an entry that the renderer omits and the validator names — never a
document that fails to open.
5. Semantic Validation (kind_validate)
TimelineKindHandler.validate is the payoff for the permissive codec. Three
classes of problems, each with a stable code:
| Code | Level | When |
|---|---|---|
timeline.mime |
error | Markdown body. |
timeline.parse |
error | YAML/JSON corrupted. |
timeline.entries-not-a-list |
error | entries is not a list. |
timeline.entry.not-an-object |
error | List element is not an object (dropped). |
timeline.entry.title-missing |
error | Entry without title (dropped) — with Index. |
timeline.entry.from-missing |
error | Entry without start position (dropped) — with index. |
timeline.entry.position-unreadable |
error | Position that the axis cannot read; message names the expected format of the declared axis. |
timeline.entry.reversed |
error | Period ends before it begins; on ago axes with the “larger number” hint. |
timeline.entry.uncertainty-window |
error | Window inverted or next to the point it qualifies. |
timeline.entry.end-bounds-without-end |
warning | toEarliest/toLatest without to. |
timeline.entry.parent-unknown |
warning | parent points to no ID — renders at level 0. |
timeline.entry.parent-cycle |
error | Circular parent chain (incl. self-parent). |
timeline.entry.duplicate-id |
error | ID used multiple times; once per ID. |
timeline.entry.lane-undeclared |
warning | Lane not in lanes — once per lane, and only if the document declares any lanes at all. |
timeline.axis.mode-unknown |
warning | Unknown mode; names the fallback. |
timeline.axis.direction-unknown |
warning | Unknown direction. |
timeline.axis.unit-ignored |
warning | unit on a datetime axis. |
timeline.axis.bounds-unreadable |
warning | axis.from/axis.to unreadable. |
timeline.axis.bounds-reversed |
warning | Axis window empty or inverted. |
Two decisions within this that are not obvious:
Dropped entries are reported with an index (entries[7]), not as a count. The
promoted list no longer contains them; “3 entries discarded” is not information
with which someone can repair the file. The validator therefore runs a second time
over the raw body (TimelineCodec.rawBody).
An uncertainty window must contain the position it qualifies — error, not warning. A window next to its point is worse than no window: it draws a statement the author did not make, and they cannot see that it is wrong.
Not reported is an undeclared lane in a document that declares no lanes at all — declaring is optional, and a validator that complains about correct files will be ignored.
6. Tooling — timeline_create
Named tool instead of doc_create_kind(kind="timeline"), for the same reason
calendar_create exists: a capability not explicitly offered by the tool inventory
is replaced by something from its training data.
Without this tool, “a timeline of geological eras” reliably becomes a
Mermaid gantt (which cannot handle negative years) or a Markdown table.
timeline_create(
title="Sequence of Events March 4/5",
axis={ "mode": "datetime", "label": "Night of March 4 to 5" },
lanes=[
{ "id": "opfer", "title": "Victim" },
{ "id": "taeter", "title": "Suspect", "color": "red" },
{ "id": "polizei", "title": "Police", "color": "blue" }
],
entries=[
{ "title": "Victim last seen", "from": "2026-03-04T21:40",
"fromLatest": "2026-03-04T22:05", "lane": "opfer",
"notes": "Neighbor's statement, time imprecise" },
{ "title": "Fire", "from": "2026-03-04T22:10", "to": "2026-03-04T23:00",
"toLatest": "2026-03-04T23:30", "lane": "opfer", "color": "orange" },
{ "title": "Emergency call", "from": "2026-03-04T22:31", "lane": "polizei" }
],
outputPath="timelines/tathergang.yaml"
)
Return: { path, entryCount, periodCount, laneCount, size, vanceUri, markdownLink }
(plus replaced: true when overwriting). Default path is
timelines/<title-slug>-<timestamp>.yaml.
Two hard points:
axis.mode is required and not inferred. Inference would have to guess between
“these are years” and “these are millions of years ago”, and the two
differ only in the author’s intent — a wrong guess draws the
timeline mirrored, without any error anywhere.
A position unreadable for the axis is rejected by the tool, not written. The codec would keep the string and the renderer would omit the entry: the model would report “written” while the reader sees a gap. A successful call therefore means that every entry is drawn.
Editing in v1 happens via the entire body (doc_read → modify → doc_edit),
followed by kind_validate. Granular tools are deliberately absent — the same
v1 boundary as for calendar.
7. Renderer (Web-UI)
TimelineView.vue in the Calendar Addon, federatedly exposed as
vance_addon_calendar/TimelineView; registered via the
Kind Registry for the Cortex tab and in
kindRenderers/registry.ts for the inline and embedded channels
(Fence vance-timeline, Icon ⏳).
Three drawing rules convey meaning:
Bars or markers. to present → bar. Missing → diamond plus label.
Stack lanes, pack rows. One band per lane; within it, for each nesting depth, greedy first-fit packs into as few rows as overlaps allow. An era and its epochs thus land on separate rows (different depth, readable as indentation), two simultaneous independent events stack (overlap). The minimum spacing is a fraction of the current viewport, so that two almost simultaneous entries get separate rows even when zoomed out, instead of overlapping their labels.
Uncertainty faded, never hard. The full range including fuzzy edges is
drawn faintly, the safe core above it in full opacity: from
max(from, fromLatest) to min(to, toEarliest). A window wider than
the bar leaves no core — which is itself the honest picture. A point
with a window gets a faint band under the diamond.
Interaction: Wheel zooms around the cursor, dragging pans, −/+/Fit as buttons;
clicking an entry opens a detail panel (range, uncertainty, lane,
parent, notes, tags). For datetime, a red line marks “now” if
it is within the window. Positions are displayed in the panel and tooltip
as they appear in the file — no re-formatted guessing.
Range display: the window comes from axis.from/axis.to if declared,
otherwise from fitting all entries (including uncertainty margins, 4% padding).
Unplaceable entries are listed as a note above the drawing, with title and position of the first three — the view does not remain silent about what the codec discarded.
The renderer uses no useI18n(): a federated remote does not share the
host’s i18n instance (see FormFieldsView.vue in the Bistromath Addon), a
key would be rendered as its own path. Labels are English literals, in
accordance with the rule “user-facing Runtime-Strings in English”.
8. Template
_vance/templates/timeline.yaml + timeline.tmpl.yaml
(document-templates): Title, axis mode as select,
unit, “count backwards”, axis label, lanes comma-separated. The
generated body contains a working period and a point depending on the mode,
as well as commented hints on uncertainty and parent.
BundledTimelineTemplateTest renders the template in all three variants
(datetime / numeric / numeric+ago), parses it with the real codec, and validates
it with the real handler. In particular, it checks that the ago example models
the rule it teaches — a template with an inverted period would
teach every user the wrong form.
9. Examples
9.1 Geological Eras (numeric, backward, nested)
See §3.1.
9.2 Sequence of Events (datetime, lanes, uncertainty)
$meta:
kind: timeline
title: Sequence of Events March 4/5
axis:
mode: datetime
label: Night of March 4 to 5
lanes:
- id: opfer
title: Victim
- id: taeter
title: Suspect
color: red
- id: zeuge
title: Witness
- id: polizei
title: Police
color: blue
entries:
- title: Victim last seen
from: "2026-03-04T21:40"
fromLatest: "2026-03-04T22:05"
lane: opfer
notes: Neighbor's statement, time imprecise
- title: Cell phone in North cell tower
from: "2026-03-04T22:02"
lane: taeter
notes: Cell tower query
- title: Fire
from: "2026-03-04T22:10"
to: "2026-03-04T23:00"
toLatest: "2026-03-04T23:30"
lane: opfer
color: orange
- title: Emergency call
from: "2026-03-04T22:31"
lane: polizei
- title: Patrol car arrival
from: "2026-03-04T22:39"
to: "2026-03-05T02:15"
lane: polizei
The zeuge lane remains empty — indicating that nothing is available for the witness on this
night.
9.3 Project Phases (datetime, no uncertainty)
$meta:
kind: timeline
title: Roadmap 2026
axis:
mode: datetime
from: "2026-01-01"
to: "2026-12-31"
lanes: [design, backend, ops]
entries:
- id: discovery
title: Discovery
from: "2026-01-06"
to: "2026-02-27"
lane: design
- title: Design Review
from: "2026-02-20"
lane: design
color: green
- id: api
title: API v1
from: "2026-03-02"
to: "2026-06-30"
lane: backend
- title: Migration
from: "2026-05-04"
to: "2026-06-15"
parent: api
lane: backend
- title: Go-Live
from: "2026-07-01"
lane: ops
color: red
9.4 Historical, Years Only
$meta:
kind: timeline
title: Spread of Printing
axis:
mode: datetime
entries:
- title: Gutenberg Bible
from: "1455"
- title: First printing press in Venice
from: "1469"
- title: Spread in Europe
from: "1470"
to: "1500"
fromLatest: "1480"
10. Implementation
| Component | Location |
|---|---|
| Model | TimelineDocument, TimelineAxis (+ TimelineAxisMode/TimelineDirection), TimelineLane, TimelineEntry |
| Codec | TimelineCodec (+ shared ScalarCoercion) |
| Projection | TimelineScale |
| Kind + Validation | TimelineKindHandler |
| Tool | TimelineCreateTool (timeline_create) |
| Client Codec | client/src/timelineCodec.ts (Mirror, incl. timelinePosition) |
| Renderer | client/src/TimelineView.vue |
| Registration | client/src/register.ts (Kind Registry), vite.config.ts (Federation Expose), vance-face/src/kindRenderers/registry.ts (Inline/Embedded), CreateDocumentModal.vue (Starter Body) |
| Manual | _vance/manuals/doc-kind-timeline.md |
| Prompt Hook | _vance/prompts/arthur/calendar.md |
| Template | _vance/templates/timeline.{yaml,tmpl.yaml} |
All in the vance-addon-brain-calendar module (Java + client/), except for the two
host files in vance-face.
Known divergence between the two codecs: JavaScript numbers cannot
literally retain 201.40; a client serialization loses the
trailing zero, which the Java side retains. Practically irrelevant — the kind is
read-only, edits happen via the raw editor, and saves go through the server.