# NotesXML .NXL File Format Specification
## Version 2.6 — Cross-Platform XML Notebook Format
**Compiled:** 2026-05-28  
**Status:** Active Specification  
**Scope:** Android • Desktop (Electron/AppImage) • Web (Browser/PWA)  
**Owner:** IWV Digital Solutions LLC  
**Related Documents:** SRS v9.16 (binding requirements), ICD v3.9 (interface contracts), Codebase Cross-Reference v1.15 (parser/serializer source-of-truth pointers).

---

## Document Control

### Revision History

| Version | Date | Summary |
|---------|------|---------|
| 2.6 | 2026-05-28 | **CR-2026-834 audit alignment — IWV Agent Coexistence implementation status update.** Brings the spec into alignment with SRS v9.16 §3.52 (NX-AGENT-001 through 014) and ICD v3.9 §29 IWV Agent Coexistence Interfaces. **(1) §1.3 File Extensions:** added `.nxl.inbox` (sidecar — was only referenced from §18.9, now formally enumerated) and `.nxl.lock` (per-notebook lock file — new in this revision). **(2) §3.3 `creator` attribute:** removed the "pending NotesXML CR / approximately 2 lines of code" caveat — the round-trip implementation is now binding under SRS NX-AGENT-001 (parsing) and NX-AGENT-002 (serialization) and reflected in the production `parseNXML()` / `toNXML()` code path at the Beta 5.130.3+ baseline. **(3) §18 External Writer Protocol summary table:** "Inbox sidecar" status corrected `Phase 2 (requires NotesXML CR)` → `Implemented (Beta 5.x)`. **(4) §18.9 Inbox Sidecar Pattern:** detail expanded to align with NX-AGENT-006 through 010 — explicit `targetPageId` for targeted page merge, duplicate `id` protection across the merged set, error-safe handling (a malformed sidecar must not block the notebook open), and a user-visible toast notification confirming the merge. **(5) NEW §16 Per-Notebook Lock File Protocol** (fills the §15→§17 numbering gap inherited from earlier revisions): normative coverage of `.nxl.lock` file naming, JSON schema, atomic acquisition, stale-PID detection, release-on-close lifecycle, sync exclusion across all providers, and the three IPC channels (`notebook:lock-acquire`, `notebook:lock-release`, `notebook:lock-release-all`) per ICD v3.9 §29.4. Aligned with SRS NX-AGENT-011 through 014. **(6) NEW §18.10 Workspace Rescan on Focus** — covers NX-AGENT-003/004/005 (window-focus trigger, 5-second debounce, 200ms perf target). **(7) Related Documents** bumped: SRS v9.6 → v9.16, ICD v3.6 → v3.9, Cross-Reference v1.10 → v1.15. **No schema changes** to existing note types, no attribute renames, no breaking changes — all edits are documentation-only catching up to implementation. Reference: NotesXML Pre-Submission Audit Findings Ledger, CR-2026-834 (audit remediation), SRS v9.16 §3.52, ICD v3.9 §29. |
| 2.5 | 2026-04-29 | **CR-2026-593 Phase 4.1 — NXL Spec Refresh.** Hotfix-style minor revision aligned with the post-Phase-3 documentation baseline (SRS v9.6, ICD v3.6, Cross-Reference v1.10, Documentation Management Plan v1.3). **(1) Header bumped 2.4 → 2.5;** date 2026-04-13 → 2026-04-29. **(2) Audit finding §11.1** — NXL parser location described as `modules/js/parsers/nxl-parser.js` was not directly contradicted by source inspection but no per-line counter drift was detected; v2.5 acknowledges that parser location may have moved during the modular refactoring (CR-2026-011 series), and references the Codebase Cross-Reference v1.10 as the authoritative pointer to current parser implementation. **(3) Audit finding §11.2** — Note-type-to-menu mapping not formally defined in this spec; v2.5 notes that NX-AI-PROMPT-002 in SRS v9.6 §3.6 is the binding specification for type-specific AI menu structure (21 categories, 92 prompts). **(4) Audit finding §11.3** — Per-format `creator` attribute on the root `<notebook>` element noted as informative-only in v2.4; v2.5 retains this informational status pending NXL Spec v3.0 redesign (any future change is gated by SRS NX-XML-FILE requirements update first). **(5) `voskModels` schema-field annotation:** v2.5 adds an explicit reference that the catalog `voskModels` field has been removed via CR-2026-593 Phase 4.5 (Beta 5.99.9); the NXL format itself never carried Vosk-related metadata, so this is purely an informational pointer. Reference: `NotesXML_Documentation_vs_Code_Audit_Beta_5_99_3.md`, `NotesXML_CR-2026-593_Documentation_Reconciliation_Multi_Phase.md`, SRS v9.6, ICD v3.6, Cross-Reference v1.10. |


---

## License

This specification is published by **IWV Digital Solutions LLC** under the [**Creative Commons Attribution 4.0 International License (CC BY 4.0)**](https://creativecommons.org/licenses/by/4.0/).

You are free to:
- **Share** — copy and redistribute this specification in any medium or format.
- **Adapt** — remix, transform, and build upon this specification for any purpose, including commercial use.

Under the following terms:
- **Attribution** — You must give appropriate credit ("NotesXML .NXL File Format Specification, IWV Digital Solutions LLC"), provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests IWV Digital Solutions endorses you or your use.

The format described by this specification (the `.nxl` XML schema, element names, and data model) is not patent-encumbered. You do not need a license from IWV Digital Solutions to read or write `.nxl` files. The CC BY license above covers only the specification *document*; the format itself is open.

For questions about the specification or to share what you've built with it: hello@iwvdigitalsolutions.com

---

## 1. Overview

### 1.1 Format Definition

The `.nxl` (NotesXML) format is an **XML-based notebook file format** designed for cross-platform note-taking applications. Each `.nxl` file represents a single Notebook containing a hierarchical structure of Pages and Notes.

### 1.2 Design Principles

- **Portability**: Single file contains all content including embedded media (Base64)
- **Human-Readable**: Standard XML that can be viewed/edited in any text editor
- **Offline-First**: No external dependencies or network requirements
- **Cross-Platform**: Identical format across Android, Desktop, and Web platforms
- **Optimized Storage**: Type-specific data stored as JSON to reduce redundancy

### 1.3 File Extensions

| Extension | Purpose | Description |
|-----------|---------|-------------|
| `.nxl` | Primary notebook | Standard NotesXML notebook file |
| `.nxl.bak` | Backup file | AutoSave rolling backup |
| `.nxl.enc` | Encrypted notebook | AES-256-GCM encrypted notebook (NX-SEC-LOCAL-xxx; transparent encrypt/decrypt via the EncryptedFileIO abstraction) |
| `.nxl.inbox` | External-writer sidecar | Inbox file written by external applications (e.g. IWV Agent) for merge into the primary notebook on next NotesXML open. See §18.9. |
| `.nxl.lock` | Per-notebook lock | Exclusive-access lock for an open notebook (atomic write, stale-PID detection, sync-excluded). See §16. |
| `.nxlt` | Template | Notebook template file |

> **External Reader Guidance (v2.4):** Files with the `.nxl.enc` extension are AES-256-GCM encrypted notebooks. An encrypted file is NOT valid XML — it is a binary blob. The encryption uses PBKDF2 key derivation (SHA-512, 600K iterations) from a user-provided passphrase that is never stored in the file. The exact binary layout (IV, authentication tag, and ciphertext positions) is defined in the NotesXML ICD (Section 11.2) and SRS (NX-SEC-LOCAL-003). External applications that do not implement NotesXML's `LocalEncryptionManager` and `EncryptedFileIO` modules MUST skip `.nxl.enc` files and report them as inaccessible. Attempting to parse an encrypted file as XML will fail.

---

## 2. XML Schema Structure

### 2.1 Root Element

The root element is `<notebook>`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<notebook version="2.0">
    <metadata>...</metadata>
    <pages>...</pages>
</notebook>
```

> **Note:** The `<backpack>` root element is used ONLY for importing legacy 37Signals Backpack XML files via the separate `parseBackpackXML()` function. Native NXL files always use `<notebook>` as root.

> **Version Attribute Clarification (v2.3):** The `version="2.0"` attribute on the root `<notebook>` element indicates the XML schema family and MUST remain `"2.0"` for all v2.x specifications (v2.0, v2.1, v2.2, v2.3). This ensures backward compatibility — parsers written for any v2.x revision can safely open files produced by any other v2.x revision. The version is only incremented when breaking schema changes are introduced. The fine-grained specification version is tracked in `<metadata><version>` and in this document's header.

**Interpretation:**
- `<notebook>` root element = **Notebook**
- `<page>` element = **Page**
- `<note>` element = **Note**

### 2.2 Complete Schema Example (v2.3 Format)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<notebook version="2.0">
  <metadata>
    <title>My Notebook</title>
    <created>2025-12-25T10:00:00Z</created>
    <modified>2025-12-25T14:30:00Z</modified>
    <author>User Name</author>
    <version>2.0</version>
    <pageSortOrder>manual</pageSortOrder>
  </metadata>
  
  <pages>
    <page id="page_uuid_123" title="Page Title" created="2025-12-25T10:00:00Z" modified="2025-12-25T14:30:00Z" isHome="true" noteSortOrder="manual">
      <tags><tag>work</tag><tag>important</tag></tags>
      
      <notes>
        <note id="note_uuid_456" type="richtext" created="2025-12-25T10:00:00Z" modified="2025-12-25T14:30:00Z">
          <title>Note Title</title>
          <content><![CDATA[<p>Rich text content here</p>]]></content>
        </note>
      </notes>
      
      <belongings>
        <belonging type="note" id="note_uuid_456" order="0"/>
      </belongings>
    </page>
  </pages>
</notebook>
```

> **v2.2 Changes from v2.1:** The `<sortOrder>` element has been removed from metadata (see Section 15.3). The `<pageSortOrder>` element and `noteSortOrder` attribute now use the updated value enumeration with a default of `manual` (see Section 3.1).

---

## 3. Element Specifications

### 3.1 Metadata Element

```xml
<metadata>
    <title>Notebook Title</title>                 <!-- From filename or explicit -->
    <created>2025-12-25T10:00:00Z</created>      <!-- ISO 8601 timestamp -->
    <modified>2025-12-25T14:30:00Z</modified>    <!-- ISO 8601 timestamp -->
    <author>User Name</author>                    <!-- Optional -->
    <version>2.0</version>                        <!-- Schema version -->
    <pageSortOrder>manual</pageSortOrder>         <!-- Page list sort order -->
</metadata>
```

**Metadata Elements:**

| Element | Required | Default | Values | Description |
|---------|----------|---------|--------|-------------|
| `<title>` | Yes | From filename | String (max 200 chars) | Notebook display title |
| `<created>` | Yes | Current time | ISO 8601 timestamp | Creation timestamp |
| `<modified>` | Yes | Current time | ISO 8601 timestamp | Last modification timestamp |
| `<author>` | No | Empty | String | Author name |
| `<version>` | Yes | `"2.0"` | String | NXL schema version (remains `"2.0"` for all v2.x revisions) |
| `<pageSortOrder>` | No | `manual` | `manual\|az\|za\|newest\|oldest\|num_az\|num_za` | Page list sort order for this notebook |

> **v2.2 Change:** The `<sortOrder>` element has been removed. Notebook list sort order (the order of notebooks in the application's sidebar) is an application-level preference, not per-notebook data. It is stored in localStorage (Web/Android) or config.json (Electron), never in NXL files. See Section 15.3 for migration details.

> **v2.3 Change:** Notebook `<title>` max length explicitly set to 200 characters, matching page title constraints.

**Sort Order Values:**

| Value | Display Label | Sort Key | Description |
|-------|--------------|----------|-------------|
| `manual` | Manual Order | Array/belongings order | User-defined order via drag-and-drop |
| `az` | Title A–Z | `title.localeCompare()` ascending | Alphabetical ascending |
| `za` | Title Z–A | `title.localeCompare()` descending | Alphabetical descending |
| `newest` | Newest First | `modified` timestamp descending | Most recently modified items first |
| `oldest` | Oldest First | `created` timestamp ascending | Earliest created items first |
| `num_az` | 0→9 | Leading number ascending | Numerical ascending by leading digits in title |
| `num_za` | 9→0 | Leading number descending | Numerical descending by leading digits in title |

### 3.2 Page Element

Pages use **attributes** for core properties (not child elements):

```xml
<page id="page_uuid" title="Page Title" created="2025-12-25T10:00:00Z" modified="2025-12-25T14:30:00Z" isHome="true" noteSortOrder="manual">
    <tags>
        <tag>tag1</tag>
        <tag>tag2</tag>
    </tags>
    <notes>
        <!-- Note elements -->
    </notes>
    <images>
        <!-- Image elements (optional) -->
    </images>
    <attachments>
        <!-- Attachment elements (optional) -->
    </attachments>
    <belongings>
        <!-- Ordered references to notes/images/attachments -->
    </belongings>
</page>
```

**Page Attributes:**

| Attribute | Required | Default | Description |
|-----------|----------|---------|-------------|
| `id` | Yes | — | Unique identifier (format: `page_` + UUID or timestamp-based ID) |
| `title` | Yes | — | Page title (max 200 characters) |
| `created` | Yes | — | ISO 8601 creation timestamp |
| `modified` | Yes | — | ISO 8601 last modified timestamp |
| `isHome` | No | `false` | Home page flag (`"true"` if home page) |
| `noteSortOrder` | No | `manual` | Note sort order within this page: `manual\|az\|za\|newest\|oldest\|num_az\|num_za` |

### 3.3 Note Element

Notes use **attributes** for metadata:

```xml
<note id="note_uuid" type="[note_type]" created="2025-12-25T10:00:00Z" modified="2025-12-25T14:30:00Z" isHome="true">
    <title>Note Title</title>                     <!-- Optional -->
    <content><![CDATA[...]]></content>            <!-- For richtext, text, html, quote, code -->
    <data><![CDATA[{...}]]></data>                <!-- JSON for type-specific data -->
</note>
```

**Note Attributes:**

| Attribute | Required | Description |
|-----------|----------|-------------|
| `id` | Yes | Unique identifier |
| `type` | Yes | Note type (see Section 4) |
| `created` | Yes | ISO 8601 creation timestamp |
| `modified` | Yes | ISO 8601 last modified timestamp |
| `isHome` | No | Pin to top flag (`"true"` if pinned) |
| `creator` | No | Identifier of the application that created this note (v2.4: new). If absent, the note is assumed to have been created by NotesXML. See below. |

**Creator Attribute (v2.4: new):**

The optional `creator` attribute identifies which application created a note. This supports multi-application ecosystems where external tools (e.g., IWV Agent) create content in NXL notebooks.

| Value | Application |
|-------|-------------|
| (absent) | NotesXML (default — backward compatible) |
| `iwv-agent` | IWV Agent |
| `import` | Created via import (ENEX, Markdown, CSV, Backpack, etc.) |

```xml
<note id="note_uuid" type="richtext" created="..." modified="..." creator="iwv-agent">
    <title>Weekly Digest — 2026-W16</title>
    <content><![CDATA[<p>Summary of your Work notebooks...</p>]]></content>
</note>
```

> **Round-Trip Requirement (Implemented).** The `creator` attribute is round-trip-safe in NotesXML Beta 5.x+ — `parseNXML()` reads it into the note data model and `toNXML()` serialises it back per SRS NX-AGENT-001 (parsing) and NX-AGENT-002 (serialization). External writers may set `creator` and expect it to be preserved on the next NotesXML save. See §18 for the complete external writer protocol.

**Content Element Extended Attributes (v2.2):**

The `<content>` element supports an optional `encoding` attribute for note types where content is structured data rather than display text:

```xml
<!-- Standard: text or HTML content (default, no encoding attribute) -->
<content><![CDATA[<p>Rich text content</p>]]></content>

<!-- Extended: JSON-encoded structured content -->
<content encoding="json"><![CDATA[{"data":"...","fileName":"doc.pdf"}]]></content>
```

| Attribute | Required | Default | Values | Description |
|-----------|----------|---------|--------|-------------|
| `encoding` | No | (none) | `json` | When present, indicates the CDATA content is JSON rather than text/HTML. Used by `pdf` note type. |

### 3.4 Belongings Element

Maintains ordered references to notes within a page. The `order` attribute values define the canonical manual sort order of items within the page.

```xml
<belongings>
    <belonging type="note" id="note_uuid_1" order="0"/>
    <belonging type="note" id="note_uuid_2" order="1"/>
    <belonging type="image" id="img_uuid_1" order="2"/>
    <belonging type="attachment" id="att_uuid_1" order="3"/>
</belongings>
```

When a page's `noteSortOrder` is `manual`, notes are displayed in ascending `order` value sequence. When `noteSortOrder` is any other value, notes are displayed in the computed sorted order, but the `<belongings>` order values are preserved unchanged (they represent the manual order that is restored when the user switches back to `manual`).

---

## 4. Supported Note Types

### 4.1 Text-Based Notes

#### Rich Text Note

The primary note type. Content is HTML stored in a `<content>` CDATA section. Supports full rich text formatting including bold, italic, underline, strikethrough, text color, highlight color, font family, font size, text alignment, headings (H1–H4), lists, blockquotes, horizontal rules, inline code, links, and embedded HTML tables.

```xml
<note id="note_uuid" type="richtext" created="..." modified="...">
    <title>Note Title</title>
    <content><![CDATA[<p>Paragraph with <strong>bold</strong> and <em>italic</em> text.</p>]]></content>
</note>
```

#### Text Note (v2.3: documented)

Plain text note. Content is stored in a `<content>` CDATA section as unformatted text (no HTML tags). The editor renders this content without rich text formatting controls.

```xml
<note id="note_uuid" type="text" created="..." modified="...">
    <title>Plain Text Note</title>
    <content><![CDATA[This is plain text content with no formatting.
Line breaks are preserved as-is.]]></content>
</note>
```

> **Relationship to `richtext`:** The `text` type uses the same `<content>` serialization but the content is plain text, not HTML. The application renders `text` notes in a plain text editor (monospace or sans-serif with no formatting toolbar). When a user creates a new note via "+ Add Note → Text", a `type="text"` note is created. If a `text` note is later converted to rich text by the user, the type changes to `richtext` and the content is wrapped in `<p>` tags.

#### HTML Note (v2.3: documented)

Raw HTML content note. Content is stored in a `<content>` CDATA section as HTML markup. Functionally identical to `richtext` for serialization purposes. This type is primarily created during import operations (e.g., HTML file import, ENEX import) where the source content is HTML that was not created through the NotesXML rich text editor.

```xml
<note id="note_uuid" type="html" created="..." modified="...">
    <title>Imported HTML Content</title>
    <content><![CDATA[<div class="imported"><h2>Imported Document</h2><p>Content from external source.</p></div>]]></content>
</note>
```

> **Relationship to `richtext`:** The `html` type is serialization-identical to `richtext` — both store HTML in `<content>`. The type distinction allows the application to flag content that may contain HTML structures not generated by the native rich text editor (e.g., `<div>`, `<span class="...">`, complex CSS). The editor renders both types identically. The parser MUST treat `html` and `richtext` as interchangeable for display purposes.

#### Quote Note

Stores quoted text. Content may be plain text or simple HTML.

```xml
<note id="note_uuid" type="quote" created="..." modified="...">
    <content><![CDATA[The quoted text here.]]></content>
</note>
```

> **v2.3 Clarification:** Quote note `<content>` may contain either plain text or HTML. When created via the NotesXML editor, content is typically plain text. When imported from external sources, content may include simple HTML formatting (e.g., `<em>`, `<strong>`). Parsers SHOULD render the content as-is within a blockquote-style presentation.

#### Code Note

Stores source code or preformatted text with optional syntax highlighting.

```xml
<note id="note_uuid" type="code" created="..." modified="...">
    <title>Code Snippet</title>
    <content><![CDATA[def hello():
    print('Hello, World!')]]></content>
    <data><![CDATA[{"language":"python"}]]></data>
</note>
```

**Code Note Data Fields (v2.3: documented):**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `language` | string | No | `"text"` | Programming language identifier for syntax highlighting. Values correspond to highlight.js language identifiers (e.g., `"python"`, `"javascript"`, `"java"`, `"cpp"`, `"html"`, `"css"`, `"sql"`, `"bash"`, `"json"`, `"xml"`, `"markdown"`, `"text"`). |

### 4.2 List Notes (v2.0 Optimized Format)

#### Checklist Note
```xml
<note id="note_uuid" type="checklist" created="..." modified="...">
    <title>My Checklist</title>
    <data><![CDATA[{"items":[
        {"checked":true,"text":"Completed item","level":0},
        {"checked":false,"text":"Pending item","level":0},
        {"checked":false,"text":"Nested item","level":1}
    ]}]]></data>
</note>
```

> **Important:** The attribute is `checked` (not `completed`).

**Checklist Item Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `checked` | boolean | Yes | Whether the item is checked/completed |
| `text` | string | Yes | Item display text (may contain simple HTML for inline formatting) |
| `level` | integer | Yes | Indentation level (0 = top level, 1+ = nested) |

#### Numbered List Note
```xml
<note id="note_uuid" type="list" created="..." modified="...">
    <title>Numbered List</title>
    <data><![CDATA[{"ordered":true,"items":[
        {"text":"First item","level":0},
        {"text":"Second item","level":0},
        {"text":"Nested item","level":1}
    ]}]]></data>
</note>
```

#### Bulleted List Note
```xml
<note id="note_uuid" type="list" created="..." modified="...">
    <title>Bulleted List</title>
    <data><![CDATA[{"ordered":false,"items":[
        {"text":"First item","level":0},
        {"text":"Second item","level":0}
    ]}]]></data>
</note>
```

**List Data Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `ordered` | boolean | Yes | `true` for numbered list, `false` for bulleted |
| `items` | array | Yes | Array of list item objects |

**List Item Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | Yes | Item display text |
| `level` | integer | Yes | Indentation level (0 = top level) |

### 4.3 Table Note (v2.0 Optimized Format)

```xml
<note id="note_uuid" type="table" created="..." modified="...">
    <title>My Table</title>
    <data><![CDATA[{
        "headers":["Column 1","Column 2","Column 3"],
        "rows":[
            ["Cell 1","Cell 2","Cell 3"],
            ["Cell 4","Cell 5","Cell 6"]
        ]
    }]]></data>
</note>
```

**Table Data Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `headers` | array of strings | Yes | Column header labels |
| `rows` | array of arrays | Yes | Row data; each inner array is one row of cell values (strings) |

### 4.4 Media Notes

#### Image Note
```xml
<note id="note_uuid" type="image" created="..." modified="...">
    <title>Photo 2025-12-25</title>
    <data><![CDATA[{
        "data":"iVBORw0KGgoAAAANSUhEUg...",
        "width":640,
        "height":480,
        "caption":"Photo caption",
        "metadata":{
            "mime-type":"image/jpeg",
            "original-filename":"photo.jpg"
        }
    }]]></data>
</note>
```

**Image Note Data Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `data` | string | Yes | Base64-encoded image data |
| `width` | number | No | Image width in pixels |
| `height` | number | No | Image height in pixels |
| `caption` | string | No | User-provided image caption |
| `metadata` | object | No | Additional image metadata |
| `metadata.mime-type` | string | No | MIME type (e.g., `"image/jpeg"`, `"image/png"`, `"image/webp"`) |
| `metadata.original-filename` | string | No | Original filename before import |

#### Image Gallery Note (v2.2)
```xml
<note id="note_uuid" type="image-gallery" created="..." modified="...">
    <title>Photo Gallery</title>
    <data><![CDATA[{
        "rows":2,
        "columns":3,
        "cells":[
            {"data":"iVBORw0KGgo...","caption":"Photo 1"},
            {"data":"iVBORw0KGgo...","caption":"Photo 2"},
            {"data":"iVBORw0KGgo...","caption":"Photo 3"},
            {"data":"iVBORw0KGgo...","caption":"Photo 4"},
            null,
            null
        ]
    }]]></data>
</note>
```

**Image Gallery Data Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `rows` | integer | Yes | Number of rows in the grid layout |
| `columns` | integer | Yes | Number of columns in the grid layout |
| `cells` | array | Yes | Array of cell objects or `null` for empty cells. Length = rows × columns. |

Each cell object contains the same fields as an Image Note's data (at minimum `data` as a Base64-encoded image string, optionally `caption`, `width`, `height`).

#### Audio Note
```xml
<note id="note_uuid" type="audio" created="..." modified="...">
    <title>Recording 2025-12-25</title>
    <data><![CDATA[{
        "data":"UklGRi4A...",
        "mimeType":"audio/webm",
        "duration":125,
        "fileSize":204800,
        "transcription":"Transcribed text here"
    }]]></data>
</note>
```

**Audio Note Data Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `data` | string | Yes | Base64-encoded audio data |
| `mimeType` | string | Yes | MIME type (e.g., `"audio/webm"`, `"audio/wav"`, `"audio/mp4"`) |
| `duration` | number | Yes | Duration in seconds |
| `fileSize` | number | No | File size in bytes |
| `transcription` | string | No | Transcribed text from speech recognition (Vosk or platform API) |

> **v2.2 Change:** Audio note data fields are now flat (not nested under a `metadata` object). The `mimeType` field replaces the legacy `metadata.mime-type` path. The `fileSize` field replaces the legacy `metadata.size`. The `original-filename` field has been removed (audio notes are always recorded in-app, not imported from files).

#### Drawing/Handwriting Note
```xml
<note id="note_uuid" type="handwriting" created="..." modified="...">
    <title>Sketch 2025-12-25</title>
    <data><![CDATA[{
        "svg":"<svg>...</svg>",
        "paths":[...],
        "width":800,
        "height":600,
        "canvasSize":"A4",
        "backgroundColor":"#ffffff",
        "backgroundPattern":"none",
        "orientation":"portrait"
    }]]></data>
</note>
```

**Handwriting/Drawing Data Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `svg` | string | Yes | Complete SVG markup for the drawing |
| `paths` | array | Yes | Array of path objects for re-editing |
| `width` | number | Yes | Canvas width in pixels |
| `height` | number | Yes | Canvas height in pixels |
| `canvasSize` | string | No | Named size preset (e.g., `"A4"`, `"US_Letter"`) |
| `backgroundColor` | string | No | CSS color value (default: `"#ffffff"`) |
| `backgroundPattern` | string | No | Pattern type: `"none"`, `"grid"`, `"lines"`, `"dots"` |
| `orientation` | string | No | `"portrait"` or `"landscape"` |

#### File Attachment Note
```xml
<note id="note_uuid" type="file" created="..." modified="...">
    <title>Document.pdf</title>
    <data><![CDATA[{
        "data":"JVBERi0xLjQK...",
        "metadata":{
            "mime-type":"application/pdf",
            "original-filename":"document.pdf",
            "size":1024000
        }
    }]]></data>
</note>
```

**File Attachment Data Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `data` | string | Yes | Base64-encoded file data |
| `metadata` | object | Yes | File metadata container |
| `metadata.mime-type` | string | Yes | MIME type of the attached file |
| `metadata.original-filename` | string | Yes | Original filename |
| `metadata.size` | number | No | File size in bytes |

#### PDF Note (v2.2)

PDF notes provide an interactive PDF viewing and annotation experience. Unlike file attachment notes, PDF notes store annotation state and support in-app rendering via PDF.js.

```xml
<note id="note_uuid" type="pdf" created="..." modified="...">
    <title>Document.pdf</title>
    <content encoding="json"><![CDATA[{
        "data":"JVBERi0xLjQK...",
        "fileName":"Document.pdf",
        "fileSize":1024000,
        "hasAnnotations":false
    }]]></content>
    <data><![CDATA[{
        "pdfData":"JVBERi0xLjQK...",
        "fileName":"Document.pdf",
        "fileSize":1024000,
        "hasAnnotations":true,
        "annotationsModified":"2026-01-15T10:00:00Z"
    }]]></data>
</note>
```

> **Note:** PDF notes use `<content encoding="json">` because the content element holds structured data rather than display text. See Section 3.3 for the `encoding` attribute specification.

**PDF Note Data Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `pdfData` | string | Yes | Base64-encoded PDF file data |
| `fileName` | string | Yes | Original filename |
| `fileSize` | number | Yes | File size in bytes |
| `hasAnnotations` | boolean | No | Whether user annotations exist on this PDF |
| `annotationsModified` | string | No | ISO 8601 timestamp of last annotation change |

#### Video Note (v2.2)

Video notes support embedded or external storage. Embedded videos store Base64-encoded data directly in the NXL file. External videos store a reference to a file in the Media/Video directory.

```xml
<note id="note_uuid" type="video" created="..." modified="...">
    <title>Meeting Recording</title>
    <data><![CDATA[{
        "storageMode":"embedded",
        "data":"AAAAIGZ0eXBpc29t...",
        "mimeType":"video/mp4",
        "duration":360,
        "width":1920,
        "height":1080,
        "fileSize":52428800,
        "thumbnail":"iVBORw0KGgo...",
        "transcription":"Meeting notes from the quarterly review..."
    }]]></data>
</note>
```

**Video Note Data Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `storageMode` | string | Yes | `"embedded"` (data in NXL) or `"external"` (file reference) |
| `data` | string | Conditional | Base64-encoded video data (required if `storageMode` is `"embedded"`) |
| `ref` | string | Conditional | Relative path to external video file (required if `storageMode` is `"external"`) |
| `hash` | string | No | SHA-256 hash of the video file for integrity verification |
| `mimeType` | string | Yes | MIME type: `"video/mp4"`, `"video/webm"`, `"video/ogg"` |
| `duration` | number | No | Duration in seconds |
| `width` | number | No | Video width in pixels |
| `height` | number | No | Video height in pixels |
| `fileSize` | number | No | File size in bytes |
| `thumbnail` | string | No | Base64-encoded thumbnail image (JPEG) |
| `transcription` | string | No | Transcribed text from audio track |
| `originalFilename` | string | No | Original filename before import |
| `externalPath` | string | No | Full path to external file on originating device |
| `codec` | string | No | Video codec identifier (e.g., `"h264"`, `"vp9"`) |
| `audioCodec` | string | No | Audio codec identifier (e.g., `"aac"`, `"opus"`) |
| `frameRate` | number | No | Frame rate in FPS |
| `bitRate` | number | No | Bit rate in bps |
| `metadata` | object | No | Additional platform-specific metadata |
| `fallback` | object | No | Fallback content for platforms that cannot play this codec |

#### Video Link Note (v2.2)

Video link notes embed external video players (YouTube, Vimeo, direct URLs) within the note editor.

```xml
<note id="note_uuid" type="videolink" created="..." modified="...">
    <title>Product Demo</title>
    <data><![CDATA[{
        "url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        "provider":"youtube",
        "videoId":"dQw4w9WgXcQ",
        "displayText":"Product Demo - Q1 2026",
        "thumbnail":"iVBORw0KGgo...",
        "embedUrl":"https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ",
        "embedWidth":560,
        "embedHeight":315,
        "autoplay":false,
        "privacyMode":true
    }]]></data>
</note>
```

**Video Link Data Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `url` | string | Yes | Original URL of the video |
| `provider` | string | No | Detected provider: `"youtube"`, `"vimeo"`, `"direct"` |
| `videoId` | string | No | Provider-specific video identifier |
| `displayText` | string | No | User-facing label for the link |
| `thumbnail` | string | No | Base64-encoded thumbnail image |
| `embedUrl` | string | No | Privacy-respecting embed URL |
| `embedWidth` | number | No | Embed iframe width in pixels |
| `embedHeight` | number | No | Embed iframe height in pixels |
| `autoplay` | boolean | No | Whether to autoplay on display (default: `false`) |
| `privacyMode` | boolean | No | Use privacy-enhanced embed URL (default: `true`) |

### 4.5 Link Note

```xml
<note id="note_uuid" type="link" created="..." modified="...">
    <title>External Link</title>
    <data><![CDATA[{
        "url":"https://example.com",
        "description":"Link description"
    }]]></data>
</note>
```

**Link Note Data Fields (v2.3: documented):**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `url` | string | Yes | Link target URL or internal reference |
| `description` | string | No | User-provided description of the link |

**Link Types:**
- External: `http://`, `https://`, `mailto:`, `tel:` → Opens in system browser
- Internal: `notesxml://` or wiki-style `[[Page Name]]` → Navigates within app

### 4.6 Widget Notes

#### Task Widget
```xml
<note id="note_uuid" type="task" created="..." modified="...">
    <title>Task Title</title>
    <data><![CDATA[{
        "completed":false,
        "due":"2025-12-31T23:59:59Z",
        "priority":"high"
    }]]></data>
</note>
```

**Task Widget Data Fields (v2.3: documented):**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `completed` | boolean | Yes | `false` | Whether the task is completed |
| `due` | string | No | `null` | Due date/time as ISO 8601 timestamp, or `null` if no due date |
| `priority` | string | No | `"normal"` | Priority level: `"low"`, `"normal"`, `"high"` |

#### Event Widget
```xml
<note id="note_uuid" type="event" created="..." modified="...">
    <title>Event Title</title>
    <data><![CDATA[{
        "date":"2025-12-25",
        "time":"14:00",
        "duration":120,
        "location":"Conference Room A"
    }]]></data>
</note>
```

**Event Widget Data Fields (v2.3: documented):**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `date` | string | Yes | — | Event date in `YYYY-MM-DD` format |
| `time` | string | No | `null` | Event start time in `HH:MM` format, or `null` for all-day events |
| `duration` | number | No | `null` | Duration in minutes |
| `location` | string | No | `""` | Event location description |

#### Contact Widget
```xml
<note id="note_uuid" type="contact" created="..." modified="...">
    <title>John Doe</title>
    <data><![CDATA[{
        "name":"John Doe",
        "phone":"+1-555-123-4567",
        "email":"john.doe@example.com",
        "address":"123 Main Street, City, State 12345",
        "company":"Acme Corp",
        "notes":"Met at the 2025 conference",
        "sortField":"name",
        "sortOrder":"asc"
    }]]></data>
</note>
```

**Contact Data Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Contact display name |
| `email` | string | No | Email address |
| `phone` | string | No | Phone number (any format) |
| `address` | string | No | Mailing/physical address |
| `company` | string | No | Company or organization name |
| `notes` | string | No | Free-text notes about the contact |
| `sortField` | string | No | Field to sort contacts by when displayed in a list (e.g., `"name"`, `"company"`) |
| `sortOrder` | string | No | Sort direction: `"asc"` or `"desc"`. This is a contact-specific display preference and is unrelated to the page/notebook sort order system. |

### 4.7 Calendar Notes (v2.3: documented)

The calendar note family consists of three note types that work together to provide personal information management within notebooks.

#### Calendar Note

The Calendar Note is a structured note type that stores a collection of named calendars (containing events) and task lists (containing tasks). It renders an interactive calendar view within the Note pane with six view modes (Day, 3-Day, Week, 2-Week, Month, Year).

Calendar data uses a versioned internal schema. The current schema is **Calendar v2**.

```xml
<note id="note_uuid" type="calendar" created="..." modified="...">
    <title>My Calendar</title>
    <content><![CDATA[{
        "version":2,
        "calendars":[
            {
                "id":"cal_default",
                "name":"Personal",
                "color":"#4A90D9",
                "visible":true,
                "events":[
                    {
                        "id":"evt_001",
                        "title":"Team Meeting",
                        "date":"2026-04-15",
                        "startTime":"10:00",
                        "endTime":"11:00",
                        "allDay":false,
                        "repeat":"weekly",
                        "description":"Weekly standup",
                        "overrides":{
                            "2026-04-22":{"cancelled":true}
                        }
                    }
                ]
            }
        ],
        "taskLists":[
            {
                "id":"tl_default",
                "name":"To Do",
                "color":"#E05C5C",
                "visible":true,
                "tasks":[
                    {
                        "id":"task_001",
                        "title":"Review Q1 report",
                        "dueDate":"2026-04-20",
                        "completed":false,
                        "priority":"high",
                        "description":"",
                        "repeat":"none"
                    }
                ]
            }
        ]
    }]]></content>
</note>
```

**Calendar v2 Top-Level Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `version` | integer | Yes | Schema version. MUST be `2` for Calendar v2 data. |
| `calendars` | array | Yes | Array of calendar objects |
| `taskLists` | array | Yes | Array of task list objects |

**Calendar Object Fields:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `id` | string | Yes | — | Unique identifier |
| `name` | string | Yes | — | Display name |
| `color` | string | Yes | — | CSS color string (hex or named) used for event rendering |
| `visible` | boolean | No | `true` | Whether this calendar's events are shown in the view |
| `events` | array | Yes | `[]` | Array of event objects |

**Event Object Fields:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `id` | string | Yes | — | Unique identifier |
| `title` | string | Yes | — | Event title |
| `date` | string | Yes | — | Event date in ISO 8601 `YYYY-MM-DD` format |
| `startTime` | string or null | Yes | — | Start time in `HH:MM` format, or `null` for all-day events |
| `endTime` | string or null | Yes | — | End time in `HH:MM` format, or `null` |
| `allDay` | boolean | Yes | — | Whether this is an all-day event |
| `repeat` | string | Yes | `"none"` | Recurrence rule: `"none"`, `"daily"`, `"weekly"`, `"monthly"`, `"yearly"` |
| `description` | string | No | `""` | Event description |
| `overrides` | object | No | `{}` | Occurrence override map (see below) |

**Occurrence Override Map:**

Recurring events may have individual occurrence overrides stored as an object keyed by occurrence date (`YYYY-MM-DD`). Each override MUST contain a `cancelled` boolean and MAY contain overridden field values:

```javascript
"overrides": {
    "2026-04-22": { "cancelled": true },
    "2026-04-29": { "cancelled": false, "title": "Special Meeting", "startTime": "14:00" }
}
```

| Override Field | Type | Required | Description |
|---------------|------|----------|-------------|
| `cancelled` | boolean | Yes | Whether this occurrence is cancelled |
| `title` | string | No | Overridden title for this occurrence |
| `startTime` | string | No | Overridden start time |
| `endTime` | string | No | Overridden end time |
| `description` | string | No | Overridden description |

**Task List Object Fields:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `id` | string | Yes | — | Unique identifier |
| `name` | string | Yes | — | Display name |
| `color` | string | Yes | — | CSS color string |
| `visible` | boolean | No | `true` | Whether this task list's tasks are shown |
| `tasks` | array | Yes | `[]` | Array of task objects |

**Task Object Fields (within Calendar):**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `id` | string | Yes | — | Unique identifier |
| `title` | string | Yes | — | Task title |
| `dueDate` | string or null | No | `null` | Due date in ISO 8601 `YYYY-MM-DD` format, or `null` |
| `completed` | boolean | Yes | `false` | Whether the task is completed |
| `priority` | string | No | `"normal"` | Priority level: `"low"`, `"normal"`, `"high"` |
| `description` | string | No | `""` | Task description |
| `repeat` | string | No | `"none"` | Recurrence rule: `"none"`, `"daily"`, `"weekly"`, `"monthly"`, `"yearly"` |

> **Note:** Calendar note data is stored in `<content>`, not `<data>`. This is because the calendar data IS the primary content of the note. The `<data>` element is not used by calendar notes.

#### Task List Note (v2.3: documented)

The `task-list` type is a **read-only aggregation view** that collects and displays all tasks from all Calendar notes within the current page (or notebook). It does not store its own task data — it dynamically queries task data from sibling `calendar` notes at render time.

```xml
<note id="note_uuid" type="task-list" created="..." modified="...">
    <title>All Tasks</title>
</note>
```

The `task-list` note type has no `<content>` or `<data>` elements. Its rendered output is computed at display time by aggregating tasks from all `calendar` notes in scope. The serializer writes only the note shell (id, type, title, timestamps).

#### Event List Note (v2.3: documented)

The `event-list` type is a **read-only aggregation view** that collects and displays all events from all Calendar notes within the current page (or notebook). Like `task-list`, it does not store its own event data.

```xml
<note id="note_uuid" type="event-list" created="..." modified="...">
    <title>Upcoming Events</title>
</note>
```

The `event-list` note type has no `<content>` or `<data>` elements. Its rendered output is computed at display time by aggregating events from all `calendar` notes in scope.

### 4.8 Encrypted Note

```xml
<note id="note_uuid" type="encrypted" created="..." modified="...">
    <title>Encrypted Note</title>
    <data><![CDATA[{
        "algorithm":"AES-256-GCM",
        "iterations":100000,
        "salt":"base64_encoded_salt",
        "encryptedContent":"base64_encrypted_data"
    }]]></data>
</note>
```

**Encrypted Note Data Fields (v2.3: documented):**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `algorithm` | string | Yes | Encryption algorithm. Currently only `"AES-256-GCM"` is supported. |
| `iterations` | number | Yes | PBKDF2 key derivation iteration count (default: `100000`) |
| `salt` | string | Yes | Base64-encoded random salt used for key derivation |
| `encryptedContent` | string | Yes | Base64-encoded ciphertext (the encrypted note content including its original type and data) |

> **Decryption:** The `encryptedContent` field, when decrypted, yields the original note data as a JSON string containing the note's pre-encryption `type`, `content`, and `data` fields. The note title is stored in plaintext to allow identification without decryption. The serializer MUST NOT decrypt notes for printing or export unless explicitly requested by the user.

### 4.9 Divider Note

```xml
<note id="note_uuid" type="divider" created="..." modified="...">
    <data><![CDATA[{"style":"line"}]]></data>
</note>
```

**Divider Data Fields:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `style` | string | No | `"line"` | Visual style: `"line"`, `"dots"`, `"stars"` |

### 4.10 System Note Types

System note types are generated by the application to represent internal state. They are rendered in the UI but have special serialization rules.

#### Sync Error Note

The `sync-error` note type is created when a sync operation fails for a specific item. Sync error notes are displayed in the UI to inform the user but are **excluded from NXL file serialization**. They exist only in the in-memory data model during the application session.

```javascript
// In-memory only — never written to NXL files
{
    id: "note_uuid",
    type: "sync-error",
    title: "Sync failed",
    created: "2026-02-15T10:00:00Z",
    modified: "2026-02-15T10:00:00Z"
}
```

The serializer (`toNXML()`) must skip notes where `type === "sync-error"`.

---

## 5. Page-Level Media Elements

### 5.1 Images Element

```xml
<images>
    <image id="img_uuid" created="2025-12-25T10:00:00Z">
        <data encoding="base64" type="image/jpeg"><![CDATA[/9j/4AAQSkZ...]]></data>
        <caption>Photo caption</caption>
    </image>
</images>
```

### 5.2 Attachments Element

```xml
<attachments>
    <attachment id="att_uuid" filename="document.pdf" content_type="application/pdf" size="1024000" created="2025-12-25T10:00:00Z">
        <data encoding="base64"><![CDATA[JVBERi0xLjQK...]]></data>
    </attachment>
</attachments>
```

---

## 6. Data Types & Constraints

### 6.1 ID Format

All `id` attributes use timestamp-based or UUID format:
```
id_1768572447474_9nb6hg4za    (timestamp-based)
page_550e8400-e29b-41d4-a716-446655440000    (UUID-based)
note_550e8400-e29b-41d4-a716-446655440000    (UUID-based)
```

### 6.2 Timestamps

All timestamps use ISO 8601 format in UTC:
```
YYYY-MM-DDTHH:mm:ss.sssZ
```
Example: `2025-12-25T14:30:00.000Z`

### 6.3 Encoding

- File encoding: **UTF-8**
- Binary content: **Base64**
- HTML content: Wrapped in **CDATA** sections
- Type-specific data: **JSON** in CDATA sections

### 6.4 Size Constraints

| Element | Constraint |
|---------|------------|
| Notebook title | Max 200 characters (v2.3: explicitly specified) |
| Page title | Max 200 characters |
| Tags per page | No explicit limit (recommended ≤20) |
| Notes per page | No explicit limit |
| Image resolution | Max 640px (auto-scaled on import) |
| File attachment | Platform-dependent memory limits |

---

## 7. Sort Order Persistence

This section summarizes the sort order persistence model. The full specification is defined in the **Sort Order Persistence Specification v1.0** (NX-SORT-xxx requirement series).

### 7.1 Sort Scope

| Sort Level | What It Controls | Where Stored | Travels With Notebook |
|------------|-----------------|--------------|----------------------|
| **Page Sort Order** | Order of pages within the notebook | `<metadata><pageSortOrder>` in NXL file | Yes |
| **Note Sort Order** | Order of notes within a single page | `<page noteSortOrder="...">` attribute in NXL file | Yes |
| **Notebook List Sort** | Order of notebooks in the application sidebar | localStorage / config.json (NOT in NXL) | No |

### 7.2 Sort Values

All sort order fields use the enumeration defined in Section 3.1 Sort Order Values:

```
manual | az | za | newest | oldest | num_az | num_za
```

### 7.3 Defaults

| Context | Default Value |
|---------|--------------|
| New notebook `pageSortOrder` | `manual` |
| New page `noteSortOrder` | `manual` |
| Imported notebook (no sort metadata) | `manual` |
| Imported page (no `noteSortOrder` attribute) | `manual` |
| Legacy NXL file without `<pageSortOrder>` | `manual` |

### 7.4 Sort Button Cycle

Pressing the sort button cycles through values in this fixed order:

```
manual → az → za → newest → oldest → num_az → num_za → manual → ...
```

### 7.5 Manual Order and the Belongings Element

When `noteSortOrder` is `manual`, notes are displayed in the order defined by `<belonging order="N">` values. When `noteSortOrder` is any automatic value, notes are displayed in computed sorted order but the belongings order is preserved unchanged. Switching back to `manual` restores the original user-defined order.

The same principle applies to pages: the order of `<page>` elements in the `<pages>` container defines the canonical manual page order. Automatic page sorts are computed at display time and do not alter the XML serialization order.

### 7.6 Cross-Reference

The Sort Order Persistence Specification v1.0 defines the complete behavioral rules including: sort button behavior (NX-SORT-001/002), new item placement (NX-SORT-010–023), drag-and-drop interaction (NX-SORT-030/031), notebook load restoration (NX-SORT-050–052), stability and tiebreakers (NX-SORT-070/071), import defaults (NX-SORT-080–082), and runtime state management (NX-SORT-090–095).

---

## 8. Directory Structure Context

### 8.1 File Locations

```
NotesXML/                           # Sync Root
├── Notebooks/                      # Primary .nxl files
│   ├── Personal/
│   │   └── Journal.nxl
│   └── Work/
│       └── Projects.nxl
├── Backups/
│   ├── Auto/                       # AutoSave: {name}_{YYYY-MM-DD}_{HHmmss}.nxl.bak
│   └── Manual/                     # Manual: {name}_backup_{YYYY-MM-DD}.nxl
├── Exports/
├── Imports/
├── Templates/                      # .nxlt files
└── Media/                          # External media storage (video, large files)
```

### 8.2 Platform Paths

| Platform | Sync Root Location |
|----------|-------------------|
| Android | `/storage/emulated/0/Documents/NotesXML/` |
| Desktop (Windows) | `<user-documents>\NotesXML\` |
| Desktop (Linux) | `~/Documents/NotesXML/` |
| Web | IndexedDB or File System Access API root |

---

## 9. Import/Export Mappings

### 9.1 ENEX → NXL Mapping

| ENEX Element | NXL Element |
|--------------|-------------|
| `<note>` | `<page>` |
| `<title>` | `<page title="...">` |
| `<tag>` | `<page><tags><tag>` |
| `<content>` (ENML) | `<note type="richtext"><content>` (HTML) |
| `<resource>` (image) | `<note type="image">` with Base64 in JSON |
| `<resource>` (file) | `<note type="file">` with Base64 in JSON |

### 9.2 Markdown → NXL Mapping

| Markdown Element | NXL Element |
|------------------|-------------|
| Filename | Notebook name |
| H1 heading | Page title |
| Content between H1s | Notes in that page |
| `**bold**` | `<strong>` in richtext |
| `*italic*` | `<em>` in richtext |
| `- item` | Bulleted list note |
| `1. item` | Numbered list note |
| `- [ ] item` | Checklist note |

### 9.3 Backpack XML Import

The `<backpack>` root element is ONLY used when importing legacy 37Signals Backpack XML files. These are converted to native NXL format (`<notebook>` root) upon import.

---

## 10. Platform-Specific Considerations

### 10.1 Android (WebView)

**16 KB Page Size Compliance:**
- Use chunked file reading (8KB buffers)
- Streaming XML parser (XmlPullParser)
- Storage Access Framework for file operations

```kotlin
// Chunked file reading
fun readFileChunked(uri: Uri): String {
    val inputStream = contentResolver.openInputStream(uri)
    val reader = BufferedReader(InputStreamReader(inputStream))
    val buffer = CharArray(8192) // Under 16KB limit
    // ...
}
```

### 10.2 Desktop (Electron/AppImage)

- Native Node.js `fs` module access
- Unrestricted file system
- Native file dialogs via Electron IPC

```javascript
// Electron readFile response format
{
    success: true,
    content: "<?xml version=\"1.0\"?>...",
    filePath: "/home/user/documents/file.nxl"
}
```

### 10.3 Web Browser

- File System Access API (Chrome, Edge)
- Fallback to `<input type="file">` + File API
- IndexedDB for settings/recent files
- Must prompt user for each save operation

---

## 11. Validation Requirements

### 11.1 XML Validation

- Well-formed XML (balanced tags, proper nesting)
- UTF-8 encoding declaration
- Root element must be `<notebook>`
- Version attribute present on root element

### 11.2 Content Validation

- All referenced IDs in `<belongings>` must exist in `<notes>`, `<images>`, or `<attachments>`
- Timestamps must be valid ISO 8601 format
- Base64 content must be valid encoding
- JSON in `<data>` elements must be valid JSON
- MIME types must match content
- Sort order values must be one of the enumeration defined in Section 3.1; unrecognized values default to `manual`
- Calendar note `<content>` JSON must conform to Calendar v2 schema (Section 4.7); legacy v1 data must be auto-migrated (Section 15.4)

---

## 12. JavaScript Data Model Reference

### 12.1 Notebook Object
```javascript
{
    id: "notebook_uuid",
    name: "My Notebook",
    filepath: "/path/to/file.nxl",
    format: "xml",
    isModified: false,
    metadata: {
        created: "2025-12-25T10:00:00Z",
        modified: "2025-12-25T14:30:00Z",
        author: "User Name",
        version: "2.0"
    },
    pages: [ /* Page objects */ ],
    pageSortOrder: "manual",
    settings: {
        pageSize: "US_Letter",
        encrypted: false,
        compression: false,
        autosave: true
    }
}
```

> **v2.2 Change:** The `sortOrder` property has been removed. Notebook list sort is an application preference stored in `window.notebookSortOrder` and persisted to localStorage/config.json. `pageSortOrder` default changed from `"az"` to `"manual"`.

### 12.2 Page Object
```javascript
{
    id: "page_uuid",
    title: "Page Title",
    tags: ["work", "important"],
    created: "2025-12-25T10:00:00Z",
    modified: "2025-12-25T14:30:00Z",
    isHome: false,
    noteSortOrder: "manual",
    notes: [ /* Note objects */ ],
    images: [ /* Image objects */ ],
    attachments: [ /* Attachment objects */ ],
    belongings: [
        { type: "note", id: "note_123", order: 0 },
        { type: "note", id: "note_456", order: 1 }
    ]
}
```

> **v2.2 Change:** `noteSortOrder` default changed from `"az"` to `"manual"`.

### 12.3 Note Object
```javascript
{
    id: "note_uuid",
    type: "richtext",  // See NOTE_TYPES in Section 13
    title: "Note Title",
    created: "2025-12-25T10:00:00Z",
    modified: "2025-12-25T14:30:00Z",
    content: "HTML content for richtext",  // For richtext, text, html, quote, code
    // Type-specific fields merged at top level:
    items: [...],      // For checklist, list
    headers: [...],    // For table
    rows: [...],       // For table
    language: "python" // For code
    // etc.
}
```

---

## 13. Constants Reference

### JavaScript
```javascript
const NOTESXML_EXTENSIONS = {
    NOTEBOOK: '.nxl',
    ENCRYPTED: '.nxl.enc',
    TEMPLATE: '.nxlt',
    BACKUP: '.nxl.bak'
};

const NOTE_TYPES = [
    'richtext', 'text', 'html',                 // Text-based (v2.3: text/html documented)
    'checklist', 'list',                         // Lists
    'table',                                     // Table
    'code', 'quote',                             // Formatted text
    'image', 'image-gallery',                    // Images (v2.2: added image-gallery)
    'audio',                                     // Audio
    'video', 'videolink',                        // Video (v2.2: new)
    'pdf',                                       // PDF viewer (v2.2: new)
    'file', 'handwriting',                       // Files & drawing
    'link',                                      // Links
    'task', 'event', 'contact',                  // Widgets
    'calendar', 'task-list', 'event-list',       // Calendar (v2.3: fully documented)
    'divider',                                   // Structural
    'encrypted'                                  // Security
];

// System note types — rendered in UI but excluded from NXL serialization
const SYSTEM_NOTE_TYPES = [
    'sync-error'                                 // Sync failure indicator (v2.2: documented)
];

const SORT_ORDER_VALUES = [
    'manual',                                    // User-defined drag-and-drop order
    'az', 'za',                                  // Alphabetical ascending / descending
    'newest', 'oldest',                          // By modified date desc / created date asc
    'num_az', 'num_za'                           // Numerical ascending / descending by leading digits
];
```

### Kotlin (Android)
```kotlin
object NotesXMLExtensions {
    const val NOTEBOOK = ".nxl"
    const val ENCRYPTED = ".nxl.enc"
    const val TEMPLATE = ".nxlt"
    const val BACKUP = ".nxl.bak"
}
```

---

## 14. Note Type Quick Reference (v2.3: new)

This table provides an at-a-glance summary of all note types, their storage elements, and Section 4 documentation references.

| Type | Category | Uses `<content>` | Uses `<data>` | Section |
|------|----------|:-:|:-:|---------|
| `richtext` | Text-based | ✓ HTML | — | 4.1 |
| `text` | Text-based | ✓ Plain text | — | 4.1 |
| `html` | Text-based | ✓ HTML | — | 4.1 |
| `quote` | Text-based | ✓ Plain text/HTML | — | 4.1 |
| `code` | Text-based | ✓ Source code | ✓ `{language}` | 4.1 |
| `checklist` | List | — | ✓ `{items[]}` | 4.2 |
| `list` | List | — | ✓ `{ordered, items[]}` | 4.2 |
| `table` | Table | — | ✓ `{headers[], rows[]}` | 4.3 |
| `image` | Media | — | ✓ `{data, metadata}` | 4.4 |
| `image-gallery` | Media | — | ✓ `{rows, columns, cells[]}` | 4.4 |
| `audio` | Media | — | ✓ `{data, mimeType, duration}` | 4.4 |
| `handwriting` | Media | — | ✓ `{svg, paths, width, height}` | 4.4 |
| `file` | Media | — | ✓ `{data, metadata}` | 4.4 |
| `pdf` | Media | ✓ JSON (`encoding="json"`) | ✓ `{pdfData, fileName}` | 4.4 |
| `video` | Media | — | ✓ `{storageMode, data/ref}` | 4.4 |
| `videolink` | Media | — | ✓ `{url, provider}` | 4.4 |
| `link` | Link | — | ✓ `{url, description}` | 4.5 |
| `task` | Widget | — | ✓ `{completed, due, priority}` | 4.6 |
| `event` | Widget | — | ✓ `{date, time, duration}` | 4.6 |
| `contact` | Widget | — | ✓ `{name, phone, email}` | 4.6 |
| `calendar` | Calendar | ✓ Calendar v2 JSON | — | 4.7 |
| `task-list` | Calendar | — | — | 4.7 |
| `event-list` | Calendar | — | — | 4.7 |
| `encrypted` | Security | — | ✓ `{algorithm, encryptedContent}` | 4.8 |
| `divider` | Structural | — | ✓ `{style}` | 4.9 |
| `sync-error` | System (non-persistent) | — | — | 4.10 |

---

## 15. Migration Notes

### 15.1 Legacy Format Support

The parser (`parseNXML()`) supports both legacy and optimized formats:

1. **Legacy format**: Type-specific data stored in individual XML elements
2. **Optimized format (v2.0+)**: Type-specific data stored as JSON in `<data>` element

The parser auto-detects the format and handles both transparently.

### 15.2 Backpack Import

Files with `<backpack>` root are automatically detected and processed by `parseBackpackXML()`, which converts them to native NXL format. This is a one-way import; native NXL files always use `<notebook>` root.

### 15.3 v2.1 → v2.2 Sort Order Migration

**`<sortOrder>` Element Removed:**

The `<sortOrder>` element in `<metadata>` has been removed in v2.2. In v2.1, this element was ambiguously defined as "Notebook sort order," but was architecturally misplaced — it stored a device-level application preference (the order of notebooks in the sidebar) inside individual NXL files, causing a "last-opened notebook wins" conflict when multiple notebooks had different values.

**Parser backward compatibility:** When the parser encounters a v2.1 file:
- If `<sortOrder>` is present and `<pageSortOrder>` is absent, the parser MUST treat the `<sortOrder>` value as `pageSortOrder` (applying legacy value migration per below).
- If both `<sortOrder>` and `<pageSortOrder>` are present, `<pageSortOrder>` takes precedence and `<sortOrder>` is ignored.
- The serializer MUST always write `<pageSortOrder>` and MUST NOT write `<sortOrder>`.

**Legacy Sort Value Migration:**

The parser MUST map legacy sort values to current values when reading from NXL files:

| Legacy Value | Current Value | Notes |
|-------------|---------------|-------|
| `created` | `oldest` | NXL Spec v2.1 enumeration |
| `modified` | `newest` | NXL Spec v2.1 enumeration |
| `old` | `oldest` | Code value from Alpha series |
| `new` | `newest` | Code value from Alpha series |
| `custom` | `manual` | Code value from Alpha series |
| `09` | `num_az` | Code value from Alpha 48.7 |
| `90` | `num_za` | Code value from Alpha 48.7 |
| `az` | `az` | Unchanged |
| `za` | `za` | Unchanged |
| `manual` | `manual` | Unchanged (NXL Spec v2.1) |
| (unrecognized) | `manual` | Fallback for any unknown value |

**Default Value Change:**

The default for both `<pageSortOrder>` and `noteSortOrder` has changed from `az` to `manual`. Parsers reading files without these elements MUST now default to `manual`.

### 15.4 Calendar v1 → v2 Schema Migration (v2.3: new)

Calendar notes use a versioned internal JSON schema. The current schema is Calendar v2 (see Section 4.7). Legacy v1 data (a bare array of events with no `version` key) MUST be automatically migrated on load.

**Detection:** `data.version === undefined || data.version < 2`

**Migration procedure:**
1. Wrap the existing events array into a single default calendar object.
2. Initialize an empty `taskLists` array (v1 had no task list support).
3. Set `version` to `2`.
4. Save the migrated data back to the NXL file immediately after migration.

```javascript
// v1 → v2 migration
const migrated = {
    version: 2,
    calendars: [{
        id: 'default',
        name: 'Calendar',
        color: '#4A90D9',
        visible: true,
        events: legacyEventsArray
    }],
    taskLists: []
};
```

Migration MUST be non-destructive: all event data MUST be preserved. The migrated data MUST be saved back to the NXL file immediately after migration to avoid repeated migration on every load.

### 15.5 v2.2 Removed Note Types

The following note types have been removed in v2.2:

| Type | Reason | Parser Behavior |
|------|--------|----------------|
| `gps-location` | GPS functionality removed from NotesXML | If encountered in legacy files, preserve as-is in memory for display but do not create new instances. The note is rendered as a read-only informational card. |
| `map-snapshot` | GPS functionality removed from NotesXML | Same as `gps-location`. |

> **Note:** Legacy NXL files containing `gps-location` or `map-snapshot` notes will load and display correctly. Users can view, export, and delete these notes but cannot create new ones. The serializer continues to write these note types if they exist in the data model to avoid data loss on round-trip.

---

## 16. Per-Notebook Lock File Protocol (v2.6: new, Normative)

NotesXML uses a per-notebook lock file to coordinate exclusive access between concurrent processes (e.g. multiple NotesXML instances, IWV Agent, IWV Chat AI). The lock file pattern is normative under SRS NX-AGENT-011 through 014 and ICD v3.9 §29.4; external applications writing to NXL notebooks MUST honour it.

### 16.1 File Naming and Location

For a notebook at path `{name}.nxl`, the lock file is `{name}.nxl.lock` co-located with the notebook (same directory). The lock applies to the notebook regardless of its encryption state — for an encrypted notebook `{name}.nxl.enc`, the lock file is still `{name}.nxl.lock` (the lock is on the **logical** notebook, not the on-disk file form).

### 16.2 Lock File Schema

The lock file is a small JSON document:

```json
{
  "schemaVersion": 1,
  "pid": 12345,
  "host": "user-laptop",
  "process": "NotesXML",
  "platform": "electron-linux",
  "appVersion": "Beta 5.131.0",
  "acquiredAt": "2026-05-28T20:38:14.706Z"
}
```

Fields are informational except `pid` (used for stale-lock detection) and `acquiredAt` (used for diagnostics).

### 16.3 Acquisition (Atomic)

1. Before opening a notebook for write, NotesXML attempts to create `{name}.nxl.lock` using an **atomic exclusive-create** primitive (`O_EXCL | O_CREAT` on POSIX, `CREATE_NEW` on Windows; the platform IPC layer in ICD §29.4 exposes this as the `notebook:lock-acquire` channel).
2. If the create succeeds, NotesXML owns the lock and the notebook may be opened for write.
3. If the create fails because the lock file already exists, NotesXML reads it and:
   - **Stale-PID detection:** if the recorded `pid` belongs to a process that no longer exists on this host, the lock is considered stale; NotesXML deletes it and retries the acquire (one re-attempt only).
   - **Cross-host case:** if `host` differs from the local hostname, NotesXML cannot prove staleness from `pid` alone (PID namespaces don't cross hosts). The notebook is opened **read-only** with a banner explaining which host holds the lock.
   - **Live local lock:** the notebook is opened **read-only** with a banner identifying the owning process (`Notebook is open in NotesXML (pid 12345)` or similar).

### 16.4 Release

The lock is released on:

- **Normal close** of the notebook (`notebook:lock-release` channel — single notebook).
- **App exit** — every held lock is released (`notebook:lock-release-all` channel — bulk release). NotesXML installs an exit handler at startup so even crashes that pass through the main process's `exit` event clean up; abrupt SIGKILL leaves the lock in place to be cleaned via §16.3 stale-PID detection on next open.

### 16.5 Sync Exclusion

`.nxl.lock` files are **machine-local state** — they MUST NOT be synced to cloud providers. NX-AGENT-014 requires the sync exclusion pattern `*.nxl.lock` (or equivalent) in every sync provider (Google Drive, Dropbox) and in the EncryptedFileIO pass-through. Syncing lock files between machines would cause spurious cross-host lock claims after a sync round-trip.

### 16.6 External Writer Compliance

External applications that write to NXL notebooks (e.g. IWV Agent) MUST EITHER:

- **(a)** Use the inbox sidecar pattern (§18.9), in which case lock acquisition is not required — the external writer touches only `.nxl.inbox`, never the primary `.nxl` file directly. This is the **recommended** path.
- **(b)** Acquire the lock per §16.3 before writing the primary `.nxl` file, and release per §16.4 immediately after the write completes. This path is only appropriate for one-shot, atomic, single-file create operations (the create-notebook procedure in §18.4); it is NOT appropriate for partial updates of an existing notebook (use the sidecar instead).

External readers (read-only consumers) MAY ignore the lock file but SHOULD honour any banner/UX that NotesXML displays for a read-only-opened notebook.

---

## 17. Text Extraction Reference Algorithm (v2.4: new, Informative)

This section defines the **reference text extraction algorithm** for external NXL consumers that need a plain-text representation of note content for non-display purposes (search indexing, AI/LLM context, summarization, export).

> **Scope:** This section is **Informative**, not Normative. NotesXML's internal `extractTextContent()` function is the authoritative implementation and may produce richer results for complex types. External consumers SHOULD use this table as their baseline and SHOULD NOT attempt to replicate NotesXML-specific extraction behavior for types marked with `[*]`.

| Note Type | Text Extraction Rule | Source Element |
|-----------|---------------------|----------------|
| `richtext` | Strip HTML tags from `<content>`, return plain text. Preserve paragraph breaks as newlines. | `<content>` |
| `text` | Return `<content>` verbatim (already plain text). | `<content>` |
| `html` | Strip HTML tags from `<content>`, return plain text. | `<content>` |
| `quote` | Return `<title>` (if present) followed by newline, then `<content>` (strip HTML if present). | `<title>` + `<content>` |
| `code` | Return `<content>` verbatim (source code is text). Optionally prefix with language from `<data>.language`. | `<content>` + `<data>.language` |
| `checklist` | For each item in `<data>.items[]`: prefix with `[x] ` (checked) or `[ ] ` (unchecked), append `text`. Join with newlines. Indent nested items (level > 0) with spaces. | `<data>.items[]` |
| `list` | For each item in `<data>.items[]`: prefix with `"N. "` (ordered) or `"- "` (unordered), append `text`. Join with newlines. Indent nested items. | `<data>.items[]` |
| `table` | Format as pipe-delimited table: header row joined with `\|`, separator row of dashes, then each data row joined with `\|`. | `<data>.headers[]`, `<data>.rows[][]` |
| `contact` | Concatenate non-empty fields: `name`, `email`, `phone`, `company`, `address`, `notes`. Each on a new line with field label prefix. | `<data>` fields |
| `task` | Return title. Append `"Due: {due}"` if due date exists. Append `"Priority: {priority}"`. Append `"Status: completed"` or `"Status: pending"`. | `<title>` + `<data>` fields |
| `event` | Return title. Append `"Date: {date}"`, `"Time: {time}"` (if present), `"Duration: {duration}min"` (if present), `"Location: {location}"` (if present). | `<title>` + `<data>` fields |
| `calendar` `[*]` | Iterate `calendars[].events[]`: emit `"{title} — {date}"` per event. Iterate `taskLists[].tasks[]`: emit `"[x] {title}"` (completed) or `"[ ] {title}"` (pending) per task. Do NOT expand recurrence rules or process occurrence overrides — these are display-time computations. | `<content>` (Calendar v2 JSON) |
| `link` | Return `"{title}\nURL: {url}\n{description}"`. | `<title>` + `<data>` fields |
| `videolink` | Return `"{title}\nURL: {url}\nProvider: {provider}"`. | `<title>` + `<data>` fields |
| `audio` | Return `<data>.transcription` if present. Otherwise return `"[Audio note — no transcription]"`. | `<data>.transcription` |
| `video` | Return `<data>.transcription` if present. Otherwise return `"[Video note — no transcription]"`. | `<data>.transcription` |
| `image` | Return `<data>.caption` if present. Otherwise return `"[Image note]"`. | `<data>.caption` |
| `image-gallery` | Return captions of all cells that have captions. Otherwise return `"[Image gallery — N images]"`. | `<data>.cells[].caption` |
| `handwriting` `[*]` | Return `<title>` if present. Otherwise return `"[Drawing/Handwriting note]"`. NotesXML's internal extraction may parse SVG textbox elements for richer results; external consumers SHOULD NOT attempt this. | `<title>` |
| `pdf` `[*]` | Return `"[PDF: {fileName}]"`. Text extraction from embedded PDF data requires a PDF parsing library and is out of scope for NXL-level extraction. | `<content>` or `<data>.fileName` |
| `file` | Return `"[File attachment: {filename}]"`. | `<data>.metadata.original-filename` |
| `divider` | Return empty string. Dividers carry no textual content. | — |
| `encrypted` | Return `"[Encrypted note]"`. Do not attempt decryption. The title (stored in plaintext) MAY be included. | `<title>` only |
| `task-list` | Return empty string. Content is computed at display time from sibling calendar notes. | — |
| `event-list` | Return empty string. Content is computed at display time from sibling calendar notes. | — |
| `sync-error` | Return empty string. System note type, non-persistent. | — |

Types marked with `[*]` indicate that NotesXML's internal `extractTextContent()` may produce richer results than external consumers can replicate. The fallback rules above are safe for all consumers.

---

## 18. External Writer Protocol (v2.4: new)

This section defines the rules for applications other than NotesXML that create content in NXL files. The protocol is designed to allow safe, interoperable content creation while preserving the integrity of existing notebook data.

> **Reference:** This section was proposed by the IWV Agent project (IWVAGENT-NXL-DELTA-001) and accepted with modifications by the NotesXML project (NXML-RESPONSE-IWVAGENT-001).

### 18.1 Concurrency Safety

**CRITICAL:** NotesXML holds open notebooks as in-memory DOM models. On save, `toNXML()` serializes the entire in-memory model to disk. NotesXML has no file watcher and no external modification detection. If an external writer modifies a notebook file on disk while NotesXML has it open, **NotesXML will silently overwrite the external changes on the next save.**

**Mandatory rule:** An external writer MUST NOT write to a notebook file that may be currently open in NotesXML. Compliance strategies include:

| Strategy | Description | Phase |
|----------|-------------|-------|
| **Agent-owned notebooks** | The Agent creates its own notebooks and never touches notebooks it did not create. No concurrency window exists because NotesXML opens the file after the Agent finishes writing. | Phase 1 (recommended for initial release) |
| **Inbox sidecar** | The Agent writes to a `.nxl.inbox` sidecar file. NotesXML detects and merges it. See Section 18.9. | **Implemented (Beta 5.x).** Binding under SRS NX-AGENT-006 through 010. |
| **IPC bridge** | NotesXML exposes a local endpoint; the Agent sends structured commands. NotesXML applies changes to its in-memory model. | Phase 3 (deferred) |

### 18.2 Scope of External Write Operations

External writers operate under a restricted permission set:

| Operation | Permitted | Notes |
|-----------|-----------|-------|
| Create new notebook file | Yes | Must produce valid NXL v2.0 XML |
| Append new page to existing notebook | Yes | Must append after all existing `<page>` elements |
| Append new note to existing page | Yes | Must append within existing `<notes>` and update `<belongings>` |
| Modify existing note content | **No** | Reserved for NotesXML until writer validation |
| Delete existing note | **No** | Reserved for NotesXML |
| Modify existing page attributes | **No** | Reserved for NotesXML (except `modified` timestamp) |
| Reorder pages or notes | **No** | Reserved for NotesXML |
| Modify `<metadata>` (except `<modified>`) | **No** | Reserved for NotesXML |

### 18.3 Append-Note Procedure

When appending a note to an existing page, the external writer MUST follow this sequence:

1. **Read** the entire NXL file into memory (or DOM).
2. **Locate** the target `<page>` element by index or ID.
3. **Generate** a unique note ID (UUID v4, prefixed with `note_`).
4. **Generate** the current timestamp in ISO 8601 UTC format.
5. **Create** the `<note>` element with all required attributes (`id`, `type`, `created`, `modified`) and the appropriate `<title>`, `<content>`, and/or `<data>` child elements per the note type's specification in Section 4. Optionally include `creator` attribute (Section 3.3).
6. **Append** the `<note>` element as the last child of the target page's `<notes>` element.
7. **Read** the existing `<belongings>` element. Find the maximum `order` value among all existing `<belonging>` entries.
8. **Append** a new `<belonging type="note" id="{noteId}" order="{maxOrder + 1}"/>` entry.
9. **Update** the page's `modified` attribute to the current timestamp.
10. **Update** the `<metadata><modified>` element to the current timestamp.
11. **Write** the complete XML back to the file.

The writer MUST NOT alter any existing `<note>`, `<belonging>`, `<image>`, `<attachment>`, or `<tags>` elements. All existing content MUST survive the round-trip byte-identical (modulo the two `modified` timestamp updates and the appended elements).

### 18.4 Create-Notebook Procedure

When creating a new notebook file, the external writer MUST produce the following minimal valid structure:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<notebook version="2.0">
  <metadata>
    <title>{notebookName}</title>
    <created>{timestamp}</created>
    <modified>{timestamp}</modified>
    <author>IWV Agent</author>
    <version>2.0</version>
    <pageSortOrder>manual</pageSortOrder>
  </metadata>
  <pages>
    <page id="{pageId}" title="Page 1" created="{timestamp}" modified="{timestamp}" noteSortOrder="manual">
      <tags/>
      <notes/>
      <belongings/>
    </page>
  </pages>
</notebook>
```

The notebook MUST contain at least one page. The `<metadata><author>` field SHOULD identify the creating application.

### 18.5 Create-Page Procedure

When appending a page to an existing notebook:

1. **Generate** a unique page ID (UUID v4, prefixed with `page_`).
2. **Create** the `<page>` element with all required attributes.
3. **Append** the `<page>` element as the last child of `<pages>`.
4. **Include** empty `<tags/>`, `<notes/>`, and `<belongings/>` child elements.
5. **Update** `<metadata><modified>` to the current timestamp.

### 18.6 XML Preservation Rules

When an external writer opens an existing NXL file to append content, it MUST preserve:

| Element / Attribute | Preservation Rule |
|---------------------|-------------------|
| XML declaration (`<?xml ...?>`) | Preserve exactly |
| `<notebook version="2.0">` | Preserve exactly — do not change version attribute |
| `<metadata>` children (except `<modified>`) | Preserve exactly — do not add, remove, or modify |
| `<metadata><modified>` | Update to current timestamp |
| All existing `<page>` elements | Preserve exactly (attributes, children, order) |
| Target page's `modified` attribute | Update to current timestamp |
| All existing `<note>` elements | Preserve exactly — do not modify content, attributes, or order |
| All existing `<belonging>` entries | Preserve exactly — do not modify order values |
| All existing `<tags>`, `<images>`, `<attachments>` | Preserve exactly |
| Comments, processing instructions | Preserve if possible (best-effort) |
| Whitespace / formatting | Best-effort preservation; minor whitespace changes are acceptable |

### 18.7 Note Type Write Classification

External writers SHOULD only create note types they can produce correctly. Note types are classified by write safety:

| Classification | Note Types | External Writer Guidance |
|----------------|-----------|--------------------------|
| **Safe to create** | `richtext`, `text`, `checklist`, `list`, `table`, `code`, `quote`, `link`, `divider` | These types use straightforward `<content>` and/or `<data>` elements with well-defined field schemas. An external writer can reliably produce valid instances by following the data field tables in Section 4. |
| **Create with caution** | `task`, `event`, `contact` | These widget types have defined field schemas but interact with NotesXML's calendar and contact systems. External writers SHOULD only create these if they understand the field semantics. Created instances will display correctly but may not participate in calendar aggregation unless associated with a `calendar` note. |
| **Do not create externally** | `image`, `image-gallery`, `audio`, `video`, `videolink`, `pdf`, `file`, `handwriting`, `calendar`, `task-list`, `event-list`, `encrypted`, `sync-error` | These types have complex data requirements (Base64 media, SVG paths, Calendar v2 JSON with migration, encryption key derivation) or are system types that should only be generated by NotesXML's internal logic. External writers attempting to create these risk producing invalid content that causes display errors or data loss. |

**Recommended defaults for AI-generated content:**

| Agent Output Type | Recommended NXL Note Type | Rationale |
|---|---|---|
| Formatted text (summaries, reports, digests) | `richtext` | Supports headings, bold, lists, links. The most versatile type for AI output. |
| Plain text (raw LLM responses) | `text` | No formatting overhead. |
| Action item lists | `checklist` | Maps directly to extracted tasks with checked/unchecked state. |
| Outlines, bullet lists | `list` | Supports ordered/unordered with nesting levels. |
| Structured data, comparisons | `table` | Headers + rows maps to any tabular output. |
| Code snippets, technical output | `code` | Preserves formatting with language identifier. |
| Quotes, excerpts | `quote` | Semantically correct for cited passages. |
| Reference links | `link` | URL + title + description. |
| Visual separators | `divider` | Lightweight structural separator between content blocks. |

### 18.8 Migration Responsibility Boundary

Format migrations defined in Section 15 (Legacy Format Support, Sort Order Migration, Calendar v1→v2 Migration, Removed Note Types) are the exclusive responsibility of NotesXML. External applications that read NXL files MUST NOT perform any migration transformation.

| Migration | NotesXML Responsibility | External Application Behavior |
|-----------|------------------------|-------------------------------|
| Legacy → v2.0 optimized format (§15.1) | Auto-detect and read both formats | Read both formats; never convert legacy to optimized |
| `<sortOrder>` removal (§15.3) | Remove on load, migrate to localStorage | Ignore `<sortOrder>` if present; do not remove it |
| Calendar v1 → v2 (§15.4) | Migrate and save back immediately | Read v1 data as-is; do not migrate to v2 |
| Removed note types — `gps-location`, `map-snapshot` (§15.5) | Preserve on round-trip | Preserve on round-trip; do not create new instances |

**Rationale:** Migrations involve writing modified data back to the NXL file, which could conflict with NotesXML's own migration logic if both applications touch the same file. By restricting migration to NotesXML, we avoid double-migration, data loss from partial migrations, and race conditions.

### 18.9 Inbox Sidecar Pattern

When an external writer needs to deliver notes into an existing notebook that may be open in NotesXML, it MUST use the inbox sidecar pattern instead of writing directly to the `.nxl` file.

**Sidecar file naming:** For a notebook at path `{name}.nxl`, the inbox file is `{name}.nxl.inbox`.

**Sidecar file format:** The inbox file is a valid NXL XML file containing only the new content to be merged. It follows the standard `<notebook>` schema with a minimal `<metadata>` section and one or more `<page>` elements containing the new notes.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<notebook version="2.0">
  <metadata>
    <title>Inbox</title>
    <created>{timestamp}</created>
    <modified>{timestamp}</modified>
    <version>2.0</version>
  </metadata>
  <pages>
    <page id="{pageId}" title="Agent Digest — 2026-04-13"
          created="{timestamp}" modified="{timestamp}" noteSortOrder="manual">
      <tags><tag>iwv-agent</tag></tags>
      <notes>
        <note id="{noteId}" type="richtext" created="{timestamp}" modified="{timestamp}" creator="iwv-agent">
          <title>Weekly Summary</title>
          <content><![CDATA[<p>Content here...</p>]]></content>
        </note>
      </notes>
      <belongings>
        <belonging type="note" id="{noteId}" order="0"/>
      </belongings>
    </page>
  </pages>
</notebook>
```

**NotesXML merge behavior (implemented in Beta 5.x per SRS NX-AGENT-006 through 010):**

The behavior below is normative for NotesXML Beta 5.x+:

- **Targeted page merge:** if the sidecar's root `<notebook>` carries a `targetPageId` attribute that matches an existing page in the primary notebook, the sidecar's notes are merged into THAT page (preserving page-level metadata). If the attribute is absent, the sidecar's pages are appended as new pages at the end of the primary notebook (NX-AGENT-007 / NX-AGENT-008).
- **Duplicate id protection:** before merging, NotesXML scans every `id` attribute across the sidecar's notes/pages/images/attachments and the primary notebook's existing IDs. Any colliding ID is regenerated on the sidecar side before merge — NotesXML's own data is never modified to resolve a collision (NX-AGENT-009).
- **Error-safe handling:** a malformed, partially-written, or corrupted `.nxl.inbox` file MUST NOT block the notebook open. NotesXML logs the failure, leaves the sidecar in place (the user can investigate or the Agent can rewrite it), and opens the primary notebook normally (NX-AGENT-010).
- **User confirmation:** on a successful merge, NotesXML displays a toast notification of the form *"IWV Agent added N page(s) to this notebook"* (or *"IWV Agent merged N note(s) into page X"* for the targeted case). The notification is non-blocking and dismissable.
- **Sidecar lifecycle:** after a successful merge AND a successful save of the now-merged primary notebook, NotesXML deletes the sidecar file. If the save fails, the sidecar is preserved so the merge can be retried.

1. On notebook open and on periodic check (60-second interval or window focus), check for `{notebook}.nxl.inbox`.
2. If found, parse it using `parseNXML()`.
3. Append each page from the inbox to the current notebook's in-memory model.
4. Save the notebook.
5. Delete the inbox file.
6. Display a toast: "IWV Agent added {N} page(s) to this notebook."

**Invariants:**
- All IDs in the inbox file MUST be unique and MUST NOT collide with existing IDs in the target notebook.
- Multiple inbox deliveries may accumulate if NotesXML is not running. Each is processed independently.
- If the Agent needs to append to an existing page, the inbox SHOULD reference the target page by ID using a `targetPageId` attribute on the `<page>` element: `<page targetPageId="{existingPageId}" ...>`. NotesXML merges the notes into the existing page rather than creating a new one. If the target page ID does not exist, the page is appended as a new page.

---

### 18.10 Workspace Rescan on Focus (v2.6: new, Normative)

When NotesXML is foregrounded (window focus or app-resume), it re-scans the workspace directory to surface any external changes — new notebooks created by IWV Agent, sidecar inbox files dropped by other applications, or notebooks renamed/moved by the filesystem.

The behaviour is normative under SRS NX-AGENT-003 (focus trigger), NX-AGENT-004 (debounce), and NX-AGENT-005 (performance bound). The interface contract is in ICD v3.9 §29.2.

- **Trigger:** any focus event on the NotesXML window (Electron `browser-window-focus` / Android `onResume`). Programmatic focus changes within NotesXML (e.g. modal open/close) do NOT trigger rescan.
- **Debounce:** rescans within 5 seconds of the previous rescan are coalesced — the last focus event in a burst is honoured, intervening events are dropped. This prevents pathological rescan storms when the user rapidly Alt-Tabs.
- **Performance bound:** the rescan operation MUST complete in ≤200 ms for a workspace of up to 500 notebooks. The implementation is a directory listing + per-file mtime check; full notebook parsing is not part of the rescan.
- **Outputs:** the rescan updates the in-app notebook list (added / removed / renamed entries) and, for any open notebook whose corresponding `.nxl.inbox` file appeared since the last rescan, triggers the inbox merge procedure of §18.9.

---

## 19. Version History

| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2024-01 | Initial format with basic note types |
| 2.0 | 2025-06 | Added optimized JSON storage, belongings, metadata extensions |
| 2.1 | 2026-01 | Documentation clarification: `<notebook>` root (not `<backpack>`), page attributes, JSON data format |
| 2.2 | 2026-02 | Sort order overhaul: removed `<sortOrder>`, unified sort value enumeration (`manual\|az\|za\|newest\|oldest\|num_az\|num_za`), defaults changed to `manual`. Added note types: `pdf`, `video`, `videolink`, `image-gallery`. Removed GPS note types (`gps-location`, `map-snapshot`). Documented `sync-error` as non-persistent system type. Added `encoding` attribute on `<content>`. Updated audio note field structure. Added contact note data fields. Added sort order persistence section (Section 7). Added sort value migration table (Section 15.3). |
| 2.3 | 2026-04 | **Documentation completeness for commercial release.** Documented previously undocumented note types: `text` (plain text), `html` (imported HTML), `calendar` (full Calendar v2 JSON schema with calendars, task lists, events, occurrence overrides), `task-list` (aggregation view), `event-list` (aggregation view). Added data field tables for: task widget, event widget, link note, code note, encrypted note, file attachment, image note, checklist items, list items, table data, divider. Added Calendar v1→v2 migration (Section 15.4). Added Note Type Quick Reference table (Section 14). Clarified schema version attribute semantics (`version="2.0"` for all v2.x). Specified notebook title max length (200 chars). Clarified quote note content format. |
| 2.4 | 2026-04 | **External writer interoperability (IWV Agent).** Added `creator` attribute on `<note>` element (§3.3) for multi-application content identification. Added encrypted file detection guidance for external readers (§1.3). Added Text Extraction Reference Algorithm (§17, Informative) defining canonical plain-text extraction per note type. Added External Writer Protocol (§18) with: concurrency safety rules (§18.1), external write operation scope (§18.2), append-note/create-notebook/create-page procedures (§18.3–18.5), XML preservation rules (§18.6), note type write classification (§18.7), migration responsibility boundary (§18.8), and inbox sidecar pattern for Phase 2 coordination (§18.9). Response to IWVAGENT-NXL-DELTA-001. |
| 2.5 | 2026-04-29 | **CR-2026-593 Phase 4.1 — Hotfix doc-baseline refresh.** Aligned revision header with the post-Phase-3 documentation baseline (SRS v9.6, ICD v3.6, Cross-Reference v1.10); annotated audit-finding closures (§11.1/§11.2/§11.3) and informational pointer to the catalog `voskModels` field removal (CR-2026-593 Phase 4.5). No schema changes. |
| 2.6 | 2026-05-28 | **CR-2026-834 audit alignment — IWV Agent Coexistence implementation status.** Removed "Phase 2 / requires NotesXML CR" caveats now that NX-AGENT-001 through 014 are implemented in Beta 5.130.3+. Added `.nxl.inbox` and `.nxl.lock` to §1.3 File Extensions. §18.9 Inbox Sidecar expanded with targetPageId / duplicate-id protection / error-safe / toast detail per NX-AGENT-006–010. **NEW §16 Per-Notebook Lock File Protocol** (NX-AGENT-011–014; fills prior §15→§17 numbering gap). **NEW §18.10 Workspace Rescan on Focus** (NX-AGENT-003/004/005). Related Documents bumped to SRS v9.16, ICD v3.9, Cross-Reference v1.15. No schema changes; documentation-only catch-up. |

---

**End of Specification**
