> ## Documentation Index
> Fetch the complete documentation index at: https://obversa.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Markdown Memory

> @obversa/memory-markdown: search a folder of Markdown files with no index or service, and hand the hits to ground through a read-only memory view.

`@obversa/memory-markdown` searches a directory of `.md` files with no
index, embedding service or network call, and gives the selected files to
`ground` through a read-only view of the memory port. Use it when a team
should read from your own notes, policies or handbook without a step being
able to edit them. Each hit names the file, the matching passage, its line
range (counted from 1) and a score.

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @obversa/memory-markdown
  ```

  ```bash pnpm theme={null}
  pnpm add @obversa/memory-markdown
  ```
</CodeGroup>

Included in `@obversa/obversa`.

## Requirements

A directory of Markdown files. Nothing else.

## Quickstart

Open the corpus, search it, and pass the hit paths to `ground`:

```ts examples/memory-markdown.ts (excerpt) {1-3,4,6} theme={null}
const corpus = openMarkdownCorpus({
  directory: fileURLToPath(new URL('./memory-markdown-corpus', import.meta.url)),
});
const hits = await corpus.search('warranty');
const paths = [...new Set(hits.map((hit) => hit.path))];
const grounded = await ground(corpus.memory, {
  sources: paths.map((path) => ({ path })),
});
```

Treat the text in a hit as untrusted: it's whatever the corpus file says,
so a file can carry instructions aimed at a model. Pass the hit's path to
`ground` instead of putting the passage text into a prompt yourself, and
`ground` marks the content as untrusted before a job sees it. The example
searches a three-file corpus beside it, grounds the one file that matches,
curates a brief and gives it to an offline agent. Run it with
`npx tsx memory-markdown.ts`:

```json Output theme={null}
{
  "hits": [
    {
      "path": "/memories/warranty.md",
      "startLine": 3,
      "endLine": 3
    }
  ],
  "grounded": [
    "/memories/warranty.md"
  ],
  "brief": "The battery warranty lasts eight years.",
  "job": "pass",
  "briefReachedJob": true
}
```

## Build the corpus

The example's corpus is three files beside it. `warranty.md` holds the
coverage policy, `charging.md` a charging note, and `notes with spaces.md`
matches the query but has a name that can't become a memory path, so search
skips it and only one hit reaches `ground`. Search and the memory view
include only directories and `.md` files whose names follow one rule: each
segment starts with a letter or number, then uses letters, numbers, dots,
underscores or hyphens, up to 128 characters. Dot-prefixed names, names
outside that rule, non-Markdown files and symbolic links inside the corpus
are skipped without failing the search or a directory view. The corpus
root itself can be a symbolic link, and a corpus path such as
`policies/warranty.md` becomes `/memories/policies/warranty.md`.

## Read through the port

`corpus.memory` is a read-only `Memory`: `view` reads the `.md` files, and
all five writing commands return an error, because search must never edit
the material it selects from, and the writable adapters don't read a plain
directory of files.

## Rank the hits

Search splits each file at headings and paragraph breaks. A passage with
the exact phrase ranks above one that has the query terms apart; more
matched terms and more occurrences rank next; path, then starting line,
break ties. An empty query, or one with no matching passage, returns `[]`.

## Options

| Field | Type | Default | Description |
| - | - | - | - |
| `directory` | `string` | required | The directory of Markdown files. |
| `limit` | `number` | none | `search(query, { limit })`: at most this many hits. |

## Errors

* **A file whose full path is longer than 1024 bytes** stops the search,
  and the error names the path. A directory view still lists the file, and
  that over-long path then makes `ground` refuse the whole directory. Keep
  the corpus shallow enough to stay under the limit.
* **A writing command on `corpus.memory`** returns a memory error.

<Accordion title="Full file">
  ```ts examples/memory-markdown.ts theme={null}
  import { access } from 'node:fs/promises';
  import { fileURLToPath } from 'node:url';

  import { openMarkdownCorpus } from '@obversa/memory-markdown';
  import { agentJob, run } from '@obversa/runtime';
  import { curate, ground } from '@obversa/runtime/memory';
  import { MockEngine } from '@obversa/runtime/testing';

  await access(fileURLToPath(new URL('./memory-markdown-corpus/notes with spaces.md', import.meta.url)));
  const corpus = openMarkdownCorpus({
    directory: fileURLToPath(new URL('./memory-markdown-corpus', import.meta.url)),
  });
  const hits = await corpus.search('warranty');
  const paths = [...new Set(hits.map((hit) => hit.path))];
  const grounded = await ground(corpus.memory, {
    sources: paths.map((path) => ({ path })),
  });

  if (!grounded.ok) throw new Error(grounded.error.message);
  if (paths.length !== 1 || paths[0] !== '/memories/warranty.md') {
    throw new Error('Search returned a corpus path that ground must not receive.');
  }

  const context = await curate(grounded.value, {
    intent: 'Answer the warranty question.',
    decide: ({ documents }) => ({
      brief: documents
        .flatMap((document) => document.text.split('\n'))
        .find((line) => line.includes('lasts')) ?? '',
      sources: documents.map((document) => document.path),
    }),
  });

  if (context.mode !== 'curated') throw new Error('The local curator did not return a brief.');

  let receivedPrompt = '';
  const engine = new MockEngine((request) => {
    receivedPrompt = request.prompt;
    return 'The warranty answer is ready.';
  });
  const result = await run(agentJob({
    label: 'answer-from-corpus',
    engine: 'offline',
    prompt: `Use this brief to answer the question:\n\n${context.brief}`,
  }), {
    engine: 'offline',
    engines: { offline: engine },
  });

  console.log(JSON.stringify({
    hits: hits.map((hit) => ({
      path: hit.path,
      startLine: hit.passage.startLine,
      endLine: hit.passage.endLine,
    })),
    grounded: grounded.value.documents.map((document) => document.path),
    brief: context.brief,
    job: result.outcome.status,
    briefReachedJob: receivedPrompt.includes(context.brief),
  }, null, 2));
  ```
</Accordion>

## API

* **`openMarkdownCorpus({ directory })`**: a `MarkdownCorpus` with `search`
  and `memory`. [Quickstart](#quickstart).
* **Types**: `MarkdownCorpus`, `MarkdownSearchHit`, `MarkdownPassage`,
  `MarkdownSearchOptions`, `OpenMarkdownCorpusOptions`.

## Next steps

* [Memory mechanics](/docs/memory/mechanics): `ground` and `curate`, which the
  hits feed.
* [Memory adapters](/docs/memory/adapters): the three adapters side by side.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.