# Contributing to Binnacle
Thank you for contributing to binnacle! This guide will help you get started.
## Development Setup
1. **Clone and build**
```bash
git clone https://github.com/yourusername/binnacle.git
cd binnacle
cargo build
```
2. **Install cargo-audit** (required for pre-commit hook)
```bash
cargo install cargo-audit
```
3. **Install Node.js dependencies** (required for JavaScript linting)
```bash
npm install
```
4. **Enable git hooks**
```bash
git config core.hooksPath hooks
```
This configures git to use the tracked hooks in the `hooks/` directory, which includes a pre-commit hook that validates:
- Code formatting (`cargo fmt --check`)
- Linting (`cargo clippy -- -D warnings`)
- JavaScript linting (`npx eslint` on staged .js files)
- Security vulnerabilities (`cargo audit`)
## Development Workflow
### Before Committing
The pre-commit hook will automatically run these checks, but you can run them manually:
```bash
# Run all checks
just check
# Run tests
just test
# Security audit
cargo audit
# Or individually
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
npm run lint:js # Lint all JavaScript files
```
### Code Style
- Follow Rust standard formatting (enforced by `rustfmt`)
- Address all clippy warnings (CI fails on warnings)
- Write tests for new features
- Update documentation when adding/changing features
### Commit Messages
We use conventional commit style:
- `feat:` for new features
- `fix:` for bug fixes
- `docs:` for documentation changes
- `test:` for test additions/changes
- `refactor:` for code refactoring
- `chore:` for maintenance tasks
### Testing
Run the full test suite:
```bash
cargo test --all-features
```
For GUI-specific tests:
```bash
cargo test --features gui
```
### Using Binnacle to Track Binnacle Development
We dogfood! Use binnacle to track your work:
```bash
# See what's ready to work on
bn ready
# Claim a task
bn task update bn-xxxx --status in_progress
# Create linked tests
bn test create "Test feature X" --cmd "cargo test feature_x" --task bn-xxxx
# When done
bn task close bn-xxxx --reason "Implemented feature X"
```
## Pull Requests
1. Create a feature branch
2. Make your changes
3. Ensure all tests pass: `cargo test --all-features`
4. Ensure checks pass: `just check`
5. Push and open a PR
6. CI will automatically run tests, clippy, and format checks
## Release Process (Maintainers)
### GitHub Repository Setup
Before publishing releases, repository maintainers must configure:
1. **Create a GitHub Environment** named `Release`:
- Go to Settings → Environments → New environment
- Name it exactly `Release`
- Optionally add protection rules (e.g., require approval)
2. **Add the `CARGO_REGISTRY_TOKEN` secret**:
- Get a token from [crates.io/settings/tokens](https://crates.io/settings/tokens)
- Go to Settings → Environments → Release → Environment secrets
- Add `CARGO_REGISTRY_TOKEN` with your crates.io API token
3. **Add the `NPM_TOKEN` secret** (for @binnacle/viewer package):
- Get a token from [npmjs.com/settings/tokens](https://www.npmjs.com/settings/~/tokens)
- Create an "Automation" token with publish permissions
- Go to Settings → Environments → Release → Environment secrets
- Add `NPM_TOKEN` with your npm token
### Creating a Release
1. Update version in `Cargo.toml`
2. Update version in `npm/package.json` (must match Cargo.toml version)
3. Commit: `git commit -am "chore: bump version to X.Y.Z"`
4. Create a GitHub Release with tag `vX.Y.Z` (must match Cargo.toml version)
The CD workflow will:
- Verify tag matches Cargo.toml version
- Run preflight checks (fmt, clippy, tests)
- Build and upload binaries to the release
- Build the WASM viewer and upload viewer.html to the release
- Publish to crates.io (only on release events)
- Publish @binnacle/viewer to npm (only on release events)
## Questions?
Open an issue or reach out to the maintainers!
## Adding a New Entity Type
When adding a new entity type to binnacle (like Task, Bug, Idea, etc.), you must update multiple places. Use this checklist to avoid data loss bugs:
### Checklist
1. **Define the model** (`src/models/mod.rs`)
- [ ] Create the struct embedding `EntityCore` via `#[serde(flatten)]`
- [ ] Implement the `Entity` trait (delegate to `self.core`)
- [ ] Add entity-specific fields after the `core` field
- [ ] Add to `EntityType` enum if needed
2. **Update storage schema** (`src/storage/mod.rs`)
- [ ] Add `CREATE TABLE` in `init_schema()`
- [ ] Add `DELETE FROM` in `rebuild_cache()` clear section
- [ ] Add re-read logic in `rebuild_cache()` for your `<entity>.jsonl`
- [ ] Implement `cache_<entity>()` method
- [ ] Implement CRUD methods (`create_<entity>`, `get_<entity>`, etc.)
3. **Add CLI commands** (`src/commands/`)
- [ ] Create command module
- [ ] Register in `main.rs`
4. **Update tests**
- [ ] Add entity to `test_compact_preserves_all_entity_types` in `cli_maintenance_test.rs`
- [ ] Add unit tests for the new entity
5. **Update documentation**
- [ ] Add to PRD.md if appropriate
- [ ] Update README.md
### Why This Matters
The `rebuild_cache()` function reconstructs the SQLite cache from JSONL files. If a new entity type is not added to this function:
- The cache will be missing data after `bn compact` runs
- Entity lookups will fail mysteriously
- Data appears "lost" even though it exists in JSONL files
The `test_compact_preserves_all_entity_types` test exists specifically to catch this bug early.
### EntityCore Composition Pattern
All primary entities (Task, Bug, Idea, Milestone) embed a common `EntityCore` struct to share fields and behavior:
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyEntity {
#[serde(flatten)]
pub core: EntityCore, // Common fields: id, entity_type, title, short_name, description, tags, created_at, updated_at
// Entity-specific fields
pub status: String,
pub custom_field: Option<String>,
}
impl Entity for MyEntity {
fn id(&self) -> &str { self.core.id() }
fn entity_type(&self) -> &str { self.core.entity_type() }
fn title(&self) -> &str { self.core.title() }
fn short_name(&self) -> Option<&str> { self.core.short_name() }
fn description(&self) -> Option<&str> { self.core.description() }
fn created_at(&self) -> &str { self.core.created_at() }
fn updated_at(&self) -> Option<&str> { self.core.updated_at() }
fn tags(&self) -> &[String] { self.core.tags() }
}
```
**Key points:**
- Use `#[serde(flatten)]` to serialize EntityCore fields at the top level (maintains backward-compatible JSON)
- Access common fields via `entity.core.field` (e.g., `task.core.id`)
- The `Entity` trait provides a uniform interface; implementations delegate to `self.core`
- Use `EntityCore::new(entity_type, id, title)` to create the core, then set optional fields