--- name: consumer-experience-review description: Review a library/package from the perspective of an external downstream consumer — build a throwaway crate/project that depends on it the way a real user would, exercise the public API, and surface gaps, inconsistencies, and inconveniences with compiler-verified evidence. Use when asked to "review the consumer/user experience", "find API gaps/inconsistencies", "evaluate the public API", "is this ready to publish/release", or before a 1.0 / major version bump. Especially valuable for breaking-change releases (the only non-breaking window to fix the public surface). --- # Consumer-experience review You are auditing a library as the people who will `cargo add` / `npm install` / `pip install` it — not as its author. Internal tests pass through the crate's own privacy and import context and therefore **cannot** see the gaps a real consumer hits (missing root re-exports, trait-import friction, method-name collisions, feature-flag dead-ends, error types you can't name). The only reliable instrument is a separate project that depends on the library **externally** and uses it the way a reasonable-but-not-omniscient user would. ## Core principle **Write the code a competent user would write on their first try — reaching for conventional names and paths — and let it fail. The natural call that doesn't compile *is* the finding.** Do not pre-correct your consumer code with insider knowledge; that hides exactly what you're looking for. Evidence is a captured compiler error, not an assertion. ## Procedure ### 1. Map the public surface (don't trust memory — read it) Enumerate what a consumer actually sees: - **Crate-root re-exports** vs what's only reachable via deeper paths (`crate::stores::Foo`). Asymmetry here is the #1 source of findings. - **Traits and every method signature** — look for two traits that define the **same method name** (collides when both are in scope; common with sync/async pairs), and for `&mut self` where a consumer would expect `&self`. - **Constructors / builders** — is `Type::new()` returning `Type` or a builder? Is the fallible path `build()` or `try_build()`? Is it consistent across sibling types? - **Error types** — can the error returned by a public function be **named via the same path the type came from**? (`cached::Foo` but only `cached::stores::FooBuildError` is a classic gap.) - **Feature flags** — what's gated, and does a natural feature combination leave a type unreferenceable or a macro unusable? - **Macros** — re-exported at the crate root (`use lib::thing;`) or only under a submodule? Ecosystem convention is root. ### 2. Scaffold a throwaway external consumer - Put it in `$TMPDIR` (or another scratch path) — **never inside the repo, never in git**. Do not pollute the project's `examples/`, `tests/`, or working tree. - Depend on the library **by path** with a realistic feature set: - Rust: `lib = { path = "/abs/path", features = [...] }` - npm: `npm link` / `file:` dependency. Python: `pip install -e`. Same idea — external module resolution, real package boundary. - Exercise the breadth: each macro, direct store/type use, sync and async paths, builders, error handling, and at least one less-common feature combination. ### 3. Write naive-but-reasonable consumer code Deliberately reach for conventions first: - Import the macro from the crate root (`use lib::macro;`) before trying submodule paths. - Bring the obvious traits into scope together (or glob `use lib::*;`) — exactly what a real user does. - Name the error type returned by a builder/constructor in a `let _: Option = None;` or a function signature. - Guess builder method names by convention (`max_size`, `capacity`, `with_*`) — a wrong guess that the compiler can't help disambiguate is a discoverability finding. ### 4. Compile, run, capture exact errors `cargo build`/`run` (or the ecosystem equivalent). For every failure, record the **exact** diagnostic code and message (`error[E0034]: multiple applicable items`, `E0432: unresolved import`, `E0599`, etc.). That verbatim error is the evidence in your report. ### 5. Isolate each gap in a minimal probe For each suspected issue, create a tiny separate binary/module that triggers **only** that issue with the smallest natural snippet. This (a) proves it's real and not a side effect, and (b) gives you a clean before/after to re-run once a fix is proposed/applied. ### 6. Severity — weight the release window Rank findings, and explicitly factor in the version context: - **Pre-1.0 / major bump:** a hard compile error on a *common* operation, or any public-surface inconsistency, is **high** — this is the only non-breaking window to fix the method/type surface. Say so. - Post-1.0 minor: a breaking fix is itself a cost; lean toward additive fixes (new re-exports, `#[doc(alias)]`, deprecations) and documentation. - Always separate: 🔴 blocks/forces ugly workarounds on common paths · 🟠 awkward but workable · 🟡 docs/discoverability. ### 7. Report — evidence first, fixes proposed not applied For each finding: the natural code that failed → the exact compiler error → root cause (cite `file:line`) → concrete proposed fix (and whether it's breaking or additive). Recommend a subset to act on given the release window. **Do not implement fixes unless the user asks** — this skill produces a review. If asked to fix, re-run the isolated probes (step 5) afterward to prove the natural code now works, and run the project's full check (`make check`/tests/clippy/doctests, golden/snapshot drift). ### 8. Clean up Remove or abandon the scratch consumer crate. Never leave it in the repo or stage it. ## Gap classes to actively check (the checklist that catches the real ones) - **Root re-export gaps / asymmetry** — type at root but its builder/error only via `crate::sub::`; one sibling exports `*Builder` at root, another doesn't. - **Trait method-name collisions** — two in-scope traits with the same method name (sync `foo` vs async `foo`) → `E0034`, forces UFCS. Fix: prefix/rename one (e.g. `async_`-prefix the async variant), settle it pre-1.0. - **Unnameable error types** — `Type::build()` returns `Result<_, E>` but `E` isn't reachable from the path `Type` is. - **Constructor/builder inconsistency** — `new()`→value vs `new()`→builder; `build` vs `try_build`; one shared error enum vs per-type ones. Often documentation-only at 1.0, but flag it. - **Macro not at crate root** — every mainstream crate puts its attribute/derive macro at the root; submodule-only is friction and a migration cost. - **Feature-flag dead-ends** — a plausible feature set where a needed type/trait/macro is unreferenceable. - **`&mut self` for reads** — forces a lock/wrapper for shared use; note it even if intentional. - **Discoverability** — natural names that don't exist and have no `#[doc(alias)]`; missing trait-import hints in errors. ## Cautions - Testing from inside the crate (its own `tests/`, `examples/`, or a workspace member) is **not** a consumer test — it inherits the crate's import/privacy context and will miss the gaps. External path dependency is mandatory. - Sandboxed builds may hit registry/network errors; this skill only needs the path-dependency and std/`tokio`-class deps. If a build fails for sandbox/network reasons (not a real API gap), retry with the sandbox disabled before concluding. - A finding is the *user's natural code failing*, not "the API is unusable" — the API may work fine once you know the trick. The cost being measured is "knowing the trick." - Generalize beyond Rust where relevant: npm (root export maps, `exports` field, dual ESM/CJS), Python (`__init__.py` re-exports, optional-extra imports) — same methodology, same gap classes.