[ast-outline]¶
A CLI that prints the structural outline of a source file — classes, methods, signatures with line numbers, no method bodies. Output is 2–10× smaller than the source, built for AI coding agents (Claude Code, Cursor, Codex CLI) that scan a repo before reading any file in full.
See it live¶
Each block is real ast-outline stdout — doc-comments, type
annotations, decorators, line ranges, no method bodies — syntax-
highlighted by Pygments. The italic line under the picker is context
(codebase, file kind), not part of the output.
Real codebase — ast-outline/cli.py (this project itself).
# src/ast_outline/cli.py [large] (563 lines, ~5,326 tokens, 2 types, 12 methods, 6 fields)
SUBCOMMANDS L30
class _LLMArgumentParser(argparse.ArgumentParser) L33-51
"""ArgumentParser that doesn't ``sys.exit`` on bad args.
Default ``argparse`` behavior on bad arguments is to print to stderr
and call ``sys.exit(2)``. For an LLM-facing CLI that breaks parallel
bash chains in Claude Code. Instead we raise a sentinel exception
that ``main()`` turns into a short ``# note:`` line on stdout..."""
def error(self, message: str) -> None L43-44
def exit(self, status: int = 0, message: str | None = None) -> None L46-51
def main(argv: list[str] | None = None) -> int L58-132
def _cmd_version(_args) -> int L135-146
"""Print version + authorship in the standard `tool x.y.z` form..."""
def _cmd_prompt(_args) -> int L149-158
"""Print the canonical copy-paste LLM-agent prompt snippet verbatim."""
Order-processing pipeline — branded types, abstract base class, async pipeline + compensation. Outline collapses 183 lines / ~1.5k tokens of source down to a single screen.
# src/services/order_service.ts [medium] (183 lines, ~1,549 tokens, 7 types, 15 methods, 22 fields)
export type OrderId = string & { readonly __brand: "OrderId" } L11
export type SkuId = string & { readonly __brand: "SkuId" } L12
export type CustomerId = string & { readonly __brand: "CustomerId" } L13
export const MAX_LINE_ITEMS = 100 L15
export const DEFAULT_CURRENCY: Currency = "USD" L16
export const RESERVATION_TTL_SEC = 600 L17
export interface LineItem L19-23
sku: SkuId L20
quantity: number L21
unitPrice: Money L22
export interface Order L25-31
id: OrderId L26
customer: CustomerId L27
items: LineItem[] L28
shippingAddress: Address L29
placedAt: Date L30
export interface Address L33-40
line1: string L34
line2?: string L35
city: string L36
region: string L37
postalCode: string L38
country: string L39
export type OrderState = | { kind: "pending" } | { kind: "validated"; total: Money } | { kind: "reserved"; total: Money; reservationId: string } | { kind: "paid"; total: Money; reservationId: string; chargeId: string } | { kind: "shipped"; total: Money; trackingNumber: string } | { kind: "failed"; reason: string } L42-48
export class OrderValidationError extends Error L50-55
constructor(public readonly order: Order, message: string) L51-54
export class StockReservationError extends Error L57-62
constructor(public readonly sku: SkuId, public readonly requested: number) L58-61
abstract class StagedProcessor<TIn, TOut> L64-72
constructor(protected readonly log: Logger) L65
protected logEntry(stage: string, info: Record<string, unknown>): void L69-71
export class OrderService extends StagedProcessor<Order, OrderState> L74-175
private readonly inflight L75
constructor( private readonly payments: PaymentProvider, private readonly stock: StockProvider, private readonly shipments: ShipmentProvider, log: Logger, ) L77-84
/** Run the full pipeline. Returns the terminal state. */
async process(order: Order): Promise<OrderState> L87-103
/** Cancel an in-flight order. No-op if the order has already settled. */
cancel(orderId: OrderId): boolean L106-111
private validate(order: Order): void L113-125
private computeTotal(order: Order): Money L127-133
private async reserveStock(order: Order): Promise<{ reservationId: string }> L135-145
private async capturePayment(order: Order, total: Money): Promise<{ chargeId: string }> L147-154
private async dispatchShipment( order: Order, reservation: { reservationId: string }, ): Promise<{ trackingNumber: string }> L156-165
private async compensate(order: Order, err: Error): Promise<void> L167-174
export function totalQuantity(order: Order): number L177-179
export const isShipped = (s: OrderState): s is Extract<OrderState, { kind: "shipped" }> => s.kind === "shipped" L181-182
Node service — payment-router with circuit-breaker, retry budget, fan-out health checks. Parsed by the TypeScript grammar, so React / ES modules / CommonJS all flow through.
# services/payments/router.js [medium] (150 lines, ~1,335 tokens, 2 types, 12 methods, 4 fields)
const PROCESSORS = ["stripe", "adyen", "braintree", "worldpay"] L10
const TIER_PRIORITY = { enterprise: ["stripe", "adyen"], business: ["adyen", "braintree", "stripe"], consumer: ["stripe", "braintree", "w... L11-15
const DEFAULT_RETRY = { attempts: 3, baseDelayMs: 250, maxDelayMs: 5000 } L17
const HEALTH_CHECK_INTERVAL_MS = 30_000 L18
class PaymentError extends Error L20-28
constructor(message, { code, retryable = false, processor } = {}) L21-27
class PaymentRouter extends EventEmitter L30-139
constructor({ processors, breakerOpts = {}, logger = new Logger("router") } = {}) L31-40
/**
* Attempt to charge `amount` using the best available processor.
* Returns a settled charge or throws PaymentError after exhausting retries.
*/
async charge(transaction) L46-69
/** Voids a previous successful charge. */
async refund(charge, amount = charge.amount) L72-80
startHealthChecks() L82-85
stopHealthChecks() L87-92
_validate(transaction) L94-102
_candidatesFor(transaction) L104-108
async _chargeWithRetry(name, transaction, opts = DEFAULT_RETRY) L110-125
async _sweepHealth() L127-138
function sleep(ms) L141-143
function summarize(charge) L145-147
Inventory crate — struct / enum / trait / impl with newtypes, lifetimes, generics, #[derive] and /// doc-comments.
# src/inventory.rs [medium] (192 lines, ~1,525 tokens, 9 types, 16 methods, 15 fields)
/// Stable SKU identifier — opaque to consumers.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Sku(pub u64) L12
pub u64 L12
/// Distinct warehouse keyed by region.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct WarehouseId(pub u32) L16
pub u32 L16
/// Customer account — only the fields the inventory layer cares about.
#[derive(Debug, Clone, Eq, PartialEq)] pub struct Account L20-24
pub id: u64 L21
pub tier: AccountTier L22
pub region: String L23
/// Account priority for reservation conflicts.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum AccountTier L28-32
Standard L29
Plus L30
Enterprise L31
/// Result of a reservation attempt against a warehouse.
#[derive(Debug, Clone)] pub enum ReservationOutcome L36-45
Granted { reservation_id: u64, expires_at: Instant } L38
Partial { granted: u32, requested: u32, reservation_id: u64 } L40
OutOfStock { available: u32, requested: u32 } L42
UnknownSku L44
/// Anything that can be reserved against (a warehouse, a virtual bucket).
pub trait Reservable L48-52
fn reserve(&mut self, sku: Sku, qty: u32) -> ReservationOutcome L49
fn release(&mut self, reservation_id: u64) -> bool L50
fn on_hand(&self, sku: Sku) -> u32 L51
/// Concrete warehouse — a flat per-SKU ledger with a small reservation log.
#[derive(Debug)] pub struct Warehouse L56-136
pub id: WarehouseId L57
on_hand: HashMap<Sku, u32> L58
reservations: HashMap<u64, ReservationEntry> L59
next_id: u64 L60
ttl: Duration L61
pub fn new(id: WarehouseId, ttl: Duration) -> Self L73-81
pub fn receive(&mut self, sku: Sku, qty: u32) L84-86
pub fn sweep_expired(&mut self, now: Instant) -> u32 L89-93
fn allocate_id(&mut self) -> u64 L95-99
fn reserve(&mut self, sku: Sku, qty: u32) -> ReservationOutcome L103-121
fn release(&mut self, reservation_id: u64) -> bool L123-131
fn on_hand(&self, sku: Sku) -> u32 L133-135
/// Cross-warehouse router. Picks a warehouse per request based on region
/// affinity, then falls back to any warehouse with stock.
pub struct InventoryRouter L140-180
warehouses: Vec<Warehouse> L141
pub fn new(warehouses: Vec<Warehouse>) -> Self L145-147
pub fn warehouse_count(&self) -> usize L149-151
pub fn reserve_for(&mut self, account: &Account, sku: Sku, qty: u32) -> ReservationOutcome L155-173
fn preferred_index(&self, account: &Account) -> Option<usize> L175-179
pub fn total_on_hand<R: Reservable>(stores: &[R], sku: Sku) -> u32 L187-189
pub const MAX_RESERVATION_QTY: u32 = 10_000 L191
Unity MonoBehaviour — [Attribute] / [Header] decorators, properties, events, generics, nested enum + struct, all preserved with their XML doc-comments.
# Assets/Scripts/HeroController.cs [medium] (193 lines, ~1,570 tokens, 7 types, 19 methods, 23 fields)
namespace Demo.Combat
/// <summary>
/// Player-controlled hero. Owns movement input, health, status
/// effects and combat events. Built as a MonoBehaviour so it
/// can sit on a prefab and read serialized data from the editor.
/// </summary>
[RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(Animator))] public class HeroController : MonoBehaviour, IDamageable, IHealable L14-158
[Header("Movement")] [SerializeField] private float _moveSpeed = 5f L18-19
[SerializeField] private float _jumpForce = 10f L20
[SerializeField] private LayerMask _groundLayer L21
[Header("Combat")] [SerializeField] private int _maxHealth = 100 L23-24
[SerializeField] private float _invulnerabilityTime = 0.6f L25
[SerializeField] private AudioClip _hurtSound L26
[Header("References")] [SerializeField] private Transform _attackOrigin L28-29
[SerializeField] private GameObject _hitEffectPrefab L30
public int CurrentHealth { get; private set; } L32
public bool IsAlive => CurrentHealth > 0 L33
public bool IsGrounded { get; private set; } L34
/// <summary>Fired whenever current health changes. Argument is the new value.</summary>
public event Action<int> OnHealthChanged L37
/// <summary>Fired exactly once when the hero dies.</summary>
public event Action OnDied L40
public UnityEvent<DamageInfo> OnDamageTaken = new UnityEvent<DamageInfo>() L42
private Rigidbody2D _body L44
private Animator _animator L45
private readonly List<IStatusEffect> _activeEffects = new List<IStatusEffect>() L46
private float _invulnerableUntil = 0f L47
private State _state = State.Idle L48
private void Awake() L50-55
private void Update() L57-63
private void FixedUpdate() L65-68
private void HandleMovementInput() L70-75
public void Jump() L77-81
/// <summary>Apply incoming damage. Respects current invulnerability window.</summary>
public void TakeDamage(DamageInfo info) L85-95
/// <summary>Restore HP up to <see cref="_maxHealth"/>.</summary>
public void Heal(int amount) L98-103
public void ApplyEffect(IStatusEffect effect) L105-109
public IEnumerator DashCoroutine(Vector2 direction, float duration) L111-118
private int GetResistance(DamageElement element) L120-125
private void TickEffects(float deltaTime) L127-134
private void UpdateAnimator() L136-141
private void Die() L143-148
public enum State L150-157
Idle, Moving, Attacking, Hurt, Dead
public interface IDamageable L160-163
public interface IHealable L165-168
public interface IStatusEffect L170-176
bool IsExpired { get; } L172
void OnApply(HeroController hero) L173
void Tick(HeroController hero, float deltaTime) L174
int ResistanceFor(DamageElement element) L175
public enum DamageElement L178
Physical, Fire, Frost, Lightning, Holy
public readonly struct DamageInfo L180-191
public readonly int Amount L182
public readonly DamageElement Element L183
public readonly GameObject Source L184
public DamageInfo(int amount, DamageElement element, GameObject source) L185-190
Unreal Engine actor — UCLASS / UPROPERTY / UFUNCTION render as decorators on the right declaration. Plain modern C++ works the same way without UE.
# MyActor.h [medium] (97 lines, ~548 tokens, 6 types, 9 methods, 7 fields)
UENUM(BlueprintType) enum class EWeaponSlot : uint8 L15-20
Primary L17
Secondary L18
Sidearm L19
USTRUCT(BlueprintType) struct FItemData L23-35
UPROPERTY(EditAnywhere, BlueprintReadWrite) FString Name L28
UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Count L31
UPROPERTY() EWeaponSlot Slot L34
UINTERFACE(MinimalAPI, Blueprintable) class UInteractable : public UInterface L38-41
class IInteractable L43-50
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interaction") void Interact() L49
UCLASS(Blueprintable, BlueprintType) class AMyActor : public AActor, public IInteractable L53-81
AMyActor() L58
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stats") float Health L61
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components") UStaticMeshComponent* Mesh L64
UPROPERTY(EditDefaultsOnly, Category="Combat") TArray<FItemData> Inventory L67
UFUNCTION(BlueprintCallable, Category="Combat") virtual void TakeDamage(float Amount) L70
UFUNCTION(BlueprintNativeEvent, Category="Events") void OnHit() L73
UFUNCTION() void HandleOverlap(AActor* Other) L76
virtual void BeginPlay() override L79
virtual void Tick(float DeltaSeconds) override L80
Spring service — Javadoc, multiple annotations, inheritance, generics, throws clauses, nested static classes for events. The outline collapses 162 lines of source body to a one-screen API surface.
# UserService.java [medium] (162 lines, ~1,287 tokens, 4 types, 13 methods, 11 fields)
namespace com.example.demo.service
/**
* Service layer for user accounts.
*
* <p>Owns persistence, in-memory caching, and event fan-out. Other
* services should never reach into the repository directly — go
* through this service so audit logs stay accurate.
*
* <p>Thread-safety: read-mostly cache guarded by a
* {@link ReentrantReadWriteLock}.
*/
@Service public class UserService extends BaseService implements UserRepository, AutoCloseable L25-161
/** Hard cap on concurrent users. */
public static final int MAX_USERS = 10_000 L29
/** Cache TTL applied to every {@link #findById} hit. */
public static final long CACHE_TTL_SECONDS = 300L L32
private final UserDao dao L34
private final EventBus events L35
private final Map<Long, CachedUser> cache = new HashMap<>() L36
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock() L37
private volatile boolean closed = false L38
@Autowired public UserService(UserDao dao, EventBus events) L40-44
/** Persist a user — new or existing. */
@Override @Transactional public void save(User user) throws IOException, IllegalArgumentException L52-66
/** Look up a user by id, hitting the cache when fresh. */
public Optional<User> findById(long id) L69-87
/** Bulk fetch — preserves request order, missing ids are skipped. */
public List<User> findAllById(List<Long> ids) L90-94
/** Remove a user. Idempotent — returns false if no row was deleted. */
@Transactional public boolean delete(long id) L97-108
private static <T extends Comparable<T>> T findMax(List<T> items) L110-115
private void ensureCapacity() L117-121
private void validate(User user) L123-127
@Override public void close() L129-133
/** Cache entry — stamped with the time it was admitted. */
public static final class CachedUser L136-148
public final User user L137
public final Instant cachedAt L138
public CachedUser(User user, Instant cachedAt) L140-143
public boolean isFresh() L145-147
public static final class UserSaved L151-154
public final long userId L152
public UserSaved(long userId) L153
public static final class UserDeleted L157-160
public final long userId L158
public UserDeleted(long userId) L159
Android Compose screen + ViewModel + intent system — data class, sealed class / sealed interface discriminated unions, suspend, companion object, @Composable, top-level extension function.
# app/src/main/kotlin/ProfileScreen.kt [medium] (199 lines, ~1,733 tokens, 14 types, 11 methods, 19 fields)
namespace com.example.app.ui.profile
/**
* Public-facing user profile shape — what we render on the screen.
*/
data class UserProfile( val id: String, val name: String, val handle: String, val avatarUrl: String?, val bio: String, val followers: Int, val following: Int, ) L32-40
val id: String L33
val name: String L34
val handle: String L35
val avatarUrl: String? L36
val bio: String L37
val followers: Int L38
val following: Int L39
/** Discriminated union of the screen's possible states. */
sealed class ProfileState L43-48
object Loading : ProfileState() L44
data class Success(val profile: UserProfile, val isCurrentUser: Boolean) : ProfileState() L45
data class Error(val message: String, val retryable: Boolean = true) : ProfileState() L46
object Empty : ProfileState() L47
/** UI-side intents the screen can emit. ViewModel folds them into state. */
sealed interface ProfileIntent L51-57
data class Load(val userId: String) : ProfileIntent L52
object Refresh : ProfileIntent L53
object Follow : ProfileIntent L54
object Unfollow : ProfileIntent L55
data class EditBio(val newBio: String) : ProfileIntent L56
@HiltViewModel class ProfileViewModel @Inject constructor( private val repository: ProfileRepository, private val authProvider: AuthProvider, ) : ViewModel() L59-123
private val repository: ProfileRepository L61
private val authProvider: AuthProvider L62
private val _state = MutableStateFlow<ProfileState>(ProfileState.Loading) L65
val state: StateFlow<ProfileState> = _state.asStateFlow() L66
private var currentUserId: String? = null L68
fun handle(intent: ProfileIntent) L70-78
private fun load(userId: String) L80-96
private fun changeFollow(follow: Boolean) L98-104
private fun editBio(newBio: String) L106-112
/** Suspend helper — preloads followers, used by the parent screen. */
suspend fun preloadFollowers(): List<UserProfile> L115-118
companion object L120-122
const val MAX_BIO_LENGTH = 280 L121
@Composable fun ProfileScreen( userId: String, viewModel: ProfileViewModel = hiltViewModel(), onNavigateToFollowers: (String) -> Unit = {}, ) L125-150
@Composable private fun LoadingBlock() L152-158
@Composable private fun EmptyBlock() L160-163
@Composable private fun ErrorBlock(message: String, onRetry: (() -> Unit)?) L165-173
@Composable private fun ProfileBlock( profile: UserProfile, isCurrentUser: Boolean, onFollow: () -> Unit, onUnfollow: () -> Unit, onShowFollowers: () -> Unit, ) L175-195
/** Extension — small convenience for previews and tests. */
fun UserProfile.shortLabel(): String L198
Scala 3 — enum, given, extension. Indentation-based bodies.
# core/src/main/scala/Json.scala [medium] (118 lines, ~492 tokens, 3 types, 7 methods)
package json L1
enum Json L4-12
case JNull [enum_member] L5
case JBool(value: Boolean) [enum_member] L6
case JNumber(value: Double) [enum_member] L7
case JString(value: String) [enum_member] L8
case JArray(items: List[Json]) [enum_member] L9
case JObject(fields: Map[String, Json]) [enum_member] L10
trait Encoder[A] L15-17
def encode(value: A): Json
object Encoder L19-58
given Encoder[Int] L21-22
given Encoder[String] L24-25
given [A](using e: Encoder[A]): Encoder[List[A]] L27-30
extension [A](value: A)(using enc: Encoder[A]) L33-37
def toJson: Json L34
def toJsonString: String L36
Standard service — methods grouped under their receiver type.
# user_service.go [medium] (105 lines, ~635 tokens, 4 types, 9 methods, 9 fields)
namespace service
const MaxUsers = 100 L16
var GlobalCounter int = 0 L23
// BaseService is a top-level service primitive other services embed.
type BaseService struct L29-44
Name string L30
closed bool L32
// Open marks the service as ready.
func (b *BaseService) Open() L36-38
// close is unexported.
func (b *BaseService) close() error L41-44
// UserService is the primary user-facing service.
//
// Embeds BaseService — Go's idiom for "extends BaseService" — and
// implements the io.Closer interface contract via Close().
type UserService struct L50-78
Repo Repository L52
cache map[string]any L53
// Save persists a user; returns an error on failure.
func (u *UserService) Save(user string) error L57-62
// Close satisfies io.Closer.
func (u *UserService) Close() error L76-78
// Repository is the contract user-stores must satisfy.
type Repository interface L81-88
Get(id string) (string, error) L83
List() []string L85
Laravel-style service — PHP 8 readonly fields, ctor property promotion, typed constants, @deprecated markers, abstract class + free functions.
# app/Services/UserService.php [medium] (181 lines, ~1,226 tokens, 2 types, 15 methods, 9 fields)
namespace App\Service
/**
* Coordinates user-related use cases.
*
* Owns persistence, simple in-memory caching, audit logging and event
* fan-out. Controllers should never reach into the repository directly
* — go through this service so audit logs stay accurate.
*/
final class UserService L23-163
private readonly Repository $repository L33
private readonly EventDispatcherInterface $events L34
private readonly LoggerInterface $logger L35
protected int $maxCacheSize = self::MAX_CACHE_SIZE L36
public const string DEFAULT_ROLE = 'guest' L25
public const int MAX_CACHE_SIZE = 1_000 L26
public const int CACHE_TTL_SECONDS = 300 L27
/** @var array<int, array{user: User, expires: int}> */
private array $cache = [] L30
public function __construct( private readonly Repository $repository, private readonly EventDispatcherInterface $events, private readonly LoggerInterface $logger, protected int $maxCacheSize = self::MAX_CACHE_SIZE, ) L32-38
/** Fetch a user by id, hitting the cache when fresh. */
public function getUser(int $id): User L45-57
/** @deprecated Use {@see getUser()} instead. */
public function loadUser(int $id): ?User L60-67
/** Save a user — new or existing. */
public function save(User $user): User L73-81
/** Bulk fetch — preserves request order, missing ids are skipped. */
public function getMany(array $ids): array L89-100
/** Remove a user. Returns false if no row was deleted. */
public function delete(int $id): bool L103-112
/** Build an order on behalf of a user. */
public function makeOrder(User $user, array $items): OrderModel L115-119
/** Promote / demote a user's role. */
public function changeRole(int $id, UserRole $role): User L122-127
public function flushCache(): void L129-132
private function validate(User $user): void L134-139
private function prime(User $user): void L141-147
private function evictOldest(): void L149-162
abstract class BaseService L165-173
abstract public function name(): string L167
final public function tag(): string L169-172
function make_user_service(Repository $r, EventDispatcherInterface $e, LoggerInterface $l): UserService L175-178
const APP_VERSION = '1.0.0' L180
Rails ActiveRecord-style model — has_many / belongs_to associations and attr_accessor surface as fields, mixins on the type header.
# app/models/user.rb [medium] (158 lines, ~824 tokens, 2 types, 24 methods, 14 fields)
# == Schema
#
# Mirrors the migration in `db/schema.rb`.
class User < ApplicationRecord L8-132
MAX_NAME_LENGTH L13
DEFAULT_ROLE L14
PASSWORD_MIN_LENGTH L15
[accessor] unhashed_password L18
[reader] id L19
[reader] created_at L19
[has_many] posts L21
[has_many] comments L22
[has_many] follows L23
[has_many] following L24
[belongs_to] company L25
[belongs_to] primary_team L26
# Build a User from raw attributes; assigns the default role
# when none is provided.
def initialize(attrs = {}) L42-45
# Display name shown in headers and lists.
def display_name L48-50
# Whether the user is an admin — convenience for views.
def admin? L53-55
def soft_delete! L57-59
def restore! L61-63
# Returns a hash of the email — used for gravatar-style URLs.
def email_hash L66-68
def follow!(other) L70-72
def unfollow!(other) L74-76
def following?(other) L78-80
# Class-level finder that takes a fuzzy name string.
[static] def self.find_by_name(query) L83-85
[static] def self.bulk_import(rows) L87-91
def <=>(other) L93-96
def ==(other) L98-100
def [](key) L102-104
def []=(key, value) L106-108
[static] def archived L111-113
[static] def with_role(role) L115-117
def normalize_email L122-124
def send_welcome_email L126-128
[alias] to_s → display_name L130
[alias] follower_count → followers_count L131
# Query object for user-facing search forms.
class UserQuery L135-157
def initialize(scope = User.active) L136-138
def by_name(name) L140-143
def in_company(company) L145-148
def page(num, size: 25) L150-152
def to_a L154-156
Also recognised: class << self blocks, alias / alias_method, private / protected state machine (private :foo, :bar flips named methods retroactively), Rakefile / Gemfile (resolved by basename, no extension needed). The MRO clause : ApplicationRecord, include Comparable, extend Searchable shows superclass + mixins as one inheritance line in the digest.
Design tokens + components — :root token block, themed selectors via [data-theme=dark], @media / @keyframes / @layer / @font-face, native nesting with &.
# src/styles/theme.css [medium] (260 lines, ~1,456 tokens)
:root L8-39
[data-theme="dark"] L41-48
[data-theme="high-contrast"] L50-54
*, *::before, *::after L59-63
html L65-68
body L70-76
.container L81-85
.stack > * + * L87-89
.cluster L91-96
.grid-12 L98-102
#main-header L104-109
#main-header > .nav .item:hover L111-113
.btn-primary, .btn-secondary, .btn-danger L118-128
.btn-primary L130-133
.btn-primary:hover L135-137
.btn-secondary L139-143
.btn-danger L145-148
.modal .btn-primary[disabled] L150-153
input[type="text"], input[type="email"], textarea L155-164
input:focus, textarea:focus L166-169
.card L174-179
.card .card-title L181-184
.card .card-body L186-188
.card.is-featured L190-192
@media (max-width: 768px) L197-207
.container L198-200
.btn-primary, .btn-secondary, .btn-danger L201-203
.grid-12 L204-206
@media (min-width: 769px) and (max-width: 1024px) L209-213
.container L210-212
@media (prefers-reduced-motion: reduce) L215-220
* L216-219
@keyframes fadeIn L225-228
@keyframes slideUp L230-233
@layer base L235-243
h1, h2, h3 L236-239
code, pre L240-242
@font-face L245-250
:is(.alert, .warning, .error) L252-255
:not(.disabled):hover L257-259
find_symbols(".btn-primary") returns every cascade-relevant rule — top-level group, the @media override, and .modal .btn-primary[disabled] descendant — with the wrapping at-rule visible in the breadcrumb. Pseudo-classes and attribute filters are stripped for matching, so .btn-primary:hover and .btn-primary[disabled] both match .btn-primary. :is() / :where() arguments recurse (additive); :not() / :has() don't.
Component stylesheet — & resolves against the parent, so nested rules become findable as their fully-qualified BEM selectors. Mixins and functions render as callables.
# styles/_components.scss [medium] (263 lines, ~1,464 tokens, 5 methods)
$primary: #007bff !default L8
$secondary: #6c757d !default L9
$danger: #dc3545 !default L10
$success: #28a745 !default L11
$radius-sm: 4px !default L12
$radius-md: 8px !default L13
$radius-lg: 16px !default L14
$transition-fast: 0.15s ease !default L15
$breakpoint-sm: 480px L17
$breakpoint-md: 768px L18
$breakpoint-lg: 1024px L19
%button-base L24-33
%card-base L35-40
@mixin button($bg: $primary, $fg: white, $size: medium) L45-71
@mixin elevated($level: 1) L73-81
@mixin respond-to($name) L83-91
@function strip-unit($value) L93-95
@function fluid($min, $max, $min-vw: 320, $max-vw: 1200) L97-105
.btn L110-112
.btn-primary L114-116
.btn-secondary L118-126
.btn-secondary.disabled L122-125
.btn-danger L128-130
.btn-success L132-134
.card L139-156
h2 L147-150
.card__actions L152-155
.card__body L158-161
.card__footer L163-167
&.card--featured L169-180
.card__header L173-179
&.card--compact L182-188
.card__body L185-187
.form-field L194-229
label L199-202
input, textarea, select L204-213
input:focus L209-212
.form-field__hint L215-218
.form-field.is-invalid L220-228
input, textarea, select L221-223
.form-field__hint L225-227
@media print L254-262
.btn, .btn-primary, .btn-secondary, .btn-danger, .btn-success L255-257
.card L258-261
find_symbols(".card__header") returns every cascade-relevant definition — top-level, inside @media, themed under .card--featured — with the wrapping at-rule visible in the breadcrumb. :is(...) / :where(...) selectors recurse (additive); :not(...) / :has(...) don't. Pseudo-classes and attribute filters are stripped for matching, so .btn-primary finds the rule whether it carries :hover, [disabled], or sits in .modal. Plain CSS gets the same treatment minus the SCSS-specific symbols.
Kubernetes Deployment — format auto-detected, sequence items use [i] paths.
# deploy/k8s_deployment.yaml [tiny] (37 lines, ~187 tokens) — Deployment apps/v1 prod/api-server
apiVersion: apps/v1 L1
kind: Deployment L2
metadata: L3-7
name: api-server L4
namespace: prod L5
labels: L6-7
app: api L7
spec: L8-36
replicas: 3 L9
selector: L10-12
matchLabels: L11-12
app: api L12
template: L13-36
metadata: L14-16
labels: L15-16
app: api L16
spec: L17-36
containers: (2 items) L18-36
- api L19-34
name: api L19
image: registry.example.com/api:v1.2.3 L20
ports: (1 item) L21-22
- 8080 L22
containerPort: 8080 L22
Hierarchical TOC — heading levels indented, code blocks tagged by language.
# README.md [medium] (185 lines, ~1,091 tokens, 22 headings, 7 code blocks)
# Project Apollo L1-184
## Status L7-15
## Quickstart L16-54
bash code block L18-22
bash code block L26-28
### Prerequisites L32-38
### Running tests L39-54
bash code block L41-45
python code block L47-51
## Architecture L55-118
### Services L61-92
#### Orders L63-72
text code block L67-71
#### Payments L73-82
go code block L78-81
#### Warehouse L83-87
#### Shipments L88-92
### Data flow L93-110
mermaid code block L101-109
### Storage L111-118
## Configuration L119-139
### Secrets L132-139
## Deployment L140-162
### Release process L145-152
### Observability L153-162
## Contributing L163-181
### Coding conventions L169-174
### Reporting bugs L175-181
## License L182-184
Order-processing module — schema + audit trigger + two views + business-logic functions. The source is 95 lines; outline is 50 (1.9× by lines, 1.5× by chars). Tables keep all columns verbatim; PL/pgSQL function bodies, view SELECTs, and trigger timing/event details get stripped — agents see the schema shape and the function contracts, not the implementation.
# orders.sql [medium] (96 lines, ~674 tokens, 6 types, 3 methods, 18 fields)
# imports: CREATE EXTENSION IF NOT EXISTS pgcrypto
CREATE TYPE order_status AS ENUM L4
'pending' L4
'paid' L4
'shipped' L4
'cancelled' L4
CREATE TABLE customers L6-11
id BIGINT PRIMARY KEY L7
email TEXT NOT NULL UNIQUE L8
display_name TEXT L9
created_at TIMESTAMPTZ NOT NULL DEFAULT now() L10
CREATE TABLE orders L13-20
id BIGINT PRIMARY KEY L14
customer_id BIGINT NOT NULL REFERENCES customers(id) L15
total_cents INTEGER NOT NULL L16
status order_status NOT NULL DEFAULT 'pending' L17
placed_at TIMESTAMPTZ NOT NULL DEFAULT now() L18
updated_at TIMESTAMPTZ L19
-- Audit log captures every status transition for compliance.
CREATE TABLE order_audit L23-30
id BIGSERIAL PRIMARY KEY L24
order_id BIGINT NOT NULL L25
old_status order_status L26
new_status order_status NOT NULL L27
actor TEXT L28
changed_at TIMESTAMPTZ NOT NULL DEFAULT now() L29
-- Computes total cents the customer has paid across all paid orders.
CREATE FUNCTION customer_lifetime_value(cid BIGINT) RETURNS BIGINT L33-47
-- Audit hook: records every status change and stamps updated_at.
CREATE FUNCTION audit_order_status() RETURNS TRIGGER L50-59
CREATE TRIGGER orders_audit ON orders L61-64
-- Active orders for the dashboard — joined with customer email,
-- excluding cancellations and anything older than 90 days.
CREATE VIEW active_orders L68-81
-- Daily revenue rollup, refreshed nightly by a cron job.
CREATE MATERIALIZED VIEW daily_revenue L84-92
CREATE INDEX idx_orders_status ON orders(status, placed_at DESC) L94
CREATE INDEX idx_orders_customer ON orders(customer_id) L95
show customers returns the full CREATE TABLE block, show customers.email returns the single column line, show customer_lifetime_value returns the function header, show active_orders returns the view's SELECT — bodies are one show away when actually needed. PostgreSQL is the primary target — every modern construct works including CREATE PROCEDURE, CREATE DOMAIN, CREATE TABLE … PARTITION OF, SECURITY DEFINER functions, LOAD, and IMPORT FOREIGN SCHEMA (a regex fallback recovers what the upstream grammar errors on, gated by AST skip-ranges so red herrings inside comments and PL/pgSQL bodies don't surface). MySQL and SQLite schemas extract tables / columns / indexes / views cleanly with some error_count > 0 noise on dialect-specifics like ENGINE=InnoDB and AUTOINCREMENT.
Same digest format, same legend, same [broken] recovery semantics across
every language. Adding another is a single new adapter file —
src/ast_outline/adapters/.
Why use ast-outline¶
Modern agentic coding tools (Claude Code, Cursor's agent mode, Aider, Copilot Chat, custom CLI agents) explore codebases by reading files directly — not via embeddings or vector search. Reliable, but on a 1000-line file the agent pays for 1000 lines of tokens just to answer "what methods exist here?".
ast-outline is a pre-reading layer:
-
2–10× fewer tokens
An outline replaces a full file read when the agent only needs structural understanding.
-
Faster exploration
A whole module's public API fits on one screen —
digest src/gives the map in one call. -
Precise navigation
Every declaration carries a line range (
L42-58). The agent jumps straight to the body it needs. -
AST accuracy, not regex
showand inheritance rendering work on real syntax — no false positives from comments or string literals. -
Zero infrastructure
No index, no cache, no embeddings, no network. Always fresh, invisible to the repo.
-
One tool, every major language
C#, C++ (incl. Unreal Engine), Python, TypeScript, JavaScript, Java, Kotlin, Scala, Go, Rust, PHP, Ruby (incl. Rails), CSS, SCSS, SQL (PostgreSQL primary), Markdown, YAML — same digest format, same legend.
How it stacks up¶
| Need | grep/rg | ctags | ast-grep | ast-outline |
|---|---|---|---|---|
| "What's the public API of this module?" | no | flat list | no | yes |
"What methods does Player expose?" |
noisy | scattered | need a query | yes |
"Show me the body of TakeDamage" |
manual | jump-to-line | need a query | yes |
| Inheritance / trait bases visible | no | no | need a query | yes |
Find a pattern (if x && y) |
regex | no | yes | overview only |
| Fixed cost regardless of repo size | yes | build index | yes | yes |
ast-outline and ast-grep are siblings: ast-grep searches
code structurally, ast-outline overviews it. They compose well
— use digest for the map, ast-grep for cross-file pattern hunts.
Install¶
Don't have uv?
After install:
ast-outline --version # latest from PyPI
ast-outline path/to/Player.cs # outline one file
ast-outline digest src/ # whole-folder map
ast-outline help # full reference
Update / uninstall¶
Install with AI¶
With ast-outline on your machine, let your coding agent wire it
into your repo's persistent-context file — one paste, no manual
markdown editing:
One-line setup — paste into your agent
Works in Claude Code, Codex CLI, Gemini CLI, or Cursor.
Re-run after every ast-outline upgrade — the bundled snippet
is refreshed in place, your local edits are preserved.
The agent reads a one-shot install-time checklist from stdout and walks you through:
- Verify the CLI — runs
ast-outline --version. If missing, offersuv tool install(recommended),pipx install, orpip installand can install on your behalf with explicit consent. Best-effort PyPI version check; surfaces the matching upgrade command without auto-upgrading. - Pick the right file and write the snippet —
./AGENTS.mdis the cross-tool default (read by Codex CLI, Claude Code via@AGENTS.mdimport, Gemini CLI withsettings.jsonconfig, and Cursor); single-vendor users get the native file (./CLAUDE.md,./GEMINI.md); a project-local vs global scope choice (~/.claude/CLAUDE.md,~/.codex/AGENTS.md,~/.gemini/GEMINI.md) is offered. The snippet lives inside<!-- ast-outline:start -->/<!-- ast-outline:end -->markers so re-runs upgrade in place. Diff-aware: if the existing block differs from the fresh canonical, the agent shows the diff and asks before overwriting — your manual edits are never lost silently. - Optional subagent patches — finds exploration-oriented
subagent files in
.claude/agents//.codex/agents//.gemini/agents/and inserts a small## Tooling — ast-outlineblock, with per-agent permission.
Cross-vendor universal — same checklist works in Claude Code, Codex
CLI, Gemini CLI, and Cursor. Cross-OS — the agent translates
which, $VIRTUAL_ENV, curl examples to PowerShell or cmd.exe
on Windows. Adapts to whichever human language the surrounding
conversation is using.
For the manual equivalent (no agent involvement), see
ast-outline prompt.
Design philosophy¶
Stateless. No index, no cache, no embeddings, no network. Parse on demand, print, exit.
Opposite of RAG-style codebase indexers (Cursor, Bloop, Continue,
the embedding-MCP crowd). AI coding agents are sharp enough to chain
ast-outline with grep, find, ast-grep and other unix tools
and navigate real code fast — without reading whole files, and
without a local index earning its complexity.
Why no MCP server?¶
Same shape: a stateless CLI is the right form factor for this tool.
In coding agents (Claude Code, Cursor, Codex CLI, Aider) ast-outline
already runs through the agent's bash tool — composes with
grep / find / xargs, runs in parallel batches, and adds zero
registration overhead. An MCP server would either become a long-running
daemon (which kills the "parse on demand, exit" model) or a thin shim
around the same CLI — either way, more setup friction for users and a
second ABI to keep in sync, with no extra capability for the actual
target audience.
Concretely, these all fold into one agent round-trip — one bash
tool call, one stdout result:
# Filter the digest for one class across a whole tree
ast-outline digest src/ | rg 'class .*Service'
# Outline only files changed since a baseline
find src -name '*.py' -newer .last-review | xargs ast-outline --imports
# List deprecated symbols across a module
ast-outline digest src/ | grep '\[deprecated\]'
# Drive one ast-outline call from the output of another tool
ast-grep -l --pattern 'class $X extends Component' src \
| xargs ast-outline --imports
Through MCP the same outcomes are N+1 round-trips: the agent
calls list_files, then outline_file per match, then filters
results itself — each step paid for in context tokens and latency.
A pipe in bash does that work in the kernel, invisibly.
The narrow case where MCP would help is non-shell hosts (e.g. Claude Desktop). If demand shows up there, a wrapper takes a day to write — but it's a separate add-on, not a redesign.
Supported languages¶
| Language | Extensions |
|---|---|
| C# | .cs |
| C++ | .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++, .ipp, .tpp, .inl, .cppm, .ixx (Unreal Engine reflection macros recognised) |
| Python | .py, .pyi |
| TypeScript | .ts, .tsx |
| JavaScript | .js, .jsx, .mjs, .cjs (via the TS grammar) |
| Java | .java |
| Kotlin | .kt, .kts |
| Scala | .scala, .sc |
| Go | .go |
| Rust | .rs |
| PHP | .php, .phtml, .phps, .php8 |
| Ruby | .rb, .rake, .gemspec, .ru, Rakefile, Gemfile (Rails associations recognised) |
| CSS | .css |
| SCSS | .scss (mixins, functions, variables, placeholders; & resolves against parent) |
| SQL | .sql (tables w/ columns, views, types, enums, functions, procedures, triggers, indexes, sequences, schemas, domains; PostgreSQL primary, MySQL/SQLite usable) |
| Markdown | .md, .markdown, .mdx, .mdown |
| YAML | .yaml, .yml |
Adding another language is a single new adapter file. See
src/ast_outline/adapters/.
Read next¶
-
Outline, digest, show, prompt — full CLI reference with examples.
-
How to wire
ast-outlineinto Claude Code, Cursor, Aider, custom agents. -
Digest legend, marker tags, size labels, broken-tag handling.
-
Source, issues, releases, contribution guide.