face 0.1.0

Fold And Cluster Entries — a Unix-style CLI for grouping, paging, and summarizing structured command output.
Documentation
# 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
$ rg TODO --json | face
face: 412 items

  [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
rg TODO --json                      | face
cargo test --message-format=json    | face
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
$ rg TODO --json | face --verbose
→ 412 items by data.path.text (auto, from rg JSONL)
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
face --cluster 1 < results.json | jq '.path'
face --cluster file:src/cli.rs < results.json | fzf
```

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
cmd | face --format=json > /tmp/face.json

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 --schema     # describe input shape, no grouping
cmd | face --explain    # full reasoning for what face would decide
cmd | face --verbose    # provenance line on stderr alongside normal output
```

Example:

```text
$ rg TODO --json | face --explain
input:
  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
cmd | face --interactive
```

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`.

| Strategy    | Auto-picked when                                    | Override                    |
| ----------- | --------------------------------------------------- | --------------------------- |
| `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

| `--format=`     | Use it for                                              |
| --------------- | ------------------------------------------------------- |
| `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)
  "$@" | face --format=json > "$saved"
  local id
  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
cmd | face --format=csv > clusters.csv
open clusters.csv      # macOS
xdg-open clusters.csv  # Linux
```

### PR / issue paste

```bash
cmd | face --format=markdown | pbcopy   # macOS
cmd | face --format=markdown | xclip    # Linux
```

---

## 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).