# Tidigar Roadmap Interoperability Specification See the [roadmap JSON example](https://tidigar.com/extend#file-format-example) for an activity with dates, progress and a dependency in this format. Status: public pre-release specification. It defines how another application, script or AI agent can create, read and edit Tidigar roadmaps stored as JSON files or `.tidigar` documents. Calendar events are described in the [calendar storage specification](https://tidigar.com/agents/docs/calendar.md). License: published by Hamacher Software AB under [CC BY 4.0](https://creativecommons.org/licenses/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 canonical Tidigar project is specified field by field, followed by the envelopes and mappings used by each storage backend. JSON field names remain descriptive; only the calendar URL has a separate packed transport, and shareable share links have their own compact encoding in the [share link specification](https://tidigar.com/agents/docs/share-links.md). The same project can also be written by hand or reviewed as Markdown, as the [Markdown specification](https://tidigar.com/agents/docs/markdown.md) describes. ## Implementer quick start 1. Read a project through one of the [storage profiles](#storage-profiles). 2. Validate and normalize it using every rule in the field tables. Unknown project fields MUST NOT be relied upon: Tidigar drops them when it writes a canonical copy. 3. Preserve array order. IDs are UUIDs and MUST remain stable when editing an existing record. New IDs SHOULD be cryptographically random UUIDv4 values. 4. Write the same storage profile unless the user explicitly converts it. 5. For calendars, follow the [calendar storage specification](https://tidigar.com/agents/docs/calendar.md). A conforming **reader** rejects unsupported versions and invalid required fields rather than guessing. A conforming **writer** emits all required fields, valid references and exactly the current versions in the [version table](#versions). Tidigar is pre-release: there is no compatibility reader or migration for an older incompatible version. ## How to read the tables - **Required:** `yes` means a record without the field is rejected. `no` means a missing field gets the default. `derived` means the value is computed from other fields and any stored value is replaced. - **Rule:** a constraint a valid value MUST meet, such as `max N chars`, `max N entries` or `one of …`. - **Default:** a JSON value in backticks is used when the field is missing; a sentence describes a computed default. - **Calendar:** where the field lives in a calendar event. `URL m.` means inside the compact metadata of the event URL (see the [calendar storage specification](https://tidigar.com/agents/docs/calendar.md#calendar-url)). - **Used by:** where Tidigar shows or uses the field. Normalization is a whitelist: any field not listed here is dropped when a project is loaded or saved. All IDs are UUIDs (versions 1–8), unique across the whole project. Dates are `YYYY-MM-DD` calendar dates from `1900-01-01` through `2200-12-31`, without time or time zone. Colors are `#rrggbb`. ## Project ```text { manifest, items, sharedViews, milestones, periodIndicators } ``` | Collection | Holds | Limit | | ------------------ | ----------------- | ------ | | `items` | Activities | 10,000 | | `sharedViews` | Saved views | 99 | | `milestones` | Milestones | 1,000 | | `periodIndicators` | Period indicators | 1,000 | Array order is meaningful and preserved in every backend; a calendar keeps it as a rank in each record's event URL. ### Manifest | Field | Type | Required | Rule | Default | Calendar | Used by | | -------------------- | ------------------ | -------- | -------------------------------------------------------------------------------------- | ----------------- | ------------------------------------ | ------------------------------------------------------ | | `projectId` | UUID | yes | Project identity; every record's `projectId` must equal it | — | URL `p` on every event | Identity in all storage profiles and links | | `formatVersion` | integer | yes | one of `1` | — | Manifest Notes | Rejects other model versions on load | | `name` | text | yes | max 100 chars; not blank | — | Manifest Notes; manifest event title | Roadmap title, library card, calendar name at creation | | `description` | text | yes | max 500 chars; may be empty | — | Manifest Notes | Roadmap dialog | | `dimensions` | array of dimension | yes | max 20 entries | — | One dimension event each | Board axes, filters, colors, grouping, List columns | | `year` | integer | yes | 1900 through 2200 | — | Manifest Notes | Default timeline range of a view without one | | `schedule` | enum | no | one of `calendar-days`, `work-days`; see [Work days](#work-days) | `"calendar-days"` | Manifest Notes | Date changes, activity dialog, compacting the plan | | `shareLinkCreatedAt` | UTC timestamp | no | ISO 8601 `YYYY-MM-DDTHH:mm:ssZ`, valid date and time | absent | Manifest Notes | Shared version in the library and properties | | `refPrefix` | text | no | 1 to 10 capital letters `A`–`Z` and digits, starting with a letter; omitted when empty | absent | Manifest Notes | Reference keys of new records; library card | `shareLinkCreatedAt` records when the sharing link that created this copy was created. It is preserved when editing or exporting the copy, independently of storage creation and modification times. An older link has no timestamp; readers must not invent one. Sharing the copy again creates a new link timestamp. `refPrefix` starts the [reference keys](#reference-keys) an editor gives new activities and milestones. ### Dimension | Field | Type | Required | Rule | Default | Calendar | Used by | | --------- | --------------- | -------- | --------------------------- | ------- | ---------------------------- | --------------------------------------------- | | `id` | UUID | yes | — | — | Dimension Notes; URL `i` | Item `values`, view axes, filters and columns | | `name` | text | yes | max 100 chars; may be empty | — | Dimension Notes; event title | Dimension labels and column headers | | `options` | array of option | yes | max 100 entries | — | Dimension Notes | Values an item can take in this dimension | ### Option | Field | Type | Required | Rule | Default | Calendar | Used by | | ------- | ----- | -------- | --------------------------- | ------- | --------------- | ------------------------------------------------- | | `id` | UUID | yes | — | — | Dimension Notes | Item `values`, filters, `colorOverrides` | | `name` | text | yes | max 100 chars; may be empty | — | Dimension Notes | Option labels | | `color` | color | yes | `#rrggbb` | — | Dimension Notes | Item color when the view colors by this dimension | ### Item (activity) | Field | Type | Required | Rule | Default | Calendar | Used by | | ------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------- | --------------------------------------- | | `id` | UUID | yes | — | — | URL `i` | Identity, dependencies, links | | `projectId` | UUID | yes | Equals `manifest.projectId` | — | URL `p` | Ownership check | | `title` | text | yes | max 180 chars; may be empty | — | Event title | All views, activity dialog | | `description` | text | yes | max 12000 chars; may be empty | — | Event notes | Activity dialog, List | | `progress` | integer | no | one of `0`, `25`, `50`, `75`, `100` | `0` | URL `m.progress` | Progress bar, List | | `start` | date | yes | Not after `end` | — | All-day event start | Timeline, List, dependencies | | `end` | date | yes | Inclusive last day | — | All-day event end, the day after `end` | Timeline, List, dependencies | | `values` | object | no | Dimension ID → option ID of that dimension, or `""` | `{}` | URL `m.values` | Board placement, filters, colors, List | | `dependsOn` | array of IDs | no | Items and fixed milestones; no duplicates or cycles; `start` at least one day after each item's `end` unless `startAfter` allows overlap, and not before each fixed milestone's `date` | `[]` | URL `m.dependsOn` | Timeline arrows, date cascades | | `startAfter` | object | no | Item ID → one of `0`, `25`, `50`, `75`; IDs outside `dependsOn` are dropped; such an item's `end` rule becomes `start` not before its `start`; omitted when empty | — | URL `m.startAfter` | Overlap, timeline arrows, date cascades | | `ref` | text | no | max 32 chars; omitted when empty; need not be unique | absent | URL `m.ref` | Reference key before the title, search | `startAfter` lets an activity overlap one it depends on. The value is how much of that activity’s planned duration has passed when this one may start, independently of its reported `progress`: `0` with its start, `50` when half its days have passed. An editor places the activity on the day after that share of days, rounded up and counted in work days when `manifest.schedule` is `work-days`, and moves it along when the other activity moves. The stored rule only requires that it does not start before the other activity, so a new vacation period never invalidates a project. An absent entry means 100%: start at least one day after the predecessor ends. The latest threshold applies when there are several dependencies. Fixed milestones cannot appear in `startAfter`; activities may start on their date. Milestones that depend on activities always wait for those activities to end. ### Milestone | Field | Type | Required | Rule | Default | Calendar | Used by | | ----------- | ----------------- | -------- | ------------------------------------------------------------------------------ | ------- | ---------------------------------------------- | -------------------------------------- | | `id` | UUID | yes | — | — | URL `i` | Identity, links | | `projectId` | UUID | yes | Equals `manifest.projectId` | — | URL `p` | Ownership check | | `title` | text | yes | max 180 chars; not blank | — | Event title | Timeline, milestone dialog | | `date` | date | yes | At least one day after each dependency's `end` | — | Timed event 12:00–12:15 local time on that day | Timeline | | `dependsOn` | array of item IDs | no | Items only; no duplicates; empty when fixed | `[]` | URL `m.dependsOn` | Timeline arrows, date cascades | | `fixed` | boolean | no | An event on a set date that items wait for | `false` | URL `m.fixed` | Timeline, date cascades | | `ref` | text | no | max 32 chars; omitted when empty; need not be unique | absent | URL `m.ref` | Reference key before the title, search | | `source` | object | no | `projectId` and `id`, both UUIDs, of another plan's milestone; only when fixed | absent | URL `m.source` | Incoming milestone, warnings | A milestone is either ordinary or fixed. An ordinary milestone, the default, marks a result: it waits for the items in its `dependsOn` and its `date` follows them. A fixed milestone is an event on a set date, such as a decision or a delivery: it waits for nothing, items list it in their own `dependsOn`, and they may start on its `date`. Editors draw an ordinary milestone as a filled diamond and a fixed one as an outlined diamond. Editors move a fixed milestone only when the user does, and move the items that wait for it with it. An _incoming milestone_ is a fixed milestone whose `source` names a milestone of another plan: that plan's `manifest.projectId` and the milestone's `id`. Its `title` and `date` are this plan's own copy. An editor compares them with the newest copy of the source plan it has and shows when the source moved or removed the milestone; it changes the date only when the user takes the change in. Removing `source` makes the milestone this plan's own. A `source` that names this plan itself is invalid. A milestone that stops being fixed loses its `source`, and a share link leaves `source` out, so the milestone opens as an ordinary fixed one. ### Reference keys `ref` is the people-facing key of an activity or a milestone, such as `LPP-12`, or a number from another system typed or pasted by a person, such as a ticket number. It is free text that a person may change at any time. Keys are not unique: two people may give records the same key before their copies merge, so a reader MUST accept duplicates, and an editor SHOULD point them out rather than refuse them. An editor that numbers records gives a new activity or milestone the key `-`, where `n` is one more than the highest number any activity or milestone already uses after that prefix, compared without regard to letter case. It does not number existing records. It derives `refPrefix` once, the first time the project has a name other than the editor's default name for a new roadmap: from two or three words their initials, otherwise the name's first two letters, with letters folded to `A`–`Z` and everything else left out, so `Launch partner portal` gives `LPP`, `Tidigar` `TI` and `Öresund` `OR`. A name without such letters gives no prefix. Renaming the project later does not change the prefix; a person may. ### Period indicator | Field | Type | Required | Rule | Default | Calendar | Used by | | ----------- | ------- | -------- | ------------------------------ | ------- | -------------------------------------- | ---------------------------------- | | `id` | UUID | yes | — | — | URL `i` | Identity, links | | `projectId` | UUID | yes | Equals `manifest.projectId` | — | URL `p` | Ownership check | | `title` | text | yes | max 180 chars; not blank | — | Event title | Timeline band label, period dialog | | `start` | date | yes | Not after `end` | — | All-day event start | Timeline band | | `end` | date | yes | Inclusive last day | — | All-day event end, the day after `end` | Timeline band | | `color` | color | yes | `#rrggbb`, stored in lowercase | — | URL `m.color` | Timeline band color | | `vacation` | boolean | no | No work days during the period | `true` | URL `m.vacation` | Work days in a `work-days` roadmap | ### Work days A roadmap whose `schedule` is `work-days` plans activities in work days: Monday through Friday, except the days of a period whose `vacation` is `true`. An activity's duration is the number of work days from `start` through `end`, at least 1. Editors keep `start` on a work day and `end` on the last work day of that duration, and move the dates when the days off change. Readers must not move stored dates. With `calendar-days`, every day counts and `vacation` has no effect. ### Saved view A saved view is a shared, named presentation. Every field is stored in the view's own event Notes; the event title is `Tidigar View – `. | Field | Type | Required | Rule | Default | Calendar | Used by | | ------------------------------ | -------------------- | -------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------- | | `id` | UUID | yes | — | — | URL `i` | Identity, links, active saved view | | `projectId` | UUID | yes | Equals `manifest.projectId` | — | URL `p` | Ownership check | | `name` | text | yes | max 100 chars; not blank | — | View Notes | Saved-view menu | | `view` | enum | no | one of `grid`, `timeline`, `list`, `flow` | `"grid"` | View Notes | Main mode; `grid` is the Board | | `row` | axis | no | Dimension ID, `"assignee"` or `""` | `""` | View Notes | Board rows | | `col` | axis | no | Dimension ID, `"assignee"` or `""` | `""` | View Notes | Board columns | | `color` | dimension ID | no | Dimension ID or `""` | `""` | View Notes | Coloring in all views and exports | | `presentationStyle` | enum | no | one of `"standard"`, `"minimal"`, `"airy"`, `"contrast"`, `"terminal"` | `"standard"` | View Notes | Coordinated appearance in light and dark mode | | `colorOverrides` | object | no | Option ID → `#rrggbb`; at most 100 kept; unknown options dropped | `{}` | View Notes | Per-view option colors | | `group` | axis | no | Dimension ID, `"assignee"` or `""` | `""` | View Notes | Timeline grouping | | `resolution` | enum | no | one of `year`, `quarter`, `month`, `week` | `"month"` | View Notes | Legacy scale preference | | `timelineScaleRows` | enum | no | one of `1`, `2`, `3` | `3` | View Notes | Maximum timeline scale rows | | `timelineShowWeeks` | boolean | no | — | `true` | View Notes | Allow weeks in the adaptive timeline scale | | `timelineShowQuarters` | boolean | no | — | `true` | View Notes | Allow quarters in the adaptive timeline scale | | `timelineWeekNumbers` | boolean | no | — | `true` | View Notes | Week numbers; otherwise each week's first date | | `timelineShowToday` | boolean | no | — | `false` | View Notes | Today marker in the timeline and exported images | | `timelineShowPeriodNames` | boolean | no | — | `false` | View Notes | Period names in the timeline legend and exported images | | `textMode` | enum | no | one of `hide`, `beside`, `run-past`, `fit`, `under` | `"beside"` | View Notes | Timeline labels, exports | | `filters` | object | no | Dimension ID → option ID or `""`; unknown entries dropped | `{}` | View Notes | Filtering in all views | | `query` | text | no | max 200 chars | `""` | View Notes | Saved search text | | `hideEmptyRows` | boolean | no | — | `true` | View Notes | Board | | `hideEmptyColumns` | boolean | no | — | `true` | View Notes | Board | | `hideEmptyTimelineGroups` | boolean | no | — | `true` | View Notes | Timeline | | `hideUnconnectedActivities` | boolean | no | — | `false` | View Notes | Hide activities without incoming or outgoing dependencies in Flow | | `flowGroupMilestones` | boolean | no | — | `false` | View Notes | Group Flow milestones in a separate leading lane | | `hideEmptyFlowGroups` | boolean | no | — | `false` | View Notes | Hide Flow dimension lanes without visible activities | | `flowHideDates` | boolean | no | — | `false` | View Notes | Hide activity and milestone dates in Flow and image exports | | `showAssignee` | boolean | no | — | `true` | View Notes | Assignee faces and List column in Tidigar Business plans | | `showRefs` | boolean | no | — | `false` | View Notes | Reference keys before titles in Timeline, List and exports | | `timelineActivityRows` | enum | no | one of `single`, `single-label`, `compact` | `single`, or `single-label` when an older `hideTimelineLabels: false` is present | View Notes | Timeline row layout | | `hideTimelineLabels` | boolean | derived | `true` exactly when `timelineActivityRows` is `single` | Computed | View Notes | Fallback only; see removal candidates | | `timelineMilestoneMode` | enum | no | one of `hide`, `marker`, `label`, `flag` | `"label"` | View Notes | Timeline milestones | | `timelineMilestonesBesideWork` | boolean | no | — | `false` | View Notes | Timeline milestones in the row of their latest predecessor | | `timelineSummarizeBy` | dimension ID | no | A manifest dimension ID or `""` | `""` | View Notes | Timeline summary rows within groups | | `timelineWeekDays` | enum | no | one of `work`, `all` | `"work"` | View Notes | Timeline day scale | | `timelineDependencyArrows` | enum | no | one of `none`, `grouped`, `every` | `"grouped"` | View Notes | Timeline dependency arrows | | `flowDirection` | enum | no | one of `auto`, `horizontal`, `vertical` | `"auto"` | View Notes | Flow direction | | `flowArrows` | enum | no | one of `curved`, `right-angle` | `"curved"` | View Notes | Flow dependency arrow shape | | `flowGroup` | dimension ID | no | A manifest dimension ID or `""` | `""` | View Notes | Flow swimlanes | | `flowShapes` | enum | no | one of `capsules`, `flowchart` | `"capsules"` | View Notes | Flow activity and milestone shapes | | `timelineStart` | date | no | Not after `timelineEnd` | January 1 of `manifest.year` | View Notes | Timeline range | | `timelineEnd` | date | no | — | December 31 of `manifest.year` | View Notes | Timeline range | | `listColumns` | array of column keys | no | `title`, `duration`, `start`, `end`, `dependsOn` or a dimension ID; duplicates and unknown keys dropped | `title`, `start`, `end`, then every dimension ID | View Notes | Visible List columns | | `listColumnOrder` | array of column keys | no | Same keys as `listColumns` | Same as `listColumns` | View Notes | List column order, including hidden ones | `"assignee"` in `row`, `col` or `group` arranges activities by the person each is assigned to, with one group per person and one for unassigned activities. `showAssignee` shows assignees on activities and as a List column. Both are meaningful only in a plan stored in Tidigar Business, where assignments are kept beside the plan; the project itself never holds assignments or people. Anywhere else an `"assignee"` axis arranges nothing, as `""` does, `showAssignee` changes nothing, and both values are kept. `duration` is the activity's length in days, or in work days with a work calendar. A view whose `listColumnOrder` does not contain `duration` shows it before the first of `start` and `end`, or after `title` without them. `dependsOn` lists the activity's predecessors by name. A view whose `listColumnOrder` does not contain `dependsOn` shows it after the last of `duration`, `start` and `end`, or after `title` without them. `timelineScaleRows` limits the adaptive time header to at most one, two or three rows. `timelineShowWeeks` and `timelineShowQuarters` allow the corresponding levels by default. Setting either to `false` excludes that level while the remaining levels still adapt to available width. Without weeks, three day-level rows use year/month/day. With `timelineWeekNumbers` set to `false`, each week is labelled with the date of its first day in the reader's own first weekday: the day alone below a month row, otherwise a short date. As space increases, a single row shows year, quarter, month, week, then day; two rows show year/quarter, year/month, month/week, then month/day; three rows show year/month/week, then month/week/day. Quarters, months, weeks and days become visible at average widths of 40, 34, 28 and 22 pixels respectively. Coarser views use fewer rows. `resolution` is retained for compatibility; it no longer fixes the displayed time scale. Year labels remain visible while scrolling horizontally. No row shows a single value: when a row would span the whole visible range with one cell, quarters replace a lone year if they divide the range, and otherwise the row is left out and its label moves into the first cell of the row below, so a view may show fewer rows than configured, but always at least one. Without a year row, the top row names the year in its first cell, and in the first cell of each new year, or in the next cell of that year with room for it; a top row of weeks or days also names the month. `timelineShowToday` shows the Today marker when enabled; it is hidden by default. `timelineShowPeriodNames` lists the periods in the visible range in the timeline's legend, each with its color and name; by default periods show only as colored bands. `timelineDependencyArrows` chooses which dependency arrows a timeline draws: `none` draws no arrow at rest: an arrow appears only while one of its ends is pointed at, focused, dragged, selected or open for editing; `grouped` also draws the arrows between activities in the same timeline group, and treats every other arrow, including every arrow into a milestone, as `none` does; `every` draws every arrow. Without grouping all activities share one group. Printed and exported images draw only the arrows that are always visible. `hideEmptyFlowGroups` defaults to `false`. When enabled, dimension lanes without visible activities are hidden in Flow, including lanes made empty by filters. Milestone lanes remain independent of this choice. `flowGroupMilestones` defaults to `false`: Flow milestones are placed near their dependent activities without dimension membership. When `true`, milestones use a separate lane, leftmost in vertical Flow and topmost in horizontal Flow. `hideUnconnectedActivities` hides activities with no incoming or outgoing dependencies in Flow. Connections to milestones or activities outside the view still count. It affects the view and its exported images. `flowDirection` sets which way a flow view reads. `horizontal` runs each chain of dependencies from left to right and stacks separate chains below each other; `vertical` runs each chain from top to bottom and places separate chains side by side. `auto` reads vertically where the view is taller than it is wide, and otherwise horizontally. Exported and printed images read the way the view is shown. `flowArrows` chooses curved dependency arrows or arrows with right-angle (90-degree) bends. `flowGroup` optionally groups activities into swimlanes in dimension option order. Unset values use a No value lane. Milestones sit near their dependency activities, or in a separate leading lane when `flowGroupMilestones` is enabled. Empty option lanes remain visible unless `hideEmptyFlowGroups` is enabled. Grouping does not assign or change values. `flowShapes` chooses capsules for every node, or `flowchart` with diamonds for activities with more than one distinct incoming dependency, rectangles for other activities, and capsule milestones retaining their milestone marker. Classification includes dependencies hidden by view filters. These independent choices affect Flow, exported and printed images. Milestone capsules represent milestones, not process start/end or decisions. `textMode` sets how an activity's title appears at its bar: `hide` omits it, `beside` places it outside the bar whenever the full title fits after its end or before its start. If neither side has room, a title that fits stays inside; longer titles use the side with more room and truncate. `run-past` lets text run past the bar, `fit` shows only what fits, and `under` shows one line below the bar. The former wrapped-below choice reads as `under`. `timelineMilestoneMode` shows milestones as a diamond (`marker`), a diamond with its title (`label`), a small flag on a thin pole with its title beside it (`flag`), or not at all (`hide`). With `timelineMilestonesBesideWork`, a milestone that is not fixed and waits for shown activities sits in the row of the one that ends last instead of the milestone group; other milestones keep the milestone rows, whose heading is left out when grouping is disabled or when there are no separate milestones. Under `timelineDependencyArrows: "grouped"` such a milestone counts as part of its row's group. `timelineSummarizeBy` adds a summary row for each of its dimension's values within every timeline group, holding the group's activities with that value; activities without a value follow the summaries. A summary spans from the earliest start to the latest end of its shown activities and is computed, not stored. Summarizing by the `group` dimension gives each group one summary. A view never records which summaries are collapsed: collapsing a summary is each reader's own choice, outside the project. A reader ignores a `timelineCollapsedSummaries` field that older data may still hold. Some values were renamed when their default changed. A reader treats the older `timelineMilestoneMode` value `show` as `label`, every older `timelineDependencyArrows` value (`all`, `groups`, `off` and `activities`) as `grouped`, and every older `textMode` value (`clip`, `below`, `wrap` and `overflow`) as `beside`; a milestone diamond without its title is now `marker`. ## Personal view state The current mode, selection, search, unsaved view changes, scroll and zoom are personal and device-local. They are never part of the project, exports, backups, documents or calendars, and this specification defines no schema for it. ## Storage profiles All JSON is UTF-8. Object key order and insignificant whitespace have no meaning; array order is significant. A file extension helps users and tools but does not replace the `format` discriminator. | Profile | Suggested extension | Root discriminator | Contents | | ---------------------------- | ------------------- | ------------------------------------------ | --------------------------------------------------------------- | | Project exchange file | `.json` | `format: "tidigar-project"` | Exactly one canonical project in `project` | | Multi-project browser backup | `.json` | `format: "tidigar-backup"` | Canonical projects in `projects` | | Native document | `.tidigar` | `format: "tidigar-document"`, `version: 1` | One project exchange value in `roadmap`, plus document metadata | | Calendar | — | Valid v3 HTTPS URL on every owned event | One event per project record | Data a browser or app keeps for itself, such as browser storage or personal view state, is not an interchange format and is outside every project payload. ### Project exchange file The root object has exactly this semantic shape: ```json { "format": "tidigar-project", "project": { "manifest": {}, "items": [], "sharedViews": [], "milestones": [], "periodIndicators": [] } } ``` `project` MUST satisfy every canonical-model rule below. Tidigar readers ignore unknown root keys, but interoperable writers SHOULD emit only `format` and `project`. The following is a complete minimal file, not pseudocode: ```json { "format": "tidigar-project", "project": { "manifest": { "projectId": "00000000-0000-4000-8000-000000000001", "formatVersion": 1, "name": "Example roadmap", "description": "", "dimensions": [], "year": 2026, "schedule": "calendar-days" }, "items": [], "sharedViews": [], "milestones": [], "periodIndicators": [] } } ``` ### Multi-project backup The root object is `{ "format": "tidigar-backup", "projects": [...] }`. Every entry in `projects` is a canonical project object, without its own `tidigar-project` wrapper. Order is preserved. This profile is for whole-browser backup and restore; use a project exchange file when sharing one roadmap. ### Native `.tidigar` document A `.tidigar` file is JSON and can be processed without Apple frameworks. Its `roadmap` value is the complete project exchange object above, not only the inner canonical project. | Field | Type | Required for writers | Rule | | --------------- | ------- | -------------------- | --------------------------------------------------------------------------------- | | `format` | text | yes | Exactly `"tidigar-document"` | | `version` | integer | yes | Exactly `1` | | `id` | UUID | yes | Document identity; independent of `manifest.projectId` | | `title` | text | yes | Library label; the canonical roadmap name remains `roadmap.project.manifest.name` | | `createdAt` | text | yes | ISO 8601 timestamp; legacy readers fall back to `updatedAt` | | `updatedAt` | text | yes | ISO 8601 timestamp | | `roadmap` | object | yes | A valid `tidigar-project` exchange value | | `thumbnail` | text | no | Standard padded Base64 bytes of an optional PNG preview in a light color scheme | | `thumbnailDark` | text | no | The same preview in a dark color scheme; only beside `thumbnail` | Readers MUST use `roadmap` as the roadmap authority. `title`, `createdAt`, `updatedAt`, `thumbnail` and `thumbnailDark` are document-library metadata and MUST NOT overwrite canonical project fields. A reader showing a dark library uses `thumbnailDark` and falls back to `thumbnail` when it is missing. Tidigar currently accepts a missing title as `"Roadmap"`, an invalid/missing timestamp as an old timestamp, and an invalid thumbnail as no thumbnail; interoperable writers MUST NOT depend on those recovery defaults. To edit a file-backed roadmap: decode the applicable envelope, validate the complete canonical project, apply changes while retaining stable IDs and array order, validate again, then atomically replace the file. Writers SHOULD retain the native document `id` when updating an existing `.tidigar` file. ## Calendar storage profile A roadmap can also be stored as events in a calendar, one event per record. The [calendar storage specification](https://tidigar.com/agents/docs/calendar.md) defines the events, their identity URLs and the rules for reading and writing them. ## Versions | Version | Where | Current | Changes when | | --------------- | -------------------------- | ------- | --------------------------------------------- | | `formatVersion` | `manifest` | `1` | The canonical model changes incompatibly | | `v` | Calendar event URL | `3` | The URL or `m` layout changes | | `version` | Calendar Notes envelope | `2` | The Notes envelope or its payload keys change | | `version` | Native `.tidigar` document | `1` | The document envelope changes |