memory-literary-analysis

Breaks down a whole book into a map of characters, events, and ideas so you understand it better.

Installation
Run `npx skills add "https://github.com/basicmachines-co/basic-memory" --skill "memory-literary-analysis"` to install this skill, then follow its SKILL.md instructions for my next request.

Paste this into Claude Code, Cursor, or any agent that can run commands.

SKILL.mdShow the author's original SKILL.md
---
name: memory-literary-analysis
description: "Analyze a complete literary work into a structured Basic Memory knowledge graph: schema design, entity seeding, chapter-by-chapter processing, cross-referencing, and validation."
---

# Memory Literary Analysis

Transform a complete literary work into a structured knowledge graph. Characters, themes, chapters, locations, symbols, and literary devices become interconnected notes — searchable, validatable, and traversable.

## When to Use

- Analyzing a novel, play, poem, or non-fiction book end-to-end
- Building a teaching or study resource for a literary text
- Creating a book club companion knowledge base
- Research projects requiring structured close reading
- Stress-testing Basic Memory at scale (~200+ notes, 1000+ relations)

## Pipeline Overview

```
Phase 0: Setup         → project, schemas, directory structure
Phase 1: Seed          → stub notes for known major entities
Phase 2: Process       → chapter-by-chapter notes in batches
Phase 3: Cross-ref     → enrich arcs, add parallels, write analysis
Phase 4: Validate      → schema checks, drift detection, consistency
Phase 5: Explore       → traverse the graph, write synthesis notes
```

## Tools

Writing goes through `bm_write` and `bm_edit`. MCP reads use `bm_read`, `bm_find`,
`bm_search`, and `bm_list`. The CLI keeps its POSIX verbs; the table and shell
examples below are CLI commands (prefix each verb with `bm`):

| Need | Use | Instead of |
|------|-----|-----------|
| A section of a long note | `cat <note> --section Observations` | reading the whole note |
| A line range of the source text | `cat <source>.txt --lines 4200-4890` | pulling the whole book into context |
| Notes matching frontmatter | `find --meta status=active` | reading notes to check fields |
| Fields across many notes | `find --meta ... --fields pov,setting` | one read per note |
| Where something lives | `ls`, `tree`, `find --name '*.md'` | listing everything |

The two rules that matter across a 100+ chapter run:

- **Never read a note to check a field.** That is what `--meta` predicates and `--fields`
  projection are for — one call answers what a read-per-note loop would cost.
- **Never pull a whole file into context to reach one part of it.** Sections and line ranges
  slice the *output*: the full note is still fetched, then cut down before it is returned. What
  they save is context, not I/O — a long chapter or a full source text costs you the tokens of
  the relevant part, not of the whole file.

Two sharp edges to know before you write a query. The first fails *quietly* — a short
answer, exit 0, no warning — so learn it here rather than from a graph you thought you had
audited:

- **`find` pages, and the default page is 10.** Any query whose answer is "all N chapters"
  needs `--page-size 200` (the maximum) — see [Coverage Checks](#coverage-checks).
- **`--name` cannot combine with `--meta`.** The metadata search has no filename glob. Scope a
  `--meta` query with the positional path instead: `find /characters --meta
  'note_type=character'`. That path is matched on a directory boundary against the *file path*
  a note is indexed under — where the note actually lives, not its permalink, which stops
  mirroring the file path once a note pins `permalink:` in frontmatter or is moved. So
  `/characters` reaches everything filed under `characters/` (including `characters/major/`),
  and never `characters-cut/`.

For MCP, use `bm_read(identifier="<note>", section="Observations")` or
`start_line`/`end_line` for slices, `bm_find(filters={"type": "chapter"},
fields=["chapter_number", "pov"])` for metadata, and `bm_search(query="doubloon",
lines=2)` for matching lines. `bm_list` lists one folder level; `bm_find` recurses.
MCP list/find/search pages cap at 100; follow `has_more` through every page.
CLI page sizes and flags below are separate from the MCP shapes.

## Phase 0: Setup

### Create the Project

```python
bm_project(command="create", name="<work-name>", path="~/basic-memory/<work-name>")
```

Use a kebab-case slug of the work's title (e.g., `great-gatsby`, `hamlet`, `beloved`).

### Define Schemas

Write 6 schema notes to `schema/`. Each schema defines the entity type's fields, observation categories, and relation types. Adapt fields to fit the work — the schemas below are starting points, not rigid templates.

#### Character Schema

```python
bm_write(
  title="Character",
  directory="schema",
  note_type="schema",
  metadata={
    "entity": "Character",
    "version": 1,
    "schema": {
      "role(enum)": "[protagonist, antagonist, supporting, minor], character's narrative role",
      "description": "string, brief character description",
      "first_appearance?": "string, chapter or scene of first appearance",
      "status?(enum)": "[alive, dead, unknown, transformed], character status at end of work"
    },
    "settings": {"validation": "warn"}
  },
  content="""# Character

Schema for character entity notes.

## Observations
- [convention] Major characters in characters/major/, minor in characters/minor/
- [convention] Observation categories: trait, motivation, arc, quote, appearance, relationship, symbolism, fate
- [convention] Relations: appears_in, contrasts_with, allied_with, commands, symbolizes, associated_with"""
)
```

Add work-specific fields as needed — e.g., `rank` for military fiction, `house` for family sagas, `species` for fantasy.

#### Theme Schema

```python
bm_write(
  title="Theme",
  directory="schema",
  note_type="schema",
  metadata={
    "entity": "Theme",
    "version": 1,
    "schema": {
      "description": "string, what this theme explores",
      "prevalence(enum)": "[major, minor], how central to the work",
      "first_introduced?": "string, where theme first appears"
    },
    "settings": {"validation": "warn"}
  },
  content="""# Theme

Schema for thematic analysis notes.

## Observations
- [convention] Observation categories: definition, manifestation, evolution, counterpoint, quote, interpretation
- [convention] Relations: embodied_by, contrasts_with, reinforced_by, explored_in, expressed_through"""
)
```

#### Chapter Schema

```python
bm_write(
  title="Chapter",
  directory="schema",
  note_type="schema",
  metadata={
    "entity": "Chapter",
    "version": 1,
    "schema": {
      "chapter_number": "integer, sequential chapter number",
      "pov?": "string, point-of-view character or narrator mode",
      "setting?": "string, primary location",
      "narrative_mode?(enum)": "[dramatic, expository, reflective, epistolary, mixed], chapter's primary mode"
    },
    "settings": {"validation": "warn"}
  },
  content="""# Chapter

Schema for chapter-level analysis notes.

## Observations
- [convention] Chapters stored in chapters/ directory
- [convention] Observation categories: summary, event, tone, technique, quote, significance, foreshadowing
- [convention] Relations: features, set_in, explores, contains, employs, follows, precedes, parallels"""
)
```

#### Location Schema

```python
bm_write(
  title="Location",
  directory="schema",
  note_type="schema",
  metadata={
    "entity": "Location",
    "version": 1,
    "schema": {
      "description": "string, what this place is",
      "location_type(enum)": "[city, building, landscape, body_of_water, region, fictional, vehicle], type of place",
      "real_or_fictional(enum)": "[real, fictional, both], whether the place exists"
    },
    "settings": {"validation": "warn"}
  },
  content="""# Location

Schema for location and setting notes.

## Observations
- [convention] Observation categories: description, atmosphere, symbolism, significance, geography
- [convention] Relations: setting_for, associated_with, symbolizes, contains, part_of"""
)
```

#### Symbol Schema

```python
bm_write(
  title="Symbol",
  directory="schema",
  note_type="schema",
  metadata={
    "entity": "Symbol",
    "version": 1,
    "schema": {
      "description": "string, what the symbol is literally",
      "symbol_type(enum)": "[object, animal, color, action, natural_phenomenon, body_part], category of symbol",
      "primary_meaning": "string, most common interpretation"
    },
    "settings": {"validation": "warn"}
  },
  content="""# Symbol

Schema for symbolic element notes.

## Observations
- [convention] Observation categories: meaning, appearance, ambiguity, interpretation, quote, evolution
- [convention] Relations: represents, associated_with, appears_in, contrasts_with, located_at"""
)
```

#### LiteraryDevice Schema

```python
bm_write(
  title="LiteraryDevice",
  directory="schema",
  note_type="schema",
  metadata={
    "entity": "LiteraryDevice",
    "version": 1,
    "schema": {
      "description": "string, what the device is",
      "device_type(enum)": "[rhetorical, structural, figurative, narrative, dramatic], category",
      "frequency(enum)": "[pervasive, frequent, occasional, rare], how often used"
    },
    "settings": {"validation": "warn"}
  },
  content="""# LiteraryDevice

Schema for literary technique and device notes.

## Observations
- [convention] Observation categories: definition, usage, effect, example, significance
- [convention] Relations: used_in, characterizes, expresses, related_to"""
)
```

### Directory Structure

```
<project>/
  <work>.txt         # the source text, verbatim (see Phase 2)
  schema/            # 6 schema definitions
  chapters/          # one note per chapter/section + prologue/epilogue
  characters/
    major/           # protagonist, antagonist, key supporting
    minor/           # named characters with limited roles
  themes/            # thematic analysis notes
  locations/         # settings and places
  symbols/           # symbolic elements
  literary-devices/  # techniques and devices
  analysis/          # cross-cutting synthesis
  tasks/             # processing tracker
```

## Phase 1: Seed Entities

Before processing chapters, create stub notes for major entities so `[[wiki-links]]` resolve from the start.

### Characters (major)

For each major character, create a stub with known metadata:

```python
bm_write(
  title="<Character Name>",
  directory="characters/major",
  note_type="Character",
  tags=["character", "major", "<role>"],
  metadata={"role": "<role>", "description": "<brief description>"},
  content="""# <Character Name>

## Observations
- [role] <Character's role in the work>
- [appearance] <Key physical description>

## Relations
- associated_with [[<Related Character>]]
- appears_in [[<Key Location>]]"""
)
```

### Seed Checklist

Identify the work's major entities before you start reading. A good starting inventory:

| Type | Typical Count | What to Include |
|------|--------------|-----------------|
| Characters (major) | 8-20 | Protagonist, antagonist, key supporting cast |
| Themes | 5-12 | Central concerns the work explores |
| Locations | 4-10 | Primary settings, symbolically significant places |
| Symbols | 4-10 | Recurring objects, images, or motifs with layered meaning |

Stubs don't need to be complete — they give `[[wiki-link]]` targets and will be enriched during chapter processing.

## Phase 2: Chapter Processing

### Source Text Preparation

Obtain the full text and identify chapter/section boundaries. For public domain works, Project Gutenberg is a good source. For copyrighted works, work from a physical or licensed digital copy.

**Put the source text inside the project directory, as `.txt`, and index it once.** `bm cat`
resolves a *note identifier*, not a filesystem path — it can only reach a file the project
index has observed. A one-time index pass gives the raw text an entity row, after which the
line-range slice works against it:

```bash
cp ~/Downloads/moby-dick.txt ~/basic-memory/moby-dick/moby-dick.txt
bm reindex --search -p moby-dick          # one pass; the .txt becomes readable
```

Two constraints that make this the right shape, both worth respecting:

- **Keep it `.txt`, do not convert it to `.md`.** Basic Memory injects frontmatter into
  markdown notes, which shifts every line number by the height of that block — an offset map
  built from the original file would then be silently wrong. A `.txt` is stored verbatim, so
  its line numbers stay 1:1 with the file on disk.
- **Keep it inside the project.** A source text elsewhere on disk is not an entity, and
  `bm cat` answers `Error: Entity not found`. If you must leave it outside, drop the BM verbs
  for the source and use plain shell (`sed -n '4200,4890p' <path>`) — the notes still get the
  BM verbs, only the raw source falls back to the shell.

**Then build a chapter offset map once, before processing.** Scan the text for chapter
headings and record the line range of each chapter, then read chapters by range rather than
re-reading the whole book into context:

```bash
grep -n '^CHAPTER ' ~/basic-memory/moby-dick/moby-dick.txt   # heading -> line number
bm cat moby-dick/moby-dick.txt --lines 4200-4890 --plain   # one chapter, not the whole text
```

`grep -n` here is the shell's grep on a filesystem path (this is the map-building step, and
it needs the file). `bm cat` then takes the *note identifier* and returns exactly that slice plus a
`lines 4200-4890 of N` footer. Spell the identifier project-qualified
(`<work>/<work>.txt`) so it routes to the right project. `bm head moby-dick/moby-dick.txt -n 40` is the cheap
way to eyeball the heading format before writing the grep pattern.

Store the map in the project (a note or a small JSON file) so later batches — and a resumed
run after context compaction — do not have to rediscover it. On a long work this is the
single largest context saving in the pipeline.

### Batching Strategy

Process ~10 chapters per batch to balance depth with progress. Group by narrative arc or thematic focus:

| Batch | Typical Content |
|-------|----------------|
| 1 | Opening: setting, character introductions, world-building |
| 2-3 | Rising action: conflicts established, relationships develop |
| 4-6 | Middle: complications, turning points, thematic deepening |
| 7-8 | Climax approach: escalation, revelations, crises |
| Final | Climax, resolution, epilogue |

Adjust batch size based on chapter length and density. Short, action-heavy chapters can be batched in larger groups; long, philosophically dense chapters may need smaller batches.

### Per-Chapter Workflow

For each chapter:

**1. Read the chapter carefully.** Read the chapter's line range from the offset map
(`bm cat <source>.txt --lines <start>-<end>`), not the whole file. Read the actual text —
never work from memory or a summary; textual evidence is the entire point.

**2. Create the chapter note:**

```python
bm_write(
  title="Chapter <N> - <Title>",
  directory="chapters",
  note_type="Chapter",
  tags=["chapter", "<arc-phase>"],
  metadata={
    "chapter_number": <N>,
    "pov": "<narrator or POV character>",
    "setting": "<primary location>",
    "narrative_mode": "<mode>"
  },
  content="""# Chapter <N> - <Title>

## Observations
- [summary] <1-2 sentence synopsis>
- [event] <Key plot events>
- [tone] <Emotional and stylistic atmosphere>
- [technique] <Notable narrative techniques>
- [quote] "<Significant passage>"
- [significance] <Why this chapter matters to the whole>
- [foreshadowing] <Hints at future events>

## Relations
- features [[<Character>]]
- set_in [[<Location>]]
- explores [[<Theme>]]
- contains [[<Symbol>]]
- employs [[<Literary Device>]]
- follows [[Chapter <N-1> - <Previous Title>]]
- precedes [[Chapter <N+1> - <Next Title>]]"""
)
```

**3. Enrich related entities:**

```python
bm_edit(
  identifier="characters/major/<character-slug>",
  command="insert_after_section",
  section="Observations",
  content="""- [arc] Ch.<N>: <What happens to this character>
- [quote] "<Attributed quote>" (Ch.<N>)"""
)
```

`append` and `prepend` do not target a section — they write at the end of the note and the
start of the body. Use `insert_after_section` / `insert_before_section` to land lines under a
heading; they fail loudly if the heading is missing.

**4. Track progress** using the memory-tasks skill to create a processing task that survives context compaction.

### What to Capture Per Chapter

| Category | What to Look For |
|----------|-----------------|
| `[summary]` | 1-2 sentence chapter synopsis |
| `[event]` | Key plot events (actions, revelations, arrivals) |
| `[tone]` | Emotional and stylistic atmosphere |
| `[technique]` | Narrative innovations (POV shifts, structural experiments, genre blending) |
| `[quote]` | Memorable or thematically significant passages |
| `[significance]` | Why this chapter matters to the whole |
| `[foreshadowing]` | Hints at future events |

### Entity Enrichment Per Chapter

As each chapter is processed, append observations to relevant entities:
- **Characters**: `[arc]` moments, new `[trait]` revelations, `[quote]` attributions
- **Themes**: `[manifestation]` in this chapter, `[evolution]` shifts
- **Symbols**: `[appearance]` with context, new `[interpretation]` angles
- **Locations**: `[atmosphere]` as described, `[significance]` in scene
- **Literary devices**: `[example]` from this chapter

### Adding Prose and Interpretation

After the structured observations are in place, consider adding interpretive prose to major entity notes. Insert 2-4 paragraphs of critical essay before the Observations section with `bm_edit(command="insert_before_section", section="Observations", content=...)`. This prose should:

- Argue for a reading of the character, theme, or symbol — not just describe it
- Connect the entity to the work's larger concerns and to literary tradition
- Include subjective opinions clearly marked as such ("In my reading...", "I find...")
- Ground claims in textual evidence cited by chapter number

The prose adds the interpretive texture that structured observations alone cannot capture.

## Phase 3: Cross-Referencing

After all chapters are processed:

### Find What Needs Enriching

Do not re-read every note to decide what is thin. Query for it:

```bash
bm find --meta 'note_type=chapter' --fields chapter_number,pov,setting --page-size 200
bm find --meta 'note_type=character' --fields role,status --page-size 200   # who is still a stub
bm find --meta 'chapter_number>100' --fields pov --page-size 200           # late-book POV drift
```

A field a note never set comes back as a blank cell (`null` under `--json`), so rows with
blanks are the work queue. This turns "audit the graph" from a read of every note into one
call per question.

`bm_write` snake-cases `note_type` (`Chapter` is stored as `chapter`), and `--meta`
type predicates are normalized the same way, so either spelling matches. `--page-size 200`
is not decoration: without it these return the first 10 rows and the work queue looks ten
items long.

### Character Arcs
For each major character, write a full `[arc]` summary observation covering their trajectory across the work.

### Theme Evolution
For each theme, add `[evolution]` observations tracing how it develops from introduction to resolution.

### Chapter Parallels
Add `parallels` and `contrasts_with` relations between structurally similar chapters (e.g., mirrored scenes, repeated settings, thematic echoes).

### Analysis Notes
Create synthesis notes in `analysis/`:

```python
bm_write(
  title="Narrative Structure",
  directory="analysis",
  note_type="note",
  tags=["analysis", "structure"],
  content="""# Narrative Structure

Analysis of the work's narrative architecture.

## Observations
- [structure] <Overall arc description>
- [technique] <Key narrative strategies>
...

## Relations
- analyzes [[<Protagonist>]]
- analyzes [[<Key Character>]]
- explores [[<Central Theme>]]
..."""
)
```

Recommended analysis notes:
- **Narrative Structure** — overall architecture and pacing
- **Work Overview** — synthesis of the complete work (summary, thesis, legacy)
- **Critical Reception** — historical and contemporary interpretations

### Discover Emergent Entities
During chapter processing, new minor characters, locations, and symbols will emerge. Create notes for any that appear in 3+ chapters or carry thematic weight.

## Phase 4: Validation

### Schema Validation

```python
# Validate each entity type
bm_schema(command="validate", note_type="Character")
bm_schema(command="validate", note_type="Theme")
bm_schema(command="validate", note_type="Chapter")
bm_schema(command="validate", note_type="Location")
bm_schema(command="validate", note_type="Symbol")
bm_schema(command="validate", note_type="LiteraryDevice")
```

### Drift Detection

```python
bm_schema(command="diff", note_type="Character")
# ... for each type
```

Fix issues found — common fixes:
- Missing required observation categories → add them via `bm_edit`
- Enum values outside allowed set → correct metadata
- Fields in notes but not schema → add as optional to schema if legitimate

### Coverage Checks

Schema validation proves notes match their shape. These prove the graph is *complete*:

```bash
bm find --meta 'note_type=chapter' --fields chapter_number --page-size 200   # every chapter present?
bm find --meta 'note_type=chapter' --fields pov,setting --page-size 200      # missing context?
bm find /characters --meta 'note_type=character' --fields role --page-size 200  # inventory vs. seed list
```

**A coverage check that pages is not a coverage check.** `bm find` defaults to
`--page-size 10`, so the un-sized form of the first query "proves" a 138-chapter work has 10
chapters. 200 is the maximum page size; past that, iterate with `--page 2`, `--page 3`, … .
Scope a `--meta` query with the positional path (`/characters`), never `--name` — the two
options are mutually exclusive, because the metadata search has no filename glob. The
positional path scopes by the *file path* a note is indexed under, matched on a directory
boundary: `/characters` admits `characters/major/ahab.md` but never `characters-cut/`. It is
not a permalink match, so a note that pins its own `permalink:` is still found where its file
lives.

Read the count off the footer, not off the rows you can see. In a terminal every `find`
result reports `page 1 • total 138`, and appends `• more available (--page)` when the page
truncated the answer — that suffix appearing is the check *failing*, whatever the visible
rows say. **The footer is a TTY feature.** Piped output without `--plain` is JSON, which
carries `total` and `has_more`; with `--plain` the rows go to stdout and a has-more notice
goes to stderr. For scripts, read `has_more` from `--json`.

For the sequence gap — the failure that a count alone cannot catch — take the numbers from
`--json`, which carries `total`, `total_is_exact`, and `has_more`:

```bash
expected=138; page=1; rows='[]'
while :; do
  resp=$(bm find --meta 'note_type=chapter' --fields chapter_number \
           --page-size 200 --page $page --project <work> --json)
  rows=$(jq -n --argjson acc "$rows" --argjson r "$resp" \
           '$acc + [$r.results[] | {title, n: .fields.chapter_number}]')
  [ "$(jq -r '.has_more' <<<"$resp")" = "true" ] || break
  page=$((page + 1))
done
jq -n --argjson rows "$rows" --argjson expected "$expected" '
  ([$rows[] | select(.n != null and (.n | tostring | test("^[0-9]+$"))) | .n | tonumber]) as $n
  | { total:        ($rows | length),
      unnumbered:   [$rows[] | select(.n == null or (.n | tostring | test("^[0-9]+$") | not))
                             | .title],
      missing:      ([range(1; $expected + 1)] - $n),
      duplicates:   ($n | group_by(.) | map(select(length > 1) | .[0])),
      out_of_range: ($n | map(select(. < 1 or . > $expected)) | unique) }'
```

The loop is not ceremony. `--page-size` caps at 200, so a single call cannot inventory a work
with more than 200 chapters — and rerunning it with `--page 2` *replaces* the numbers rather
than accumulating them, which reports chapters 1-200 as missing on a corpus that is complete.
Walk until `has_more` is false and check the union.

`--fields` returns YAML-native types, so `chapter_number: 63` comes back as `63`. The
`tostring | test(...) | tonumber` guard still catches hand-typed values such as `"63"` or
`"12a"`.

Pass the work's **actual** chapter count as `$expected` — deriving the range from the highest
number found lets an incomplete graph pass. With 138 rows numbered 1..137 plus one duplicate,
a max-derived check reports `missing: []` while a chapter is genuinely absent: the duplicate
keeps the count right and the missing tail moves the goalpost. The check passes on
`unnumbered: []`, `missing: []`, `duplicates: []`, **and** `out_of_range: []` together, over
the combined pages.

`unnumbered` is not decoration either. A `chapter` note that never got a `chapter_number` comes
back as `null`, and feeding that straight to `tonumber` aborts the whole pipeline with `null
cannot be parsed as a number` — so the check *crashes on exactly the malformed inventory it
exists to find*. Partitioning first turns that into a named row.

`out_of_range` is not hypothetical: a prologue or epilogue typed as `chapter` lands at 0 or at
`$expected + 1`, and without that key the report reads clean — every expected number present,
none repeated — while the inventory holds a note the numbering does not account for. Type
front and back matter as its own note type, or widen `$expected` deliberately.

A gap in the middle of a batch is the most common processing failure and the easiest to miss
by eye; a duplicated chapter number is the second, and it hides the first.

### Relation Consistency
Spot-check bidirectional relations: if Chapter X `features [[Character]]`, does Character have observations referencing Chapter X? Fix gaps.

Orphans are the other half of this check — a note with no inbound or outbound relations is
either genuinely isolated or was never linked back into the graph:

```bash
bm orphans          # entities with no relations in the graph
```

`orphans` finds notes with no relations. It does not find a `[[target]]` that resolves to
nothing — a misspelled or renamed entity name. Check a chapter's links with `bm tool build-context memory://chapters/<slug> --depth 1
--project <work> --json` and look for relations whose `to_entity_id` is `null`; fix the
spelling or add the alias, then re-run the chapter.

Graph quality is relation *density*, not note count. A pass that adds notes while leaving
orphans behind has made the graph worse.

## Phase 5: Explore the Graph

With the graph complete, traverse it to find what the chapter-by-chapter pass could not see:

```bash
bm grep "features [[" --page-size 200 --project <work> --json   # every chapter's cast, one call
bm find --meta 'note_type=theme' --fields prevalence --page-size 200  # thematic weight
bm grep "doubloon" --page-size 100 --project <work>                # every mention of a symbol
```

"Which characters share the most chapters" is the first line plus a local parse of each
row's `content` for `features [[...]]` — one call instead of a read per chapter. Do not
reach for `bm tool build-context 'memory://characters/major/*'` here: a
wildcard context is capped at 100 related rows across all primaries and returns one primary
row per indexed observation, so it neither enumerates the cast nor walks the web.
`build-context` on a *single* note is the right tool for a different question, below.
`build-context` takes its URL as a positional argument — there is no `--url` option.

`grep` matches keywords and returns a page of 10, which quietly truncates "where does this
appear?" — a symbol in 40 chapters comes back as 10. For symbol tracing, raise
`--page-size`; the meaning shifts you are hunting are usually in the later occurrences, which
the default page would have dropped. `--semantic` ranks by meaning instead, which answers
"what is this about?" rather than "where does this word appear?".

Two more facts about `grep` rows. Matching is case-insensitive and note-level: a hit is a
note, not a line, and there is no `-n` or context. And each row's `content` is capped at
4000 characters; `content_truncated: true` marks a cut row, so a long note's tail (such as a
final chapter's `follows`/`precedes` relations) may be missing from a grep-driven scan.
When a parse depends on the end of a note, `cat` that note.

`--page-size` raises the ceiling, it does not remove it. A symbol in a long work can exceed
even 100, so check whether the last page was full and walk `--page 2`, `--page 3` until it is
not. A truncated symbol search fails the same silent way as an unpaginated `find`: a plausible
answer, exit 0, and no sign that the tail is missing.

And `grep` searches **your notes, not the source**. The `<work>.txt` is indexed as an entity,
but its body is not in the searchable text, so an occurrence you never carried into a note is
unreachable: a word present only in the source returns `total: 0`. So this answers "where have I written about the doubloon", not
"where does the doubloon appear in the book". For the latter, search the file itself and use
the [chapter offset map](#source-text-preparation) to turn a hit into a chapter.

Traversal is where second-order questions get answered — which characters share the most
chapters, which themes converge in the final act, where a symbol's meaning shifts. Capture
what you find as `analysis/` notes; those syntheses are the payoff of having built the graph.

## Adapting to Other Genres

This pipeline works for any literary text. Adjust schemas for genre:

| Genre | Schema Adjustments |
|-------|-------------------|
| **Novel** | Base schemas work as-is; add genre-specific Character fields as needed |
| **Play** | Add `Act` and `Scene` schemas; Character gets `speaking_lines` field |
| **Poetry collection** | Replace Chapter with `Poem`; add `form`, `meter`, `rhyme_scheme` fields |
| **Non-fiction** | Replace Chapter with `Section`; add `Argument`, `Evidence` schemas |
| **Short story collection** | Add `Story` schema with `narrator`, `setting`, `word_count` |
| **Epic/myth** | Add `Deity`, `Prophecy` schemas; Location gets `mythological_significance` |
| **Memoir** | Character schema gets `relationship_to_narrator`; add `Memory` schema |

### Scaling Guidance

| Work Length | Batch Size | Estimated Notes |
|-------------|-----------|----------------|
| Novella (~40K words) | 5-10 chapters | ~50-80 |
| Novel (~80K words) | 8-12 chapters | ~100-150 |
| Long novel (~200K+ words) | 10-15 chapters | ~200-300 |
| Series (multiple volumes) | 1 volume at a time | ~200+ per volume |

## Related Skills

- **memory-schema** — Schema creation, validation, and drift detection
- **memory-tasks** — Track chapter processing progress across context compaction
- **memory-notes** — Note writing patterns, observation categories, wiki-links
- **memory-ingest** — Processing external input into structured entities
- **memory-metadata-search** — Querying notes by frontmatter fields
- **memory-lifecycle** — Archiving completed analysis phases

## Guidelines

- **Seed before processing.** Create entity stubs first so wiki-links resolve immediately during chapter processing.
- **Batch for sanity.** Processing ~10 chapters at a time balances depth with momentum. Track progress with a Task note.
- **Read the source text.** Don't rely on memory or summaries. Read (or re-read) the actual text for each batch before creating notes. Textual evidence is everything.
- **Read narrowly.** Keep the source text in the project as `.txt`, index it once, build the chapter offset map once, then read chapters by line range and notes by section. On a long work, whole files landing in context are the largest avoidable cost in the pipeline.
- **Query, don't scan.** When you need to know which notes have a field, ask with `--meta` predicates and `--fields` projection. Reading notes to check frontmatter is the mistake this pipeline makes at scale. One way these queries lie quietly: `find` returns 10 rows unless you pass `--page-size`.
- **Observations are your index.** The knowledge graph's value comes from categorized observations. Be generous with categories and specific with content.
- **Relations are your web.** Every chapter should link to characters, themes, locations, and devices. Every entity should link back to chapters where it appears.
- **Enrich iteratively.** Entity notes grow richer with each chapter. Don't try to write the perfect character note upfront — append as you go.
- **Add prose for depth.** After structured data is in place, add interpretive essays to major notes. The prose captures what observations cannot: argument, nuance, opinion, and voice.
- **Validate periodically.** Run `bm_schema(command="validate")` after each batch, not just at the end. Catch drift early.
- **Quote generously.** Literary analysis lives on textual evidence. Include significant quotes as `[quote]` observations with chapter attribution.
- **Review and revise.** After completing all chapters, review the full graph from an external perspective. Look for thin notes, missing connections, and gaps in coverage. The first pass is never the last.
- **Analysis comes last.** Synthesis notes in `analysis/` should be written after all chapters are processed, when you have the full picture.

Mirrored from the author's public source. Install counts from the open skills registry.

The systems behind these skills get built for partners every week.

Partner with us