Source code for ov

"""``ov`` -- OverView: agent-driven web reconnaissance & analysis.

Point ``ov`` at a URL and it drives the target like a user, recording two
parallel streams -- the *behavioral* stream (what the app does) and the *static*
stream (what it is made of) -- then runs two analysis lenses (UX and
software-design) and renders Markdown reports plus a single synopsis.

This module is the **progressive-disclosure facade**: the five top-level
functions below are all most callers need, and each works with sensible defaults.
Everything underneath (capture probes, operate primitives, analyzers, report
sections) is reachable but never required.

    >>> import ov
    >>> callable(ov.observe) and callable(ov.overview)
    True

The primary consumption model is a *host agent* (Claude Code) wielding these
functions via the skills in ``.claude/skills/`` -- the deterministic core here
runs with no model and no host.
"""

from __future__ import annotations

from typing import Any, Iterable

from .base import CaptureRun, RunDiff
from .config import OvConfig
from .util import check_requirements

# Naming note: the facade exposes ``analyze``/``report``/``overview`` as
# *functions* (the public API of §4). The internal subpackages are therefore
# named ``ov/analysis/`` and ``ov/reporting/`` (not ``analyze``/``report``) so a
# subpackage import can never shadow the same-named facade function on ``ov`` --
# a real Python attribute collision the spec's two halves would otherwise create.

__all__ = [
    "observe",
    "analyze",
    "diff",
    "report",
    "synopsis",
    "overview",
    "check_requirements",
    "OvConfig",
    "CaptureRun",
    "RunDiff",
    "__version__",
]

__version__ = "0.1.0"


[docs] def observe( url: str, *, goal: str | None = None, journey: list | None = None, mode: str = "reconstruct", probes: Any = "default", headed: bool = False, store: Any = None, crawl_pages: int | None = None, config: OvConfig | None = None, authorized: bool | None = None, ) -> CaptureRun: """Drive the target and capture everything. Zero-config default works. Args: url: the target to study. goal: a natural-language objective (goal-pursuit is *host* policy; the deterministic core records it and does the baseline journey). journey: an optional scripted action list (guided-replay), each item an :class:`~ov.base.Action` or a dict like ``{"type": "click", "ref": "e3"}``. mode: ``"reconstruct"`` (foreign target) or ``"review"`` (own target). probes: ``"default"``, ``"all"``, or an iterable of probe names. headed: run the browser headed (default headless). store: a :class:`~ov.capture.stores.CaptureStore`, a path, or ``None``. crawl_pages: if set (>1), politely crawl that many same-origin pages. config: an explicit :class:`OvConfig` (overrides ``headed``). authorized: acknowledge authorization to study a *foreign* target. Returns: The populated :class:`~ov.base.CaptureRun` (also persisted to the store). """ from .capture.session import CaptureSession from .operate.driver import crawl as _crawl from .operate.driver import replay as _replay cfg = config or OvConfig.from_env(headed=headed) if authorized is not None: cfg.authorized = authorized session = CaptureSession( target_url=url, config=cfg, store=store, mode=mode, probes=probes ) with session: session.open() if goal: session.run.notes.append(f"goal: {goal} (goal-pursuit is host policy)") if journey: _replay(session, journey) elif crawl_pages and crawl_pages > 1: _crawl(session, max_pages=crawl_pages) return session.run
[docs] def analyze( run: Any, *, lenses: Iterable[str] = ("ux", "arch"), llm: Any = None, store: Any = None, ) -> dict: """Run the deterministic UX + architecture analyzers over a captured run. Runs fully without a model (``llm=None`` is the default and the only path needed in Phases 1-2). The host agent reasons over the returned analyses + evidence bundle to add narrative judgment. """ try: from .analysis.run import run_analysis except ModuleNotFoundError as e: # the deterministic analyzers land in Phase 2 if (e.name or "").startswith("ov.analysis"): raise NotImplementedError( "ov.analyze (deterministic UX + architecture analyzers) is a Phase 2 feature" ) from e raise return run_analysis(run, lenses=tuple(lenses), store=store)
[docs] def diff( run: Any, *, baseline: Any = None, store: Any = None, ) -> RunDiff | None: """Diff an analyzed run against a prior baseline run (own-target review mode). The distinguishing capability of ``mode="review"``: report what is *new*, *changed*, or *resolved* versus a stored prior run of the same target (regression/drift detection, §10). Fully deterministic -- no browser, no model. Args: run: an analyzed :class:`~ov.base.CaptureRun` or a run id. baseline: a run/run-id to compare against, or ``None`` to auto-discover the latest prior run of the same target. store: a :class:`~ov.capture.stores.CaptureStore`, a path, or ``None``. Returns: A :class:`~ov.base.RunDiff`, or ``None`` if no baseline exists yet (the first review run). Sets ``Finding.diff_status`` on the run in place and persists a ``diff_<run_id>`` blob that the review report + synopsis read. """ from .analysis.diff import build_diff return build_diff(run, baseline=baseline, store=store)
[docs] def report( run_or_analyses: Any, *, sections: Any = "default", out_dir: Any = None, store: Any = None, ) -> list: """Render Markdown reports from analyses. Returns the written paths/keys.""" try: from .reporting.render import render_reports except ModuleNotFoundError as e: # report rendering lands in Phase 2 if (e.name or "").startswith("ov.reporting"): raise NotImplementedError( "ov.report (Markdown report rendering) is a Phase 2 feature" ) from e raise return render_reports( run_or_analyses, sections=sections, out_dir=out_dir, store=store )
[docs] def synopsis(reports_or_dir: Any, *, out: Any = None, store: Any = None) -> Any: """Extract-and-aggregate many reports into one synopsis for downstream agents.""" try: from .reporting.synopsis import build_synopsis except ModuleNotFoundError as e: # the synopsis map-reduce lands in Phase 2 if (e.name or "").startswith("ov.reporting"): raise NotImplementedError( "ov.synopsis (extract-and-aggregate synopsis) is a Phase 2 feature" ) from e raise return build_synopsis(reports_or_dir, out=out, store=store)
[docs] def overview(url: str, **kw: Any) -> Any: """``observe -> analyze -> report -> synopsis``, the one-liner. Returns the synopsis path/handle. This is the pit-of-success entry point. """ try: from .reporting.overview import run_overview except ModuleNotFoundError as e: # the full pipeline lands in Phase 2 if (e.name or "").startswith("ov.reporting"): raise NotImplementedError( "ov.overview (observe -> analyze -> report -> synopsis) is a Phase 2 feature; " "ov.observe(url) is available now" ) from e raise return run_overview(url, **kw)