Thanks for your interest in contributing. This guide covers development setup, architecture, and guidelines.
# Clone the repository
git clone https://github.com/Ian-stetsenko/lore-protocol.git
cd lore-protocol
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test
# Type-check without emitting
npm run typecheck| Script | Description |
|---|---|
npm run build |
Build with tsup |
npm run dev |
Build in watch mode |
npm test |
Run tests with vitest |
npm run test:watch |
Run tests in watch mode |
npm run lint |
Lint with eslint |
npm run format |
Format with prettier |
npm run typecheck |
Type-check with tsc |
The codebase follows a layered architecture with strict dependency direction: lower layers never import from higher layers.
main.ts (composition root)
|
+-- commands/ CLI command handlers (one file per command)
| |
| +-- helpers/ Shared command logic (e.g., path-query pattern)
|
+-- formatters/ Output formatting (text, JSON)
|
+-- services/ Core business logic
|
+-- interfaces/ Abstractions (ports)
|
+-- types/ Domain types, config, query/output shapes
|
+-- util/ Constants, errors, helpers
-
Dependency Injection via Composition Root:
main.tsis the only file that instantiates concrete implementations. All other code depends on interfaces. This makes testing straightforward and keeps coupling low. -
Interface Segregation: Services depend on narrow interfaces (
IGitClient,IConfigLoader,IOutputFormatter), not on concrete classes. -
One Command per File: Each CLI command lives in its own file under
src/commands/. Commands are thin -- they parse options, call services, and format output. -
Formatter Abstraction: All output goes through
IOutputFormatter. Text and JSON formatters implement the same interface, so--jsonworks on every command without per-command logic.
- Create the command file at
src/commands/your-command.ts:
import type { Command } from 'commander';
export function registerYourCommand(
program: Command,
deps: {
// Declare your dependencies here
},
): void {
program
.command('your-command <arg>')
.description('One-line description')
.option('--flag <value>', 'Description')
.action(async (arg: string, options) => {
// Implementation
});
}-
Register it in
main.ts:- Import the register function
- Call it with the program and its dependencies
-
Add tests under
tests/.
The standard trailer vocabulary is defined in the Lore protocol paper. Custom trailers can be added via config without code changes:
[trailers]
custom = ["My-trailer"]If you're extending the protocol-level trailer set (requires paper discussion):
- Add the key to
TrailerKeyinsrc/types/domain.ts - Add the field to
LoreTrailersinsrc/types/domain.ts - Add the key to
LORE_TRAILER_KEYSinsrc/util/constants.ts - If it's an array trailer, add to
ARRAY_TRAILER_KEYS; if enum, add toENUM_TRAILER_KEYS - Update
TrailerParserto handle parsing/serialization - Update
CommitBuilderto accept and validate the new trailer - Update both formatters (
TextFormatter,JsonFormatter) - Add tests for parsing, building, and formatting
Tests use vitest. Run them with:
# Run all tests
npm test
# Run in watch mode during development
npm run test:watch
# Run a specific test file
npx vitest run tests/services/trailer-parser.test.ts- Place tests in
tests/mirroring thesrc/directory structure - Test services through their public interface, not internal methods
- Mock external dependencies (git, filesystem) via the interface boundaries
- Every new feature or bug fix should include tests
- One concern per PR. A single bug fix, a single feature, or a single refactor. Don't mix.
- Tests required. PRs without tests for new behavior will be asked to add them.
- Build must pass. Run
npm run build && npm run typecheck && npm testbefore submitting. - Clear description. Explain what changed and why. If it closes an issue, reference it.
- Small diffs preferred. If your change is large, consider splitting it into a stack of PRs.
- Prettier handles formatting. Run
npm run formatbefore committing. - ESLint handles linting. Run
npm run lintto check. - TypeScript strict mode is enabled. No
anywithout justification. - Readonly by default. Use
readonlyon interface fields and preferReadonlyArray/ReadonlyMap. - Explicit return types on exported functions.
- No default exports. Use named exports everywhere.
- Bugs: Use the bug report template
- Features: Use the feature request template
By contributing, you agree that your contributions will be licensed under the MIT License.