# Vault Structure

How the parser discovers what to ingest from an extracted Obsidian vault.

## Entry point

The operator supplies an `extractedDir` — an absolute path to the root of an extracted vault. The vault zip is unpacked elsewhere (typically by `unzip-attachment` under `{accountDir}/extracted/<id>/`); this skill assumes the path already exists and is a directory.

## What counts as a page

A vault page is any file under `extractedDir` ending in `.md` (case-insensitive) that is **not** under a hidden directory. Hidden directories — names starting with `.` (`.obsidian/`, `.trash/`, `.git/`) — are skipped at walk time. The `.obsidian/` config tree is metadata about the vault, not vault content.

Symlinks pointing outside the vault root, and any path containing a `..` segment, are rejected at the parser boundary as a defence-in-depth measure on top of the unzip sandboxing.

## What counts as a folder

Every directory under `extractedDir` is walked depth-first. Empty folders are silently skipped. The parser does not infer "this is a Daily Notes folder, this is People" — folder semantics come from filters the operator supplies (see [daily-notes.md](daily-notes.md) for the one exception, where the parser uses folder names as a fallback signal for daily-note detection).

## Frontmatter format

The parser recognises YAML frontmatter delimited by `---` on the first line. The frontmatter block:

- Must start at byte 0 of the file (no BOM, no preceding blank lines).
- Must close with a line that is exactly `---`.
- Is parsed as a minimal YAML subset — top-level `key: value` pairs and `key:` followed by `- item` block sequences, plus flow-style `key: [a, b, c]` arrays.
- Recognised keys: `aliases`, `tags`, `date`, `created`. Other keys are read but ignored.
- A malformed frontmatter block does NOT abort parsing — the parser falls back to "no frontmatter" and uses the file body verbatim. This is by design: a single broken page does not break the batch.

Pages without frontmatter are ingested with empty `aliases` and `tags` fields; their body is the entire file.

## Title

Title is the filename without `.md`, normalised to NFC unicode form. Folder path is NOT part of the title — only the basename. Two pages can share a title (`Projects/Maxy.md` and `People/Adam.md/Maxy.md`); they MERGE separately because the natural key is the full `obsidianPath`, not the title.

## Content hash

Every page contributes a SHA-256 hash of its on-disk bytes. The hash is the idempotency key on re-import: a page whose hash matches the existing graph's `:KnowledgeDocument.contentHash` is skipped during commit. Re-importing the exact same vault is a no-op write.

## Out-of-scope file types

- `.canvas` files — Obsidian Canvas graphs are JSON, not markdown. Not ingested.
- `.base` files — Obsidian Bases are not ingested.
- Loose attachment files (`.png`, `.pdf`, …) not referenced by any `.md` page are not ingested. They're only picked up via the embed syntax in [attachments.md](attachments.md).
- `.obsidian/` config files — never ingested.

Adding any of these means extending the parser library (`platform/lib/obsidian-parser/`); file a task before improvising.
