Skip to content

Conversation

@flyingrobots
Copy link
Member

@flyingrobots flyingrobots commented Jan 12, 2026

Getting ready to release v2.1.0

Summary by CodeRabbit

  • New Features

    • Public release: encode/decode APIs, commit message & trailer models, configurable codec/parser, formatter hooks, and distinct error types (validation, missing-separator, size limits).
  • Documentation

    • Comprehensive docs added: API reference, architecture, security, integration, migration, testing, advanced guides, README, changelog, contributing, code of conduct, and NOTICE.
  • Tests

    • Extensive unit tests covering parsers, services, entities, adapters, and helpers.
  • Chores

    • CI workflow, package manifest, gitignore, linting config, and pre-commit hook added.

✏️ Tip: You can customize this high-level summary in your review settings.

- Implements strict Hexagonal Architecture and DDD principles
- Adds comprehensive unit tests for domain entities and services
- Formalizes error handling with custom domain errors
- Standardizes project structure matching @git-stunts/plumbing
- Includes architecture and contribution documentation
…treaming log parser, and docker-only safety guards across all blocks.
Applied all AUDITS.md security and quality fixes:

Security Hardening:
- Add 5MB message size limit to prevent DoS attacks
- Implement 100-char max on trailer keys (ReDoS prevention)
- Fix regex consistency between schema and service parsing
- Use TRAILER_KEY_REGEX constant across validation boundaries

Code Quality:
- Extract _findTrailerStartIndex() private method
- Add comprehensive JSDoc to decode algorithm
- Replace z.array(z.any()) with strict z.array(GitTrailerSchema)
- Remove stale test imports

Package Metadata:
- Update package.json with repository URLs, keywords, engines
- Change plumbing dependency from file: to npm version ^2.7.0
- Add standard files: LICENSE, NOTICE, SECURITY.md, CODE_OF_CONDUCT.md
- Create GitHub Actions CI workflow

Documentation:
- Enhance README with badges, validation rules table, security section
- Add detailed usage examples
- Document all constraints and error types
- Update CHANGELOG with security fixes

All 23 tests passing in Docker.
Ready for npm publication.
Comprehensive re-evaluation of trailer-codec after security fixes.

Health Score: 9/10 - Production Ready
- Zero critical issues remaining
- All AUDITS.md mitigations verified
- Documentation complete and accurate
- Dependencies healthy
- Architecture exemplary

Status: APPROVED FOR NPM PUBLICATION
Identified two key code snippets with high educational value:

1. Backward Walk Algorithm (_findTrailerStartIndex)
   - Demonstrates Git protocol expertise
   - Shows optimization via domain knowledge
   - Foundation for git-cms stunt

2. Key Normalization (GitTrailer.js:13)
   - Mirrors Git's internal behavior
   - Subtle but critical for query compatibility
   - Example of production-ready stunt engineering

Blog angle: 'Most parsers walk forward. This one walks backward.'
Rationale: Trailers are at the END, so backward is optimal.

Use cases:
- Git Stunts #1 (Git as CMS) intro material
- Architecture patterns: boring engineering + wild ideas
- Teaching systems thinking via Git object model
…ral cleanup

- API Ergonomics: Added `encode()`/`decode()` aliases to `TrailerCodec` for improved DX and synchronized `FacadeAdapter` error handling.
- Refactor: Reduced cyclomatic complexity in `GitCommitMessage`, `TrailerCodecService`, and `GitTrailer` constructors by extracting validation logic into private methods.
- Infrastructure: Removed unnecessary Docker artifacts (`Dockerfile`, `docker-compose`) to align with hexagonal architecture principles (pure domain logic/zero I/O dependencies).
- Documentation: Added comprehensive JSDoc to all `index.js` exports, created `docs/MIGRATION.md`, and expanded `SECURITY.md` with DoS protection strategies.
- Quality Assurance: Resolved all ESLint complexity warnings and migrated CI workflow to direct Node.js execution.

Refs: #v2.1.0
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 12, 2026

Warning

Rate limit exceeded

@flyingrobots has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 30 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b129437 and 68a4dd2.

📒 Files selected for processing (10)
  • API_REFERENCE.md
  • CONTRIBUTING.md
  • TESTING.md
  • docs/INTEGRATION.md
  • docs/MIGRATION.md
  • docs/SERVICE.md
  • scripts/pre-commit.sh
  • src/domain/services/helpers/TitleExtractor.js
  • src/domain/value-objects/GitTrailer.js
  • test/unit/domain/services/TrailerCodecService.test.js
📝 Walkthrough

Walkthrough

Adds a new npm package @git-stunts/trailer-codec: public facade and exports, domain entities and value objects, parsers and services, Zod schemas and validators, error hierarchy, helpers, CI/hooks, docs, and comprehensive unit tests for encode/decode and security guards.

Changes

Cohort / File(s) Summary
CI & Hooks
\.github/workflows/ci.yml, scripts/pre-commit.sh
Adds GitHub Actions CI (lint + test) and a pre-commit script that runs lint and format.
Repo Metadata & Policy
package.json, .gitignore, NOTICE, CODE_OF_CONDUCT.md, SECURITY.md
New package manifest & exports map, comprehensive .gitignore, Apache-2.0 notice, contributor code of conduct, and security policy with size/DoS limits.
Linting
eslint.config.js
Adds ESLint configuration with project-wide rules and test overrides.
Top-level API & Facade
index.js, src/adapters/FacadeAdapter.js, src/adapters/CodecBuilder.js, src/utils/zodValidator.js
Exposes public API surface (entities, service, codec, helpers), TrailerCodec class and convenience helpers, createConfiguredCodec builder, and zod-based options validator.
Core Service & Parser
src/domain/services/TrailerCodecService.js, src/domain/services/TrailerParser.js
Implements TrailerCodecService (decode/encode, DI points, formatters) and TrailerParser (backward walk to locate trailers, blank-line guard).
Entities & Value Objects
src/domain/entities/GitCommitMessage.js, src/domain/value-objects/GitTrailer.js
Adds GitCommitMessage and GitTrailer with normalization, Zod validation, toString/toJSON, and mapped error handling.
Schemas & Validation
src/domain/schemas/GitTrailerSchema.js, src/domain/schemas/GitCommitMessageSchema.js
Adds configurable trailer schema bundle (pattern compilation, caching, safety checks) and commit message Zod schema.
Error Types
src/domain/errors/*
src/domain/errors/TrailerCodecError.js, src/domain/errors/CommitMessageInvalidError.js, src/domain/errors/TrailerInvalidError.js, src/domain/errors/TrailerNoSeparatorError.js, src/domain/errors/TrailerTooLargeError.js, src/domain/errors/TrailerValueInvalidError.js
Introduces base TrailerCodecError and specific subclasses for validation, separator, size, and value/key issues with meta payloads.
Helpers
src/domain/services/helpers/*
.../MessageNormalizer.js, .../TitleExtractor.js, .../BodyComposer.js
Adds MessageNormalizer (CRLF→LF, byte-size guard), title extractor, and body composer (trim outer blank lines).
Adapters & Builders
src/adapters/FacadeAdapter.js, src/adapters/CodecBuilder.js
Facade helpers (decode/encode wrappers, TrailerCodec class, createDefaultTrailerCodec) and createConfiguredCodec builder wiring schema/parser/service.
Utilities
src/utils/zodValidator.js
Adds schema registry and validateType/registerSchema utilities for runtime option validation.
Docs & Guides
README.md, API_REFERENCE.md, ARCHITECTURE.md, CHANGELOG.md, CONTRIBUTING.md, TESTING.md, docs/*
Adds comprehensive documentation and multiple usage/architecture/migration guides.
Tests
test/unit/** (many files)
Adds extensive unit tests for builder, facade, entities, parser, service, helpers, and error conditions.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Facade as FacadeAdapter/TrailerCodec
    participant Service as TrailerCodecService
    participant Normalizer as MessageNormalizer
    participant Parser as TrailerParser
    participant Entity as GitCommitMessage

    Client->>Facade: decodeMessage(raw)
    activate Facade
    Facade->>Service: decode(raw)
    activate Service
    Service->>Normalizer: guardMessageSize(raw)
    Normalizer-->>Service: ok / throws TrailerTooLargeError
    Service->>Normalizer: normalizeLines(raw)
    Normalizer-->>Service: lines[]
    Service->>Parser: split(lines)
    Parser-->>Service: { bodyLines, trailerLines, trailerStart } or throws TrailerNoSeparatorError
    Service->>Entity: new GitCommitMessage({ title, body, trailers })
    Entity-->>Service: GitCommitMessage
    Service-->>Facade: GitCommitMessage
    Facade-->>Client: result
    deactivate Facade
Loading
sequenceDiagram
    participant Client
    participant Facade as FacadeAdapter/TrailerCodec
    participant Service as TrailerCodecService
    participant Entity as GitCommitMessage

    Client->>Facade: encodeMessage(payload)
    activate Facade
    alt payload is GitCommitMessage
        Facade->>Entity: toString()
        Entity-->>Facade: formatted string
    else payload is plain object
        Facade->>Service: encode(payload)
        Service->>Entity: new GitCommitMessage(payload)
        Entity-->>Service: GitCommitMessage
        Service->>Entity: toString()
        Entity-->>Service: formatted string
        Service-->>Facade: formatted string
    end
    Facade-->>Client: encoded string
    deactivate Facade
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • V2.1.0 rc #1 — Likely duplicate or preceding PR that adds the same core services, parsers, value objects, errors, adapters, and tests.

"🐰
I hopped through schemas, patterns in tow,
Lowercased the keys and trimmed values so,
Parsers walked back and Zod gave the sign,
Tests guard the edges where trailers align,
A tiny codec, neat and ready to go."

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'V2.1.0 rc' is vague and lacks context about the actual changes. It only indicates a version tag without describing what was implemented or the primary purpose of the release. Consider using a more descriptive title that conveys the main changes, such as 'Add comprehensive documentation and CI/CD pipeline for v2.1.0 release' or 'Initialize project scaffolding with docs, workflows, and core implementation'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🤖 Fix all issues with AI agents
In @API_REFERENCE.md:
- Line 8: The API reference currently labels the functions as "Deprecated
convenience wrapper[s]" but omits the migration guidance present in the source
@deprecated tags; update the two entries that read "Deprecated convenience
wrapper around `new TrailerCodec().decode(message)`" to instead say "Deprecated
convenience wrapper around `new TrailerCodec().decode(message)`. Use
`TrailerCodec` instances instead." and, if there is a planned removal version,
append a short deprecation timeline (e.g., "to be removed in v3.0") or
explicitly state they remain as compatibility shims; cross-check the @deprecated
notes in index.js (the tags that read "Use TrailerCodec class instead") to
ensure wording matches.

In @CONTRIBUTING.md:
- Around line 25-33: The fenced code block showing the directory tree (starting
with "src/" and lines under "domain/") is missing a language specifier; update
the opening fence from ``` to ```text (or another appropriate language like
```bash) so the block is labeled (e.g., ```text) to improve rendering and
accessibility.

In @docs/ADVANCED.md:
- Around line 10-13: The keyPattern manipulation is broken: stop using
string.replace('-', '.') and appending '\\.+'; either supply an explicit correct
regex (e.g. keyPattern: '[A-Za-z0-9_.]+') or, if you must derive from
TRAILER_KEY_RAW_PATTERN_STRING, surgically replace hyphens inside the character
class with a global replacement and do NOT append a literal-dot requirement —
for example, derive newPattern by using a regex that targets the [...] class and
does cls.replace(/-/g, '.') on that capture, then pass that newPattern to
createGitTrailerSchemaBundle (refer to createGitTrailerSchemaBundle and
TRAILER_KEY_RAW_PATTERN_STRING).

In @docs/INTEGRATION.md:
- Around line 33-35: The current commit parsing in the process.stdin.on('end'
...) handler is fragile because it splits on '\n\n' which breaks when commit
bodies contain blank lines; update the logic to use a robust delimiter: emit
commits from git using a null-prefixed format (e.g., --format=%x00%B or similar)
and then split buffer on '\0' instead of '\n\n' (keep the existing
filter(Boolean) to drop empties), or at minimum add a note in INTEGRATION.md
documenting the limitation; locate the buffer/commits handling inside the
process.stdin.on('end' callback and change the git invocation to use the null
delimiter and the split call to buffer.split('\0').
- Around line 55-61: The example mixes ESM and CommonJS: it uses ESM imports for
decodeMessage and formatBodySegment but calls require('readline') for reader;
replace the CommonJS require with an ESM import for the Node readline module and
then call the existing createInterface usage on that imported readline object
(i.e., import readline and then use readline.createInterface({ input:
process.stdin }) to initialize reader) so the example consistently uses ESM
syntax.

In @docs/MIGRATION.md:
- Around line 256-259: The grep pattern is using incorrectly escaped brackets
and won't match uppercase trailer keys; replace it with an extended or PCRE
pattern. Update the command to use an extended regex such as grep -rE
"trailers\['[A-Z]" src/ or a stricter PCRE like grep -rP
"trailers\['[A-Z][a-zA-Z-]*'\]" src/ so the character class [A-Z] is interpreted
correctly and full trailer keys (e.g., trailers['Foo-Bar']) are matched.
- Around line 199-210: The docs import of MessageNormalizer is invalid because
MessageNormalizer is not part of the package public API; either export it from
the package entry (add an export for MessageNormalizer from index.js that
re-exports src/domain/services/helpers/MessageNormalizer.js) so the example can
keep using new MessageNormalizer(...), or update the MIGRATION.md example to
avoid direct instantiation and instead show using the public API (e.g., pass a
configuration object to TrailerCodecService or use the existing public
factory/constructor options) and replace the import of MessageNormalizer with
only public exports such as TrailerCodecService.

In @docs/SERVICE.md:
- Around line 25-27: The table cell for step 3 contains literal newline
characters instead of escaped sequences; update the
MessageNormalizer.normalizeLines() description to show the actual escape
sequences by replacing the raw newlines with code spans like `\n` and `\r\n`
(e.g., "MessageNormalizer.normalizeLines() converts `\n` to `\r\n` and splits
into lines.") so the markdown table renders correctly and the backslashes are
preserved.
- Around line 12-14: The table row for messageNormalizer contains literal
newline characters that break the markdown table; edit the `messageNormalizer`
row (referencing `MessageNormalizer` and `TrailerTooLargeError`) to escape or
render line endings as inline code (e.g., use `\n` and `\r\n` inside backticks)
and ensure the cell remains a single line with the required pipe separators;
also keep the note about guarding against messages > 5 MB (throws
`TrailerTooLargeError`) and how to override max size or normalization logic.

In @scripts/pre-commit.sh:
- Around line 1-2: The script uses a POSIX sh shebang but also sets the
non-POSIX option `pipefail` in the `set -euo pipefail` line, which will break on
shells like dash; fix by making the script consistently Bash or POSIX: either
change the shebang from `/usr/bin/env sh` to `/usr/bin/env bash` so `pipefail`
is valid, or if you need strict POSIX compatibility remove `pipefail` from the
`set -euo pipefail` invocation (leaving `set -eu`), then run the script under a
POSIX shell.

In @src/domain/services/helpers/TitleExtractor.js:
- Around line 1-7: Remove the duplicate JSDoc block at the top of
TitleExtractor.js and keep only the complete JSDoc that includes the @param
annotation for the title extraction function (the block that documents "Extracts
the title line and the body start index" and contains "@param {string[]}
lines"). Ensure there is a single JSDoc immediately above the corresponding
function/export so the function comment is not duplicated.

In @src/domain/value-objects/GitTrailer.js:
- Around line 18-30: The constructor of GitTrailer validates raw inputs before
normalization, which allows whitespace-only values to pass the
GitTrailerSchema.min(1) check and then become empty after trim; to fix,
normalize the inputs before schema validation (e.g., call trim() on value and
lowercase on key prior to calling actualSchema.parse), or alternatively perform
an explicit post-parse check that ensures trimmed value.length > 0 and throw via
_handleValidationError if empty; update the constructor logic around
_validateSchema, the parse call, and the error handling in the constructor to
use the normalized values for validation (refer to constructor, _validateSchema,
GitTrailerSchema, _handleValidationError, this.key, this.value).

In @test/unit/domain/services/TrailerCodecService.test.js:
- Around line 66-73: The test "rejects trailers without a blank line separator"
currently uses a try/catch around service.decode(raw) so it will pass silently
if no exception is thrown; change the assertion to explicitly expect an
exception by replacing the try/catch with expect(() =>
service.decode(raw)).toThrow(TrailerNoSeparatorError) (or alternatively call
fail() right after service.decode(raw) inside the try block) so the test fails
when service.decode does not throw the TrailerNoSeparatorError.
- Around line 75-81: The test currently swallows failures by using try/catch;
replace the try/catch pattern in the 'rejects trailer values containing line
breaks' test with a proper Jest assertion that expects the synchronous call to
service._buildTrailers([...]) to throw TrailerValueInvalidError (e.g., use
expect(() => service._buildTrailers(['Key:
Value\nInjected'])).toThrow(TrailerValueInvalidError)); target the test named
'rejects trailer values containing line breaks' and call the _buildTrailers
method directly so the assertion fails if no exception is thrown.

In @TESTING.md:
- Line 29: The doc incorrectly references `.eslintrc.js`; update the TESTING.md
sentence that mentions `.eslintrc.js` to reference the new flat config filename
`eslint.config.js` (keep the `.prettierrc` mention and the lint/format command
instructions unchanged) so the docs match the PR's introduced config file.
🧹 Nitpick comments (28)
.github/workflows/ci.yml (1)

19-19: Consider using npm ci for reproducible CI builds.

npm ci is preferred over npm install in CI environments as it ensures clean, reproducible installs from package-lock.json and fails if the lock file is out of sync.

Proposed fix
       - run: npm ci
-      - run: npm install

Apply to both jobs (lines 19 and 31).

Also applies to: 31-31

.gitignore (5)

3-3: Optional: Remove duplicate jspm_packages/ entry.

The pattern jspm_packages/ appears at both line 3 and line 61. Consider removing one of the duplicates for cleaner maintainability.

♻️ Suggested cleanup
 # Dependency directories
 jspm_packages/
-
-# TypeScript v1 declaration files
-typings/

Also applies to: 61-61


17-17: Optional: Remove duplicate .cache entry.

The pattern .cache appears at line 17 (under "Build outputs") and line 95 (under "parcel-bundler cache"). Consider consolidating to avoid duplication.

Also applies to: 95-95


12-12: Optional: Remove duplicate dist/ entries.

The pattern dist/ appears at lines 12, 104, and 111. Consider removing the duplicates for cleaner maintainability.

Also applies to: 104-104, 111-111


14-14: Optional: Remove duplicate .next entries.

The pattern .next (line 14) and .next/ (line 99) both ignore Next.js build outputs. Consider keeping only one form for consistency.

Also applies to: 99-99


18-18: Optional: Remove duplicate out entries.

The pattern out/ (line 18) and out (line 100) are duplicates. Consider removing one for cleaner maintainability.

Also applies to: 100-100

SECURITY.md (1)

96-96: Optional: Wrap email address in angle brackets.

Per markdown best practices, email addresses can be wrapped in angle brackets for improved parsing and rendering.

♻️ Suggested formatting
-If you discover a security vulnerability, please send an e-mail to james@flyingrobots.dev.
+If you discover a security vulnerability, please send an e-mail to <james@flyingrobots.dev>.
CHANGELOG.md (1)

43-49: Consider moving [unreleased] section to the top.

Per Keep a Changelog convention, the [unreleased] section should appear at the top of the changelog, before all versioned releases. This allows users to easily see what's coming next and makes it easier to promote unreleased changes when cutting a new version.

Suggested reordering
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

+## [unreleased]
+
+### Planned
+- Integration test suite for real Git commit scenarios
+- Performance benchmarks for large messages
+- Additional edge case tests (unicode, empty input, concurrent usage)
+
 ## [2.1.0] - 2026-01-11

Then remove the [unreleased] section from lines 43-49.

eslint.config.js (1)

9-15: Consider using the globals package for Node.js globals.

Manual global definitions may miss some commonly used Node.js globals (e.g., setInterval, clearInterval, URL, URLSearchParams). The globals package provides comprehensive predefined sets.

Example using globals package
 import js from '@eslint/js';
+import globals from 'globals';

 export default [
   js.configs.recommended,
   {
     languageOptions: {
       ecmaVersion: 2022,
       sourceType: 'module',
-      globals: {
-        process: 'readonly',
-        Buffer: 'readonly',
-        console: 'readonly',
-        setTimeout: 'readonly',
-        clearTimeout: 'readonly',
-      },
+      globals: {
+        ...globals.node,
+      },
     },
test/unit/domain/services/TrailerParser.test.js (1)

8-13: Consider consistency in array handling.

Line 10 uses lines.slice() to create a copy, but line 22 passes lines directly. While split() doesn't mutate the input array, using .slice() inconsistently may confuse future readers.

Suggested fix for consistency

Either remove .slice() on line 10:

-    const { bodyLines, trailerLines } = parser.split(lines.slice());
+    const { bodyLines, trailerLines } = parser.split(lines);

Or add .slice() to other tests for consistency.

ARCHITECTURE.md (1)

21-29: Add language specifier to fenced code block and include adapters directory.

The directory structure code block lacks a language specifier, and the src/adapters/ directory (containing CodecBuilder.js, FacadeAdapter.js) is missing from the structure.

Suggested fix
-```
+```text
 src/
+├── adapters/         # CodecBuilder, FacadeAdapter
 ├── domain/
 │   ├── entities/       # GitCommitMessage
 │   ├── errors/         # TrailerCodecError and validation subclasses
 │   ├── schemas/        # Zod schemas
 │   ├── services/       # TrailerCodecService
 │   └── value-objects/  # GitTrailer
+└── index.js            # Public API facade
</details>

</blockquote></details>
<details>
<summary>src/domain/services/helpers/BodyComposer.js (1)</summary><blockquote>

`1-8`: **Duplicate JSDoc comment block.**

Lines 1-3 and 4-8 describe the same functionality. Consider removing the redundant block.

<details>
<summary>Suggested fix</summary>

```diff
-/**
- * Trims leading/trailing blank lines from the decoded body block while preserving interior spacing.
- */
 /**
  * Trim leading/trailing blank lines while preserving interior spacing.
  * @param {string[]} lines
  * @returns {string}
  */
src/domain/schemas/GitCommitMessageSchema.js (1)

12-16: Consider adding a default for trailers.

The body field has .default(''), but trailers has no default. If the schema is used with .parse() on input lacking a trailers field, it will fail validation. Consider adding .default([]) for consistency if optional trailers are intended.

Suggested change
 export const GitCommitMessageSchema = z.object({
   title: z.string().min(1),
   body: z.string().default(''),
-  trailers: z.array(GitTrailerSchema),
+  trailers: z.array(GitTrailerSchema).default([]),
 });
src/domain/errors/TrailerCodecError.js (1)

13-13: Guard Error.captureStackTrace for non-V8 environments.

Error.captureStackTrace is V8-specific and will throw in Firefox, Safari, or other non-V8 runtimes. Consider adding a guard:

Suggested fix
-    Error.captureStackTrace(this, this.constructor);
+    if (Error.captureStackTrace) {
+      Error.captureStackTrace(this, this.constructor);
+    }
test/unit/domain/value-objects/GitTrailer.test.js (1)

35-61: Simplify complex test patterns.

These tests use a convoluted try/catch/re-throw pattern. A cleaner approach captures the error directly:

Suggested simplification
-  it('throws TrailerValueInvalidError when value includes newline', () => {
-    const attempt = () => {
-      try {
-        new GitTrailer('Key', 'Line\nBreak');
-      } catch (error) {
-        expect(error).toBeInstanceOf(TrailerValueInvalidError);
-        throw error;
-      }
-    };
-
-    expect(attempt).toThrow(TrailerValueInvalidError);
-  });
-
-  it('includes documentation link in TrailerValueInvalidError metadata', () => {
-    const attempt = () => {
-      try {
-        new GitTrailer('Key', '');
-      } catch (error) {
-        expect(error).toBeInstanceOf(TrailerValueInvalidError);
-        expect(error.meta.docs).toBe('docs/ADVANCED.md#custom-validation-rules');
-        expect(/invalid trailer/i.test(error.message)).toBe(true);
-        throw error;
-      }
-    };
-
-    expect(attempt).toThrow(TrailerValueInvalidError);
-  });
+  it('throws TrailerValueInvalidError when value includes newline', () => {
+    expect(() => new GitTrailer('Key', 'Line\nBreak')).toThrow(TrailerValueInvalidError);
+  });
+
+  it('includes documentation link in TrailerValueInvalidError metadata', () => {
+    let caughtError;
+    try {
+      new GitTrailer('Key', '');
+    } catch (error) {
+      caughtError = error;
+    }
+    expect(caughtError).toBeInstanceOf(TrailerValueInvalidError);
+    expect(caughtError.meta.docs).toBe('docs/ADVANCED.md#custom-validation-rules');
+    expect(caughtError.message).toMatch(/invalid trailer/i);
+  });
src/adapters/CodecBuilder.js (1)

32-34: Inconsistent validation between options fields.

keyPattern and keyMaxLength go through Zod validation (line 32), but parserOptions, formatters, and bodyFormatOptions are extracted from raw options (line 34) and only checked to be objects. This could allow invalid nested structures to pass through.

Consider validating all fields through the same Zod schema or document this as intentional pass-through behavior.

docs/PARSER.md (1)

9-11: Consider avoiding private method names in public documentation.

Lines 9-11 reference private method names (_findTrailerStartIndex, _validateTrailerSeparation, _trimBody). Documenting internal implementation details in user-facing docs creates tight coupling and may confuse users who cannot call these methods directly. Consider either:

  • Rephrasing to describe behavior without naming private methods ("walks backward until a non-matching line", "validates blank line separation")
  • Moving this implementation detail to internal developer documentation
📝 Suggested rephrasing
-3. **Walk backward** with `_findTrailerStartIndex` until either a non-matching line or an empty line appears; contiguous `Key: Value` patterns form the trailer block.
-4. **Validate the separator**: `_validateTrailerSeparation` ensures there is a blank line before the trailers. Messages that omit the blank line now throw `TrailerNoSeparatorError`.
-5. **Trim the body** without double allocations: `_trimBody` trims leading/trailing blank lines via index arithmetic and one `join`.
+3. **Walk backward** until either a non-matching line or an empty line appears; contiguous `Key: Value` patterns form the trailer block.
+4. **Validate the separator**: The parser ensures there is a blank line before the trailers. Messages that omit the blank line throw `TrailerNoSeparatorError`.
+5. **Trim the body** without double allocations: leading/trailing blank lines are trimmed via index arithmetic and one `join`.
API_REFERENCE.md (2)

39-44: Consider using JSDoc or plain JavaScript syntax for consistency.

Lines 39-44 use TypeScript-style type annotations (payload: { title: string; body?: string; ... }), but this appears to be a JavaScript library. This syntax will not work if users copy-paste it into JavaScript code. Consider either:

  1. Using JSDoc format:
/**
 * @param {Object} payload
 * @param {string} payload.title
 * @param {string} [payload.body]
 * @param {GitTrailerInput[]} [payload.trailers]
 * @param {Object} [options]
 */
new GitCommitMessage(payload, options);
  1. Using plain JavaScript with inline comments:
new GitCommitMessage(
  { title, body, trailers }, // payload object
  { trailerSchema, formatters } // optional options
);

88-94: Clarify the distinction between TrailerInvalidError and TrailerValueInvalidError.

Lines 92 and 93 document both TrailerValueInvalidError and TrailerInvalidError, but their meanings overlap:

  • TrailerValueInvalidError: "trailer value violated the GitTrailerSchema"
  • TrailerInvalidError: "Trailer key or value failed validation"

It's unclear when each error is thrown. Consider clarifying:

  • Is TrailerInvalidError the parent class for all trailer validation failures?
  • Is TrailerValueInvalidError thrown specifically for value violations, while TrailerInvalidError covers key violations?
  • Or do they represent different validation stages?

Adding concrete examples for each would help users understand when to catch which error.

docs/ADVANCED.md (1)

62-62: Avoid referencing private methods in public documentation.

Line 62 states "custom parsing strategies replace _findTrailerStartIndex and _validateTrailerSeparation without subclassing the service." This references private implementation details that users shouldn't depend on.

Consider rephrasing to focus on the behavior, not implementation:

-Pass your parser into `TrailerCodecService` so custom parsing strategies replace `_findTrailerStartIndex` and `_validateTrailerSeparation` without subclassing the service.
+Pass your parser into `TrailerCodecService` to customize trailer detection and validation logic without subclassing the service.
src/utils/zodValidator.js (1)

27-37: Consider exposing a reset mechanism for testing.

The module-level schemaRegistry singleton works well for production, but tests that call registerSchema multiple times with the same typeId will fail due to the duplicate check. Consider adding an internal _resetRegistry() function (or exporting schemaRegistry for test access) to support test isolation.

🧪 Proposed test helper
+/**
+ * Clears the registry. Intended for testing only.
+ * @internal
+ */
+export function _resetRegistry() {
+  schemaRegistry.clear();
+}
src/domain/entities/GitCommitMessage.js (1)

91-102: Minor inconsistency: Extra whitespace in template literal.

Line 98 has trailing whitespace before the closing backtick in the template literal (${...} }\n). This appears intentional for formatting but is inconsistent with the trimEnd() on line 101.

🔧 Suggested cleanup
     if (this.trailers.length > 0) {
-      message += `${this.trailers.map((t) => t.toString()).join('\n')  }\n`;
+      message += `${this.trailers.map((t) => t.toString()).join('\n')}\n`;
     }
package.json (1)

28-34: Consider pinning @babel/parser more tightly or documenting its purpose.

The @babel/parser dev dependency is unusual for a runtime package. If it's used for testing or tooling, consider adding a comment in the README or moving it to optional dependencies if not essential for npm test.

src/domain/services/TrailerCodecService.js (1)

142-150: Consider using flatMap for cleaner trailer extraction.

The current implementation works correctly, but flatMap would be more idiomatic for this filter-and-transform pattern.

♻️ Optional refactor
 _buildTrailers(lines) {
-  return lines.reduce((acc, line) => {
-    const match = this.parser.lineRegex.exec(line);
-    if (match) {
-      acc.push(this.trailerFactory(match[1], match[2], this.schemaBundle.schema));
-    }
-    return acc;
-  }, []);
+  return lines.flatMap((line) => {
+    const match = this.parser.lineRegex.exec(line);
+    return match
+      ? [this.trailerFactory(match[1], match[2], this.schemaBundle.schema)]
+      : [];
+  });
 }
src/adapters/FacadeAdapter.js (4)

12-38: Consider case-insensitive duplicate key detection.

Looking at GitTrailer (from relevant snippets), keys are normalized to lowercase (this.key = normalizedKey.toLowerCase()). The current duplicate check at line 28 uses the original trailer.key, which means Signed-Off-By and signed-off-by would not be detected as duplicates here, but would both normalize to the same key in the domain model.

♻️ Suggested fix
 function normalizeTrailers(entity) {
   if (!entity || !Array.isArray(entity.trailers)) {
     throw new TypeError('Invalid entity: trailers array is required');
   }

   return entity.trailers.reduce((acc, trailer) => {
     if (!trailer || typeof trailer.key !== 'string' || trailer.key.trim() === '') {
       throw new TypeError('Invalid trailer: non-empty string key is required');
     }
     if (typeof trailer.value !== 'string') {
       throw new TrailerInvalidError('Trailer value must be a string', {
         key: trailer.key,
         invalidValue: trailer.value,
         valueType: typeof trailer.value,
       });
     }
-    if (acc[trailer.key] !== undefined) {
-      throw new TrailerInvalidError(`Duplicate trailer key: "${trailer.key}"`, {
+    const normalizedKey = trailer.key.toLowerCase();
+    if (acc[normalizedKey] !== undefined) {
+      throw new TrailerInvalidError(`Duplicate trailer key: "${trailer.key}"`, {
         key: trailer.key,
-        existingValue: acc[trailer.key],
+        existingValue: acc[normalizedKey],
         duplicateValue: trailer.value,
       });
     }
-    acc[trailer.key] = trailer.value;
+    acc[normalizedKey] = trailer.value;
     return acc;
   }, {});
 }

55-64: Remove duplicate JSDoc block.

There are two consecutive JSDoc comments before createMessageHelpers. The first block (lines 55-57) is redundant.

♻️ Suggested fix
-/**
- * Advanced helper factory for tests or tools that need direct access to helpers.
- */
 /**
  * Advanced helper factory for tests or when you need to control the service instance.
  * @param {Object} [options]
  * @param {TrailerCodecService} [options.service] - Optional custom service (defaults to new one).
  * @param {Object} [options.bodyFormatOptions] - Options forwarded to `formatBodySegment`.
  * @returns {{ decodeMessage: (input: string) => { title: string, body: string, trailers: Record<string,string> }, encodeMessage: (payload: { title: string, body?: string, trailers?: Record<string,string> }) => string }}
  */

84-90: Remove duplicate JSDoc block.

There are two consecutive JSDoc comments before the TrailerCodec class. The first block (lines 84-86) is redundant.

♻️ Suggested fix
-/**
- * TrailerCodec is the main public API. Provide a `TrailerCodecService` to reuse configuration
- * and helper instances.
- */
 /**
  * TrailerCodec is the main public API for encode/decode through an injectable service.
  */

147-163: Fix duplicate and incomplete JSDoc blocks for deprecated functions.

The deprecated functions have duplicate JSDoc blocks and encodeMessage is missing its description.

♻️ Suggested fix
 /**
- * @deprecated Use `TrailerCodec` instances for most call sites.
- */
-/**
  * @deprecated Use `TrailerCodec.decodeMessage` directly.
  * Convenience wrapper that builds a default codec and decodes the message.
  */
 export function decodeMessage(message, bodyFormatOptions) {
   return createDefaultTrailerCodec({ bodyFormatOptions }).decodeMessage(message);
 }

 /**
- * @deprecated Use `TrailerCodec` instances for most call sites.
+ * @deprecated Use `TrailerCodec.encodeMessage` directly.
+ * Convenience wrapper that builds a default codec and encodes the payload.
  */
 export function encodeMessage(payload, bodyFormatOptions) {
   return createDefaultTrailerCodec({ bodyFormatOptions }).encodeMessage(payload);
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c4a40d and e8bcc0f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (45)
  • .github/workflows/ci.yml
  • .gitignore
  • API_REFERENCE.md
  • ARCHITECTURE.md
  • CHANGELOG.md
  • CODE_OF_CONDUCT.md
  • CONTRIBUTING.md
  • NOTICE
  • README.md
  • SECURITY.md
  • TESTING.md
  • docs/ADVANCED.md
  • docs/INTEGRATION.md
  • docs/MIGRATION.md
  • docs/PARSER.md
  • docs/SERVICE.md
  • eslint.config.js
  • index.js
  • package.json
  • scripts/pre-commit.sh
  • src/adapters/CodecBuilder.js
  • src/adapters/FacadeAdapter.js
  • src/domain/entities/GitCommitMessage.js
  • src/domain/errors/CommitMessageInvalidError.js
  • src/domain/errors/TrailerCodecError.js
  • src/domain/errors/TrailerInvalidError.js
  • src/domain/errors/TrailerNoSeparatorError.js
  • src/domain/errors/TrailerTooLargeError.js
  • src/domain/errors/TrailerValueInvalidError.js
  • src/domain/schemas/GitCommitMessageSchema.js
  • src/domain/schemas/GitTrailerSchema.js
  • src/domain/services/TrailerCodecService.js
  • src/domain/services/TrailerParser.js
  • src/domain/services/helpers/BodyComposer.js
  • src/domain/services/helpers/MessageNormalizer.js
  • src/domain/services/helpers/TitleExtractor.js
  • src/domain/value-objects/GitTrailer.js
  • src/utils/zodValidator.js
  • test/unit/adapters/CodecBuilder.test.js
  • test/unit/adapters/FacadeAdapter.test.js
  • test/unit/domain/entities/GitCommitMessage.test.js
  • test/unit/domain/services/TrailerCodecService.test.js
  • test/unit/domain/services/TrailerParser.test.js
  • test/unit/domain/value-objects/GitTrailer.test.js
  • test/unit/index.test.js
🧰 Additional context used
🧬 Code graph analysis (19)
test/unit/adapters/FacadeAdapter.test.js (1)
src/domain/services/TrailerCodecService.js (1)
  • TrailerCodecService (15-159)
src/domain/errors/TrailerTooLargeError.js (1)
src/domain/errors/TrailerCodecError.js (1)
  • TrailerCodecError (4-15)
src/domain/errors/TrailerInvalidError.js (1)
src/domain/errors/TrailerCodecError.js (1)
  • TrailerCodecError (4-15)
src/domain/errors/CommitMessageInvalidError.js (1)
src/domain/errors/TrailerCodecError.js (1)
  • TrailerCodecError (4-15)
src/domain/services/TrailerParser.js (2)
src/domain/schemas/GitTrailerSchema.js (2)
  • TRAILER_KEY_RAW_PATTERN_STRING (72-72)
  • TRAILER_KEY_RAW_PATTERN_STRING (72-72)
src/domain/errors/TrailerNoSeparatorError.js (1)
  • TrailerNoSeparatorError (4-12)
test/unit/adapters/CodecBuilder.test.js (2)
src/adapters/CodecBuilder.js (1)
  • createConfiguredCodec (31-53)
src/adapters/FacadeAdapter.js (2)
  • decodeMessage (154-156)
  • encodeMessage (161-163)
test/unit/domain/entities/GitCommitMessage.test.js (3)
src/domain/entities/GitCommitMessage.js (1)
  • GitCommitMessage (21-114)
src/domain/value-objects/GitTrailer.js (1)
  • GitTrailer (11-84)
src/domain/errors/CommitMessageInvalidError.js (1)
  • CommitMessageInvalidError (4-12)
src/domain/services/helpers/MessageNormalizer.js (1)
src/domain/errors/TrailerTooLargeError.js (1)
  • TrailerTooLargeError (4-12)
src/domain/schemas/GitCommitMessageSchema.js (1)
src/domain/schemas/GitTrailerSchema.js (2)
  • GitTrailerSchema (71-71)
  • GitTrailerSchema (71-71)
src/domain/entities/GitCommitMessage.js (4)
src/domain/errors/CommitMessageInvalidError.js (1)
  • CommitMessageInvalidError (4-12)
src/domain/schemas/GitTrailerSchema.js (2)
  • GitTrailerSchema (71-71)
  • GitTrailerSchema (71-71)
src/domain/schemas/GitCommitMessageSchema.js (2)
  • GitCommitMessageSchema (12-16)
  • GitCommitMessageSchema (12-16)
src/domain/value-objects/GitTrailer.js (1)
  • GitTrailer (11-84)
src/domain/errors/TrailerValueInvalidError.js (1)
src/domain/errors/TrailerCodecError.js (1)
  • TrailerCodecError (4-15)
src/domain/value-objects/GitTrailer.js (4)
src/utils/zodValidator.js (1)
  • schema (28-28)
src/domain/schemas/GitTrailerSchema.js (2)
  • GitTrailerSchema (71-71)
  • GitTrailerSchema (71-71)
src/domain/errors/TrailerValueInvalidError.js (1)
  • TrailerValueInvalidError (4-12)
src/domain/errors/TrailerInvalidError.js (1)
  • TrailerInvalidError (4-12)
test/unit/domain/services/TrailerParser.test.js (3)
src/adapters/CodecBuilder.js (1)
  • parser (40-40)
src/domain/services/TrailerParser.js (1)
  • TrailerParser (8-88)
src/domain/errors/TrailerNoSeparatorError.js (1)
  • TrailerNoSeparatorError (4-12)
test/unit/domain/value-objects/GitTrailer.test.js (3)
src/domain/value-objects/GitTrailer.js (1)
  • GitTrailer (11-84)
src/domain/errors/TrailerInvalidError.js (1)
  • TrailerInvalidError (4-12)
src/domain/errors/TrailerValueInvalidError.js (1)
  • TrailerValueInvalidError (4-12)
src/domain/schemas/GitTrailerSchema.js (1)
src/domain/errors/TrailerInvalidError.js (1)
  • TrailerInvalidError (4-12)
test/unit/index.test.js (2)
src/adapters/CodecBuilder.js (2)
  • service (41-45)
  • helpers (46-46)
src/adapters/FacadeAdapter.js (2)
  • createMessageHelpers (65-82)
  • formatBodySegment (47-53)
src/adapters/FacadeAdapter.js (2)
src/domain/errors/TrailerInvalidError.js (1)
  • TrailerInvalidError (4-12)
src/domain/services/TrailerCodecService.js (1)
  • TrailerCodecService (15-159)
src/domain/services/TrailerCodecService.js (8)
src/domain/value-objects/GitTrailer.js (1)
  • GitTrailer (11-84)
src/domain/schemas/GitTrailerSchema.js (2)
  • getDefaultTrailerSchemaBundle (74-74)
  • getDefaultTrailerSchemaBundle (74-74)
src/domain/services/helpers/MessageNormalizer.js (1)
  • MessageNormalizer (13-48)
src/domain/services/helpers/TitleExtractor.js (2)
  • extractTitle (8-15)
  • nextIndex (10-10)
src/domain/services/helpers/BodyComposer.js (1)
  • composeBody (9-26)
src/adapters/CodecBuilder.js (1)
  • schemaBundle (39-39)
src/domain/services/TrailerParser.js (1)
  • TrailerParser (8-88)
src/domain/entities/GitCommitMessage.js (1)
  • GitCommitMessage (21-114)
test/unit/domain/services/TrailerCodecService.test.js (6)
src/adapters/CodecBuilder.js (1)
  • service (41-45)
src/domain/entities/GitCommitMessage.js (1)
  • GitCommitMessage (21-114)
src/domain/errors/TrailerNoSeparatorError.js (1)
  • TrailerNoSeparatorError (4-12)
src/domain/errors/TrailerValueInvalidError.js (1)
  • TrailerValueInvalidError (4-12)
src/domain/errors/TrailerTooLargeError.js (1)
  • TrailerTooLargeError (4-12)
src/domain/services/helpers/TitleExtractor.js (2)
  • title (9-9)
  • nextIndex (10-10)
🪛 LanguageTool
TESTING.md

[style] ~16-~16: Consider using a different verb to strengthen your wording.
Context: ...ttier** with the configured settings to keep formatting consistent. Commit the forma...

(KEEP_PRESERVE)

README.md

[style] ~148-~148: Consider a different adjective to strengthen your wording.
Context: ...poseBody); see docs/SERVICE.md` for a deeper explanation of how to customize each st...

(DEEP_PROFOUND)

CODE_OF_CONDUCT.md

[style] ~20-~20: Try using a synonym here to strengthen your wording.
Context: ...vances * Trolling, insulting/derogatory comments, and personal or political attacks * Pu...

(COMMENT_REMARK)


[style] ~37-~37: ‘with regard to’ might be wordy. Consider a shorter alternative.
Context: ...s obligated to maintain confidentiality with regard to the reporter of an incident. Further de...

(EN_WORDINESS_PREMIUM_WITH_REGARD_TO)

🪛 markdownlint-cli2 (0.18.1)
CONTRIBUTING.md

25-25: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

ARCHITECTURE.md

21-21: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/SERVICE.md

12-12: Table pipe style
Expected: leading_and_trailing; Actual: leading_only; Missing trailing pipe

(MD055, table-pipe-style)


13-13: Table pipe style
Expected: leading_and_trailing; Actual: no_leading_or_trailing; Missing leading pipe

(MD055, table-pipe-style)


13-13: Table pipe style
Expected: leading_and_trailing; Actual: no_leading_or_trailing; Missing trailing pipe

(MD055, table-pipe-style)


13-13: Table column count
Expected: 3; Actual: 1; Too few cells, row will be missing data

(MD056, table-column-count)


14-14: Spaces inside code span elements

(MD038, no-space-in-code)


14-14: Table pipe style
Expected: leading_and_trailing; Actual: trailing_only; Missing leading pipe

(MD055, table-pipe-style)


14-14: Table column count
Expected: 3; Actual: 1; Too few cells, row will be missing data

(MD056, table-column-count)

SECURITY.md

96-96: Bare URL used

(MD034, no-bare-urls)

CODE_OF_CONDUCT.md

37-37: Bare URL used

(MD034, no-bare-urls)


43-43: Bare URL used

(MD034, no-bare-urls)


47-47: Bare URL used

(MD034, no-bare-urls)

🪛 Shellcheck (0.11.0)
scripts/pre-commit.sh

[warning] 2-2: In POSIX sh, set option pipefail is undefined.

(SC3040)

flyingrobots and others added 7 commits January 12, 2026 00:50
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: James Ross <james@flyingrobots.dev>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: James Ross <james@flyingrobots.dev>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: James Ross <james@flyingrobots.dev>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: James Ross <james@flyingrobots.dev>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: James Ross <james@flyingrobots.dev>
@flyingrobots flyingrobots merged commit 4aaabb4 into main Jan 12, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants