Maintain the package

Protect the public contract

Test documented behavior and types so refactors cannot quietly break consumers.

Unit tests that import internal modules can pass while the public API breaks. Contract tests exercise only documented entry points, the same way consumers do.

For @acme/slugify-title, the contract is whatever the README promises: input strings, optional { strict: true }, outputs, and error cases for invalid input.

Test the packed artifact, not the source tree

tests/contract.test.mjs:

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { slugifyTitle } from '@acme/slugify-title'

test('basic slug', () => {
  assert.equal(slugifyTitle('Hello World!'), 'hello-world')
})

test('unicode title', () => {
  assert.equal(slugifyTitle('Caffè'), 'caffe')
})

Run contract tests against an installed tarball in CI, not against ../src. That catches missing files, broken exports, and stale dist/ in one step.

Turn bug fixes into permanent examples

Every consumer-reported bug becomes a contract test with the failing input attached. When someone refactors normalize.js three months later, the test screams before publish.

Also cover types if you ship them. A breaking change to a public option type is still a breaking change, even when runtime JavaScript looks fine.

Refactor freely inside the boundary

My rule: rewrite internals aggressively while contract tests stay green. If a refactor breaks a contract test, treat it as an API question first. Either the test is wrong (rare) or you need a major version bump.

Documented examples in the README should match the tests line for line. Drift between docs and tests is how silent breakages slip out.

Try this: pick one README example and assert its output in a test file today. Future you will thank present you.

When a contract test fails during a refactor, read the diff as a product question. Did you mean to change behavior for every consumer, or did you break an internal helper?

Keep contract tests fast. They should run on every push, including the packed-artifact install step if your CI cache allows it.

Lesson completed