Summary
ana scan is our most-run, most-shareable surface — it's instant, needs no API key, no subscription, and it's the npx anatomia-cli scan funnel (the first thing a new user ever runs). Today it shows a single frozen ora('Scanning project...') spinner for the whole run, then dumps a result. The engine — our actual differentiator ("we have an engine, not a prompt library") — is completely invisible while it works.
Make the scan show its work: a fast pre-scan summary panel ("here's your repo") followed by a live, animated, phase-by-phase progress display as the engine runs. This turns a 10-second black box into a screenshot/GIF-able moment that proves the engine is real.
Why it matters
- Demo / discovery value — a visible engine is a shareable engine. This is the cheapest high-leverage improvement to the top-of-funnel experience.
- On-thesis — "the engine it can't skip" only lands if people can see the engine doing something. Right now our biggest differentiator runs behind a spinner.
- No downside to the artifact — progress is transient stderr UI;
scan.json output and determinism are unaffected.
Current state (researched)
scanProject(rootPath, { depth }) (scan-engine.ts:709) takes only depth. It runs ~12 cleanly-numbered sequential phases — census (:717), monorepo (:719), package manager (:731), deps (:733), three-tier identity (:740), project type (:764), framework, structure, deep-tier parse, patterns, conventions, findings, assembly (:1104) — as one opaque await. It emits no progress events.
scan.ts:436-445 wraps the entire call in one spinner: ora('Scanning project...').start() → await scanProject(...) → spinner.stop(). The spinner text never changes.
- So the gap is structural: the engine emits nothing, so the CLI can't render anything. A prettier spinner alone won't fix it.
Proposed approach
1. Instrument the engine with an optional progress callback (the real work)
Add to scanProject options:
interface ScanOptions {
depth: 'surface' | 'deep';
onProgress?: ProgressReporter; // optional — undefined = silent (preserves all current callers)
}
interface ProgressReporter {
phaseStart(name: string, total?: number): void; // total optional for indeterminate phases
phaseDone(name: string, summary?: string): void; // summary line, e.g. "TypeScript + Vitest"
itemDone?(name: string): void; // optional per-item tick (e.g. files parsed)
}
Call it at each existing numbered phase boundary. The phases are already cleanly delimited in scanProject, so this is additive, low-risk instrumentation — no control-flow change. Deep-tier file parsing (parseProjectFiles) is the one phase worth a per-item total (file count) so it can show "Parsing 268/447 files".
Must preserve fail-soft: phases are wrapped in try/catch (the engine is designed to degrade gracefully). Progress calls must never throw out of a phase; a failed phase reports phaseDone(name, 'skipped') rather than crashing the display.
2. Fast pre-scan summary panel ("here's your repo")
Before the heavy deep tier, render an instant panel from census data (already computed cheaply): file count, language breakdown, monorepo packages, commit count. This is the "wow, it already knows my repo" moment and it's nearly free — the census (buildCensus, scan-engine.ts:717) and the git detector already produce these counts.
3. Render live in the CLI
Replace the single ora spinner in scan.ts:436 with a task-list renderer driven by the progress events. Recommended: listr2 (the Node analog of Python rich.Live — animated task tree, per-task spinner → checkmark, subtext lines). Alternatives: log-update + hand-rolled rendering, or cli-progress for the parse bar. listr2 is the natural fit and handles non-TTY/CI fallback.
Wire it so the CLI constructs a ProgressReporter that updates listr2 tasks, and passes it as onProgress. When --json or --quiet (scan.ts:436), pass no onProgress (silent — current behavior preserved exactly).
Phasing
- v1:
onProgress plumbed through scanProject + phase-boundary calls; listr2 task list in scan.ts showing each phase ticking to a checkmark with a one-line summary; pre-scan panel. Deep-tier parse shows a file count bar.
- Full: per-item ticks across more phases; richer summaries (findings count, hot files) folded into phase-done lines; a short "scan complete" result panel; tune timing so fast repos still feel substantial (don't flash-and-vanish).
Gotchas / constraints
--json / --quiet must be byte-identical to today — no progress output on those paths (stdout stays clean for machine consumption).
- Non-TTY / CI: listr2 degrades to plain line output; verify no ANSI garbage in piped/CI logs.
- Determinism: progress is transient UI only — it must not touch
scan.json content or ordering.
- Fail-soft: a phase that throws still advances the display (mark skipped); the scan must complete and save regardless.
- Performance: progress callbacks are cheap, but don't add per-file I/O just to count — reuse counts the census/sampler already have.
- Caller compatibility:
onProgress is optional; every existing scanProject caller (init, setup check, symbol-index) keeps working unchanged with no progress.
- Test count must not decrease: add tests for the reporter contract (phases fire in order, silent when omitted, fail-soft on phase error); existing scan tests stay green.
Acceptance criteria
ana scan (TTY) renders a pre-scan summary panel, then an animated per-phase task list where each phase shows a spinner → checkmark with a one-line summary; the deep-tier parse shows a file-count progress indicator.
ana scan --json and ana scan --quiet produce byte-identical stdout to today (no progress output).
- Piped/non-TTY/CI output contains no ANSI cursor garbage (listr2 fallback verified).
- A phase that throws is shown as skipped and the scan still completes and saves
scan.json (fail-soft preserved).
scan.json content and ordering are unchanged vs. current output for the fixture repos.
- All existing
scanProject callers (init, setup, symbol-index) compile and behave unchanged with onProgress omitted.
Effort
Medium — ~3–4 engineer-days. The rendering is straightforward (listr2 + a thin ProgressReporter adapter, ~0.5–1d). The bulk is instrumenting scanProject's phases with callback calls without disturbing the fail-soft design (~1–1.5d), the pre-scan panel from census (~0.5d), and tests + non-TTY/CI fallback + --json parity verification (~1d). Low architectural risk — the change is additive (optional callback) and the phases are already cleanly delimited.
Note
This is a CLI/engine change (not the website). It pairs naturally with a README hero GIF — a visible scan is exactly the asset the top-of-funnel needs.
Summary
ana scanis our most-run, most-shareable surface — it's instant, needs no API key, no subscription, and it's thenpx anatomia-cli scanfunnel (the first thing a new user ever runs). Today it shows a single frozenora('Scanning project...')spinner for the whole run, then dumps a result. The engine — our actual differentiator ("we have an engine, not a prompt library") — is completely invisible while it works.Make the scan show its work: a fast pre-scan summary panel ("here's your repo") followed by a live, animated, phase-by-phase progress display as the engine runs. This turns a 10-second black box into a screenshot/GIF-able moment that proves the engine is real.
Why it matters
scan.jsonoutput and determinism are unaffected.Current state (researched)
scanProject(rootPath, { depth })(scan-engine.ts:709) takes onlydepth. It runs ~12 cleanly-numbered sequential phases — census (:717), monorepo (:719), package manager (:731), deps (:733), three-tier identity (:740), project type (:764), framework, structure, deep-tier parse, patterns, conventions, findings, assembly (:1104) — as one opaqueawait. It emits no progress events.scan.ts:436-445wraps the entire call in one spinner:ora('Scanning project...').start()→await scanProject(...)→spinner.stop(). The spinner text never changes.Proposed approach
1. Instrument the engine with an optional progress callback (the real work)
Add to
scanProjectoptions:Call it at each existing numbered phase boundary. The phases are already cleanly delimited in
scanProject, so this is additive, low-risk instrumentation — no control-flow change. Deep-tier file parsing (parseProjectFiles) is the one phase worth a per-itemtotal(file count) so it can show "Parsing 268/447 files".Must preserve fail-soft: phases are wrapped in try/catch (the engine is designed to degrade gracefully). Progress calls must never throw out of a phase; a failed phase reports
phaseDone(name, 'skipped')rather than crashing the display.2. Fast pre-scan summary panel ("here's your repo")
Before the heavy deep tier, render an instant panel from census data (already computed cheaply): file count, language breakdown, monorepo packages, commit count. This is the "wow, it already knows my repo" moment and it's nearly free — the census (
buildCensus,scan-engine.ts:717) and the git detector already produce these counts.3. Render live in the CLI
Replace the single
oraspinner inscan.ts:436with a task-list renderer driven by the progress events. Recommended:listr2(the Node analog of Pythonrich.Live— animated task tree, per-task spinner → checkmark, subtext lines). Alternatives:log-update+ hand-rolled rendering, orcli-progressfor the parse bar. listr2 is the natural fit and handles non-TTY/CI fallback.Wire it so the CLI constructs a
ProgressReporterthat updates listr2 tasks, and passes it asonProgress. When--jsonor--quiet(scan.ts:436), pass noonProgress(silent — current behavior preserved exactly).Phasing
onProgressplumbed throughscanProject+ phase-boundary calls; listr2 task list inscan.tsshowing each phase ticking to a checkmark with a one-line summary; pre-scan panel. Deep-tier parse shows a file count bar.Gotchas / constraints
--json/--quietmust be byte-identical to today — no progress output on those paths (stdout stays clean for machine consumption).scan.jsoncontent or ordering.onProgressis optional; every existingscanProjectcaller (init, setup check, symbol-index) keeps working unchanged with no progress.Acceptance criteria
ana scan(TTY) renders a pre-scan summary panel, then an animated per-phase task list where each phase shows a spinner → checkmark with a one-line summary; the deep-tier parse shows a file-count progress indicator.ana scan --jsonandana scan --quietproduce byte-identical stdout to today (no progress output).scan.json(fail-soft preserved).scan.jsoncontent and ordering are unchanged vs. current output for the fixture repos.scanProjectcallers (init, setup, symbol-index) compile and behave unchanged withonProgressomitted.Effort
Medium — ~3–4 engineer-days. The rendering is straightforward (listr2 + a thin
ProgressReporteradapter, ~0.5–1d). The bulk is instrumentingscanProject's phases with callback calls without disturbing the fail-soft design (~1–1.5d), the pre-scan panel from census (~0.5d), and tests + non-TTY/CI fallback +--jsonparity verification (~1d). Low architectural risk — the change is additive (optional callback) and the phases are already cleanly delimited.Note
This is a CLI/engine change (not the website). It pairs naturally with a README hero GIF — a visible scan is exactly the asset the top-of-funnel needs.