# page-tree

Reference for ingesting Notion pages — the `.md` files at every depth of the export.

## Shape on disk

A Notion page is a markdown file with a 32-hex Notion ID suffix in its basename. Front-matter at the top (when present) carries page-level properties: `Created`, `Created by`, `Last edited`, `Last edited by`, tags. The body is markdown — headings, paragraphs, lists, embedded media references, inline `@person` mentions, and inline `@page` mentions (page-to-page links).

A page with children lives alongside a directory of the same basename containing the child pages and their own subdirectories.

## What each page becomes

One `NotionPageRow` appended to `pageBuffer`; the server-side `notion-pages` handler MERGEs it as a `:KnowledgeDocument { kind: 'notion-page' }` at flush time. Row fields:

- `notionId` — 32-hex from the basename.
- `title` — human title from the basename.
- `text` — full markdown body (front-matter stripped; the front-matter fields are mapped to row fields below).
- `parentNotionId` — `null` for workspace-root pages; otherwise the basename `notionId` of the enclosing directory.
- `notionCreatedAt`, `notionLastEditedAt` — ISO 8601 strings from front-matter `Created` / `Last edited`, or `null`.
- `authorPersonNodeId` — elementId of an in-account `:Person` resolved per the Authorship section below, or `null`.

Provenance (`accountId`, `importId`, `source='notion'`, `createdByAgent='notion-import'`, `createdBySession`, `createdAt`) is stamped server-side; the row does not carry it.

## Hierarchy

Page hierarchy is written as `:HAS_SECTION` between `:KnowledgeDocument` nodes:

- **Top-level pages** — `parentNotionId: null` in the row. The `notion-pages` handler attaches `(:NotionWorkspace)-[:HAS_SECTION]->(:KnowledgeDocument)` at insert time.
- **Nested pages** — every non-null `parentNotionId` is appended to `deferredRelations` with `edgeType: 'HAS_SECTION'`. The `notion-relations-pass-2` flush resolves them once every page is MERGEd, including parents that land in a later batch than their children.

The parent's `notionId` is the basename of the enclosing directory.

## Authorship

When the front-matter records `Created by` (or `Author`), resolve the named user against in-account `:Person` per SKILL invariant 4 (account-filtered fuzzy match on `givenName + familyName`). Write `(:Person)-[:AUTHORED]->(:KnowledgeDocument)` when a match is found or a new `:Person` is created. If the front-matter only carries a Notion display name that resolves to no in-account `:Person` and the import flow already created a `:Person` for an earlier mention with the same display name, MERGE on `(accountId, displayName)` so a Notion user named once in front-matter and once inline becomes one node, not two.

`Last edited by` is not currently written as an edge — page-level edit provenance is downstream work (a future task can add `:LAST_EDITED_BY` if needed).

## Inline `@person` mentions

Notion exports inline person mentions as plain text in the markdown body — typically the user's display name without an `@` prefix in the markdown source, but operators sometimes preserve `@`-prefixed text. The skill matches both shapes:

1. Detect candidate mentions — bracketed names from Notion's link syntax `[Person Name](https://www.notion.so/...)`, plain `@`-prefixed tokens, and front-matter `Mentioned` lists (when present).
2. For each candidate, run `mcp__plugin_memory_memory__memory-search` with `accountId` filter and the candidate's display name as the search term. Use the search's existing fuzzy semantics — do not invent a new matcher.
3. On match, append a `MENTIONS` entry to `deferredRelations` (`sourceNotionId` = the page's `notionId`, `targetNotionId` requires the matched `:Person` to itself carry a `notionId` — if the `:Person` was created by this import the skill assigns one; if the match is an existing in-account `:Person` without a `notionId`, the mention is written via `memory-write` at walk time instead, because pass-2 endpoint resolution is by `(accountId, notionId)` only). On miss, the new `:Person { givenName, familyName, notionId: <fresh-uuid>, source='notion', accountId }` is created via `memory-write` immediately and the mention queued to pass-2 — the mention is preserved either way.

The account-filter step is load-bearing. A "John Smith" mention in the importing account never matches a `:Person` in another account, even if the names collide. See SKILL invariant 4.

## Inline `@page` mentions (page-to-page links)

A page-to-page link in Notion exports as `[Other Page Title](Other%20Page%20Title%20<32-hex-notionId>.md)`. Append a `MENTIONS` entry to `deferredRelations` with `sourceNotionId` = the current page, `targetNotionId` = the linked page, and `properties: { mentionContext: 'page-link' }`. Pass-2 handles resolution — links to pages outside the import set are counted as `skippedUnknownTarget` in the pass-2 counters, not errors.

## Idempotency

Pages MERGE on `(accountId, notionId)`. Re-importing the same export with no changes is a no-op — every property update writes the same value back, every edge MERGE matches existing. The `skipped` counter advances on each unchanged page. Property-level updates (a page edited in Notion between exports) overwrite without versioning; the previous body is lost. A future task could write `:KnowledgeDocumentRevision` audit nodes if version history becomes a need.

## Failure modes

- `[notion-import:error] reason=front-matter-parse file=<path>` — front-matter is malformed YAML. Treat as no front-matter; write the page with body-only.
- `[notion-import:error] reason=mention-search-failed name=<name>` — `memory-search` raised. Log and continue; the mention is dropped (no fallback Person creation when the search itself failed, only when it returned no match).
- `[notion-import:error] reason=parent-not-found notionId=<id>` — the enclosing directory's `notionId` did not resolve to a `:KnowledgeDocument`. Anchor the orphan page to the `:NotionWorkspace` and log.
