Tidigar Calendar Storage Specification

See the calendar example for a visual preview of roadmap activities and milestones as calendar events.

Status: public pre-release specification. It defines how another application can read and edit a Tidigar roadmap stored as events in a calendar. The records, fields and validation rules are those of the Tidigar Roadmap Interoperability Specification; this document only defines how a calendar holds them.

License: published by Hamacher Software AB under CC BY 4.0. Anyone may implement the formats it describes, in any software, without asking.

The keywords MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are normative. The current calendar URL and Notes envelope versions are listed in the model specification's version table.

Events

A calendar-backed roadmap consists of exactly one manifest event plus one event for every item, shared view, milestone, period indicator and dimension. The calendar MAY contain unrelated events; a conforming implementation MUST leave them untouched. Every owned event is non-recurring and carries a validated identity URL.

RecordEvent titleNotesDate shape
manifestTidigar Project Metadata – <project name>Manifest Notes envelopeAll-day 2000-01-01 through exclusive 2000-01-02
itemItem titleItem description as ordinary textAll-day start through the day after inclusive end
viewTidigar View – <view name>View Notes envelopeAll-day 2000-01-01 through exclusive 2000-01-02
milestoneMilestone titleEmptyTimed 12:00–12:15 local calendar time on date
periodPeriod titleEmptyAll-day start through the day after inclusive end
dimensionTidigar Dimension – <dimension name>Dimension Notes envelopeAll-day 2000-01-01 through exclusive 2000-01-02

Calendar record type comes only from the URL path. Items and periods MUST be all-day; milestones MUST be timed; manifest, view and dimension records MUST use the fixed metadata day. No Tidigar record may recur. An external move within a milestone's calendar day does not change its canonical date; a later Tidigar write restores the fixed 12:00–12:15 window. Calendar dates MUST be calculated as calendar days rather than 86,400-second durations.

Each record is a separate event so that people who edit the same calendar from several devices, also offline, touch only the records they change. Adding, removing or reordering records never rewrites the manifest event. A calendar provider settles two edits of the same event by keeping one of them.

Calendar URL

Every Tidigar event stores its identity in EKEvent.url:

https://open.tidigar.com/<path>?v=3&p=<projectId22>[&i=<recordId22>&m=<metadata>]

The origin has no user, port or fragment. Query order does not matter. The validated path is the only record-type discriminator. Duplicate known keys, an unknown v, a missing required key, a non-canonical UUID or metadata on the manifest are errors. Unknown keys are ignored and never taken as a newer version.

The address opens the record in the Tidigar apps. Opened in a web browser, it shows a page asking the reader to install the app.

KeymanifestitemviewmilestoneperioddimensionValue
path/project/item/view/milestone/period/dimensionRecord type; the manifest uses /project
vyesyesyesyesyesyesCalendar representation version, currently 3; independent of formatVersion
pyesyesyesyesyesyesmanifest.projectId as a 22-character compact UUID
i—yesyesyesyesyesRecord ID as a 22-character compact UUID
m—yesyesyesyesyesCompact metadata, below

p and i encode the UUID's 16 bytes directly as unpadded base64url. They do not shorten, hash or otherwise replace the canonical UUID; all 128 bits survive.

m is an unpadded base64url representation of deterministic, type-specific binary data. Canonical project JSON and exported JSON retain the descriptive field names in the tables above. The binary transport starts with this header:

ByteMeaning
0Compact metadata codec version, currently 0x02
1Record tag: item 0x01, milestone 0x02, period 0x03, view 0x04, dimension 0x05
2rankLength:varuint, 1 through 32
3…rankLength rank bytes; the last one is not 0

Integers named count or length use canonical unsigned LEB128: the shortest encoding is required. uuid is the UUID's 16 bytes in the same order as its 32 hexadecimal digits. A decoder rejects truncation, trailing data, a wrong record tag, an invalid rank, non-minimal counts, duplicate item dimension IDs and non-canonical base64url.

RecordBytes after the rank
itemprogressIndex:u8, valueCount:varuint, sorted repetitions of dimensionUuid:uuid + optionUuid:uuid, dependencyCount:varuint, repetitions of dependencyUuid:uuid, then, only when startAfter is not empty, one startAfterIndex:u8 per dependency in the same order, then the reference key
milestonedependencyCount:varuint, repetitions of dependencyUuid:uuid, then 1 as one more byte only when fixed is true, then the source, then the reference key
periodred, green and blue as three bytes, then 0 as one more byte only when vacation is false
viewNothing
dimensionNothing

progressIndex is 0 through 4, representing 0, 25, 50, 75 and 100. startAfterIndex uses the same scale; 4 marks a dependency without a share, and at least one is below 4. The reference key ref is written only when the record has one: the byte 0x52, refLength:varuint from 1 through 128, and the key's UTF-8 bytes. A byte 0x52 where a startAfterIndex or the fixed byte could start is the reference key. An incoming milestone's source is written only when it has one, after the fixed byte: the byte 0x53, then source.projectId:uuid and source.id:uuid. Only a fixed milestone has one. Item values sort by dimension UUID bytes; dependency array order remains canonical project order. Title, dates and description are never duplicated in m. Examples, all with the rank 0x80:

TypemDescriptive fields represented
itemAgEBgAIBAAAAAAAAQACAAAAAAAAAAgAAAAAAAEAAgAAAAAAAAAMAprogress: 50, one values entry, empty dependsOn
milestoneAgIBgAEAAAAAAABAAIAAAAAAAAAGone dependsOn UUID, fixed: false
milestoneAgIBgAABempty dependsOn, fixed: true
milestoneAgIBgAABUgNNLTEempty dependsOn, fixed: true, ref: "M-1"
milestoneAgIBgAABUwAAAAAAAEAAgAAAAAAAAAoAAAAAAABAAIAAAAAAAAALfixed: true, source with two UUIDs
periodAgMBgJqntQcolor: "#9aa7b5", vacation: true
periodAgMBgJqntQAcolor: "#9aa7b5", vacation: false
viewAgQBgAthe rank only
dimensionAgUBgAthe rank only

Record order

The rank orders the records of one type. Readers sort items, milestones, period indicators, views and dimensions each by their rank bytes, where a shorter rank that is a prefix of a longer one comes first, and then by record ID. Ranks need not be unique or evenly spaced.

A rank is a fraction in base 256: byte b at position n is worth b / 256^(n+1). Because no rank ends with a 0 byte, there is always a rank strictly between two different ones. A writer SHOULD keep the rank of every record whose relative order did not change and give a moved or new record a rank between its neighbours, so that a reorder rewrites only the records that moved. When a rank would exceed 32 bytes, the writer gives every record of that type a new rank.

ValueMaximum UTF-8/decoded bytes
Complete event URL4096
Decoded m metadata3072
Rank32
Complete metadata Notes16384

Exceeding a limit fails the save rather than truncating.

Notes envelopes

The manifest, view and dimension events carry their data in Notes, inside a versioned envelope (format: "tidigar-calendar-notes", version: 2) between delimiter lines. The complete UTF-8 Notes value has this form, with one line feed (LF) at each shown line break and no final line feed:

Tidigar calendar metadata.
--- TIDIGAR-METADATA-V2 ---
{"format":"tidigar-calendar-notes","version":2,"section":"<section>","payload":{...}}
--- END TIDIGAR-METADATA-V2 ---

JSON object key order is insignificant. Writers SHOULD use compact JSON to leave maximum provider headroom. Complete Notes are limited to 16,384 bytes.

SectionPayload keys
manifestmanifest
viewview
dimensiondimension

The manifest payload is the manifest without dimensions; each dimension, with its options, is the payload of its own dimension event.

Reading a calendar roadmap

  1. Find the single manifest event by the fixed metadata day and validate its URL, Notes envelope and manifest payload. A name match is not ownership.
  2. Scan the complete supported model date domain, 1900 through 2200, for events in that same calendar. Select only events whose validated URL has the same project UUID and a supported path/version.
  3. Duplicate record IDs or a second manifest are errors. A record that has no event is not part of the project.
  4. Map event fields, Notes and packed metadata into descriptive canonical records, order each record type by rank, set the manifest's dimensions from the dimension events and validate the whole project.

Scanning only a visible date range is not a complete read. Calendar provider event identifiers are provider-local and MUST NOT enter the canonical project or be used as stable Tidigar identity.

Creating or editing a calendar roadmap

Creation is safe only in a newly created, verified calendar. Editing an existing calendar is safe only after a complete valid Tidigar project has been read from it. A writer MUST:

  1. Validate the complete updated canonical project.
  2. Build the full desired event set using the mapping above, v3 URLs with ranks and Notes envelopes. Preserve unknown URL query items only when they do not duplicate v, p, i or m.
  3. Compare with a fresh provider snapshot to detect concurrent changes.
  4. Create/update/delete only events owned by that validated project. Never delete the calendar or unrelated events.
  5. Read back provider results before acknowledging the save. Truncation or an incomplete record set is an error, never a partial success.

The compact m examples above are sufficient to verify byte ordering and encoding independently.