{"_id":"@aforemendude/diff","_rev":"4-eea61f4be18528447c16e29bb0e8e99a","name":"@aforemendude/diff","dist-tags":{"latest":"1.1.2"},"versions":{"1.0.0":{"name":"@aforemendude/diff","version":"1.0.0","keywords":["diff","grapheme","text","typescript","unicode"],"author":{"name":"aforemendude"},"license":"MIT AND Apache-2.0","_id":"@aforemendude/diff@1.0.0","maintainers":[{"name":"aforemendude","email":"aforementioned.dude@gmail.com"}],"homepage":"https://github.com/aforemendude/diff","bugs":{"url":"https://github.com/aforemendude/diff/issues"},"dist":{"shasum":"e25a65865e3467e62860436dc05a579606338f86","tarball":"https://registry.npmjs.org/@aforemendude/diff/-/diff-1.0.0.tgz","fileCount":78,"integrity":"sha512-AIduIpECIkOsQmK0e8cR/6xF9SHVt4TfoAPvvog93c2KIQkf9FjRGloSJqAiYgyw7f/+y13vYq8zgWcfA1l71w==","signatures":[{"sig":"MEUCICT+Wt+xZpSqaBkG803E9rQ48eKJCbBD1ExF8bP4DP6tAiEA2wWq+aI3gc48/IfEoVpT5VhOGgrPfAU7BwjNYotBOtQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":290348},"type":"commonjs","engines":{"node":">=20.0.0"},"exports":{"./line":{"import":{"types":"./dist/esm/line.d.ts","default":"./dist/esm/line.js"},"require":{"types":"./dist/cjs/line.d.ts","default":"./dist/cjs/line.js"}},"./cleanup":{"import":{"types":"./dist/esm/cleanup.d.ts","default":"./dist/esm/cleanup.js"},"require":{"types":"./dist/cjs/cleanup.d.ts","default":"./dist/cjs/cleanup.js"}},"./grapheme":{"import":{"types":"./dist/esm/grapheme.d.ts","default":"./dist/esm/grapheme.js"},"require":{"types":"./dist/cjs/grapheme.d.ts","default":"./dist/cjs/grapheme.js"}}},"gitHead":"d2785084431fa2f62622f983450692945719d1fa","scripts":{"test":"tsc --project tsconfig.test.json && vitest run","build":"npm run clean && npm run build:cjs && npm run build:esm && node scripts/write-dist-package-json.mjs","clean":"node scripts/clean.mjs","format":"prettier --write --cache --cache-strategy metadata .","verify":"prettier --check . && npm run build && npm run test && npm run test:package","prepack":"npm install && git diff --exit-code -- package-lock.json && npm run verify","benchmark":"tsc --project tsconfig.benchmark.json && vitest bench --run test/benchmark","build:cjs":"tsc --project tsconfig.json","build:esm":"tsc --project tsconfig.esm.json","test:watch":"vitest","format:check":"prettier --check --cache --cache-strategy metadata .","test:package":"node scripts/verify-package.mjs","format:nocache":"prettier --write ."},"_npmUser":{"name":"aforemendude","email":"aforementioned.dude@gmail.com"},"devEngines":{"runtime":{"name":"node","onFail":"error","version":">=22.12.0"}},"repository":{"url":"git+https://github.com/aforemendude/diff.git","type":"git"},"_npmVersion":"11.16.0","description":"A small, typed line and Unicode text diff library.","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"4.1.10","prettier":"3.9.6","typescript":"7.0.2","@types/node":"20.19.43"},"_npmOperationalInternal":{"tmp":"tmp/diff_1.0.0_1786841104452_0.07906671712358859","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@aforemendude/diff","version":"1.1.0","keywords":["diff","grapheme","text","typescript","unicode"],"author":{"name":"aforemendude"},"license":"MIT AND Apache-2.0","_id":"@aforemendude/diff@1.1.0","maintainers":[{"name":"aforemendude","email":"aforementioned.dude@gmail.com"}],"homepage":"https://github.com/aforemendude/diff","bugs":{"url":"https://github.com/aforemendude/diff/issues"},"dist":{"shasum":"03a52e8861db846fd8225471450d6077716c8e47","tarball":"https://registry.npmjs.org/@aforemendude/diff/-/diff-1.1.0.tgz","fileCount":96,"integrity":"sha512-Gor++xPBEl0xSIQgcbYpkEIYjmG7Q/DLnceVa59ypWaSreYU43u7GByYnrqYGKyS1MwwkLNsrIQm5Pu9PJoPUQ==","signatures":[{"sig":"MEYCIQDu2wz8ymu5U2I8AoPRyhsdi9LInPtiDigUC+7aQyzZOwIhAKrCOdMkhL64AnZMDwD17lhfi8rVS+P1C2mQanBcTt3U","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":517204},"type":"commonjs","engines":{"node":">=20.0.0"},"exports":{"./line":{"import":{"types":"./dist/esm/line.d.ts","default":"./dist/esm/line.js"},"require":{"types":"./dist/cjs/line.d.ts","default":"./dist/cjs/line.js"}},"./cleanup":{"import":{"types":"./dist/esm/cleanup.d.ts","default":"./dist/esm/cleanup.js"},"require":{"types":"./dist/cjs/cleanup.d.ts","default":"./dist/cjs/cleanup.js"}},"./grapheme":{"import":{"types":"./dist/esm/grapheme.d.ts","default":"./dist/esm/grapheme.js"},"require":{"types":"./dist/cjs/grapheme.d.ts","default":"./dist/cjs/grapheme.js"}}},"gitHead":"f29f92ab4fe10a652459b1ee539494fa69080ee2","scripts":{"test":"tsc --project tsconfig.test.json && vitest run","build":"npm run clean && npm run build:cjs && npm run build:esm && node scripts/write-dist-package-json.mjs","clean":"node scripts/clean.mjs","format":"prettier --write --cache --cache-strategy metadata .","verify":"prettier --check . && npm run build && npm run test && npm run test:package","prepack":"npm install && git diff --exit-code -- package-lock.json && npm run verify","benchmark":"npm run benchmark:typecheck && vitest bench --run --no-file-parallelism test/benchmark/diff-lines.bench.ts test/benchmark/diff-graphemes.bench.ts test/benchmark/semantic-cleanup.bench.ts test/benchmark/efficiency-cleanup.bench.ts","build:cjs":"tsc --project tsconfig.json","build:esm":"tsc --project tsconfig.esm.json","test:watch":"vitest","format:check":"prettier --check --cache --cache-strategy metadata .","test:package":"node scripts/verify-package.mjs","format:nocache":"prettier --write .","benchmark:memory":"npm run benchmark:typecheck && node scripts/run-memory-benchmarks.mjs representative","benchmark:typecheck":"tsc --project tsconfig.benchmark.json","benchmark:adversarial":"npm run benchmark:typecheck && vitest bench --run --no-file-parallelism test/benchmark/diff-lines.adversarial.bench.ts test/benchmark/diff-graphemes.adversarial.bench.ts test/benchmark/semantic-cleanup.adversarial.bench.ts test/benchmark/efficiency-cleanup.adversarial.bench.ts","benchmark:adversarial:memory":"npm run benchmark:typecheck && node scripts/run-memory-benchmarks.mjs adversarial"},"_npmUser":{"name":"aforemendude","email":"aforementioned.dude@gmail.com"},"devEngines":{"runtime":{"name":"node","onFail":"error","version":">=22.12.0"}},"repository":{"url":"git+https://github.com/aforemendude/diff.git","type":"git"},"_npmVersion":"11.17.0","description":"A small, typed line and Unicode text diff library.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"4.1.10","prettier":"3.9.6","typescript":"7.0.2","@types/node":"20.19.43","prettier-plugin-jsdoc":"1.8.1","@aforemendude/prettier-plugin-wrap-comments":"1.2.0"},"_npmOperationalInternal":{"tmp":"tmp/diff_1.1.0_1787370712241_0.14539121159374213","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@aforemendude/diff","version":"1.1.1","keywords":["diff","grapheme","text","typescript","unicode"],"author":{"name":"aforemendude"},"license":"MIT AND Apache-2.0","_id":"@aforemendude/diff@1.1.1","maintainers":[{"name":"aforemendude","email":"aforementioned.dude@gmail.com"}],"homepage":"https://github.com/aforemendude/diff","bugs":{"url":"https://github.com/aforemendude/diff/issues"},"dist":{"shasum":"b1b3486fd9b14dc8e60360333b704fe426405eff","tarball":"https://registry.npmjs.org/@aforemendude/diff/-/diff-1.1.1.tgz","fileCount":96,"integrity":"sha512-jTvAeJfKTUiin3KsdLzctPHtp2ZPQNTL1SH73jO+7uHdbZOIg7egAWwmYLFSIvzZpUyImaN4qPVVZE977+CF5w==","signatures":[{"sig":"MEYCIQC14TZDZ/OT195OTPjFvG4qsNqr0h6BEdLV5Tj6qhDEpQIhAKaSo1C1MwO2eYjj/YSTpi7H/mhFe6WCkBtzs1w6Iwqt","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":523358},"type":"commonjs","engines":{"node":">=20.0.0"},"exports":{"./line":{"import":{"types":"./dist/esm/line.d.ts","default":"./dist/esm/line.js"},"require":{"types":"./dist/cjs/line.d.ts","default":"./dist/cjs/line.js"}},"./cleanup":{"import":{"types":"./dist/esm/cleanup.d.ts","default":"./dist/esm/cleanup.js"},"require":{"types":"./dist/cjs/cleanup.d.ts","default":"./dist/cjs/cleanup.js"}},"./grapheme":{"import":{"types":"./dist/esm/grapheme.d.ts","default":"./dist/esm/grapheme.js"},"require":{"types":"./dist/cjs/grapheme.d.ts","default":"./dist/cjs/grapheme.js"}}},"gitHead":"38ffb5bce0df5c87d9591f7898685367b6514cf1","scripts":{"test":"tsc --project tsconfig.test.json && vitest run","build":"npm run clean && npm run build:cjs && npm run build:esm && node scripts/write-dist-package-json.mjs","clean":"node scripts/clean.mjs","format":"prettier --write --cache --cache-strategy metadata .","verify":"prettier --check . && npm run build && npm run test && npm run test:package","prepack":"npm install && git diff --exit-code -- package-lock.json && npm run verify","benchmark":"npm run benchmark:typecheck && vitest bench --run --no-file-parallelism test/benchmark/diff-lines.bench.ts test/benchmark/diff-graphemes.bench.ts test/benchmark/semantic-cleanup.bench.ts test/benchmark/efficiency-cleanup.bench.ts","build:cjs":"tsc --project tsconfig.json","build:esm":"tsc --project tsconfig.esm.json","test:watch":"vitest","format:check":"prettier --check --cache --cache-strategy metadata .","test:package":"node scripts/verify-package.mjs","format:nocache":"prettier --write .","benchmark:memory":"npm run benchmark:typecheck && node scripts/run-memory-benchmarks.mjs representative","benchmark:typecheck":"tsc --project tsconfig.benchmark.json","benchmark:adversarial":"npm run benchmark:typecheck && vitest bench --run --no-file-parallelism test/benchmark/diff-lines.adversarial.bench.ts test/benchmark/diff-graphemes.adversarial.bench.ts test/benchmark/semantic-cleanup.adversarial.bench.ts test/benchmark/efficiency-cleanup.adversarial.bench.ts","benchmark:adversarial:memory":"npm run benchmark:typecheck && node scripts/run-memory-benchmarks.mjs adversarial"},"_npmUser":{"name":"aforemendude","email":"aforementioned.dude@gmail.com"},"devEngines":{"runtime":{"name":"node","onFail":"error","version":">=22.12.0"}},"repository":{"url":"git+https://github.com/aforemendude/diff.git","type":"git"},"_npmVersion":"11.17.0","description":"A small, typed line and Unicode text diff library.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"4.1.10","prettier":"3.9.6","typescript":"7.0.2","@types/node":"20.19.43","prettier-plugin-jsdoc":"1.8.1","@aforemendude/prettier-plugin-wrap-comments":"1.2.0"},"_npmOperationalInternal":{"tmp":"tmp/diff_1.1.1_1787376383285_0.029991339995181","host":"s3://npm-registry-packages-npm-production"}},"1.1.2":{"author":{"name":"aforemendude"},"bugs":{"url":"https://github.com/aforemendude/diff/issues"},"description":"A small, typed line and Unicode text diff library.","devDependencies":{"@aforemendude/prettier-plugin-wrap-comments":"1.2.0","@types/node":"20.19.43","prettier":"3.9.6","prettier-plugin-jsdoc":"1.8.1","typescript":"7.0.2","vitest":"4.1.10"},"devEngines":{"runtime":{"name":"node","onFail":"error","version":">=22.12.0"}},"engines":{"node":">=20.0.0"},"exports":{"./cleanup":{"import":{"types":"./dist/esm/cleanup.d.ts","default":"./dist/esm/cleanup.js"},"require":{"types":"./dist/cjs/cleanup.d.ts","default":"./dist/cjs/cleanup.js"}},"./grapheme":{"import":{"types":"./dist/esm/grapheme.d.ts","default":"./dist/esm/grapheme.js"},"require":{"types":"./dist/cjs/grapheme.d.ts","default":"./dist/cjs/grapheme.js"}},"./line":{"import":{"types":"./dist/esm/line.d.ts","default":"./dist/esm/line.js"},"require":{"types":"./dist/cjs/line.d.ts","default":"./dist/cjs/line.js"}}},"homepage":"https://github.com/aforemendude/diff","keywords":["diff","grapheme","text","typescript","unicode"],"license":"MIT AND Apache-2.0","name":"@aforemendude/diff","publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/aforemendude/diff.git"},"scripts":{"benchmark":"npm run benchmark:typecheck && vitest bench --run --no-file-parallelism test/benchmark/diff-lines.bench.ts test/benchmark/diff-graphemes.bench.ts test/benchmark/semantic-cleanup.bench.ts test/benchmark/efficiency-cleanup.bench.ts","benchmark:adversarial":"npm run benchmark:typecheck && vitest bench --run --no-file-parallelism test/benchmark/diff-lines.adversarial.bench.ts test/benchmark/diff-graphemes.adversarial.bench.ts test/benchmark/semantic-cleanup.adversarial.bench.ts test/benchmark/efficiency-cleanup.adversarial.bench.ts","benchmark:adversarial:memory":"npm run benchmark:typecheck && node scripts/run-memory-benchmarks.mjs adversarial","benchmark:memory":"npm run benchmark:typecheck && node scripts/run-memory-benchmarks.mjs representative","benchmark:typecheck":"tsc --project tsconfig.benchmark.json","build":"npm run clean && npm run build:cjs && npm run build:esm && node scripts/write-dist-package-json.mjs","build:cjs":"tsc --project tsconfig.json","build:esm":"tsc --project tsconfig.esm.json","clean":"node scripts/clean.mjs","format":"prettier --write --cache --cache-strategy metadata .","format:check":"prettier --check --cache --cache-strategy metadata .","format:nocache":"prettier --write .","prepack":"npm install && git diff --exit-code -- package-lock.json && npm run verify","test":"tsc --project tsconfig.test.json && vitest run","test:package":"node scripts/verify-package.mjs","test:watch":"vitest","verify":"prettier --check . && npm run build && npm run test && npm run test:package"},"sideEffects":false,"type":"commonjs","version":"1.1.2","gitHead":"6b375c36a5f6f039d3bb9b78b19bcaf42da6f533","_id":"@aforemendude/diff@1.1.2","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-6qUFxVjpra/TjyZpfVS7DNMQbSq3UjpmoJgHFMNQ2sLchIJNiaWlpmdWwotbQ0tmIDky61rsoLMcAm0mv1J8iA==","shasum":"b218e58b4b697adb10568aef422d86566907542a","tarball":"https://registry.npmjs.org/@aforemendude/diff/-/diff-1.1.2.tgz","fileCount":96,"unpackedSize":523501,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCG+vI7TPneeCS/kJoJ+sLw4gBWQ5CPhCPD1ionG9pvtQIhAJOxJTcMZOf9vlMKiE97WKr2v3x3UeJ2PNBrZ9xx2SoK"}]},"_npmUser":{"name":"aforemendude","email":"aforementioned.dude@gmail.com"},"directories":{},"maintainers":[{"name":"aforemendude","email":"aforementioned.dude@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/diff_1.1.2_1787377116114_0.7168339594500541"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-16T00:45:04.328Z","modified":"2026-08-22T05:38:36.401Z","1.0.0":"2026-08-16T00:45:04.607Z","1.1.0":"2026-08-22T03:51:52.380Z","1.1.1":"2026-08-22T05:26:23.438Z","1.1.2":"2026-08-22T05:38:36.257Z"},"bugs":{"url":"https://github.com/aforemendude/diff/issues"},"author":{"name":"aforemendude"},"license":"MIT AND Apache-2.0","homepage":"https://github.com/aforemendude/diff","keywords":["diff","grapheme","text","typescript","unicode"],"repository":{"type":"git","url":"git+https://github.com/aforemendude/diff.git"},"description":"A small, typed line and Unicode text diff library.","maintainers":[{"name":"aforemendude","email":"aforementioned.dude@gmail.com"}],"readme":"# Diff\n\nA small, typed text diff library for Node.js and browsers. It provides line-level and Unicode grapheme-level diffs, plus\noptional Diff Match Patch-style semantic and efficiency cleanup.\n\n## Requirements\n\n- Node.js 20 or newer when running on Node.js\n- `Intl.Segmenter` in the target Node.js or browser runtime\n\n## Installation\n\n```bash\nnpm install @aforemendude/diff\n```\n\n## Browser usage\n\nThe library also works in browser applications when included through a bundler. It has no runtime dependencies and its\nruntime code does not use Node.js-only APIs, so it can be imported from browser application code in the same way as in\nthe examples below. Target browsers must provide `Intl.Segmenter`.\n\n## Quick start\n\n```typescript\nimport { cleanupSemantic } from '@aforemendude/diff/cleanup';\nimport { DELETE, EQUAL, INSERT, diffGraphemes } from '@aforemendude/diff/grapheme';\n\nconst changes = cleanupSemantic(diffGraphemes('The cat sat.', 'The dog sat.'));\n```\n\n```text\n[\n  [EQUAL,  ['T', 'h', 'e', ' ']],\n  [DELETE, ['c', 'a', 't']],\n  [INSERT, ['d', 'o', 'g']],\n  [EQUAL,  [' ', 's', 'a', 't', '.']],\n]\n```\n\nDiffs use compact tuples. The first item is an operation and the second is the affected array of tokens:\n\n```typescript\nexport const DELETE = -1;\nexport const EQUAL = 0;\nexport const INSERT = 1;\n\nexport type DiffOperation = -1 | 0 | 1;\nexport type Diff = readonly [operation: DiffOperation, tokens: readonly string[]];\n```\n\nEntries with an empty token array are omitted, and adjacent entries with the same operation are merged. An empty string\ncan still be a valid token - for example, it represents a blank line in a line diff.\n\n## API\n\nThe package exposes feature-specific subpaths and deliberately has no root entry point. Importing one diff engine does\nnot load either cleanup implementation or the other diff engine:\n\n| Subpath                       | Runtime exports                                                     |\n| ----------------------------- | ------------------------------------------------------------------- |\n| `@aforemendude/diff/line`     | `diffLines`, `DELETE`, `EQUAL`, `INSERT`                            |\n| `@aforemendude/diff/grapheme` | `diffGraphemes`, `DELETE`, `EQUAL`, `INSERT`                        |\n| `@aforemendude/diff/cleanup`  | `cleanupSemantic`, `cleanupEfficiency`, `DELETE`, `EQUAL`, `INSERT` |\n\nEach subpath supports both ESM `import` and CommonJS `require`. ESM consumers receive native side-effect-free modules,\nallowing bundlers to remove unused named exports. Each entry also exports the `Diff` and `DiffOperation` types;\nfeature-specific option types are exported from the subpath that uses them, and both diff subpaths export the shared\n`DiffAlgorithm` type.\n\n```typescript\nexport type DiffAlgorithm = 'adaptive' | 'myers' | 'sparse';\nexport type LineEnding = '\\r' | '\\n' | '\\r\\n';\n\nexport function diffLines(\n  before: string,\n  after: string,\n  options?: {\n    readonly algorithm?: DiffAlgorithm;\n    readonly lineEnding?: LineEnding;\n    readonly optimizeTrivialCases?: boolean;\n  },\n): readonly Diff[];\n\nexport function diffGraphemes(\n  before: string,\n  after: string,\n  options?: {\n    readonly algorithm?: DiffAlgorithm;\n    readonly locale?: Intl.LocalesArgument;\n    readonly optimizeTrivialCases?: boolean;\n  },\n): readonly Diff[];\n\nexport function cleanupSemantic(\n  diffs: readonly Diff[],\n  options?: { readonly locale?: Intl.LocalesArgument },\n): readonly Diff[];\n\nexport function cleanupEfficiency(diffs: readonly Diff[], options?: { readonly editCost?: number }): readonly Diff[];\n```\n\n### Mutation and ownership guarantees\n\nThe public methods never mutate their arguments. This includes option objects and, for the cleanup methods, the input\ndiff array, its tuples, and their token arrays.\n\nEvery call returns a freshly allocated result. The top-level array, every tuple, and every token array are distinct from\nthe arguments, from arrays elsewhere in the same result, and from arrays returned by other calls. Consequently, even if\nthe TypeScript `readonly` types are bypassed, changing one returned array cannot change an input, another entry in that\nresult, or the result of another call.\n\n### `diffLines(before, after, options?)`\n\nComputes a line-level diff using one exact line-ending sequence as the separator throughout both inputs. The supported\nline endings are `\\r`, `\\n`, and `\\r\\n`; the default is `\\n`. Other newline characters remain part of the surrounding\nline content.\n\nLine tokens never contain the selected ending. Tokenization splits on the separator and removes exactly one trailing\nempty segment. It does not remove every trailing empty segment, because preceding empty segments represent real blank\nlines. With the default `\\n`, tokenization is:\n\n| Input     | Tokens      |\n| --------- | ----------- |\n| `''`      | `[]`        |\n| `'a'`     | `['a']`     |\n| `'a\\n'`   | `['a']`     |\n| `'a\\n\\n'` | `['a', '']` |\n| `'\\n'`    | `['']`      |\n\nConsequently, adding one selected ending after a nonempty final line is insignificant, while an additional ending\nrepresents a blank line:\n\n```typescript\nimport { EQUAL, INSERT, diffLines } from '@aforemendude/diff/line';\n\ndiffLines('a', 'a\\n');\ndiffLines('a\\n', 'a\\n\\n');\n```\n\nThe results are, respectively:\n\n```text\n[[EQUAL, ['a']]]\n\n[\n  [EQUAL, ['a']],\n  [INSERT, ['']],\n]\n```\n\nPass the ending explicitly for CRLF or CR text. For example, `diffLines('a', 'a\\r\\n', { lineEnding: '\\r\\n' })` returns\n`[[EQUAL, ['a']]]`.\n\nRemoving insertion entries reconstructs the first input's canonical line-token stream; removing deletion entries\nreconstructs the second. The original separators cannot be reconstructed from a line diff because they are delimiters,\nnot tokens.\n\n### `diffGraphemes(before, after, options?)`\n\nComputes a raw grapheme-level diff using `Intl.Segmenter` with `granularity: 'grapheme'`. Every item in each token array\nis one extended grapheme cluster. Combining sequences, emoji ZWJ sequences, flags, skin-tone sequences, and other\nclusters are never split into partial edits.\n\n```typescript\nimport { DELETE, INSERT, diffGraphemes } from '@aforemendude/diff/grapheme';\n\nconst thumbsUpSign = '\\u{1F44D}';\nconst lightSkinTone = '\\u{1F3FB}';\nconst mediumSkinTone = '\\u{1F3FD}';\nconst before = thumbsUpSign + lightSkinTone;\nconst after = thumbsUpSign + mediumSkinTone;\n\ndiffGraphemes(before, after);\n// [[DELETE, [before]], [INSERT, [after]]]\n```\n\nThe library does not Unicode-normalize either input. Canonically equivalent but byte-distinct strings can therefore\nremain different. Removing insertion entries and joining the remaining tokens reconstructs the first input; removing\ndeletion entries and joining reconstructs the second.\n\nThe optional `locale` is passed to `Intl.Segmenter`:\n\n```typescript\nimport { diffGraphemes } from '@aforemendude/diff/grapheme';\n\ndiffGraphemes(before, after, { locale: 'th' });\n```\n\nIts type is `Intl.LocalesArgument`; when omitted, the runtime's default locale selection is used.\n\n### Algorithm selection\n\nBoth diff functions accept `algorithm: 'adaptive' | 'myers' | 'sparse'`. The default is `adaptive`. All three settings\nretain the shared common-prefix, common-suffix, empty-side, containment, and one-token shortcuts; the setting selects\nthe exact engine used for a remaining nontrivial token range.\n\n- `adaptive` estimates the relative work and peak workspace of both engines. It selects sparse-match LCS only when the\n  estimated work advantage is substantial and its estimated workspace remains within a conservative multiple of the\n  Myers frontier; otherwise it uses Myers. Once adaptive mode prefers Myers, that choice is retained for the rest of the\n  call so child ranges do not repeatedly build match indexes.\n- `myers` always uses Myers bisection after the shared shortcuts.\n- `sparse` always uses exact Hunt-Szymanski sparse-match LCS after the shared shortcuts. This can be much faster for\n  disjoint inputs, reversed unique tokens, and other high-distance ranges with few matching position pairs, but it can\n  require substantial time and memory for repetitive inputs.\n\nAll modes produce a normalized shortest insertion/deletion script. When more than one shortest script exists, the public\nAPI does not guarantee which matching tokens or tuple placements an algorithm will select. Placements may differ between\nalgorithms or implementation versions, and cleanup results derived from different valid raw diffs can likewise differ.\n\n### Trivial-case optimizations\n\nBoth diff functions offer opt-in shortcuts for workloads that frequently compare identical strings or inputs where one\nside is empty. Set `optimizeTrivialCases` to `true` to detect those cases before tokenizing both strings. Identical text\nis tokenized once and returned as one equality (or an empty diff for two empty strings). When exactly one input is\nempty, only the nonempty text is tokenized and returned as one insertion or deletion. `diffLines` also tokenizes only\nthe shorter input when the strings differ solely by one insignificant selected terminal delimiter:\n\n```typescript\ndiffLines(text, text, { optimizeTrivialCases: true });\ndiffLines('a', 'a\\n', { optimizeTrivialCases: true });\ndiffGraphemes('', text, { locale: 'en', optimizeTrivialCases: true });\n```\n\nThe option defaults to `false`, so the library does not add up-front whole-string checks to workloads where trivial\ninputs may be uncommon. The terminal-delimiter shortcut requires a nonempty shorter string that does not already end in\nthe selected delimiter, and the longer string must be exactly that shorter string plus the delimiter. A nonempty\nline-ending string still represents one blank-line token, so `diffLines('', '\\n', { optimizeTrivialCases: true })`\nreturns an insertion of `['']`; likewise, `'a\\n'` versus `'a\\n\\n'` remains a real blank-line edit. After enforcing the\ncombined input-size limit described below, `diffGraphemes` constructs the requested `Intl.Segmenter` before taking any\nshortcut, so invalid locales continue to throw for admitted inputs.\n\n### `cleanupSemantic(diffs, options?)`\n\nApplies Diff Match Patch-style semantic cleanup to a grapheme-token diff and returns a new, normalized tuple array\nwithout mutating the input. Each supplied token must be one complete grapheme cluster. Cleanup moves and combines whole\ntokens only, so it never splits a token.\n\nWord boundaries are detected with `Intl.Segmenter` using the optional `locale`:\n\n```typescript\nimport { cleanupSemantic } from '@aforemendude/diff/cleanup';\n\nconst cleaned = cleanupSemantic(changes, { locale: 'ja' });\n```\n\nWord boundaries guide the cleanup rather than constrain it. The algorithm may keep a smaller, partial-word edit when\nmoving the edit to a whole-word boundary would be less useful.\n\nSemantic cleanup is not guaranteed to be idempotent. Applying `cleanupSemantic` again to its output may produce further\nchanges, so callers should not rely on repeated calls returning the same diff.\n\nTo compute and clean up a grapheme-level diff, compose the two operations explicitly:\n\n```typescript\nimport { cleanupSemantic } from '@aforemendude/diff/cleanup';\nimport { diffGraphemes } from '@aforemendude/diff/grapheme';\n\nconst options = { locale: ['zh-Hant', 'zh'] };\nconst changes = cleanupSemantic(diffGraphemes(before, after, options), options);\n```\n\n### `cleanupEfficiency(diffs, options?)`\n\nApplies Diff Match Patch-style efficiency cleanup to a grapheme-token diff and returns a new, normalized tuple array\nwithout mutating the input. Each supplied token must be one complete grapheme cluster. Short equalities are folded into\nsurrounding edits when retaining them would cost more than expanding those edits.\n\nThe optional `editCost` is the cost of starting a new edit, measured in tokens. It defaults to `4`; larger values\nproduce more aggressive cleanup. It must be a finite, non-negative number, otherwise `cleanupEfficiency` throws a\n`RangeError`. Equalities exactly at a cost threshold are retained.\n\n```typescript\nimport { cleanupEfficiency } from '@aforemendude/diff/cleanup';\nimport { diffGraphemes } from '@aforemendude/diff/grapheme';\n\nconst changes = cleanupEfficiency(diffGraphemes(before, after), { editCost: 5 });\n```\n\n## Runtime argument handling\n\nThe package assumes callers supply values that satisfy its exported TypeScript types. Apart from the supported\n`algorithm`, combined string length, and finite, non-negative `editCost` checks described here, it does not validate\nargument types, diff operation values, tuple or token-array shapes, supported line-ending values, or option shapes at\nruntime. Passing out-of-contract values from JavaScript, `any`, or type assertions is unsupported and may produce\nincorrect results or errors.\n\nAn unsupported `algorithm` value throws a `RangeError` after the combined input-size check but before tokenization,\ntrivial-case shortcuts, or `Intl.Segmenter` construction.\n\nUnderlying platform APIs can still reject values themselves. For example, `Intl.Segmenter` may throw for an invalid\nlocale.\n\n## Input size and complexity\n\nThe combined length of `before` and `after` may not exceed 4,294,967,294 UTF-16 code units. `diffLines` and\n`diffGraphemes` throw a `RangeError` above that limit before tokenization, trivial-case shortcuts, or `Intl.Segmenter`\nconstruction. This worst-case bound keeps every token coordinate representable in the compact Myers frontier even when\neach token is one UTF-16 code unit.\n\nWithin that bound, the implementation works directly with token sequences. It does not encode line identifiers into\nUTF-16 characters, impose a fixed line-count ceiling, or stop at an internal timeout. In particular, there is no\n40,000-line ceiling. Available memory and processing time are the practical limits; adversarial inputs can still be\nexpensive.\n\nMyers mode has output-sensitive `O((N + M)D)` time for `N` and `M` input tokens and edit distance `D`, with quadratic\nworst cases. Its bisection frontier uses `O(D)` space, bounded by `O(N + M)`. Let `S = min(N, M)`. Sparse mode has\n`O(N + M + r log L)` time and `O(S + r + L)` workspace, where `r` is the number of strict-equality matching position\npairs and `L` is the LCS length. It is linear for disjoint ranges but can approach quadratic storage on repetitive\ninputs.\n\nSparse mode builds its occurrence index over the shorter remaining token range. One compact link is retained per indexed\nposition, while bucket heads and counts are retained only per distinct indexed token. Adaptive mode builds the same\nindex and counts `r`. A relative memory estimate includes that index, the LIS frontier, and all predecessor records; it\nis compared with the peak compact-frontier allocation implied by Myers' geometric growth. The deliberately optimistic\nMyers estimate can omit its final search layer, so uncertain ranges favor Myers. Ranges admitted by the memory gate\nreceive a length-only LIS probe. Sparse is selected only when its full estimated workspace is at most four times the\nMyers estimate and its estimated work is at least eight times lower. The estimates use saturating arithmetic and are\nselection policy rather than a loss of exactness: both candidate engines still compute a shortest script without a\ndeadline or heuristic edit limit.\n\nTokenization is linear in input size, and cleanup adds passes over the produced diff. Tokens are line contents without\nthe selected line ending for `diffLines` and grapheme clusters for the grapheme APIs.\n\n## Benchmark results\n\nThe benchmark suite has one focused entry point for each public workflow. `npm run benchmark` runs four representative\n1,000-call schedules. `npm run benchmark:adversarial` runs four opt-in calibrated worst-case public-workflow schedules,\none two-call unbalanced sparse-index schedule, eleven short `diffLines` schedules around adaptive-selection boundaries,\nand direct `cleanupSemantic` and `cleanupEfficiency` stress cases at several scales. Timed callbacks invoke only the\npublic entry points. Every benchmark and preflight passes a complete options object containing adaptive selection, LF\nline endings, disabled trivial-case shortcuts, the pinned `en` locale, and an edit cost of 4. These are the documented\ndefaults except for the locale pin, which avoids host-default differences between benchmark environments. Forced\nalgorithms and other non-default options remain correctness-test inputs rather than benchmark scores. Fixture generation\nand correctness preflight are outside the timed regions, and neither command enforces a machine-specific performance\nthreshold.\n\nThe following calibration results were measured on 2026-08-22 with Node.js 24.19.0, npm 11.17.0, Vitest 4.1.10, Linux\n7.0.0 on x86-64, and a four-core Intel N95.\n\n### Runtime\n\nTimes are arithmetic means per measured schedule from the regular runtime commands. RME is Vitest's reported relative\nmargin of error.\n\n#### Representative workloads\n\n| Workflow                              | Schedule                                   | Calls | Mean (ms) | RME      | Samples |\n| ------------------------------------- | ------------------------------------------ | ----: | --------: | -------- | ------: |\n| `diffLines`                           | Representative size/edit mix               | 1,000 |  1,015.40 | +/-1.22% |       3 |\n| `diffGraphemes`                       | Representative prose and mixed-Unicode mix | 1,000 |  2,057.85 | +/-1.18% |       3 |\n| `diffGraphemes` + `cleanupSemantic`   | Scaled representative grapheme mix         | 1,000 |  2,004.82 | +/-1.23% |       3 |\n| `diffGraphemes` + `cleanupEfficiency` | Scaled representative grapheme mix         | 1,000 |  2,040.89 | +/-8.24% |       3 |\n\n#### Adversarial workloads\n\nEach row below is a distinct result reported by `npm run benchmark:adversarial`. Multi-call `diffLines` schedules run\neach listed size or orientation once per measured sample; all other schedules run one workload per sample.\n\n##### Diff workflows\n\n| Workflow        | Schedule                                                                                     | Calls | Mean (ms) | RME       | Samples |\n| --------------- | -------------------------------------------------------------------------------------------- | ----: | --------: | --------- | ------: |\n| `diffLines`     | 9,500 disjoint unique lines per side                                                         |     1 |    2.8138 | +/-14.97% |       3 |\n| `diffLines`     | 128 repeated lines and 500,000 disjoint unique lines, in both orientations                   |     2 |   74.9019 | +/-31.84% |       3 |\n| `diffLines`     | Reversed unique lines at 256, 512, 1,024, and 2,048 lines per side                           |     4 |    1.2739 | +/-1.16%  |       3 |\n| `diffLines`     | Myers side of the relative work crossover at 100 lines per side                              |     1 |    0.2161 | +/-4.40%  |       3 |\n| `diffLines`     | Sparse side of the relative work crossover at 101 lines per side                             |     1 |    0.0452 | +/-45.50% |       3 |\n| `diffLines`     | Disjoint unique lines at 256, 512, 1,024, and 2,048 lines per side                           |     4 |    0.8296 | +/-3.57%  |       3 |\n| `diffLines`     | 1% shared position pairs over unique alphabets at 256, 512, 1,024, and 2,048 lines per side  |     4 |    1.1146 | +/-9.14%  |       3 |\n| `diffLines`     | 5% shared position pairs over unique alphabets at 256, 512, 1,024, and 2,048 lines per side  |     4 |    1.3120 | +/-11.82% |       3 |\n| `diffLines`     | 10% shared position pairs over unique alphabets at 256, 512, 1,024, and 2,048 lines per side |     4 |    1.3273 | +/-21.92% |       3 |\n| `diffLines`     | Duplicate-heavy low-distance fallback at 256, 512, 1,024, and 2,048 lines per side           |     4 |    0.6563 | +/-1.38%  |       3 |\n| `diffLines`     | Mostly equal unique low-distance inputs at 256, 512, 1,024, and 2,048 lines per side         |     4 |    1.2352 | +/-0.88%  |       3 |\n| `diffLines`     | Lower-match side of the relative memory crossover at 512, 1,024, and 2,048 lines per side    |     3 |    1.5390 | +/-0.83%  |       3 |\n| `diffLines`     | Higher-match side of the relative memory crossover at 512, 1,024, and 2,048 lines per side   |     3 |   47.4203 | +/-0.44%  |       3 |\n| `diffGraphemes` | 11,000 disjoint graphemes per side                                                           |     1 |    3.8488 | +/-2.07%  |       3 |\n\n##### Semantic cleanup workflows\n\n| Workflow                            | Schedule                                 | Calls | Mean (ms) | RME       | Samples |\n| ----------------------------------- | ---------------------------------------- | ----: | --------: | --------- | ------: |\n| `diffGraphemes` + `cleanupSemantic` | 4,250,000 equivalent semantic placements |     1 |  1,886.09 | +/-7.85%  |       3 |\n| `cleanupSemantic`                   | 100 consecutive shiftable edits          |     1 |    0.2373 | +/-8.23%  |       3 |\n| `cleanupSemantic`                   | 400 consecutive shiftable edits          |     1 |    0.8529 | +/-1.80%  |       3 |\n| `cleanupSemantic`                   | 1,600 consecutive shiftable edits        |     1 |    3.6013 | +/-4.95%  |       3 |\n| `cleanupSemantic`                   | 6,400 consecutive shiftable edits        |     1 |   16.7458 | +/-5.13%  |       3 |\n| `cleanupSemantic`                   | 250 chained trivial equalities           |     1 |    0.6296 | +/-9.24%  |       3 |\n| `cleanupSemantic`                   | 500 chained trivial equalities           |     1 |    1.1926 | +/-2.97%  |       3 |\n| `cleanupSemantic`                   | 1,000 chained trivial equalities         |     1 |    2.4092 | +/-0.99%  |       3 |\n| `cleanupSemantic`                   | 2,000 chained trivial equalities         |     1 |    5.2282 | +/-0.45%  |       3 |\n| `cleanupSemantic`                   | 4,000 chained trivial equalities         |     1 |   11.7667 | +/-23.30% |       3 |\n\n##### Efficiency cleanup workflows\n\n| Workflow                              | Schedule                                    | Calls | Mean (ms) | RME       | Samples |\n| ------------------------------------- | ------------------------------------------- | ----: | --------: | --------- | ------: |\n| `diffGraphemes` + `cleanupEfficiency` | 8,200 interleaved single-token replacements |     1 |  1,923.60 | +/-0.94%  |       3 |\n| `cleanupEfficiency`                   | One 100,000-token equality                  |     1 |    0.4015 | +/-6.41%  |       3 |\n| `cleanupEfficiency`                   | One replacement among 100,000 tokens        |     1 |    0.4305 | +/-12.42% |       3 |\n| `cleanupEfficiency`                   | 100 consecutive shiftable edits             |     1 |    0.4859 | +/-15.19% |       3 |\n| `cleanupEfficiency`                   | 400 consecutive shiftable edits             |     1 |    1.0184 | +/-24.37% |       3 |\n| `cleanupEfficiency`                   | 1,600 consecutive shiftable edits           |     1 |    3.6654 | +/-4.33%  |       3 |\n| `cleanupEfficiency`                   | 6,400 consecutive shiftable edits           |     1 |   26.9863 | +/-35.36% |       3 |\n| `cleanupEfficiency`                   | 250 chained trivial equalities              |     1 |    0.6495 | +/-19.65% |       3 |\n| `cleanupEfficiency`                   | 500 chained trivial equalities              |     1 |    1.2077 | +/-2.09%  |       3 |\n| `cleanupEfficiency`                   | 1,000 chained trivial equalities            |     1 |    2.4976 | +/-1.80%  |       3 |\n| `cleanupEfficiency`                   | 2,000 chained trivial equalities            |     1 |    5.7722 | +/-0.95%  |       3 |\n| `cleanupEfficiency`                   | 4,000 chained trivial equalities            |     1 |   12.5890 | +/-2.20%  |       3 |\n| `cleanupEfficiency`                   | 160 alternating equality candidates         |     1 |    0.1930 | +/-19.37% |       3 |\n| `cleanupEfficiency`                   | 320 alternating equality candidates         |     1 |    0.3630 | +/-13.62% |       3 |\n| `cleanupEfficiency`                   | 640 alternating equality candidates         |     1 |    0.7322 | +/-0.65%  |       3 |\n| `cleanupEfficiency`                   | 1,280 alternating equality candidates       |     1 |    1.4961 | +/-11.42% |       3 |\n\n### Memory\n\n`npm run benchmark:memory` and `npm run benchmark:adversarial:memory` run the corresponding benchmark files one at a\ntime in fresh Node.js processes and report the baseline RSS, peak RSS, and peak increase for each workflow. The memory\nrunner uses Node.js's operating-system-backed maximum resident set size instead of a V8 heap snapshot, so the peak also\ncaptures typed-array backing stores, strings, `Intl.Segmenter`, and other native or external allocations. The reported\nincrease still includes Vitest, fixture loading, inputs, outputs, and allocator behavior; use it to compare identical\nworkloads on the same Node.js and operating-system versions rather than as an exact count of retained algorithm objects.\nUse the regular benchmark commands above for timing comparisons.\n\nEach row reports the process high-water mark after the warmup and three measured iterations in its fresh benchmark\nprocess. An adversarial row covers every runtime schedule for that workflow because its benchmark file runs in one\nprocess.\n\n#### Representative workloads\n\n| Workflow                              | Schedule                                   | Baseline RSS (MiB) | Peak RSS (MiB) | Peak increase (MiB) |\n| ------------------------------------- | ------------------------------------------ | -----------------: | -------------: | ------------------: |\n| `diffLines`                           | Representative size/edit mix               |              99.27 |         373.55 |              274.28 |\n| `diffGraphemes`                       | Representative prose and mixed-Unicode mix |              99.10 |         269.14 |              170.04 |\n| `diffGraphemes` + `cleanupSemantic`   | Scaled representative grapheme mix         |              99.16 |         270.39 |              171.23 |\n| `diffGraphemes` + `cleanupEfficiency` | Scaled representative grapheme mix         |              99.07 |         274.59 |              175.51 |\n\n#### Adversarial workloads\n\n| Workflow            | Schedules                                    | Baseline RSS (MiB) | Peak RSS (MiB) | Peak increase (MiB) |\n| ------------------- | -------------------------------------------- | -----------------: | -------------: | ------------------: |\n| `diffLines`         | All adversarial line-diff schedules          |              98.99 |         423.72 |              324.73 |\n| `diffGraphemes`     | 11,000 disjoint graphemes per side           |              99.49 |         214.97 |              115.48 |\n| `cleanupSemantic`   | All adversarial semantic-cleanup schedules   |              98.74 |       1,143.62 |            1,044.88 |\n| `cleanupEfficiency` | All adversarial efficiency-cleanup schedules |              99.00 |         383.39 |              284.38 |\n\nThe four representative schedules and the two composed cleanup stress fixtures target roughly two seconds per measured\nschedule on the reference machine. The disjoint fixtures retain their historical sizes and are now intentionally much\nshorter under sparse-match selection. All measurements remain machine-specific observations, not performance guarantees.\nSee [Expected input distribution and benchmark mapping](docs/expected-input-distribution.md) for the heuristic\ndistribution, fixture construction, correctness checks, and interpretation guidance.\n\n## Licensing\n\nOriginal project code is distributed under the [MIT license](LICENSE). Portions of the diff and cleanup implementation\nare derived from `diff-match-patch-es` and Google Diff Match Patch and remain under the Apache License, Version 2.0. The\npackage therefore declares `MIT AND Apache-2.0`. See the [third-party notices](THIRD_PARTY_NOTICES.md) and the included\n[Apache-2.0 license text](LICENSES/Apache-2.0.txt).\n\n## Development\n\nDevelopment requires Node.js 22.12 or newer. The published library supports Node.js 20 or newer. `npm run test` runs the\nunit and integration suites. After a build, `npm run test:package` packs and installs the tarball in a temporary\nconsumer to verify runtime and declaration resolution. Benchmarks use deterministic generated workloads and run\nseparately; they report measurements without enforcing machine-specific performance thresholds. The distribution\nassumptions, workload groups, fixture mapping, and interpretation policy are documented in\n[Expected input distribution and benchmark mapping](docs/expected-input-distribution.md).\n\n```bash\nnpm run format:check\nnpm run build\nnpm run test\nnpm run test:package\nnpm run benchmark\nnpm run benchmark:adversarial\nnpm run benchmark:memory\nnpm run benchmark:adversarial:memory\nnpm run verify\n```\n","readmeFilename":"README.md"}