# The `.pumapack` file format (PumaNoter, schema 1)

This document describes PumaNoter's `.pumapack` files in enough detail to
**edit an exported one** or **generate one from scratch** so that it imports
cleanly. The app opens the result with no warnings, no lost notes and no
reordering surprises. It is written for a reader, human or AI, who has no
access to the app's source.

A `.pumapack` holds **one notebook**: its folders, its notes, and any images
the notes show. It is a UTF-8 JSON file.

To get one, right-click a notebook's tab and choose **Export as .pumapack**.
That is the file to hand an AI along with this document.

PumaNoter imports a `.pumapack` in any of three ways:

- from the topbar **Import** button;
- with **Ctrl/Cmd+O**;
- by dropping the file anywhere on the window.

Importing a `.pumapack` always **adds a new notebook** and switches to it. It
never replaces or merges into a notebook you already have.

PumaNoter also imports two other JSON shapes of its own: the per-notebook
**JSON** export and the **full backup** that the topbar Export button writes.
They are described in §9. A full backup **replaces everything** in the
browser, so for handing work to and from an AI, prefer the `.pumapack`.

---

## 1. The short version

If you only read one section, read this one.

1. Wrap the notebook in the envelope from §2. `puma.app` must be
   `"pumanoter"` and `puma.format` must be the **number** `1`.
2. Put the notebook's name in `data.notebook.name`, its folders in
   `data.folders` and its notes in `data.notes`. `data.notes` must be an
   array, even an empty one.
3. Give every folder and every note an `id` that is **unique across both
   arrays**. A missing or repeated id silently loses notes. See §5.
4. A folder's `parent_id` and a note's `folder_id` are a folder id, or `null`
   for the top level.
5. Order siblings with `position`, counting from 0 in each parent. **Within a
   parent, folders always come before notes**, whatever the positions say.
   See §6.
6. A note's `body` is Markdown, with `\n` line breaks. Tags are written in the
   body as `#tag`. See §6.
7. Write **every field** shown in §4. Never write `null` except for a
   top-level `parent_id` or `folder_id`.
8. Check the result against the checklist in §11.

§12 is a complete, valid example you can copy and adapt.

---

## 2. The envelope

```json
{
  "$schema": "https://pumaworx.dev/pumapack/v1",
  "puma": {
    "app": "pumanoter",
    "appVersion": "generated",
    "format": 1,
    "exportedAt": "2026-09-28T09:00:00.000Z",
    "title": "IR playbooks"
  },
  "data": {
    "notebook": { },
    "folders":  [ ],
    "notes":    [ ],
    "assets":   [ ]
  }
}
```

| Key | Value | Notes |
|---|---|---|
| `$schema` | `"https://pumaworx.dev/pumapack/v1"` | Not read on import. Write it anyway. |
| `puma.app` | `"pumanoter"` | **Required.** It tells the app which app made the file. |
| `puma.format` | `1` | **Required, and must be the number `1`.** The string `"1"` is refused. |
| `puma.appVersion` | any string | Free text with no meaning. The app writes its build id. |
| `puma.exportedAt` | ISO 8601 datetime | Informational. |
| `puma.title` | string | Used as the notebook name **only** when `data.notebook` is missing. |
| `data.notebook` | object | See §3. |
| `data.folders` | array | See §4.1. `[]` when there are none. |
| `data.notes` | array | See §4.2. **Must be an array.** |
| `data.assets` | array | See §4.3. `[]` when no note shows an image. |

What the importer actually requires:

- The file is valid JSON.
- It has a `puma` object with a non-empty `app`.
- `puma.format` is exactly the number `1`.
- `puma.app` is `"pumanoter"`.
- `data.notes` is an array.

Every other envelope key is ignored.

When any of these fails, the notebook is not added and the app shows one
error toast. For almost every failure it reads only *"Import failed"*, so
check the list above rather than hunting for a specific message. The one
exception is a pack made by a different app, for example `"app": "pumarisk"`:
*"That's a PumaRisk pumapack — PumaNoter doesn't know how to read it. Open it
in PumaRisk instead."*

On success the toast reads *"Imported 1 file"*. There is no dialog.

### Packs from other apps

PumaNoter also reads a PumaLogger `.pumapack`: each log entry becomes a note,
grouped into one folder per category. Every other app's pack is refused with
the message above. Those formats are not described here.

---

## 3. `data.notebook`

```json
{ "id": "nb_irplaybooks", "name": "IR playbooks", "slug": "ir-playbooks", "accent_color": "#5b8af0" }
```

| Field | Type | What the importer does with it |
|---|---|---|
| `id` | string | **Ignored.** The imported notebook gets a new id. |
| `name` | string | The notebook's name, shown on its tab. **Always write it.** |
| `slug` | string | **Ignored.** The app makes a new one from `name`: lower case, spaces as `-`, with `-2`, `-3` and so on added if you already have a notebook by that name. |
| `accent_color` | `#rgb` or `#rrggbb` | The tab's color. If missing, the app's default gold. Any other value is kept, but the tab shows a default blue. |

Two notebooks may share a name. They get different slugs.

If `data.notebook` is left out entirely, the notebook is named after
`puma.title`, or *Imported* if that is missing too, and gets the default
color. If `data.notebook` is present but has no `name`, the tab reads
*Imported*, but the notebook carries no name of its own and exports without
one. Write `name`.

Any other key on this object is ignored.

---

## 4. Record shapes

### Conventions for every record

- **`id`** is any non-empty string. It must be **unique across `folders` and
  `notes` together**, not just within one array.
  - The app generates ids like `note_k3h9x2mq` and `folder_p0a8d1zc`. Short
    readable ids (`n_triage`, `f_playbooks`) work just as well.
  - Note ids are **kept exactly** on import. Folder ids are kept too.
- **Timestamps** are full ISO 8601 datetimes in UTC, like
  `"2026-09-28T09:00:00.000Z"`. They are not validated. A malformed one is
  stored and later shows as *Invalid Date*.
- **Any key not listed below is dropped** on import.

### 4.1 `folders[]`

```json
{ "id": "f_phishing", "name": "Phishing", "parent_id": "f_playbooks", "position": 0 }
```

| Field | Type | Meaning |
|---|---|---|
| `id` | string | See the conventions above. |
| `name` | string | Shown in the tree. Missing or `""` becomes *Folder*. |
| `parent_id` | folder id, or `null` | The folder this one sits in. `null` is the top level of the notebook. |
| `position` | integer ≥ 0 | Order among the folders in the same parent. See §6. |

- Folders nest to any depth.
- A folder may be empty.
- Every imported folder starts expanded.
- There is no record for the notebook's top level. It is implied, and
  `null` points at it.

### 4.2 `notes[]`

```json
{
  "id": "n_triage",
  "title": "Triage checklist",
  "body": "# Triage checklist\n\n- [ ] Record who reported it\n\n#triage",
  "folder_id": "f_playbooks",
  "tags": ["triage"],
  "created_at": "2026-09-14T08:45:00.000Z",
  "updated_at": "2026-09-24T11:20:00.000Z",
  "archived": false,
  "position": 1
}
```

| Field | Type | Meaning |
|---|---|---|
| `id` | string | **Required and unique.** See §5. |
| `title` | string | The note's name, shown in the tree and above the editor. Missing or `""` becomes *Untitled*. See §6. |
| `body` | string | The note's Markdown. Missing becomes `""`. See §6. |
| `folder_id` | folder id, or `null` | The folder the note sits in. `null` is the top level. |
| `tags` | array of strings | Stored and exported, and written into the front matter when the note is exported as Markdown. Anything that is not an array becomes `[]`. The Tags panel does **not** read this field. See §6. |
| `created_at` | ISO datetime | When the note was made. Missing becomes the moment of import. Used as the `date` in Markdown export. |
| `updated_at` | ISO datetime | Last edit. Missing becomes the moment of import. Shown as *Updated …* above the editor. |
| `archived` | boolean | **Ignored** on import. PumaNoter has no archive. Write `false`. |
| `position` | integer ≥ 0 | Order among the notes in the same parent. See §6. |

A note imported from a pack is never pinned, and nothing in the pack can
change that.

### 4.3 `assets[]` (images)

Images are optional. Most generated packs have `"assets": []`.

When a note shows an image, the body carries a short token in place of the
picture, and the picture itself travels in `assets`:

```json
{
  "id": "a7c31e09b2d4f6e8",
  "type": "image/png",
  "w": 3, "h": 1, "bytes": 75,
  "createdAt": "2026-09-16T09:14:00.000Z",
  "data": "data:image/png;base64,iVBORw0KGgo..."
}
```

| Field | Type | Meaning |
|---|---|---|
| `id` | string of letters and digits only | Referenced from a body as `![alt text](asset:<id>)`. The app's own ids are 16 hex characters. |
| `type` | MIME type | e.g. `image/webp`, `image/png`. |
| `w`, `h` | integers | Pixel size. |
| `bytes` | integer | Size of the decoded image. |
| `createdAt` | ISO datetime | Note the **camelCase** spelling here, unlike on notes. |
| `data` | data URI | The image itself, base64 encoded. **Required.** An entry without `id` or `data` is skipped. |

- The body token must stand on its own line for the editor to show the
  picture: `![Severity color key](asset:a7c31e09b2d4f6e8)`.
- An image whose id is already stored in the browser is not overwritten.
- A token with no matching asset stays in the note as text. Nothing warns.
- The app exports only the assets the notebook's notes actually reference.

---

## 5. Cross-references

| From | Field | To |
|---|---|---|
| folder | `parent_id` | `folders[].id`, or `null` |
| note | `folder_id` | `folders[].id`, or `null` |
| note body | `asset:<id>` token | `assets[].id` |

What happens when a reference is wrong:

- **A `folder_id` that matches no folder** puts the note at the top level.
  The note is kept.
- **A `parent_id` that matches no folder**, points at the folder itself, or
  forms a loop puts that folder at the top level, after the other top-level
  folders. Its contents come with it.
- **A note with no `id`**, or with the same `id` as another note or a
  folder, is not caught. Notes that share an id overwrite each other, so all
  but one of them is lost, and the tree lists the survivor twice. The toast
  still reads *"Imported 1 file"*.

---

## 6. Order, titles, tags and the note body

### Order

- `position` orders **folders among folders** and **notes among notes**
  within one parent. Count from 0 in each parent.
- After import, **each folder lists its subfolders first, then its notes.**
  A note cannot be placed above a sibling folder by a pack, even with a lower
  `position`. (PumaNoter's own export numbers folders and notes with one
  shared counter, so an exported note that sat above a folder comes back
  below it.)
- Ties, or a missing `position`, fall back to the order of the array.
- Array order does not otherwise matter. The app's own export lists folders
  and notes depth-first, in tree order.

### Titles

- `title` is the note's name everywhere in the app. The body does not need to
  repeat it, though starting the body with `# <title>` is the common style.
- A note titled `"Untitled"`, or with no title, renames itself from the first
  line of its body the first time someone edits it. Give every note a real
  title.

### Tags

- The **Tags** panel in the sidebar and tag search are built from `#tag`
  words in note bodies: a `#` followed by a letter, then letters, digits,
  `_`, `/`, `-` or `.`. `#triage` and `#ir/phishing` are tags.
- The `tags` array is separate. The app keeps it, exports it, and writes it
  into the front matter of a Markdown export, but never displays it and never
  fills it from the body.
- To make tags visible in the app, put them in the body. To keep a Markdown
  export's front matter in step, also list them in `tags`, without the `#`.

### The body

The body is plain Markdown, stored exactly as written. The editor styles it in
place and understands:

- headings `#` to `######`, **bold**, *italic*, `~~strike~~`, `==highlight==`,
  `` `code` ``, links `[text](url)`;
- bullet lists (`- `), numbered lists (`1. `), two-space indents for nesting;
- task lists: `- [ ]` open, `- [x]` done;
- blockquotes (`> `), and callouts that open a blockquote with `[!NOTE]`,
  `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]`;
- fenced code blocks, colored for `js`, `ts`, `python`, `css`, `html`,
  `json`, `yaml`, `bash` and `md`;
- horizontal rules (`---` alone on a line);
- pipe tables, which are kept and shown as monospace source;
- image tokens, see §4.3.

Use `\n` for line breaks inside the JSON string. A body beginning with a
`---` line is treated as already carrying its own front matter, and Markdown
export does not add another.

---

## 7. Editing an existing export

An exported `.pumapack` is already in the right shape. When editing one:

- **Keep every `id`.** They are how notes and folders find each other. New
  records need new ids that do not collide with any existing one.
- **Keep `created_at`.** Set `updated_at` to the current time on any note you
  change; the app does not do it for you on import.
- **Keep `assets` entries byte for byte**, and keep the `![...](asset:...)`
  tokens that point at them. A changed `data` string is a different picture.
  Drop an asset only if no body references it any more.
- **Do not change `puma.app` or `puma.format`.**
- Renaming a note means changing `title`. Moving one means changing
  `folder_id`. Reordering means changing `position`.

What the app sets for itself on import, so edits there have no effect:

| Field | What happens |
|---|---|
| `data.notebook.id`, `data.notebook.slug` | Replaced with new values. |
| The notebook's creation time | Set to the moment of import. |
| `archived` | Ignored. |
| Pinned state | Always off. |
| Folder expanded state | Always expanded. |
| Unknown keys anywhere in `data` | Dropped. |

Re-importing an edited pack **adds a second notebook**. It does not update
the original. The user deletes the old notebook if they want only the new
one.

---

## 8. Things that go wrong

| Mistake | What happens |
|---|---|
| Notes and folders with no `puma` wrapper | Rejected: *"Import failed"*. |
| `"format": "1"` (a string) or any number other than `1` | Rejected: *"Import failed"*. |
| `puma.app` set to another app | Rejected, naming that app. |
| `data.notes` missing, `null` or not an array | Rejected: *"Import failed"*. |
| `data.folders` as an object instead of an array | Rejected: *"Import failed"*. |
| A `null` element inside `notes` or `folders` | Rejected: *"Import failed"*. |
| Invalid JSON | Rejected: *"Import failed"*. |
| A note with no `id` | Imported, but it and every other id-less note share one slot; all but one are lost. |
| Two records with the same `id` | Imported, but all but one are lost, and the survivor shows twice in the tree. |
| A dangling `folder_id` or `parent_id` | The note or folder lands at the top level. Nothing is lost. |
| A note positioned above a sibling folder | It comes out below the folder. |
| `title` missing or `"Untitled"` | Shown as *Untitled*, then renamed from the first line on first edit. |
| `tags` as a string | Becomes `[]`. |
| Tags only in `tags`, not in the body | Kept, but the Tags panel does not show them. |
| `data.notebook` without `name` | The tab reads *Imported*, and the notebook exports with no name. |
| An `asset:` token with no matching asset | The token stays as text. |
| A malformed timestamp | Stored; shows as *Invalid Date*. |
| A timestamp in the future | Stored; shows as *Updated just now*. |
| An extra key on a note or folder | Dropped. |

---

## 9. The other JSON files PumaNoter imports

These are PumaNoter's own formats. They store notes in the shape the app keeps
them in, so they are more detailed than a `.pumapack`, and they keep a note's
exact position among folders. They are less suited to generating from scratch.

### 9.1 Notebook JSON (right-click a tab → **Export as JSON**)

```json
{
  "format": "pumanoter", "version": 1, "kind": "notebook",
  "exportedAt": "2026-09-28T09:00:00.000Z", "sourceDeviceId": "dev_x1y2z3",
  "notebook": { "id": "nb_...", "name": "IR playbooks", "slug": "ir-playbooks", "accent_color": "#5b8af0" },
  "tree":  { },
  "notes": { },
  "assets": [ ]
}
```

Importing one **adds** a notebook, exactly like a `.pumapack`: new id, new
slug from `notebook.name`, same success toast. It is recognized by
`"format": "pumanoter"` together with `"kind": "notebook"` or a `tree` key.

`tree` is stored as written, apart from its `slug` and `notebookId`, which the
app overwrites:

```json
{
  "notebookId": "nb_...", "name": "IR playbooks", "slug": "ir-playbooks",
  "accent_color": "#5b8af0",
  "rootId": "node_root",
  "nodes": {
    "node_root":  { "id": "node_root", "kind": "folder", "name": "/", "parentId": null, "children": ["f_playbooks", "n_start"], "collapsed": false },
    "f_playbooks": { "id": "f_playbooks", "kind": "folder", "name": "Playbooks", "parentId": "node_root", "children": ["n_triage"], "collapsed": false },
    "n_triage":   { "id": "n_triage", "kind": "note", "name": "Triage checklist", "parentId": "f_playbooks", "noteId": "n_triage" },
    "n_start":    { "id": "n_start", "kind": "note", "name": "Start here", "parentId": "node_root", "noteId": "n_start" }
  },
  "lastActiveNoteId": "n_start"
}
```

- `nodes` is keyed by id. Each node's `id` equals its key.
- `rootId` names the top-level folder node, whose `name` is `"/"` and whose
  `parentId` is `null`.
- A folder's `children` is the **display order**, folders and notes mixed.
  Every child's `parentId` must name that folder.
- A note node's `noteId` equals its `id`, and its `name` should equal the
  note's `title`.
- `lastActiveNoteId` is the note opened after import, or `null`.

`notes` is an object keyed by note id. Each value is stored **exactly as
written**, unknown keys included:

```json
{
  "id": "n_triage", "format": "pumanoter-note", "version": 1,
  "title": "Triage checklist", "body": "# Triage checklist\n...",
  "createdAt": "2026-09-14T08:45:00.000Z", "updatedAt": "2026-09-24T11:20:00.000Z",
  "tags": [], "pinned": false
}
```

The timestamps here are **camelCase** (`createdAt`, `updatedAt`), not the
`.pumapack` spelling. The note's key in `notes` must equal a note node's id,
or the note opens blank. `assets` is as in §4.3.

A notebook JSON with `"kind": "notebook"` but no `tree` fails with *"Import
failed"* and leaves an empty notebook tab behind.

### 9.2 Full backup (topbar **Export**, or **Ctrl/Cmd+S**)

```json
{
  "format": "pumanoter", "version": 1, "kind": "backup",
  "exportedAt": "2026-09-28T09:00:00.000Z", "sourceDeviceId": "dev_x1y2z3",
  "theme": "dark", "accent": null, "activeNotebookId": "ir-playbooks",
  "settings": { "fontSize": 16, "autosaveMs": 250, "sidebarOpen": true, "sidebarWidth": null, "editorMode": "styled", "sidebarMode": "notes" },
  "notebooks": [ { "notebook": { }, "tree": { }, "notes": { } } ],
  "assets": [ ]
}
```

Importing a backup **replaces every notebook in the browser**. The user is
asked first:

> Restore backup of 2 notebook(s)?
>
> This will REPLACE all notebooks currently in this browser.

Cancelling changes nothing and shows no toast. Confirming shows *"Imported 1
file"*.

- Each `notebooks[]` entry holds a `notebook` (with `id`, `name`, `slug`,
  `accent_color`, `createdAt`), a `tree` and `notes`, shaped as in §9.1.
- Unlike the other imports, a backup keeps each notebook's **`id` and `slug`
  exactly**, so slugs must be unique within the file.
- An entry with no `notebook`, no `notebook.slug` or no `tree` is **skipped
  without a word**, even though the question counted it. If every entry is
  skipped, nothing is replaced and the toast reads *"Backup has no valid
  notebooks — nothing was changed"*.
- `activeNotebookId` holds a notebook's **slug**, despite its name.
- `theme`, `accent` and `settings` restore the user's preferences when
  present.

---

## 10. Where the data ends up

For checking an import by hand in the browser's developer tools: PumaNoter
keeps notebooks in `localStorage` under keys beginning `pumanoter.`, one key
per notebook tree (`pumanoter.nb.<slug>.tree`) and one per note
(`pumanoter.nb.<slug>.note.<id>`). Images live in IndexedDB, in a database
named `pumanoter-assets`. A `.pumapack` note becomes a stored note with the
camelCase fields shown in §9.1.

---

## 11. Checklist before handing a pack over

A pack that passes all of these imports with the *"Imported 1 file"* toast and
loses nothing.

**Envelope**
- [ ] `puma.app` is `"pumanoter"` and `puma.format` is the number `1`.
- [ ] `data.notes` and `data.folders` are arrays, with no `null` elements.
- [ ] `data.notebook.name` is set.

**Ids and references**
- [ ] Every note and folder has an `id`, unique across both arrays.
- [ ] Every `folder_id` and `parent_id` is `null` or a folder id in this pack.
- [ ] No folder is its own ancestor.
- [ ] Every `asset:<id>` token in a body has a matching `assets[]` entry.

**Order**
- [ ] `position` counts from 0 among the folders of each parent, and
      separately among the notes of each parent.
- [ ] Nothing relies on a note sitting above a sibling folder.

**Content**
- [ ] Every note has a real `title` (not `"Untitled"`) and a `body` string.
- [ ] Tags meant to show in the app are written as `#tag` in the body.
- [ ] `created_at` and `updated_at` are full ISO datetimes in UTC.
- [ ] Edited notes have a fresh `updated_at`; untouched ones keep theirs.

---

## 12. A complete example

A small incident-response notebook: two top-level folders, one nested folder,
a note at the top level, a note in each folder, and one image. It imports with
the *"Imported 1 file"* toast and no warnings, and the app's own export of the
result matches it apart from the notebook `id`, `puma.appVersion` and
`puma.exportedAt`.

```json
{
  "$schema": "https://pumaworx.dev/pumapack/v1",
  "puma": {
    "app": "pumanoter",
    "appVersion": "generated",
    "format": 1,
    "exportedAt": "2026-09-28T09:00:00.000Z",
    "title": "IR playbooks"
  },
  "data": {
    "notebook": {
      "id": "nb_irplaybooks",
      "name": "IR playbooks",
      "slug": "ir-playbooks",
      "accent_color": "#5b8af0"
    },
    "folders": [
      { "id": "f_playbooks", "name": "Playbooks", "parent_id": null, "position": 0 },
      { "id": "f_phishing", "name": "Phishing", "parent_id": "f_playbooks", "position": 0 },
      { "id": "f_reference", "name": "Reference", "parent_id": null, "position": 1 }
    ],
    "notes": [
      {
        "id": "n_start",
        "title": "Start here",
        "body": "# Start here\n\nThese playbooks cover the first hour of an incident. Open **Triage checklist** first, then the playbook that matches.\n\n> [!IMPORTANT]\n> If anyone reports a ransom note, skip triage and call the incident lead.\n\n#index",
        "folder_id": null,
        "tags": ["index"],
        "created_at": "2026-09-14T08:30:00.000Z",
        "updated_at": "2026-09-25T16:05:00.000Z",
        "archived": false,
        "position": 2
      },
      {
        "id": "n_triage",
        "title": "Triage checklist",
        "body": "# Triage checklist\n\n- [ ] Record who reported it, when, and how\n- [ ] Confirm the affected host or account\n- [ ] Set a severity using the key in **Contacts**\n- [x] Open a case number before touching anything\n\nPull the last day of sign-ins for the account:\n\n```bash\nsearch-signins --user \"$ACCOUNT\" --since 24h\n```\n\n#triage #checklist",
        "folder_id": "f_playbooks",
        "tags": ["triage", "checklist"],
        "created_at": "2026-09-14T08:45:00.000Z",
        "updated_at": "2026-09-24T11:20:00.000Z",
        "archived": false,
        "position": 1
      },
      {
        "id": "n_phish",
        "title": "Reported phishing email",
        "body": "# Reported phishing email\n\n1. Get the original message as an attachment, not a forward\n2. Check whether anyone clicked or replied\n3. Block the sender and purge matching messages\n4. Reset credentials for anyone who entered them\n\n> [!WARNING]\n> Do not open links from the message on a work machine.\n\n#phishing #email",
        "folder_id": "f_phishing",
        "tags": ["phishing", "email"],
        "created_at": "2026-09-15T10:00:00.000Z",
        "updated_at": "2026-09-15T10:00:00.000Z",
        "archived": false,
        "position": 0
      },
      {
        "id": "n_contacts",
        "title": "Contacts",
        "body": "# Contacts\n\n| Role | Name | Phone |\n|---|---|---|\n| Incident lead | Priya Shah | 555-0142 |\n| IT on call | Tom Reyes | 555-0199 |\n\nSeverity key, low to high:\n\n![Severity color key](asset:a7c31e09b2d4f6e8)\n\n#contacts",
        "folder_id": "f_reference",
        "tags": ["contacts"],
        "created_at": "2026-09-16T09:15:00.000Z",
        "updated_at": "2026-09-16T09:15:00.000Z",
        "archived": false,
        "position": 0
      }
    ],
    "assets": [
      {
        "id": "a7c31e09b2d4f6e8",
        "type": "image/png",
        "w": 3,
        "h": 1,
        "bytes": 75,
        "createdAt": "2026-09-16T09:14:00.000Z",
        "data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAABCAIAAACUgoPjAAAAEklEQVR42mPQW+B8ZYXFzeJiABWBBIV7i3XxAAAAAElFTkSuQmCC"
      }
    ]
  }
}
```

What the app shows after importing this, as a check on your own reasoning.
These results come from importing this exact file into the app:

- A new tab, **IR playbooks**, in blue, is added and becomes the active
  notebook. A notebook you already had keeps its own tab.
- The tree reads, top to bottom:
  - **Playbooks**
    - **Phishing**
      - Reported phishing email
    - Triage checklist
  - **Reference**
    - Contacts
  - Start here
- *Start here* has `position` 2 among the top level's three items, but it
  would come after the two folders at any position, because folders always
  come first.
- The Tags panel lists `checklist`, `contacts`, `email`, `index`,
  `phishing` and `triage`, one note each.
- *Contacts* shows the three-color severity key where the image token sits.
- Every note keeps the id, title, body, tags and timestamps given here.
