# attachments

Reference for copying Notion attachments — images, PDFs, and other media — into the account's local archive and recording them as graph nodes.

## Shape on disk

Notion exports media files alongside the page that references them. The export's directory layout varies — small media may live in the same directory as the referencing `.md`, larger media in a sibling subdirectory named after the page. The skill does not assume a single layout; it follows attachment references **from the page markdown**, not by enumerating media files.

Referenced shapes:

- `![alt](Image%20Name%20<32-hex-notionId>.png)` — image embed.
- `[Doc](Doc%20Name%20<32-hex-notionId>.pdf)` — file link.
- HTML `<img src="...">` tags inside the markdown (older exports) — also captured.

The referenced filename is URL-decoded to get the relative path on disk.

## Where they go

Attachments copy to `{accountDir}/archive/notion/<importId>/<sha256>.<ext>`:

- `{accountDir}` is the account's directory (the same directory that holds `account.json`), resolved per the platform's existing `accountDir` convention used by `platform/lib/admins-write` and elsewhere.
- `<importId>` is the UUID for this skill run — keeps a single import's attachments grouped on disk.
- `<sha256>` is the content hash of the file, computed at copy time. Filename collisions on the same hash skip the copy (already present from a prior import of the same file).
- `<ext>` is the original extension preserved from the source filename.

The path scheme is deliberately content-hash-keyed so multiple pages referencing the same file (a logo embedded across many pages, say) share one copy on disk and one `:DigitalDocument` node in the graph.

## What each attachment becomes

One `:DigitalDocument { kind: 'notion-attachment' }` node per content hash:

- `sha256` — the content hash; the MERGE natural key together with `accountId`.
- `name` — original filename (without the Notion ID suffix where present).
- `mimeType` — inferred from extension.
- `bytes` — file size in bytes.
- `localPath` — absolute path under `{accountDir}/archive/notion/<importId>/`.
- `accountId`, `importId`, `source='notion'`, plus standard provenance per SKILL invariant 6.

Page-to-attachment edge: `(:KnowledgeDocument)-[:HAS_ATTACHMENT]->(:DigitalDocument)`. One edge per referencing page; if a single page references the same attachment three times, one edge is MERGEd (idempotent).

## Idempotency

`:DigitalDocument` MERGEs on `(accountId, sha256)`. Re-import of the same file by content hash matches the existing node — `localPath` updates if the file was copied into a fresh `<importId>` directory, every other property stays. The `:HAS_ATTACHMENT` edge also MERGEs, so re-imports do not duplicate.

## What the attachment step does NOT do

- **Does not** OCR scanned PDFs or extract text from images. That belongs to a separate ingest step (a future task can chain `memory-ingest` to derive text from `:DigitalDocument` nodes by mime type).
- **Does not** fetch from Notion's CDN. Notion exports are self-contained — every referenced file is in the zip. If a markdown reference points at an absolute Notion URL (not a relative export path), it is treated as an external link and a `:MENTIONS` edge to a `:KnowledgeDocument { kind: 'external-link', url }` is written instead, with no file copy.
- **Does not** delete attachments on re-import when the page no longer references the file. Orphan attachment detection is a graph-prune concern, not an importer concern.

## Failure modes

- `[notion-import:error] reason=attachment-not-on-disk path=<rel-path>` — the page markdown references a file the walker cannot locate. Skip the copy, skip the edge, log.
- `[notion-import:error] reason=copy-failed path=<rel-path> errno=<…>` — filesystem error during copy. Log and continue; the page itself is still written, only the attachment is missing.
- `[notion-import:error] reason=hash-collision-different-content sha256=<hash>` — two files in the same import produced the same hash but the filenames disagree on extension. Should be impossible in practice; if observed, log and write both as separate `:DigitalDocument` nodes with disambiguated extensions.
