Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
229bf22
feat: initial release of @git-stunts/trailer-codec
flyingrobots Jan 8, 2026
d243810
feat(git-stunts): architect the 900 stunt. sharded roaring bitmaps, s…
flyingrobots Jan 8, 2026
fbec60a
feat: harden trailer-codec for npm publication
flyingrobots Jan 8, 2026
35627c1
docs: add fresh post-hardening audit
flyingrobots Jan 8, 2026
d56dd63
docs: capture best material for Git Stunts blog series
flyingrobots Jan 8, 2026
2cafd87
fix(trailer-codec): guard docker tests and reuse key pattern
flyingrobots Jan 8, 2026
46abef2
chore(trailer-codec): drop guard and unused dependency
flyingrobots Jan 8, 2026
0c4db96
chore(trailer-codec): remove benchmarks
flyingrobots Jan 8, 2026
77723f4
feat(trailer-codec): polish helpers and docs
flyingrobots Jan 8, 2026
40d364d
refactor(trailer-codec): expose parser strategy and clean facade
flyingrobots Jan 8, 2026
ef46834
docs: expand README and add reference guides
flyingrobots Jan 8, 2026
ba88a51
docs: normalize changelog bullet spacing
flyingrobots Jan 8, 2026
6aa60ac
docs: clarify encodeMessage factory
flyingrobots Jan 8, 2026
40d5349
docs: clarify codec helpers
flyingrobots Jan 8, 2026
c4c4765
docs: format git commit message signature
flyingrobots Jan 8, 2026
e2591ec
docs: note schema bundle defaults
flyingrobots Jan 8, 2026
30a65cf
docs: clarify encode payload validation
flyingrobots Jan 8, 2026
990c830
docs: add validation error origins
flyingrobots Jan 8, 2026
0ed237d
docs: clarify usage structure
flyingrobots Jan 8, 2026
089631b
docs: add breaking change guidance
flyingrobots Jan 8, 2026
a1fd4a5
docs: refine configured codec example
flyingrobots Jan 8, 2026
98979d2
docs: restructure helper descriptions
flyingrobots Jan 8, 2026
d701779
chore: Remove wack files
flyingrobots Jan 8, 2026
14d24bb
docs: clarify security scope
flyingrobots Jan 8, 2026
c7837a1
docs: detail vitest debug flags
flyingrobots Jan 8, 2026
50377f7
docs: clarify snapshot policy
flyingrobots Jan 8, 2026
f4b0c07
docs: describe integration API contract
flyingrobots Jan 8, 2026
7a64126
docs: clarify parser regex
flyingrobots Jan 8, 2026
3c1ef40
fix: harden trailer normalization
flyingrobots Jan 8, 2026
5a272b1
refactor: remove shared codec service
flyingrobots Jan 8, 2026
b8a4e23
fix: validate normalized input
flyingrobots Jan 8, 2026
c1900ab
docs: make TrailerCodec primary
flyingrobots Jan 8, 2026
88b8ab5
refactor: clarify TrailerCodec API
flyingrobots Jan 8, 2026
7f1c3ed
chore: clarify trailer key exports
flyingrobots Jan 8, 2026
da2807e
chore: rename trailer key pattern export
flyingrobots Jan 8, 2026
75d03eb
test: ensure service not called on invalid input
flyingrobots Jan 8, 2026
ffaa74d
test: assert service stays untouched
flyingrobots Jan 8, 2026
8f99482
test: guard invalid message objects
flyingrobots Jan 8, 2026
33c690c
test: assert body format helper calls
flyingrobots Jan 8, 2026
000835d
test: assert trailer newline code
flyingrobots Jan 8, 2026
aedcfc7
test: verify docs metadata
flyingrobots Jan 8, 2026
925cebf
test: drop private composeBody assertion
flyingrobots Jan 8, 2026
e3af0a7
test: drop private prepareLines assertion
flyingrobots Jan 8, 2026
380511c
refactor: reuse formatter helper
flyingrobots Jan 8, 2026
5ebfb26
feat: validate commit formatter inputs
flyingrobots Jan 8, 2026
1826f84
fix: sanitize key pattern regex
flyingrobots Jan 8, 2026
e0360da
fix: harden trailer schema
flyingrobots Jan 8, 2026
7a2465d
chore: remove jsdoctor scripts
flyingrobots Jan 9, 2026
e8bcc0f
chore(release): v2.1.0 - production readiness overhaul and architectu…
flyingrobots Jan 12, 2026
b129437
Update docs/ADVANCED.md
flyingrobots Jan 12, 2026
12911f5
Update docs/MIGRATION.md
flyingrobots Jan 12, 2026
150993e
Update docs/SERVICE.md
flyingrobots Jan 12, 2026
22b636e
Update scripts/pre-commit.sh
flyingrobots Jan 12, 2026
202712e
Update docs/SERVICE.md
flyingrobots Jan 12, 2026
0c8e02b
chore: Fixes from PR code review
flyingrobots Jan 12, 2026
68a4dd2
Merge branch 'v2.1.0-rc' of github.com:git-stunts/trailer-codec into …
flyingrobots Jan 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm install
- run: npm run lint

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm install
- run: npm test
168 changes: 168 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Dependency directories
node_modules/
jspm_packages/
bun.lockb
node_modules.bun/

# Deno
.deno/
deno.lock

# Build outputs
dist/
build/
.next/
.nuxt/
.output/
.cache/
out/

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc documentation
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
public/

# vue-cli dist
dist/

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# Tern JS port file
.tern-port

# Stores VS Code state
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/code-actions.json

# IntelliJ IDEA
.idea/

.crush
.obsidian

# macOS
.DS_Store
.AppleDouble
.LSOverride
Icon


# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created by macOS
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

# Other Editors
*.swp
*.swo
*~
94 changes: 94 additions & 0 deletions API_REFERENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# API Reference

This file catalogs every public export from `@git-stunts/trailer-codec` so you can confidently import, configure, and extend the codec.

## Encoding & decoding helpers

### `decodeMessage(message: string)`
- Deprecated convenience wrapper around `new TrailerCodec().decode(message)`. Use `TrailerCodec` instances instead. (to be removed in v3.0)
- Input: a raw commit payload (title, optional body, trailers) as a string.
- Output: `{ title: string, body: string, trailers: Record<string, string> }` where `body` is trimmed via `formatBodySegment` (see below) and trailer keys are normalized to lowercase.
- Throws `TrailerCodecError` subclasses (e.g., `TrailerNoSeparatorError`, `TrailerValueInvalidError`, or `CommitMessageInvalidError`) for invalid titles, missing blank lines, oversized messages, or malformed trailers.

### `encodeMessage({ title: string, body?: string, trailers?: Record<string, string> })`
-- Deprecated convenience wrapper around `new TrailerCodec().encode(payload)`. Use `TrailerCodec` instances instead. (to be removed in v3.0)
- Builds a `GitCommitMessage` under the hood and returns the canonical string. Trailers are converted from plain objects to `GitTrailer` instances via the default factory (see `GitTrailer` below).

### `formatBodySegment(body?: string, { keepTrailingNewline = false })`
- Shared helper for `decodeMessage` to trim whitespace while optionally keeping a trailing newline when you plan to write the body back into a template.

### `createMessageHelpers({ service, bodyFormatOptions } = {})`
- Returns `{ decodeMessage, encodeMessage }` bound to the injected `TrailerCodecService` instance; a new service is created when none is provided.
- Supports `bodyFormatOptions` (forwarded to `formatBodySegment`) and is useful for advanced/test wiring.

### `createDefaultTrailerCodec({ bodyFormatOptions } = {})`
- Creates a new `TrailerCodecService`, builds a `TrailerCodec`, and returns it so you can call `encodeMessage()`/`decodeMessage()` without manually wiring services.

### `TrailerCodec`
- Constructor opts: `{ service = new TrailerCodecService(), bodyFormatOptions }`.
- Exposes `decodeMessage(input)`/`decode(input)` and `encodeMessage(payload)`/`encode(payload)` methods that delegate to `createMessageHelpers()`.
- The `decode()` and `encode()` methods are convenience aliases added in v2.1.0.

### `createConfiguredCodec({ keyPattern, keyMaxLength, parserOptions, formatters, bodyFormatOptions } = {})`
- Creates a schema bundle via `createGitTrailerSchemaBundle`, a `TrailerParser`, and a `TrailerCodecService`, then exposes `{ service, helpers, decodeMessage, encodeMessage }`.
- Use this when you need custom trailer patterns, parser tweaks, formatter hooks, or tightly controlled key lengths.

## Domain model exports

### `GitCommitMessage`
```ts
new GitCommitMessage(
payload: { title: string; body?: string; trailers?: GitTrailerInput[] },
options?: { trailerSchema?: ReturnType<typeof getDefaultTrailerSchemaBundle>['schema']; formatters?: { titleFormatter?: (value: string) => string; bodyFormatter?: (value: string) => string } }
);
```
- Validates via `GitCommitMessageSchema`, normalizes title/body with optional formatters, and converts `trailers` to `GitTrailer` instances.
- `toString()` returns a Git-style commit string (title, optional body, blank line, trailers).

### `GitTrailer`
- Accepts `(key: string, value: string, schema = GitTrailerSchema)`.
- Validates using `GitTrailerSchema` and normalizes the key to lowercase and the value to a trimmed string.
- Throws `TrailerInvalidError` or `TrailerValueInvalidError` when the provided key/value fail schema validation.

## Services & parsers

### `TrailerCodecService`
- Core decode/encode logic; see `docs/SERVICE.md` for how the pipeline is wired.
- Constructor options:
- `schemaBundle`: result of `createGitTrailerSchemaBundle({ keyPattern, keyMaxLength })` (defaults: `keyPattern` = `[A-Za-z0-9_-]+`, `keyMaxLength` = `100`, see the schema section below).
- `trailerFactory`: function that instantiates trailers (defaults to `GitTrailer`).
- `parser`: instance of `TrailerParser`.
- `messageNormalizer`, `titleExtractor`, `bodyComposer`: helper classes that normalize lines, extract the title, and compose the body.
- `formatters`: optional `{ titleFormatter, bodyFormatter }` that run before serialization.
- `decode(message)` enforces message size, normalizes lines, extracts the title, splits body/trailers with `TrailerParser`, composes the body, builds trailers with `trailerFactory`, and constructs a `GitCommitMessage`.
- `encode(messageEntity)` accepts either a `GitCommitMessage` instance or a plain payload object, validates it against `GitCommitMessageSchema`, and returns the canonical commit string produced by the entity.

### `TrailerParser`
- Constructor takes `{ keyPattern = TRAILER_KEY_RAW_PATTERN_STRING }`.
- `split(lines)` finds where the trailer block begins (walks backward, validates the blank-line separator) and returns `{ bodyLines, trailerLines }`.
- Used internally by `TrailerCodecService` and is injectable for custom parsing strategies.

## Schemas & constants

### `createGitTrailerSchemaBundle({ keyPattern, keyMaxLength } = {})`
- Returns `{ schema, keyPattern, keyRegex }` with the schema used by `GitTrailer` and `GitCommitMessage`.
- Default `keyPattern` is `[A-Za-z0-9_-]+` and `keyMaxLength` defaults to `100`.

### `TRAILER_KEY_RAW_PATTERN_STRING` / `TRAILER_KEY_REGEX`
- Exported from the default schema bundle; use them to keep custom parsers aligned with validation rules.

## Errors

### `TrailerCodecError`
- Base error type used throughout the codec (`index.js` re-exports it).
- Signature: `(message: string, meta: Record<string, unknown> = {})`.

### Validation error subclasses

| Error | Thrown by | Meaning |
| --- | --- | --- |
| `TrailerTooLargeError` | `MessageNormalizer.guardMessageSize` (called by `TrailerCodecService.decode` and the exported `decodeMessage`) | Message exceeds the 5 MB guard in `MessageNormalizer`. |
| `TrailerNoSeparatorError` | `TrailerParser.split` / `TrailerCodecService.decode` when the blank-line guard fails | A trailer block was found without a blank line separating it from the body (see `TrailerParser`). |
| `TrailerValueInvalidError` | `GitTrailer` via `GitTrailerSchema.parse` when constructing trailers | A trailer value violated the `GitTrailerSchema` (e.g., contained `\n`). |
| `TrailerInvalidError` | `GitTrailer` via `GitTrailerSchema.parse` when constructing trailers | Trailer key or value failed validation (`GitTrailerSchema`). |
| `CommitMessageInvalidError` | `GitCommitMessage` via `GitCommitMessageSchema.parse` (triggered by `TrailerCodecService.decode` or `encode`) | The `GitCommitMessageSchema` rejected the title/body/trailers combination. |
40 changes: 40 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Architecture: @git-stunts/trailer-codec

This project adheres to **Hexagonal Architecture** (Ports and Adapters) and **Domain-Driven Design (DDD)** principles to ensure robustness, testability, and separation of concerns.

## 🧱 Core Concepts

### Domain Layer (`src/domain/`)
The core business logic, isolated from external frameworks or I/O.

- **Entities**: Mutable objects with identity and lifecycle (e.g., `GitCommitMessage`).
- **Value Objects**: Immutable objects defined by their attributes (e.g., `GitTrailer`).
- **Services**: Domain logic that doesn't fit naturally into an entity (e.g., `TrailerCodecService` for parsing/serializing).
- **Errors**: Domain-specific error hierarchy (`TrailerCodecError` plus concrete subclasses such as `TrailerNoSeparatorError` and `TrailerValueInvalidError`).
- **Schemas**: Zod schemas for validation of domain objects.

### Ports Layer (`src/ports/`)
*Currently implicit.* The public API (exported via `index.js`) serves as the primary input port/facade for consumers. Since this library is primarily a data transformation tool (codec), it does not currently have complex output ports for I/O.

## 📂 Directory Structure

```
src/
├── domain/
│ ├── entities/ # GitCommitMessage
│ ├── errors/ # TrailerCodecError and validation subclasses
│ ├── schemas/ # Zod schemas
│ ├── services/ # TrailerCodecService
│ └── value-objects/ # GitTrailer
```

## 🧪 Testing Strategy

- **Unit Tests** (`test/unit/`): Comprehensive tests for entities, value objects, and services.
- **Test Doubles**: The architecture supports easy mocking of dependencies if the system grows.

## 🛠️ Design Decisions

1. **Zod for Validation**: We use Zod for runtime schema validation but wrap it in domain-specific `TrailerCodecError` subclasses to avoid leaking implementation details.
2. **Case Normalization**: Git trailer keys are case-insensitive. We normalize them to lowercase in the `GitTrailer` Value Object to ensure consistency.
3. **Facade Pattern**: `index.js` acts as a facade, providing a simple, backward-compatible API while exposing the rich domain model for advanced users.
Loading
Loading