{"_id":"@condorcet.vote/cef-writer","_rev":"2-202ce12daece1f9f66f345734987ed0f","name":"@condorcet.vote/cef-writer","dist-tags":{"latest":"1.2.2"},"versions":{"1.2.0":{"name":"@condorcet.vote/cef-writer","version":"1.2.0","keywords":["cef","condorcet","election","condorcet-election-format","voting","writer","typescript"],"author":{"name":"Julien Boudry"},"license":"MIT","_id":"@condorcet.vote/cef-writer@1.2.0","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"homepage":"https://github.com/CondorcetVote/CEF-Writer-Typescript#readme","bugs":{"url":"https://github.com/CondorcetVote/CEF-Writer-Typescript/issues"},"dist":{"shasum":"f82643691e61c559e2dbaa60c2a32a00f140fc6e","tarball":"https://registry.npmjs.org/@condorcet.vote/cef-writer/-/cef-writer-1.2.0.tgz","fileCount":107,"integrity":"sha512-cGY5XoHisim8TkF9bXnZftst0FZD6i8zDYASlTfdJAeTphgZ30N8qJ6G3NinIlfoNNwVl6m9H6QtzOvMBKVicA==","signatures":[{"sig":"MEUCIASkaIPn0wiYCqS5n2Y+PtR3XErFSWatB02t3XfEKmYrAiEA7u4WNL97od2L5IKP13eyWGj041+8aHwRG4p3BWCECv0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":167143},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=24.0.0"},"exports":{".":{"node":"./dist/index.node.js","types":"./dist/index.d.ts","import":"./dist/index.js","browser":"./dist/index.browser.js","default":"./dist/index.js"},"./browser":{"types":"./dist/index.browser.d.ts","import":"./dist/index.browser.js","browser":"./dist/index.browser.js","default":"./dist/index.browser.js"}},"gitHead":"f3387844ee35125e6dd4b29f5e3fbc6b18130563","scripts":{"dev":"tsc --watch","lint":"eslint src","test":"bun test","build":"tsc","clean":"rm -rf dist","format":"prettier --write \"src/**/*.ts\"","lint:fix":"eslint src --fix","prebuild":"npm run clean","type-check":"tsc --noEmit","format:check":"prettier --check \"src/**/*.ts\"","prepublishOnly":"npm run lint && npm run type-check && npm run build"},"_npmUser":{"name":"julien-boudry","email":"julien.boudry@proton.me"},"repository":{"url":"git+https://github.com/CondorcetVote/CEF-Writer-Typescript.git","type":"git"},"_npmVersion":"11.12.1","description":"Streaming TypeScript writer that produces valid Condorcet Election Format (CEF) documents line by line. A faithful port of CondorcetVote/CEF-Writer.","directories":{},"sideEffects":["./dist/index.node.js"],"_nodeVersion":"25.9.0","_hasShrinkwrap":false,"packageManager":"bun@latest","devDependencies":{"eslint":"^10.4.1","prettier":"^3.0.0","@eslint/js":"^10.0.0","@types/bun":"1.3.14","typescript":"^6.0.3","@types/node":"^22.0.0","typescript-eslint":"8.61.0","@typescript-eslint/parser":"^8.0.0","@typescript-eslint/eslint-plugin":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/cef-writer_1.2.0_1781183883910_0.42707878664462684","host":"s3://npm-registry-packages-npm-production"}},"1.2.2":{"name":"@condorcet.vote/cef-writer","version":"1.2.2","description":"Streaming TypeScript writer that produces valid Condorcet Election Format (CEF) documents line by line. A faithful port of CondorcetVote/CEF-Writer.","type":"module","main":"dist/index.js","types":"dist/index.d.ts","sideEffects":["./dist/index.node.js"],"exports":{".":{"types":"./dist/index.d.ts","browser":"./dist/index.browser.js","node":"./dist/index.node.js","import":"./dist/index.js","default":"./dist/index.js"},"./browser":{"types":"./dist/index.browser.d.ts","browser":"./dist/index.browser.js","import":"./dist/index.browser.js","default":"./dist/index.browser.js"}},"scripts":{"build":"tsc","dev":"tsc --watch","clean":"rm -rf dist","prebuild":"npm run clean","lint":"eslint src","lint:fix":"eslint src --fix","format":"prettier --write \"src/**/*.ts\"","format:check":"prettier --check \"src/**/*.ts\"","type-check":"tsc --noEmit","test":"bun test","prepublishOnly":"npm run lint && npm run type-check && npm run build"},"packageManager":"bun@latest","keywords":["cef","condorcet","election","condorcet-election-format","voting","writer","typescript","condorcet.vote"],"author":{"name":"Julien Boudry"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/CondorcetVote/CEF-Writer-Typescript.git"},"bugs":{"url":"https://github.com/CondorcetVote/CEF-Writer-Typescript/issues"},"homepage":"https://github.com/CondorcetVote/CEF-Writer-Typescript#readme","devDependencies":{"@eslint/js":"^10.0.0","@types/bun":"1.3.14","@types/node":"^22.0.0","@typescript-eslint/eslint-plugin":"^8.0.0","@typescript-eslint/parser":"^8.0.0","eslint":"^10.4.1","prettier":"^3.0.0","typescript":"^6.0.3","typescript-eslint":"8.61.0"},"engines":{"node":">=24.0.0"},"gitHead":"00952d31f3c0a257ad27b1b52302f8e97a46c3c7","_id":"@condorcet.vote/cef-writer@1.2.2","_nodeVersion":"22.22.3","_npmVersion":"11.16.0","dist":{"integrity":"sha512-C0ggjINp5xqSU0tEiTocDewaECfCfexnHdw9+uhIKrHVHRag4Zds4Sn3ugWNrU2QWF8S20uijpKoPdfbkHmOmw==","shasum":"cd499d9a733a488944d55df9e137c111da1c490e","tarball":"https://registry.npmjs.org/@condorcet.vote/cef-writer/-/cef-writer-1.2.2.tgz","fileCount":107,"unpackedSize":167562,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@condorcet.vote%2fcef-writer@1.2.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHVn1VEhKPy6o5kTbxS1BtQd2QU+eUhOY8Lvic+iDPKNAiAyT/k5ZHfiwYUMmLfUEPa7243RKrCT8XB1FCrBkg2G4w=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ce2f25c5-3150-4c5f-b8b9-ec73ce6ebf2c"}},"directories":{},"maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cef-writer_1.2.2_1781184679163_0.3934711156118249"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-11T13:18:03.717Z","modified":"2026-06-11T13:31:19.611Z","1.2.0":"2026-06-11T13:18:04.041Z","1.2.2":"2026-06-11T13:31:19.289Z"},"bugs":{"url":"https://github.com/CondorcetVote/CEF-Writer-Typescript/issues"},"author":{"name":"Julien Boudry"},"license":"MIT","homepage":"https://github.com/CondorcetVote/CEF-Writer-Typescript#readme","keywords":["cef","condorcet","election","condorcet-election-format","voting","writer","typescript","condorcet.vote"],"repository":{"type":"git","url":"git+https://github.com/CondorcetVote/CEF-Writer-Typescript.git"},"description":"Streaming TypeScript writer that produces valid Condorcet Election Format (CEF) documents line by line. A faithful port of CondorcetVote/CEF-Writer.","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"readme":"# CEF Writer Typescript\n\n[![npm version](https://img.shields.io/npm/v/@condorcet.vote/cef-writer.svg)](https://www.npmjs.com/package/@condorcet.vote/cef-writer)\n[![CI](https://github.com/CondorcetVote/CEF-Writer-Typescript/actions/workflows/ci.yml/badge.svg)](https://github.com/CondorcetVote/CEF-Writer-Typescript/actions/workflows/ci.yml)\n[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)\n\nA TypeScript library that streams valid [Condorcet Election Format](https://github.com/CondorcetVote/CondorcetElectionFormat) (CEF) documents to a file or string buffer with a friendly object API.\n\nThis is a faithful TypeScript port of the PHP library [CondorcetVote/CEF-Writer](https://github.com/CondorcetVote/CEF-Writer): same public API, same architecture, same guarantees.\n\n- **Streaming**: every `add*()` call writes one line immediately — nothing is buffered, nothing can be edited afterwards.\n- **Format-safe**: the spec's syntactic rules (reserved characters, blank-ballot sentinel, single-line constraints, parameter-before-vote ordering) are enforced. Invalid input throws.\n- **Semantics-free on purpose**: this library checks format, never election logic (it will, for example, happily let a vote reference a candidate that is not in `#/Candidates:`).\n- **Zero runtime dependencies** and full TypeScript types.\n- Works with a filesystem path, any `WriteTarget`, or a `StringBuffer`.\n\n## Requirements\n\n- Node.js 24+ (ES2024, ESM only)\n- Bun (compatible with modern versions)\n- Modern browsers with ES2024 support (Chrome 127+, Firefox 133+, Safari 18+, Edge 127+)\n\nFor file output in Node.js, the native `node:fs` API is used.\n\n## Installation\n\n```bash\nnpm install @condorcet.vote/cef-writer\n# or\nyarn add @condorcet.vote/cef-writer\n# or\npnpm add @condorcet.vote/cef-writer\n# or\nbun add @condorcet.vote/cef-writer\n```\n\n## Quick start\n\n```typescript\nimport {\n  Cef,\n  CommentLine,\n  VoteLine,\n  CandidatesParameter,\n  ImplicitRankingParameter,\n  WeightAllowedParameter,\n} from '@condorcet.vote/cef-writer';\n\nconst cef = new Cef({ file: '/tmp/election.cvotes' });\n\ncef.addComment(new CommentLine('My beautiful election'));\ncef.addParameter(new CandidatesParameter(['Alice', 'Bob', 'Charlie']));\ncef.addParameter(new ImplicitRankingParameter(true));\ncef.addParameter(new WeightAllowedParameter(true));\n\ncef.addVote(VoteLine.fromRanking([['Alice'], ['Bob'], ['Charlie']], { quantifier: 42 }));\ncef.addVote(VoteLine.fromRanking([['Charlie'], ['Alice', 'Bob']], { weight: 7, quantifier: 8 }));\ncef.addVote(VoteLine.fromRanking([])); // blank ballot (/EMPTY_RANKING/)\n\ncef.close();\n```\n\nproduces:\n\n```\n# My beautiful election\n#/Candidates: Alice ; Bob ; Charlie\n#/Implicit Ranking: true\n#/Weight Allowed: true\n\nAlice > Bob > Charlie * 42\nCharlie > Alice = Bob ^7 * 8\n/EMPTY_RANKING/\n```\n\n## Browser usage\n\nThe library is distributed as modern ESM and can be used in browser environments. File operations (`FileWriteTarget`) require Node.js, but you can use `StringBuffer` or implement a custom `WriteTarget` for browser-based workflows:\n\n```typescript\nimport {\n  Cef,\n  VoteLine,\n  CandidatesParameter,\n  StringBuffer,\n} from '@condorcet.vote/cef-writer';\n\n// In browser: use StringBuffer to generate CEF as a string\nconst buffer = new StringBuffer();\nconst cef = new Cef({ string: buffer });\n\ncef.addParameter(new CandidatesParameter(['Alice', 'Bob', 'Charlie']));\ncef.addVote(VoteLine.fromRanking([['Alice'], ['Bob'], ['Charlie']]));\n\nconst csvContent = buffer.toString();\nconsole.log(csvContent);\n\n// Download the file client-side\nconst blob = new Blob([csvContent], { type: 'text/plain' });\nconst url = URL.createObjectURL(blob);\nconst link = document.createElement('a');\nlink.href = url;\nlink.download = 'election.cvotes';\nlink.click();\n```\n\nOr implement your own `WriteTarget` for custom backends:\n\n```typescript\nclass CustomTarget implements WriteTarget {\n  private lines: string[] = [];\n\n  write(chunk: string): number {\n    this.lines.push(chunk);\n    return chunk.length;\n  }\n\n  getLinesArray(): string[] {\n    return this.lines;\n  }\n}\n\nconst target = new CustomTarget();\nconst cef = new Cef({ file: target });\n// ... add parameters and votes\n```\n\n## Output targets\n\nThe `Cef` constructor takes an options object with **exactly one** of the\nfollowing keys:\n\n| Option | Type | Behavior |\n| --- | --- | --- |\n| `file: path` | `string` | A filesystem path, opened with mode `w` (created/truncated). **Node.js only.** |\n| `file: target` | `WriteTarget` | Any object with `write(chunk: string): number` (e.g. an already-open `FileWriteTarget`); used as-is. |\n| `string: buffer` | `StringBuffer` | Each line is appended to the supplied buffer. |\n\nThe \"string passed by reference\" target of the PHP original maps to the\n`StringBuffer` value object — the idiomatic TypeScript stand-in:\n\n```typescript\nimport { Cef, CandidatesParameter, StringBuffer } from '@condorcet.vote/cef-writer';\n\nconst buffer = new StringBuffer();\nconst cef = new Cef({ string: buffer });\n\ncef.addParameter(new CandidatesParameter(['A', 'B']));\n\nconsole.log(buffer.toString()); // \"#/Candidates: A ; B\\n\"\n```\n\n`cef.file` is the active `WriteTarget` in file mode and `null` in string mode.\nCall `cef.close()` to close the underlying file descriptor when you are done\n(no-op in string mode).\n\n## `autoFormat`\n\n`cef.autoFormat` is a public `boolean` (default `true`):\n\n- `true` — writes the readable flavor of the spec: spaces around `>`, `=`, `;`, `,`, `||`, `^`, `*`; one blank line is inserted automatically between the parameter block and the first vote.\n- `false` — writes the compact form with no optional whitespace and no auto blank line.\n\n```typescript\ncef.autoFormat = false;\ncef.addParameter(new CandidatesParameter(['A', 'B']));\ncef.addVote(VoteLine.fromRanking([['A'], ['B']]));\n// \"#/Candidates:A;B\\nA>B\\n\"\n```\n\n## Building blocks\n\n### Parameters\n\nEach standard parameter has its own typed class. Custom parameters are supported\nvia `CustomParameter`. The `StandardParameter` enum lists the exact spec names.\n\n| Class | Parameter | Value |\n| --- | --- | --- |\n| `CandidatesParameter` | `Candidates` | `string[]` |\n| `NumberOfSeatsParameter` | `Number of Seats` | integer ≥ 1 |\n| `ImplicitRankingParameter` | `Implicit Ranking` | `boolean` |\n| `VotingMethodsParameter` | `Voting Methods` | `string[]` |\n| `WeightAllowedParameter` | `Weight Allowed` | `boolean` |\n| `CustomParameter` | (free-form) | `(name: string, value: string)` |\n\nParameters can only be added before the first vote — any later call throws\n`InvalidWriterStateException`.\n\n### Vote lines\n\n`VoteLine` instances are built through static named constructors — its\nconstructor is private, so never use `new VoteLine(...)`.\n\nThe typed way — `VoteLine.fromRanking()` — then pass it to `cef.addVote()`:\n\n```typescript\nVoteLine.fromRanking(\n  [['Alice'], ['Bob', 'Charlie']], // [] => /EMPTY_RANKING/\n  {\n    tags: ['voter@example.com'],\n    weight: 7,\n    quantifier: 3,\n    inlineComment: 'late ballot',\n  },\n);\n```\n\nEach rank is itself a list of tied candidates. An empty top-level ranking emits\nthe `/EMPTY_RANKING/` blank-ballot sentinel.\n\nThe first argument also accepts a ready-made `Ranking` object (see below). Once\nbuilt, the parsed ranking is exposed on the read-only `voteLine.ranking`\nproperty (use `voteLine.ranking.ranks` for the `string[][]` structure). It is\n`null` only when the ballot was built verbatim via\n`VoteLine.fromRawRankingString()`.\n\n#### The `Ranking` value object\n\nA ranking can be built, validated and rendered on its own through the `Ranking`\nclass — the same abstraction `VoteLine` uses internally:\n\n```typescript\nimport { Ranking, VoteLine } from '@condorcet.vote/cef-writer';\n\nconst ranking = new Ranking([['Alice'], ['Bob', 'Charlie']]); // [] => /EMPTY_RANKING/\nRanking.fromString('Alice > Bob = Charlie'); // or parse a ranking-only string\n\nranking.ranks; // [['Alice'], ['Bob', 'Charlie']]\nranking.format(); // \"Alice > Bob = Charlie\"  (relaxed flavor)\nranking.format(false); // \"Alice>Bob=Charlie\"   (compact flavor)\nString(ranking); // same as format()\n\nVoteLine.fromRanking(ranking, { weight: 7 });\n```\n\n`Ranking` is immutable and self-validating: reserved characters, empty ranks and\nduplicate candidates throw a `CefFormatException` at construction time.\n`Ranking.fromString()` accepts only a ranking — every reserved character, the\n`||` tag separator and line breaks are rejected.\n\n#### Verbatim ranking — `VoteLine.fromRawRankingString()`\n\nWhen you already have a ranking as text and want it written verbatim (its exact\nspacing preserved, no re-rendering), build the ballot with\n`fromRawRankingString()`. It validates the ranking string with the same rules as\n`Ranking.fromString()` but skips parsing it — the string is stored as-is and\n`voteLine.ranking` is therefore `null`:\n\n```typescript\nconst line = VoteLine.fromRawRankingString('Alice>Bob=Charlie', { weight: 7 });\nline.format(true); // \"Alice>Bob=Charlie ^7\"  (ranking kept verbatim)\nline.ranking; // null\n```\n\nOnly the library-built companions (the `||` tag separator, `^weight`,\n`*quantifier`) follow `autoFormat`; the ranking itself is never reformatted.\nThis is the engine behind `Cef.addRawVote()`.\n\n#### From a raw string — `VoteLine.fromString()`\n\nParse a full CEF vote-line string into a `VoteLine` instance. Every component is\noptional except the ranking; both the relaxed (`A > B ^7 * 2`) and the compact\n(`A>B^7*2`) spacing flavors are accepted, plus the `/EMPTY_RANKING/` sentinel.\n\n```typescript\ncef.addVote(VoteLine.fromString('voter@example.com || Alice > Bob ^7 * 3 # late ballot'));\n```\n\nThrows `CefFormatException` on any malformed component.\n\n#### Pre-validated raw lines — `Cef.addRawVoteLine()`\n\nWhen you already have ballots as text and want the fastest write path,\n`addRawVoteLine()` skips the `VoteLine` allocation while still enforcing the full\nCEF format:\n\n```typescript\ncef.addRawVoteLine('Alice > Bob = Charlie ^7 * 8');\n```\n\nIt strips one trailing line terminator (`\\r\\n`, `\\n`, `\\r`), trims, rejects\nempty / multi-line / leading-`#` inputs, then runs `VoteLine.assertValidString()`\nfor the same deep validation as `fromString()`. The `autoFormat` flag is not\napplied — what you pass is what gets written.\n\n#### Strict, ranking-only raw votes — `Cef.addRawVote()`\n\n`addRawVoteLine()` is deliberately permissive: because it accepts a whole vote\nline, the caller can embed tags, a weight, a quantifier or an inline comment\ndirectly in the text. When the ranking comes from an untrusted source and you\nwant a hard guarantee that it cannot smuggle any of that in, use the strict\nsibling `addRawVote()`:\n\n```typescript\ncef.addRawVote('Alice > Bob = Charlie', {\n  quantifier: 8,\n  weight: 7,\n  tags: ['voter@example.com'],\n});\n// \"voter@example.com || Alice > Bob = Charlie ^7 * 8\"\n```\n\n`vote` may contain only a ranking — candidate names joined by `>` and `=`, or\nthe `/EMPTY_RANKING/` sentinel. Any line break, the `||` tag separator, and every\nreserved character (`^`, `*`, `#`, `;`, `,`, `/`) is rejected, so the string can\nnever inject a weight, quantifier, tag, inline comment or a second vote. Those\ncompanions are supplied exclusively through the typed options:\n\n```typescript\ncef.addRawVote(\n  vote: string,\n  options?: {\n    quantifier?: number | null;\n    weight?: number | null;\n    tags?: readonly string[] | null;\n  },\n): Cef\n```\n\n`weight` and `quantifier` are nullable and default to `null`, in which case they\nare omitted from the output; when provided they must be strictly positive\nintegers. Just like `addRawVoteLine()`, the ranking string is written verbatim —\nits original spacing is preserved and `autoFormat` does not reformat it. The\n`autoFormat` flag still governs the layout of the library-built companions.\nThrows `CefFormatException` on any malformed input.\n\n#### Validation-only — `VoteLine.assertValidString()`\n\nIf you want to validate a vote-line string without allocating a `VoteLine` (e.g.\nto pre-flight user input before queueing it elsewhere), call the static\n`assertValidString()` — same pipeline as `fromString()`, no object returned,\nthrows `CefFormatException` on any violation.\n\n### Comments and blank lines\n\n```typescript\ncef.addComment(new CommentLine('section divider'));\ncef.addCommentLine('shortcut — builds the CommentLine for you');\ncef.addEmptyLine();\n```\n\nInline comments attached to vote lines live on `VoteLine.inlineComment`. The CEF\nspec forbids inline comments on parameter lines, so the parameter classes\nintentionally do not expose one.\n\n## Errors\n\nTwo top-level hierarchies, each for a different layer.\n\n### Format & input violations — `CefFormatException`\n\nBase class for every specification or input violation. Catch this one to handle\nany format-related failure uniformly; catch a specific subclass to branch on a\nkind of violation. Each message names the offending field and the rule that was\nbroken.\n\n| Subclass | Cause |\n| --- | --- |\n| `InvalidUtf8Exception` | A string carrying an unpaired UTF-16 surrogate, which cannot be encoded to well-formed UTF-8 (the TypeScript analog of \"non-UTF-8 bytes\"). |\n| `ReservedCharacterException` | One of the spec-reserved characters (`> = ; , # / * ^`), a `:` in a custom parameter name, `||` inside a tag, or a leading `#` on a raw vote line. |\n| `InvalidValueException` | Empty required string, embedded line break, null byte, non-positive weight / quantifier, empty `#/Candidates:` or `#/Voting Methods:` list, or empty rank inside a ranking. |\n| `DuplicateCandidateException` | Same candidate label appearing twice in `#/Candidates:` or anywhere inside a ranking (including across tied groups). |\n| `InvalidWriterStateException` | `Cef` constructed with neither a file nor a string target (or with both); parameter added after the first vote; vote-line string parsed without a ranking. |\n\nAll subclasses extend `CefFormatException`.\n\n### I/O failures — `CefWriteException`\n\nThrown when writing to the underlying target (file or string buffer) fails —\ntypically a closed handle, a read-only file, or a full disk. Distinct from\n`CefFormatException` because the cause is I/O, not your input. When the write\ntarget throws, the original error is preserved on the `cause` property.\n\n> [!NOTE]\n> **Differences from the PHP original.** TypeScript strings are sequences of\n> UTF-16 code units, so \"invalid UTF-8\" is detected as an ill-formed string\n> carrying an unpaired surrogate. The \"string passed by reference\" target is\n> modeled as the `StringBuffer` value object, and the named-argument\n> constructors (`fromRanking`, `addRawVote`, …) take an options object instead.\n> The public class names, methods, validation rules and output are otherwise\n> identical to the source library.\n\n## Development\n\nThis repository uses [Bun](https://bun.sh) for development.\n\n```bash\nbun install\nbun run dev          # TypeScript watch mode\nbun run build        # Compile TypeScript to dist/\nbun run lint         # ESLint\nbun run lint:fix     # ESLint with auto-fix\nbun run format       # Prettier\nbun run type-check   # tsc --noEmit\nbun test             # Run the test suite\n```\n\nThe library is organized to mirror the PHP source architecture:\n\n```\nsrc/\n├── index.ts                 # Public API barrel\n├── Cef.ts                   # Streaming writer\n├── CefFormat.ts             # Internal validation helpers (@internal)\n├── Ranking.ts               # Ranking value object\n├── VoteLine.ts              # Ballot value object\n├── CommentLine.ts           # Standalone comment line\n├── Exception/               # CefFormatException hierarchy + CefWriteException\n└── Parameter/               # ParameterInterface, StandardParameter + typed parameters\n```\n\n## Contributing\n\nContributions are welcome! Please read our [contributing guide](CONTRIBUTING.md) for details.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md) for a list of changes in each version.\n","readmeFilename":"README.md"}