This file documents conventions, commands, and guidelines for agents working in
the @echecs/pgn repository.
@echecs/pgn is a PGN (Portable Game Notation) chess parser. It uses a
Peggy PEG parser compiled from src/grammar.pegjs. The
public API is a single default export: parse(input: string): PGN[].
Key source files:
| File | Role |
|---|---|
src/index.ts |
Public parse() entry point and TypeScript types |
src/grammar.pegjs |
Peggy grammar source — edit this, not the .cjs |
src/grammar.cjs |
Generated — compiled from grammar.pegjs, gitignored |
src/__tests__/index.spec.ts |
Snapshot test suite (13 fixtures) |
src/__tests__/index.bench.ts |
Self-benchmarks |
src/__tests__/comparison.bench.ts |
Cross-parser comparison benchmarks |
src/__tests__/grammar/ |
PGN fixture files used by tests |
Use pnpm exclusively (no npm/yarn).
pnpm build # compile grammar + emit dist/ via tsc
pnpm grammar:compile # peggy --format commonjs -o src/grammar.cjs src/grammar.pegjspnpm test # grammar:compile + vitest run (all tests)pnpm test always recompiles the grammar first — no need to do it manually.
Run a single test by fixture label:
pnpm test -- --reporter=verbose -t "basic"Available labels: basic, benko, checkmate, comment, comments,
games32, lichess, long, multiple, promotion, single, twic,
variants.
Update snapshots (only after intentional output changes):
pnpm test -- --update-snapshotsSnapshots live in src/__tests__/__snapshots__/<label>.snap and are committed
to git. Do not update them for pure performance changes.
pnpm lint # ESLint + tsc --noEmit
pnpm lint:ci # same, zero warnings allowed
pnpm format # prettier --write
pnpm format:ci # prettier --list-different (check only)pnpm bench # vitest bench --run (all benchmarks, takes ~60s)Whenever src/grammar.pegjs is modified:
- Run
pnpm test— it recompiles and runs all 13 snapshots. - Never edit
src/grammar.cjsdirectly — it is generated and gitignored. - Grammar rule names use
SCREAMING_SNAKE_CASE(Peggy convention).
- Single quotes for strings
- Trailing commas everywhere (
trailingComma: 'all') quoteProps: 'consistent'- Prose wrapping always in markdown
Prettier runs automatically on commit via lint-staged.
- Use ESM (
import/export). The package is"type": "module". - Always include
.jsextensions on relative imports (NodeNext resolution). - Import order enforced by
eslint-plugin-import-x— violation is an error:- Built-ins and external packages
- Internal aliases (
@/**) - Parent and sibling paths
- Type-only imports
- Each group separated by a blank line.
- Named imports must be sorted alphabetically (
sort-importsrule). - Use
import typefor type-only imports — mixing type/value in one import statement is an error (@typescript-eslint/consistent-type-imports).
// Correct
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import parse from '../index.js';
// Wrong — missing 'type', wrong extension, mixed type/value
import { Parser, type Grammar } from './grammar';- Strict mode on:
strict,noUncheckedIndexedAccess,noImplicitOverride. noImplicitAnyis off — relaxed for Peggy action block interop.- All exported functions must have explicit return types
(
@typescript-eslint/explicit-module-boundary-typesis an error). - Prefer
interfacefor object shapes,typefor unions/aliases. - Avoid
!non-null assertions; use explicit narrowing or optional chaining (@typescript-eslint/no-non-null-assertionis a warning). @ts-expect-erroris acceptable only at Peggy grammar interop boundaries.
- Files:
camelCase.tsfor source,kebab-case.pgnfor fixtures. - Types / Interfaces:
PascalCase. - Variables and functions:
camelCase. - Grammar rule names:
SCREAMING_SNAKE_CASE. - Object keys: sorted alphabetically —
sort-keysis an error in source, relaxed in tests.
// Correct — keys sorted
const move = { capture: true, from: 'e', piece: 'P', to: 'd5' };
// Wrong — unsorted
const move = { piece: 'P', to: 'd5', capture: true };curly: 'all'— always use braces, even for single-lineifbodies.eqeqeq— use===/!==, never==.- No
console.log(no-consoleis a warning). Useconsole.warnonly for expected diagnostic output (e.g. move number mismatches in the grammar). - The parser is fully synchronous — do not introduce
async/awaitanywhere insrc/index.tsorsrc/grammar.pegjs.
parse()returns[]on any parse failure — it never throws to callers. Errors are caught and silenced insrc/index.ts.
- Tests use vitest with file snapshots (
toMatchFileSnapshot). - Each of the 13 PGN fixtures has one snapshot file in
src/__tests__/__snapshots__/. - A 15-second per-test timeout accommodates
long.pgn(~3 500 games). - Benchmarks (
*.bench.ts) are never run in CI. sort-keysandno-consoleare relaxed inside__tests__/.
Releases are automated via GitHub Actions on pushes to main:
- The workflow checks whether
versioninpackage.jsonchanged. - If changed, it runs format → lint → test, then publishes to npm.
- Bump version before committing:
npm version <major|minor|patch> --no-git-tag-version
Husky hooks enforce quality locally:
- pre-commit:
lint-staged(prettier + eslint --fix on staged files) - pre-push: format, lint, test (full suite)