Agent guidance for the @echecs/san package — SAN move notation parser,
resolver, and stringifier.
See also: REFERENCES.md |
COMPARISON.md
See the root AGENTS.md for workspace-wide conventions.
Backlog: tracked in GitHub Issues.
@echecs/san exposes three pure functions for working with Standard Algebraic
Notation (SAN) chess moves.
| Function | Input | Output | Throws |
|---|---|---|---|
parse(san) |
SAN string | SanMove |
RangeError on empty/invalid input |
parse(san, position) |
SAN string + Position |
Move |
RangeError on empty/invalid/illegal |
resolve() |
SanMove + Position |
Move |
RangeError on illegal/ambiguous |
stringify() |
Move + Position |
SAN string | RangeError if no piece on square |
The entire implementation lives in a single source file: src/index.ts.
parse('')→RangeError('Empty SAN string')parse('invalid')→RangeError('Invalid SAN: "invalid"')resolve()with no legal match →RangeErrordescribing the moveresolve()with multiple matches →RangeErrorlisting candidate countstringify()with no piece onmove.from→RangeError
Never use generic Error — always RangeError for domain violations.
Input validation is mostly provided by TypeScript's strict type system at
compile time. There is no runtime validation library — the type signatures
enforce correct usage. Do not add runtime type-checking guards (e.g. typeof
checks, assertion functions) unless there is an explicit trust boundary.
- ESM-only — the package ships only ESM. Do not add a CJS build.
canAttack and isKingInCheck use the 0x88 lookup tables (ATTACKS, RAYS,
PIECE_MASKS, DIFF_OFFSET, OFF_BOARD) and the boardFromMap /
squareToIndex helpers from @echecs/position/internal. This is the only
package besides @echecs/game that may use the ./internal export condition.
Do not use it in application code.
The regex at the top of src/index.ts parses the full SAN grammar in one pass.
Annotation glyphs (!, ?) are stripped before matching. Castling (O-O,
O-O-O) is detected before regex matching.
Regex group order:
1=piece, 2=fromFile, 3=fromRank, 4=capture, 5=toFile, 6=toRank, 7=promotion, 8=check.
- Iterate all pieces of the active color matching
move.piece. - Apply file/rank disambiguation filters.
- For pawns: validate direction and distance separately for pushes vs. captures
(the generic
canAttacktable does not model pawn pushes). - For all other pieces: use
canAttackwith the 0x88 tables. - Apply the move with
applyMoveToBoardand discard candidates that leave the own king in check (isKingInCheck). - Exactly one candidate must remain — zero or many both throw
RangeError.
When multiple pieces of the same type can reach the destination:
- If no ambiguous piece shares the from-file → disambiguate by file.
- Else if an ambiguous piece shares the from-rank → use the full square.
- Else → disambiguate by rank.
The internal isCheckmate helper iterates all 0x88 indices and attempts every
canAttack-reachable move. It is intentionally simple (not perft- optimised)
because it is only called from stringify() to append #.
pnpm build # bundle TypeScript → dist/ via tsdown
pnpm test # run all tests once (vitest run)
pnpm test:watch # watch mode
pnpm test:coverage # with v8 coverage report
pnpm lint # ESLint + tsc type-check (auto-fixes style issues)
pnpm lint:ci # strict — zero warnings, no auto-fix
pnpm format # Prettier --write
pnpm lint && pnpm test && pnpm build # full pre-PR checkStep-by-step process for releasing a new version. CI auto-publishes to npm when
version in package.json changes on main.
-
Verify the package is clean:
pnpm lint && pnpm test && pnpm build
Do not proceed if any step fails.
-
Decide the semver level:
patch— bug fixes, internal refactors with no API changeminor— new features, new exports, non-breaking additionsmajor— breaking changes to the public API
-
Update
CHANGELOG.mdfollowing Keep a Changelog format:## [x.y.z] - YYYY-MM-DD ### Added - … ### Changed - … ### Fixed - … ### Removed - …
Include only sections that apply. Use past tense.
-
Update
README.mdif the release introduces new public API, changes usage examples, or deprecates/removes existing features. -
Bump the version:
npm version <major|minor|patch> --no-git-tag-version
-
Open a release PR:
git checkout -b release/x.y.z git add package.json CHANGELOG.md README.md git commit -m "release: @echecs/san@x.y.z" git push -u origin release/x.y.z gh pr create --title "release: @echecs/san@x.y.z" --body "<description>"
Wait for CI (format, lint, test) to pass on the PR before merging.
-
Merge the PR: Once CI is green, merge (squash) into
main. The release workflow detects the version bump, publishes to npm, and creates a GitHub Release with a git tag.
Do not manually publish with npm publish. Do not create git tags manually —
the release workflow handles tagging.