# face
**Fold And Cluster Entries** — a Unix-style CLI that groups, ranks, and pages
structured command output.
`face` is a standalone result organizer. Query tools — `rg`, `cargo`, `gh`,
your CI logs, your CSV exports, your LLM agent's JSON — answer *what are the
results?*. `face` answers the next question: *how should this result set be
grouped, ranked, and paged?*
One binary. One job. No daemon, no UI, no config file required.
```text
[1] src/cli.rs 38 ████████
[2] src/core.rs 27 █████▋
[3] src/parser.rs 19 ████
... 44 more clusters (face --cluster N to drill)
```
---
## Why face
Most tools stop at *here is a flat list of N matches*. The moment N grows
past a screen, the next question is always the same:
- "Which file has the most hits?"
- "Show me only the high-confidence ones."
- "Group by repo, then by severity, then page through the top bucket."
That step is `face`. Pipe any structured stream in, get a grouped, paged
summary out. Drill into any cluster by number or by ID. Save the envelope and
re-process it without re-running the upstream command. No new query language
to learn — `face` reads the data and decides.
### Highlights
- **Zero-config, zero-flag by default.** `cmd | face` always produces
something useful. Every flag exists to override an automatic decision —
none are required.
- **Reads JSON, JSONL, CSV, and TSV** with sniffing. Force the parser with
`--format-in` if you must.
- **Auto-detects** the items array, the score field, the numeric distribution
preset, and a per-axis grouping strategy.
- **Six numeric and categorical strategies** plus five algorithmic
string-similarity flavors (token, n-gram, edit, LSH, simhash). No neural
black-box clustering.
- **Nested grouping** to any depth via `--by FIELD,FIELD,...` or repeatable
`--within FIELD`.
- **Self-consumable JSON envelope.** `face --format=json | tee` and re-pipe
for instant redrills — no auto-cache, no daemon, no implicit state.
- **Seven output formats** — human tree, JSON (nested or flat), JSONL items,
TSV, CSV, Markdown — TTY-aware coloring, `NO_COLOR` honored.
- **Inline `--interactive` picker** for the moments you want to click around
without standing up a TUI.
The full design rationale lives in [`docs/design.md`](docs/design.md).
---
## Install
With Homebrew, after the tap has been updated by a GitHub release:
```bash
brew install oops-rs/tap/face
```
From crates.io, after the crates are published:
```bash
cargo install face
```
Or build from source today:
```bash
# Latest from the main branch
cargo install --git https://github.com/oops-rs/face --bin face
# Or from a local clone
git clone https://github.com/oops-rs/face
cd face
cargo install --path crates/face-cli
```
`face` targets edition 2024 / resolver 3, so a recent stable Rust toolchain
(1.85+) is required to build from source.
> **Note** — `face` is at `0.1.0`. The CLI surface is implemented end-to-end
> and stable enough for daily use, but the JSON envelope's exact shape may
> still evolve before `1.0`. See [Status](#status) for details.
---
## Quick tour
### Group anything
```bash
gh pr list --json number,title,labels --limit 200 | face
psql -c "SELECT ..." --csv | face
cat results.tsv | face
```
No flags. `face` sniffs the format, finds the items, picks a grouping field,
and prints a tree. With `--verbose` you get a one-line provenance summary on
stderr explaining what it decided:
```text
face: 412 items
...
```
### Drill into a cluster
```bash
# 1-based ordinal — matches the [N] labels in human output
face --cluster 1 < results.json
# Canonical comma form
face --cluster file:src/cli.rs < results.json
# Or as repeatable axis=value pairs
face --cluster file=src/cli.rs --cluster score=excellent < results.json
```
When the destination is a pipe, drilled output defaults to **JSONL items**
(one record per line, no envelope) so `jq`, `fzf`, `awk`, and friends just
work:
```bash
```
Pass `--format=human` to force the tree view through a pipe, or
`--format=json` to get the full envelope back.
### Two-axis grouping
```bash
# Group by file, band the score within each file
face --by file --within score --bands 4 < results.json
# Chain --by axes (comma is "next nesting level")
face --by repo,file,kind < results.json
```
Auto-detection runs **per axis**, so each `--within` without an explicit
strategy picks its own.
### Save once, drill many
`face --format=json` produces a self-consumable envelope. Pipe it back in and
`face` re-processes the cached clusters instead of re-running upstream:
```bash
face --cluster file:src/cli.rs < /tmp/face.json
face --cluster file:src/cli.rs --page 2 < /tmp/face.json
face --format=markdown < /tmp/face.json
face --within tag < /tmp/face.json # re-cluster
```
No auto-cache, no daemon — just a shell redirect you control.
### Sniff before committing
```bash
cmd | face --verbose # provenance line on stderr alongside normal output
```
Example:
```text
format jsonl (sniffed: 412 newline-separated JSON objects)
items . (input is a flat record stream)
score (none detected)
strategy:
axis 1 data.path.text (auto: most-discriminating string field, cardinality=47)
output:
format human (default)
color auto (stdout is a TTY)
```
### Inline picker
```bash
Arrow keys to navigate, Enter to drill, Space to multi-select at the leaf
level, Esc / `q` to back out. When stdout isn't a TTY, `face` falls back
silently to the configured non-interactive format — scripts that
conditionally pass `--interactive` work uniformly in CI.
---
## Strategies
Detected per axis from the data; override with `--strategy`.
| `exact` | Enum-ish field (low cardinality, high coverage) | `--strategy=exact` |
| `prefix` | Path-like strings (`/`, `::`, or `.` separators) | `--strategy=prefix=2` |
| `top` | Free-form strings (top-N by frequency + `(other)`) | `--strategy=top=10` |
| `bands` | Continuous numeric (default 5 equal-width bands) | `--strategy=bands` `--bands=N` |
| `quantiles` | Heavily skewed numeric distributions | `--strategy=quantiles` |
| `natural` | One-dimensional Jenks breaks | `--strategy=natural` |
| `similar` | Algorithmic string clustering | `--strategy=similar=token` (or `ngram`, `edit`, `lsh`, `simhash`) |
Numeric clusters render highest-to-lowest by default so score-like data leads
with the strongest bucket. Cluster labels come from the data itself — value
ranges for numeric strategies, the field value for enum strategies, the
shared prefix for prefix strategies.
---
## Output formats
| `human` | Reading. Tree-rendered, colored if TTY, with bars. |
| `json` | Programmatic consumers. Nested envelope, stable schema. |
| `json-flat` | `jq` pipelines that prefer flat iteration. |
| `jsonl-items` | Downstream record consumers. One item per line. |
| `tsv` | Spreadsheets, `fzf`, `awk` / `cut`. |
| `csv` | Spreadsheets. RFC-4180 quoting. |
| `markdown` | Pasting into PRs, issues, docs. |
`-j` is shorthand for `--format=json`. Color follows
`--color={auto,always,never}` (default `auto`) and respects `NO_COLOR` and
`CLICOLOR_FORCE`.
When `--cluster` is set, no `--format` was passed, and stdout is a pipe,
`face` automatically emits `jsonl-items` for the drilled page so `jq`
pipelines stay clean.
---
## Configuration
Optional. If absent, every default is built into the binary. Lookup order:
1. `$FACE_CONFIG` (full path)
2. `$XDG_CONFIG_HOME/face/config.toml`
3. `~/.config/face/config.toml`
```toml
[output]
default_format = "human"
per_page = 20
color = "auto"
[detect]
score_candidates = ["score", "rank", "relevance", "confidence", "bm25"]
items_candidates = ["items", "results", "data", "hits", "matches"]
[strategy]
default_bands = 5
[interactive]
save_threshold = 200
[presets.my-search]
score = "relevance"
invert = false
scale = 1.0
```
Precedence:
```text
CLI flag > $FACE_* env > config.toml > built-in default
```
`--verbose` provenance and `--explain` say which layer won when something
looks unexpected. The full schema lives in [`docs/design.md`](docs/design.md)
§13.
---
## Composition recipes
### fzf as the picker, `face` as the cluster engine
```bash
ff() {
local saved
saved=$(mktemp)
id=$(face --format=tsv < "$saved" | fzf --with-nth=2,3 | cut -f1)
[ -n "$id" ] && face --cluster "$id" < "$saved"
}
ff rg TODO --json
ff cargo test --message-format=json
```
### Spreadsheet handoff
```bash
xdg-open clusters.csv # Linux
```
### PR / issue paste
```bash
```
---
## Project layout
```text
crates/
face-cli/ binary `face` + thin CLI library
face-core/ detection, grouping, clustering, paging, envelope
docs/
design.md canonical CLI / data-model design reference
```
---
## Status
`0.1.0`. The full design surface in [`docs/design.md`](docs/design.md) is
implemented end-to-end:
- JSON / JSONL / CSV / TSV input with sniffing and `--format-in` override
- Auto-detection: items array, score field, preset, per-axis strategy
- Strategies: `exact`, `prefix`, `top`, `bands`, `quantiles`, `natural`,
and `similar` (token / n-gram / edit / LSH / simhash)
- Nested grouping via `--by FIELD,FIELD,...` and repeatable `--within FIELD`
- Cluster addressing: 1-based ordinal, canonical comma ID, or `axis=value`
- Pagination via `--page` / `--per-page`
- Self-consumable envelope: `face --format=json` round-trips through stdin
- Output formats: `human`, `json`, `json-flat`, `jsonl-items`, `tsv`,
`csv`, `markdown`
- Inline `--interactive` picker (the only carve-out from "no UI")
- Optional TOML config under `$XDG_CONFIG_HOME/face/`
The CLI surface is considered stable enough for daily use. The `0.x` line
means the JSON envelope's exact shape may still shift if a downstream
consumer surfaces a need before `1.0`.
---
## Contributing
Issues and PRs welcome. Before opening a PR:
```bash
cargo test --workspace --all-features
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo fmt --all
```
The design doc is the source of truth for the CLI surface; when in doubt,
match it.
---
## License
MIT — see [`LICENSE`](LICENSE).