# Image enclosures

Image files referenced from post bodies become `:DigitalDocument` nodes attached to the parent post KD via the canonical `:HAS_ENCLOSURE` edge (or `:MENTIONS {mentionContext:'substack-image-enclosure'}` as the documented closed-edge fallback if `schema-base.md` does not declare `:HAS_ENCLOSURE` for `:KnowledgeDocument → :DigitalDocument` at skill-load).

## Discovery walk

After Phase 3a (post writes) but before Phase 3c (image writes), build a per-post image-reference map:

1. For each successfully-imported post HTML body (the operator's source file, before HTML→markdown), parse `<img src="…">` tags.
2. Resolve the `src` relative to the archive root. Substack export sources are typically `images/<filename>` or `../images/<filename>` from a post-body directory; resolve both to the absolute path under `<archive-root>/images/`.
3. Record `{ postAttachmentId → [imageAbsolutePath, …] }`.

Image files referenced from `<img>` are the only enclosures considered. Audio (`<audio>`) and video (`<video>`) embeds are out of scope per SKILL.md.

## Per-image write

For each distinct image path (one MERGE per file, regardless of how many posts reference it):

1. **Compute content hash.** SHA-256 of the file bytes. Persist as `$sha256`.
2. **Copy file.** From the archive's `images/<filename>` to `{accountDir}/archive/substack/<importId>/<sha256><ext>` (perms 0o600). The destination filename is content-addressed so the same image referenced from multiple imports overlays cleanly.
3. **Detect mime type.** From the file extension first (`.jpg`/`.jpeg` → `image/jpeg`, `.png` → `image/png`, `.gif` → `image/gif`, `.webp` → `image/webp`). If the extension is missing or unrecognised, sniff the first 16 bytes against known image magic numbers. Fallback: `application/octet-stream`.
4. **MERGE the `:DigitalDocument`.**
   ```cypher
   MERGE (d:DigitalDocument {accountId: $accountId, sha256: $sha256})
   ON CREATE SET d.kind             = 'substack-image',
                 d.source           = 'substack',
                 d.storagePath      = $storagePath,
                 d.mimeType         = $mimeType,
                 d.originalFilename = $originalFilename,
                 d.createdByAgent   = 'substack-import',
                 d.createdBySession = $sessionId,
                 d.createdAt        = datetime(),
                 d.importId         = $importId
   ```
   Re-imports of the same image are property-update no-ops.

## Per-post attach

For each `(postAttachmentId, imageElementId)` pair from the discovery map:

```cypher
MATCH (k:KnowledgeDocument {accountId: $accountId, attachmentId: $postAttachmentId}),
      (d:DigitalDocument)   WHERE elementId(d) = $imageElementId
MERGE (k)-[e:HAS_ENCLOSURE]->(d)
ON CREATE SET e.createdByAgent = 'substack-import',
              e.createdAt      = datetime()
```

If at skill-load the canonical `:HAS_ENCLOSURE` edge is absent from `schema-base.md` for `:KnowledgeDocument → :DigitalDocument`, swap to:

```cypher
MERGE (k)-[e:MENTIONS {mentionContext: 'substack-image-enclosure'}]->(d)
ON CREATE SET e.createdByAgent = 'substack-import',
              e.createdAt      = datetime()
```

The choice is fixed at skill-load (one read of `schema-base.md`), not per-image — so every image enclosure in this run uses the same edge type. The closed-edge audit query distinguishes the two.

## Missing image files

If an `<img src="…">` reference points at a file that does not exist in the archive's `images/` directory:

- Emit `[substack-import:error] file=<postFilename> reason="image reference missing: <src>"`.
- The post KD is unaffected (it already wrote in Phase 3a); the enclosure is simply absent.
- Continue with remaining images.

## Counter

The per-pass image counter (in the completion log) accumulates:

- `copied`: count of files where the destination at `{accountDir}/archive/substack/<importId>/<sha256><ext>` did not exist before (first-time content). Re-imports of the same image bytes do not advance this counter.
- `skipped`: count of files where the destination already existed (re-import of identical content).
