Libraries

Exporting the Catalog

Generate your package's error reference from the catalog: a Markdown page for humans, a JSON file for tooling, and an llms.txt section for agents.

Your catalog already holds the error reference: every code, its status, its cause, its fix, and its link. Writing that reference a second time by hand, in a docs page, is how the two drift apart within a release. Generate it instead. The catalog is a plain object whose values are factories, and every factory exposes its metadata as enumerable properties: code, status, message, why, fix, link, tags, internal. That is enough for a script to produce every artifact this page covers.

evlog ships no generator for this. The script below is the whole thing, and it is yours to shape.

Read the catalog as data

Object.entries() on a catalog yields one [KEY, factory] pair per entry; _prefix and _codes are non-enumerable, so they stay out of the loop. A message defined as a function is a template, so the export records that rather than calling it:

scripts/export-errors.ts
import { githubToolsErrors } from '../src/core/errors'

export interface ErrorReferenceEntry {
  code: string
  status: number
  message: string | null
  why?: string
  fix?: string
  link?: string
}

export function readCatalog(): ErrorReferenceEntry[] {
  return Object.entries(githubToolsErrors).map(([, entry]) => ({
    code: entry.code,
    status: entry.status,
    message: typeof entry.message === 'string' ? entry.message : null,
    why: entry.why,
    fix: entry.fix,
    link: entry.link,
  }))
}

internal is left out on purpose. It is the field you keep off the wire, and a reference page is the wire.

Write the Markdown reference

A table per status band reads better than one long table, but the minimal version is one table. The output goes into your docs source, wherever your docs framework reads Markdown from:

scripts/export-errors.ts
import { writeFileSync } from 'node:fs'

function cell(value: string | null | undefined): string {
  return (value ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ')
}

export function toMarkdown(entries: ErrorReferenceEntry[]): string {
  const rows = entries.map(entry =>
    `| \`${entry.code}\` | ${entry.status} | ${cell(entry.why)} | ${cell(entry.fix)} | ${entry.link ? `[docs](${entry.link})` : ''} |`,
  )
  return [
    '# Errors',
    '',
    'Generated from `src/core/errors.ts`. Do not edit by hand.',
    '',
    '| Code | Status | Why | Fix | Link |',
    '| --- | --- | --- | --- | --- |',
    ...rows,
    '',
  ].join('\n')
}

writeFileSync('docs/reference/errors.md', toMarkdown(readCatalog()))
writeFileSync('docs/public/errors.json', JSON.stringify(readCatalog(), null, 2))

Two of the four github_tools entries from Structured Errors come out like this:

docs/reference/errors.md
| Code | Status | Why | Fix | Link |
| --- | --- | --- | --- | --- |
| `github_tools.RATE_LIMITED` | 429 | The token used up its GitHub API rate limit for this resource. | Stop calling this tool and retry after the reset timestamp in the message; batch reads or narrow the query to spend fewer requests. | [docs](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api) |
| `github_tools.NOT_FOUND` | 404 | Either the resource does not exist, or the token cannot see it. GitHub deliberately returns 404 instead of 403 for private resources the token has no access to. | Check the owner/repo/number input first. If it is correct, the token lacks access: grant the repository to the PAT or App installation, or use a Connect subject that has access. | [docs](https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api#404-not-found-for-an-existing-resource) |

The JSON file is the same array, and it is what a consumer's tooling reads: a test that asserts your codes did not change, a support macro generator, or an agent that loads the catalog once at the start of a session instead of discovering errors one at a time.

Put it where agents read

An agent that hits github_tools.NOT_FOUND already has why and fix in the error. An agent that has not called your package yet, and is deciding whether to, benefits from seeing the catalog up front: the codes tell it what can go wrong before it writes the retry logic. Two placements cost nothing extra once the reference exists.

Your llms.txt. Link the generated page from the file, or inline the table under an ## Errors heading. Docs frameworks that build llms.txt from the content tree (Docus does, through nuxt-llms) pick the page up when it lives in the content directory, so the generated Markdown only needs to land there.

Your skill or AGENTS.md. If your package ships an agent skill, the error table is the one section an agent reads before every retry. Reference the generated file from the skill instead of pasting it, so a catalog change reaches the skill on the next build. Agent Skills covers how evlog itself does this.

Keep the reference honest

Run the script in the build that publishes the docs, and add one test that fails when the committed reference is stale:

test/errors-reference.test.ts
import { readFileSync } from 'node:fs'
import { readCatalog, toMarkdown } from '../scripts/export-errors'

it('keeps docs/reference/errors.md in sync with the catalog', () => {
  const committed = readFileSync('docs/reference/errors.md', 'utf8')
  expect(committed).toBe(toMarkdown(readCatalog()))
})

A why you rewrite in the catalog now fails CI until the reference is regenerated, which is the direction you want the dependency to run. Testing covers the rest of what a library's suite locks down.