Skip to content

feat: live, animated scan progress — make the engine visible (ana scan) #299

Description

@rpatricksmith

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

  1. 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.
  2. ana scan --json and ana scan --quiet produce byte-identical stdout to today (no progress output).
  3. Piped/non-TTY/CI output contains no ANSI cursor garbage (listr2 fallback verified).
  4. A phase that throws is shown as skipped and the scan still completes and saves scan.json (fail-soft preserved).
  5. scan.json content and ordering are unchanged vs. current output for the fixture repos.
  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions