mossaic 0.5.0

Plan and track GitHub contribution art — what today owes to draw your name by December, and whether the year can still be drawn at all
Documentation
# Contributing to mossaic

Thanks for looking. Bug reports, terminal compatibility reports and PRs are all
welcome — the compatibility ones especially, since mossaic's whole job is
getting along with terminals none of us has.

## 1. Dev setup

```sh
git clone https://github.com/vyncint/mossaic
cd mossaic
cargo test          # 150-odd tests, hermetic and offline
cargo run           # the chart, for whoever `gh` is logged in as
```

You need [the GitHub CLI](https://cli.github.com) authenticated (`gh auth
login`) to run the chart against a real account, but **not** to run the tests:
the two that call the API are `#[ignore]`d.

The toolchain is pinned to stable in `rust-toolchain.toml`, so `cargo fmt` and
`cargo clippy` give the same answers here as in CI. That file is not the MSRV —
that is `rust-version` in `Cargo.toml`, verified by the `msrv` job.

## 2. Project layout

| Path | What lives there |
| --- | --- |
| `src/lib.rs` | the library the three binaries share |
| `src/main.rs` | `mossaic`: arguments, terminal setup, event loop |
| `src/bin/` | `mossaic-art` and `mossaic-glyphs` |
| `src/plan.rs` | comparing a plan with what was actually contributed |
| `src/{calendar,github}.rs` | the day grid and the GraphQL call behind it |
| `src/{primer,term}.rs` | Primer's colours; what the terminal says it can do |
| `src/graphics.rs` | the rasteriser, the kitty and sixel encoders, the painter |
| `src/{ui,app}.rs` | rendering and layout; state, keys and mouse |
| `src/{art,png}.rs` | the 5×5 font and its costing; a small PNG encoder |
| `src/render_tests.rs` | in-process tests: layout, colour, encoders, art, PNG |
| `tests/smoke.rs` | out-of-process tests: the real binary in a real PTY |
| `tests/pixels.rs` | the same, in a PTY that answers the graphics probe |
| `docs/ART.md` | drawing text into a graph, and tracking the plan |
| `docs/DESIGN.md` | why the pixel path is shaped the way it is |
| `docs/RELEASING.md` | how a version gets cut |

## 3. Testing policy

Every behavioural change needs a test, and which of the three layers it belongs
in is usually obvious:

- **In process** (`src/render_tests.rs`) for anything that is a function of
  inputs: layout maths, hit-testing, palettes, the encoders, the art font.
  Encoders are tested against the formats, not against themselves — the sixel is
  decoded back into pixels and compared to what the rasteriser drew.
- **Out of process** (`tests/smoke.rs`, through
  [termlens]https://crates.io/crates/termlens) for anything that involves the
  event loop, the PTY, or escapes written around ratatui rather than through it.
- **Out of process, with pixels** (`tests/pixels.rs`) for anything that depends on
  the terminal *answering* the capability probe. Declare what is being simulated —
  `.graphics(Graphics::Kitty).cell_size(9, 19)` — rather than forcing the outcome
  with `--graphics`/`--cell`, so the probe, the fallbacks and the auto choice all
  run. `Screen::graphics()` then reports what went out, by protocol and in bytes,
  which is how a claim about the wire gets a check behind it. What it cannot tell
  you is what the image *looks* like; that stays with the in-process encoder tests
  and `--png`.

Two rules that are easy to get wrong:

- **Wait on frames, not on content.** mossaic brackets repaints in DEC 2026
  synchronized updates, so `wait_frame` sees only complete ones. `wait_until`
  re-checks on every chunk and will happily match a frame half-applied — a test
  written that way fails about three runs in four, for reasons that look like
  magic.
- **Keep tests hermetic.** `env_clear()`, `--file art/vyncint-2027.json`, no
  network. If a change genuinely needs the API, mark it `#[ignore]` and say so.
- **Pin `--today` in anything that reads a report.** The clock is an input to
  `mossaic-art --track`, and an assertion that does not pin it is an assertion
  about the day it was written. Two of them were: one claimed whatever day CI
  saw was a lit day inside the letters, and it passed for exactly as long as
  that was true. `--today 2026-08-19` against `art/vyncint-2026.json` is the
  fixture pair to reach for. The `test` job runs the CLI suite a second time
  under `TZ=Pacific/Kiritimati`, which is a backstop rather than the rule.

Anything user-visible in the terminal deserves a look in a real one too. The
harness is a VT emulator: it agrees with terminals about text, and about *whether*
an image was sent and how large it was, but it does not draw one — so nothing in
it can tell you the picture is right.

If a change touches frames, waits or the event loop, run the flake hunter before
asking for review — it is the whole suite, a hundred times, on both OSes:

```sh
gh workflow run stress.yml            # or, locally:
for i in $(seq 20); do cargo test --release || break; done
```

## 4. Commit conventions

Short imperative subject, body explaining *why*. Reference issues as
`Closes #123`. Squash merges, so the PR title becomes the commit subject.

**No AI attribution.** AI assistance is welcome here — use whatever helps. AI
*attribution* is not: no `Co-Authored-By` trailer naming an assistant, no
"Generated with" watermark, no bot identity as author or committer. Whoever
opens the pull request is the author of record, and the history should say so.

This is enforced, not requested: [`commit-policy.yml`](.github/workflows/commit-policy.yml)
runs [`check-no-ai-attribution.sh`](.github/scripts/check-no-ai-attribution.sh)
over every commit in a pull request. If it fails, drop the trailer and
force-push:

```sh
git commit --amend        # the last commit
git rebase -i main        # several, marking each `reword`
git push --force-with-lease
```

Agents that read repository settings are also told up front: `.claude/settings.json`
turns co-author trailers off. That is a courtesy, not the boundary — settings
files are advisory and CI is what actually holds.

## 5. Pull requests

A pull request is the only way anything lands: `main` rejects direct pushes —
the maintainer's too — and merges are squashes that need the `required-green`
and `commit-policy` checks green first.

1. Open an issue first for anything larger than a fix — especially anything
   that changes what the chart looks like.
2. `cargo fmt --all`, `cargo clippy --all-targets --all-features -- -D warnings`
   and `cargo test` before pushing.
3. Update `CHANGELOG.md` under `[Unreleased]` for user-facing changes.
4. Fill in the PR checklist.

## 6. Terminal compatibility reports

These are worth their own note, because they are the most useful thing an
outside contributor can send. If the chart looks wrong on your terminal:

1. Run `mossaic --capabilities` and include its output verbatim.
2. Say which terminal and version, and whether `--graphics text` looks right.
3. If pixels are involved, `mossaic --png /tmp/chart.png` renders the same
   image to a file — comparing that with your screen separates "the rasteriser
   is wrong" from "the protocol emission is wrong", which are different bugs.

## 7. Adding a glyph to the font

The `mossaic-art` binary draws text with a 5×5 font in `src/art.rs`. It has A–Z,
0–9, punctuation, and a set of named shapes — everything else is a gap someone
can fill, and filling one is a single table entry:

```rust
('^', ["..#..", ".#.#.", ".....", ".....", "....."]),
```

A **shape** is two entries, because a symbol nobody can type is a shape nobody
can draw. The glyph goes in `FONT` keyed by its character, and its name — or
names — go in `SHAPES` beside it:

```rust
('\u{2601}', ["..##.", ".####", "#####", ".....", "....."]),   // FONT
("cloud", '\u{2601}'),                                          // SHAPES
```

That is what makes `mossaic-art ":cloud:"` work. If the shape has a common
emoji spelling, one line in `FOLD` points it at the same glyph, so someone who
pastes 🌥 gets what they meant rather than an error about a codepoint.

See what you made:

```sh
cargo run --bin mossaic-art -- --font                # every glyph, side by side
cargo run --bin mossaic-art -- "HI!" --year 2027     # in a year, at the real size
cargo run --bin mossaic-art -- ":cloud:" --year 2027 # a shape, at the real size
```

Six rules are checked **when the crate compiles**, so a mistake is a build
failure with the reason rather than a panic for whoever draws it first:

1. exactly five rows of exactly five characters,
2. only `#` and `.`,
3. no character twice,
4. a shape name names a character the font actually has,
5. shape names are lowercase ASCII, and no name twice — `:STAR:` has to fold to
   one thing, because every plan is stored uppercased,
6. nothing folds to a character the font lacks, and a folded character is not
   itself in the font — one bitmap per shape.

Two more rules are checked by the tests rather than the compiler, because they
are judgement rather than shape: no glyph may be blank or a solid block (either
reads as no character at all), and no two glyphs may draw the same pixels. The
README's list of what the font can draw is checked against the font too, so a
new glyph that is not documented fails the suite.

## 8. Code style

The code is commented for the reader who wonders *why*, not *what*. Comments
that explain a trade-off, a protocol quirk, or a decision that looks arbitrary
earn their place; comments that restate the line below them do not. Public items
carry documentation — `missing_docs` is a warning and CI runs clippy with
`-D warnings`.