# Contributing to Bristlenose

Thanks for your interest in contributing!

## Contributor Licence Agreement

By submitting a pull request or patch to this project, you agree that:

1. You have the right to assign the contribution.
2. You grant the project maintainer (Cassio) a perpetual, worldwide,
   irrevocable, royalty-free licence to use, modify, sublicence, and
   relicence your contribution — including under licences other than
   AGPL-3.0.
3. Your contribution is provided as-is, without warranty.

This allows the maintainer to offer commercial or dual-licence versions
of Bristlenose in the future without needing to contact every contributor
individually.

## How to contribute

1. Fork the repo and create a branch.
2. Make your changes.
3. Run `ruff check` and `pytest` before submitting.
4. Open a pull request with a clear description of what and why.

## Code style

- Python 3.10+
- Ruff for linting (config in `pyproject.toml`)
- Type hints everywhere

## Project layout

```
bristlenose/          # main package
  cli.py              # Typer CLI (run, transcribe-only, analyze, render)
  config.py           # Pydantic settings (env vars, .env, bristlenose.toml)
  models.py           # Pydantic data models (quotes, themes, enums)
  pipeline.py         # orchestrator (full run, transcribe-only, analyze-only, render-only)
  stages/             # 12-stage pipeline (ingest → render)
    render_html.py    # HTML report renderer (loads CSS from theme/, embeds JS)
  llm/
    prompts.py        # LLM prompt templates
    structured.py     # Pydantic schemas for LLM structured output
  theme/              # design system (atomic CSS) — see below
    tokens.css
    atoms/
    molecules/
    organisms/
    templates/
    index.css         # documents concatenation order
  utils/
    hardware.py       # GPU/CPU detection
tests/
pyproject.toml        # package metadata, deps, tool config (hatchling build)
```

---

## Design system (`bristlenose/theme/`)

The report stylesheet follows [atomic design](https://bradfrost.com/blog/post/atomic-web-design/) principles. Each CSS concern lives in its own file. At render time, `render_html.py` reads and concatenates them in order into a single `bristlenose-theme.css` that ships alongside the report.

### Architecture

```
theme/
  tokens.css                  # 1. Design tokens  (CSS custom properties)
  atoms/                      # 2. Atoms          (smallest reusable pieces)
    badge.css                 #    base badge, sentiment variants, AI/user/add
    button.css                #    fav-star, edit-pencil, restore, toolbar-btn
    input.css                 #    tag input + sizer
    toast.css                 #    clipboard toast
    timecode.css              #    clickable timecodes
    bar.css                   #    sentiment bar, count, label, divider
  molecules/                  # 3. Molecules       (small groups of atoms)
    badge-row.css             #    badges flex container
    bar-group.css             #    bar-group row (label + bar + count)
    quote-actions.css         #    favourite/edit states, animations
    tag-input.css             #    input wrapper + suggest dropdown
  organisms/                  # 4. Organisms       (self-contained UI sections)
    blockquote.css            #    full quote card, rewatch items
    sentiment-chart.css       #    chart layout, side-by-side row
    toolbar.css               #    sticky toolbar
    toc.css                   #    table of contents columns
  templates/                  # 5. Templates       (page-level layout)
    report.css                #    body, article, headings, tables, links
    print.css                 #    @media print overrides
  index.css                   # human-readable index (not used by code)
```

### How it works

`render_html.py` defines a `_THEME_FILES` list that specifies the exact concatenation order. The function `_load_default_css()` reads each file, wraps it with a section comment, and joins them into one string. This is cached once per process, then written to `bristlenose-theme.css` in the output directory on every run (always overwritten -- user state like favourites and tags lives in localStorage, not CSS).

### Design tokens

All visual decisions live in `tokens.css` as CSS custom properties with a `--bn-` prefix:

```css
--bn-colour-accent: #2563eb;
--bn-font-body: "Inter", system-ui, sans-serif;
--bn-space-md: 0.75rem;
--bn-radius-md: 6px;
--bn-transition-fast: 0.15s ease;
```

Every other CSS file references tokens via `var(--bn-colour-accent)` etc. -- never hard-coded values. This makes the entire visual language overridable from a single file.

**Legacy aliases.** The Python code in `render_html.py` generates inline `style` attributes that reference the older unprefixed names (e.g. `var(--colour-confusion)`). To avoid a breaking change, `tokens.css` defines aliases at the bottom:

```css
--colour-confusion: var(--bn-colour-confusion);
```

These aliases point to the `--bn-` versions, so theme authors only need to override `--bn-*` tokens.

### Working with the CSS

**Adding a new component:**

1. Decide the atomic layer (is it an atom, molecule, or organism?).
2. Create a new `.css` file in the right folder.
3. Reference tokens, never hard-coded values.
4. Add the file to the `_THEME_FILES` list in `render_html.py` (order matters -- later files can override earlier ones).

**Adding a new token:**

1. Add the `--bn-*` property in `tokens.css`.
2. If the token is used in inline styles generated by Python, also add a legacy alias.

**Modifying existing styles:**

1. Find the right file by layer (use `index.css` as a map).
2. Edit the file directly. The change will appear on the next pipeline run.
3. No need to delete old output -- `bristlenose-theme.css` is always overwritten.

**Quick reference -- which file owns what:**

| I want to change...            | Edit this file              |
|--------------------------------|-----------------------------|
| Colours, fonts, spacing        | `tokens.css`                |
| How badges look                | `atoms/badge.css`           |
| How star/pencil buttons work   | `atoms/button.css`          |
| The tag input or suggest list  | `atoms/input.css` + `molecules/tag-input.css` |
| The whole quote card layout    | `organisms/blockquote.css`  |
| The sentiment chart            | `atoms/bar.css` + `molecules/bar-group.css` + `organisms/sentiment-chart.css` |
| Page layout, headings, tables  | `templates/report.css`      |
| What gets hidden when printing | `templates/print.css`       |

### Future: user-generated themes

The token architecture is designed to support user themes. A theme is just a CSS file that overrides `--bn-*` properties:

```css
/* dark-theme.css */
:root {
    --bn-colour-bg: #1a1a2e;
    --bn-colour-text: #e0e0e0;
    --bn-colour-border: #333;
}
```

This will be loaded via a theme picker in the browser toolbar (see roadmap). The infrastructure is ready -- what's needed is the picker UI and a way to bundle/discover theme files.

## Releasing

Day-to-day development just means committing and pushing to `main`. CI runs automatically. PyPI and Homebrew are updated when you tag a release.

### Version — single source of truth

The version lives in **one place only**: `bristlenose/__init__.py`.

```python
__version__ = "0.4.0"
```

`pyproject.toml` uses `dynamic = ["version"]` with `[tool.hatch.version] path = "bristlenose/__init__.py"`, so hatchling reads it from there. Do **not** add a `version` key to `[project]`.

### Cutting a release

```bash
# 1. Bump the version
#    Edit bristlenose/__init__.py → __version__ = "X.Y.Z"

# 2. Add a changelog entry
#    Edit README.md → add a ### X.Y.Z section under ## Changelog

# 3. Commit and tag
git add bristlenose/__init__.py README.md
git commit -m "vX.Y.Z"
git tag vX.Y.Z
git push origin main --tags
```

That's it. GitHub Actions handles the rest automatically.

### What happens after you push a tag

The release pipeline spans **two repos** and runs five jobs:

```
bristlenose repo (release.yml)
├─ ci           → ruff, mypy, pytest (via workflow_call to ci.yml)
├─ build        → sdist + wheel (python -m build)
├─ publish      → PyPI via OIDC trusted publishing (no token needed)
├─ github-release → creates GitHub Release with auto-generated notes
└─ notify-homebrew → sends repository_dispatch to tap repo
                        │
                        ▼
homebrew-bristlenose repo (update-formula.yml)
└─ update       → fetches sdist URL + sha256 from PyPI JSON API
                  → patches Formula/bristlenose.rb (url, sha256, version)
                  → commits and pushes
```

### Cross-repo topology

The release system spans two repos and GitHub settings:

| Component | Location |
|-----------|----------|
| CI workflow | `bristlenose` repo → `.github/workflows/ci.yml` |
| Release workflow (build, publish, GitHub Release, dispatch) | `bristlenose` repo → `.github/workflows/release.yml` |
| Reference copy of tap workflow | `bristlenose` repo → `.github/workflows/homebrew-tap/update-formula.yml` |
| Homebrew formula | `homebrew-bristlenose` repo → `Formula/bristlenose.rb` |
| Tap update workflow (authoritative) | `homebrew-bristlenose` repo → `.github/workflows/update-formula.yml` |
| `HOMEBREW_TAP_TOKEN` secret | `bristlenose` repo → Settings → Secrets → Actions |
| PyPI trusted publisher | pypi.org → bristlenose project → Publishing settings |
| PyPI `pypi` environment | `bristlenose` repo → Settings → Environments |

### Secrets

| Secret | Where | What it does | Rotation |
|--------|-------|-------------|----------|
| `HOMEBREW_TAP_TOKEN` | bristlenose repo → Actions secrets | Classic PAT with `repo` scope; lets `notify-homebrew` dispatch to the tap repo | No expiry set; rotate if compromised |
| PyPI OIDC | pypi.org trusted publisher | `release.yml` `publish` job uses `id-token: write` — no token stored anywhere | N/A (keyless) |
| `GITHUB_TOKEN` | automatic per workflow run | `github-release` job uses it to create GitHub Releases | Automatic |

### Homebrew tap automation

The Homebrew tap updates automatically after every PyPI publish. The `notify-homebrew` job in `release.yml` sends a `repository_dispatch` event to the [`homebrew-bristlenose`](https://github.com/cassiocassio/homebrew-bristlenose) repo. The tap's `update-formula.yml` workflow:

1. Receives the version from the dispatch payload
2. Fetches the sdist URL + sha256 from `https://pypi.org/pypi/bristlenose/{version}/json` (with a retry loop for CDN propagation)
3. Uses `sed` to patch the `url`, `sha256`, and `version` lines in `Formula/bristlenose.rb`
4. Commits as `github-actions[bot]` and pushes

**Manual fallback** — if the automation fails (e.g. expired token, PyPI API issue), update the tap manually:

```bash
# Get the new sdist URL and sha256
curl -s https://pypi.org/pypi/bristlenose/X.Y.Z/json | python3 -c "
import json, sys
data = json.load(sys.stdin)
for f in data['urls']:
    if f['packagetype'] == 'sdist':
        print(f'url: {f[\"url\"]}')
        print(f'sha256: {f[\"digests\"][\"sha256\"]}')
"
```

Clone `cassiocassio/homebrew-bristlenose`, edit `Formula/bristlenose.rb` — update the `url`, `sha256`, and `version` lines. Commit and push.

### Homebrew architecture note

The formula at `Formula/bristlenose.rb` creates a Python 3.12 virtualenv and runs `pip install bristlenose==<version>` from PyPI. This uses pre-built wheels rather than individual Homebrew resource stanzas. A traditional resource-stanza formula would require maintaining 100+ pinned dependencies (including PyTorch, onnxruntime, spacy) — impractical for an ML-heavy tool. The pip-in-venv approach is standard for custom taps with complex dependency trees.

### Summary

| What              | When                  | How                                            |
|-------------------|-----------------------|------------------------------------------------|
| Commit to `main`  | Every change          | `git push` (CI runs automatically)             |
| PyPI release      | Each release          | Tag `vX.Y.Z` and push (automated via Actions)  |
| GitHub Release    | Each release          | Auto-created by Actions with generated notes   |
| Homebrew tap      | After PyPI publish    | Auto-dispatched to tap repo by Actions         |
