# onym — Project Context

## What It Is

Rust CLI that checks whether a name is available across package registries
(crates.io, PyPI, npm) + GitHub + common TLDs, with qualitative classification
of taken results (active / placeholder / dead / unknown) to distinguish real
projects from squatters.

Key differentiator vs `namae` and similar tools: **classification signal**.

## Repository

- **GitHub:** https://github.com/rvben/onym (private)
- **crates.io:** https://crates.io/crates/onym
- **Branch:** `main`
- **Current version:** 0.1.1
- **License:** MIT (2026 Ruben J. Jongejan)

## Stack

- Rust edition 2024, `rust-version = "1.90"`
- clap 4 (derive, subcommands)
- tokio 1 (multi-thread + macros)
- reqwest 0.12 (rustls-tls, default-features=false)
- serde / serde_json / anyhow
- comfy-table 7 (human table rendering)
- chrono 0.4 (date parsing for classification)

## Layout

```
Cargo.toml          → hybrid package + workspace manifest (room for crates/onym-wasm)
src/main.rs         → clap subcommands (check, schema), exit-code mapping
src/lib.rs          → check_name (batch) + check_name_stream (streaming) over Transport
src/transport.rs    → Transport trait + ReqwestTransport impl + MockTransport (test-support)
src/types.rs        → NameReport, CheckResult, Status, TakenInfo, Classification
src/classify.rs     → heuristics + unit tests
src/validate.rs     → input allowlist validation + tests
src/errors.rs       → ErrorKind, CliError, JSON envelope to stderr + tests
src/schema.rs       → machine-readable CLI description per clispec + tests
src/output.rs       → Format (Auto/Json/Table/Markdown), TTY detect, render
src/sources/
  mod.rs
  crates_io.rs      (404=free, 200=full metadata) — uses Transport
  pypi.rs           (releases[version] upload_time for last_release) — uses Transport
  npm.rs            (dist-tags.latest, time.modified) — uses Transport
  github.rs         (HEAD /{name} — avoids API quota) — uses Transport
  rdap.rs           (authoritative RDAP per TLD, rdap.org fallback) — uses Transport
tests/streaming.rs  → integration test for check_name_stream
```

## Conventions

- **clispec.dev conformant** — subcommand + schema + structured errors + TTY-aware output
- **Exit codes:**
  - 0 = all free
  - 1 = at least one taken, no ACTIVE classification
  - 2 = at least one ACTIVE project holds the name
  - 3 = invalid input
  - 4 = network / timeout / rate-limit / upstream
  - 5 = parse / internal
- **stdout = data only, stderr = diagnostics**
- **Errors are JSON envelopes on stderr:** `{"error":{"kind":"...","message":"...","retryable":bool}}`
- **Input allowlist:** `[A-Za-z0-9._-]`, 1..100 chars, can't start with `.` or `-`
- **Classification thresholds:**
  - Placeholder: version starts with `0.0.` AND downloads < 100
  - Active: recent_downloads >= 100
  - Dead: recent_downloads <= 10 AND last_release > 730 days old
  - Otherwise: Unknown

## RDAP Gotcha (CRITICAL)

`rdap.org` bootstrap is **unreliable** — it returns 404 for domains that are
actually registered (the original bug that prompted the authoritative-endpoint
rewrite). Always query the TLD's authoritative RDAP server first, fall back to
`rdap.org`.

Canonical mapping (verified via `https://data.iana.org/rdap/dns.json`):

| TLD(s)                                  | Endpoint                                            |
|-----------------------------------------|-----------------------------------------------------|
| `.io`, `.ai`                            | `https://rdap.identitydigital.services/rdap/domain/`|
| `.dev`, `.app`, `.page`, `.new`, `.foo` | `https://pubapi.registry.google/rdap/domain/`       |
| `.com`, `.net`                          | `https://rdap.verisign.com/com/v1/domain/`          |
| `.org`                                  | `https://rdap.publicinterestregistry.org/rdap/domain/` |
| fallback                                | `https://rdap.org/domain/`                          |

If a new default TLD is added, update `endpoints_for` in `src/sources/rdap.rs`
AND the `every_default_tld_has_an_authoritative_endpoint` test will guard
against regressions.

## Dev Commands

```bash
cargo nextest run                         # prefer over cargo test
cargo clippy --all-targets -- -D warnings # must be clean
cargo fmt                                 # applied pre-commit
cargo build --release
cargo publish --dry-run                   # always before real publish
```

## Release Policy

- **Per user directive: no minor bumps until there are real users.**
  Iterate on 0.1.x patch releases. If a published version has a bug, yank +
  publish next patch. Don't ceremonially bump minor/major.
- Uses Conventional Commits (`feat:`, `fix:`, `chore:`, `test:`, `ci:`).
- Follows the user's Rust CLI portfolio pattern (vership, shelly, unifi) —
  standalone repo, MIT license, crates.io publish.

## Known Issues / Backlog

- **v0.1.0 still needs yanking** from crates.io — current cargo token returned
  403 on `cargo yank`. Rotate to a token with yank scope, then:
  `cargo yank --version 0.1.0`.
- No CI / release pipeline yet. Options: cargo-dist, GitHub Actions +
  release-plz, or wait for `vership` Rust support.
- Potential new sources to add: rubygems, hackage, hex (Elixir), Go module
  proxy.
- Potential TLDs to add to `DEFAULT_TLDS`: `.ai`, `.co`.
- Possible `--concurrent <N>` flag with a bounded semaphore when checking many
  names at once.

## Testing the Real Thing

```bash
./target/release/onym schema              # machine-readable description
./target/release/onym check onym          # auto → JSON when piped, table on TTY
./target/release/onym check onym -o table
./target/release/onym check onym -o markdown
./target/release/onym check "foo bar"     # → invalid_input on stderr, exit 3
./target/release/onym check foo --tld io --tld dev
```
