| 2026-05-31 |
v1.3.3: prompt snippet now notes that output is already compact and that grep \| head hides counted matches. One cross-cutting paragraph added to the canonical ast-outline prompt snippet: the renderers emit a signatures-only skeleton, so output is usually small and worth narrowing with the tool's own flags before piping to head; a grep \| head cut in particular drops matches the header still counts in (N matches) — so results look complete but aren't — with -m N as the per-file cap that doesn't lie. Motivated by transcript analysis (a third of real ast-outline calls were wrapped in \| head -N while the built-in limiters went nearly unused, and most grep \| head cuts silently truncated counted matches). Text only — no behavior change. |
| 2026-05-31 |
v1.3.2: show <dir\|glob> <symbol> at N>1 lists candidates instead of dumping all bodies. When a symbol resolves to several definitions (across files, or several declarations), show no longer prints every body — it prints a single pointer note naming the candidate locations and asks the agent to re-run against one file: # note: 2 definitions of 'MailSpec' — re-run with one of: a/MailSpec.cs:12 (class), b/MailSpec.cs:5 (class). This makes show single-shape: when it prints content that content is always source code; when it can't (an ambiguous symbol) it prints a # note: and no code — never a mix, so a parser of show's output no longer has to branch on "is this code or a list?". It also matches how agents disambiguate in practice — pick one definition (or a named subset), don't read all N at once. This supersedes the v1.3.0/v1.3.1 behavior where a multi-definition show dumped all bodies under an … all shown below note. The N=1 case (body + found … in <file> note), the N=0 case (not found + did-you-mean hint), and per-file show <file> <symbol> mode are all unchanged; no --all flag was added (it would optimize for a usage pattern that does not occur). In --json the ambiguous result is flagged ambiguous: true and its matches become body-less candidate locators (file / kind / qualified_name / start_line / end_line, no source), with the re-run guidance echoed in notes; the N=1 JSON (full match with source) is unchanged, with ambiguous: false now present on every result. |
| 2026-05-31 |
v1.3.1: show accepts a glob target, not just a file or directory. A quoted glob — show "src/**/*.cs" MailSpec — is expanded by show itself (quote it so the shell leaves ** literal) and the matched files are searched exactly like the v1.3.0 directory target: one definition prints its body with a found … in <file> note, several print all bodies with a count note, none yields symbol not found (naming the glob as the scope) plus a did-you-mean hint. Reuses the same grep pre-filter + find_symbols resolver — a glob just hands grep the matched file list instead of a directory. A glob that matches no files returns # note: no files match glob: … (exit 0); a plain non-glob path that doesn't exist still gets the precise # note: file not found (the glob branch only triggers on * / ? / [). A directory search honors .gitignore / .ignore; a glob is expanded literally with no ignore-filtering (you already narrowed via the pattern). In --json the multi-file show envelope now carries two always-present locator fields — directory and glob, exactly one non-empty (glob is additive; the v1.3.0 directory JSON is otherwise unchanged). |
| 2026-05-31 |
v1.3.0: show <dir> <symbol> finds the definition itself — one call instead of two. Pointed at a directory rather than a file, show now locates the symbol's definition(s) under it and prints the body directly, collapsing the agent's most common show failure loop — show <dir> <symbol> → # note: not a file → grep <symbol> <dir> --kind def → show <file> <symbol> — into a single call. The directory walk and def classification reuse the existing grep machinery (the same collection that honors .gitignore / .ignore), and the authoritative find_symbols resolver runs only on the candidate files, so a substring near-miss like MailSpecHelper is collected cheaply then dropped precisely. One definition → its body, preceded by # note: found 'MailSpec' (class) in <path> naming where it lives. Several definitions (same name across files) → all bodies printed (a directory show is still a show), preceded by # note: N definitions of 'MailSpec' across M files — all shown below, each body header carrying its own path. No match → # note: symbol not found, plus a did-you-mean hint (reusing grep's edit-distance suggester) when a close name exists. All show flags (--signature, --no-doc, --view) and --json apply to the located file(s); --no-ignore / --exclude (new on show, directory-only) steer the search. In --json the envelope uses a directory locator and tags every match with its own file. File-target show <file> <symbol> is unchanged. |
| 2026-05-31 |
v1.2.0: grep recovers from empty results instead of leaving an agent to guess. An empty grep is the most expensive miss for an LLM agent — the usual next move is a blind retry or abandoning the tool for raw rg / file reads. Two heuristics make the first call land, or hand back enough to make the second call correct. Leading definition-keyword stripping — a literal single pattern of the shape <keyword> Identifier (enum ItemSoundFamily, class MailSpec, def handler, fn parse, type Config, …) has the keyword stripped, the identifier searched, and — when no explicit --kind was given — the search auto-narrowed to def. Agents habitually paste the source keyword in front of a symbol; as a literal substring the match landed on the keyword (not the name), classified as a ref, and a --kind def narrow then dropped it — a silent "no matches". ast-outline grep "enum Foo" now behaves like ast-outline grep Foo --kind def, with a # note: documenting the strip. The recognised keywords are owned per-language by each adapter (a new definition_keywords attribute) and unioned at search time, so a new language adapter extends the behaviour for free. Did-you-mean by edit distance — on a true no-match for a plain identifier, grep gathers the declaration names in scope and surfaces the closest real symbol(s) via the standard library's difflib (no new dependency), catching plural/singular slips and typos (MissSortPiles → MissSortPile). Names sharing no structure with any real symbol produce no suggestion (no false leads), and the lookup is bounded so a no-match never turns into a stall. Both ride the established exit-0 / # note: / # hint: convention, are disabled under explicit --regex, and compose with the existing --kind-mismatch hint (one follow-up line per empty result keeps the output scannable). |
| 2026-05-25 |
v1.1.0: grep no longer tracebacks on auto-promoted regex with unbalanced parens. The BRE→Python regex auto-promote converts \| (alternation) but leaves ( untouched — so a pattern like foo\|bar\.method( became the Python regex foo|bar\.method(, which has an unterminated group, and re.compile raised. That violated the CLI exit-0 invariant: agents driving the tool in parallel batches saw a Python traceback and a non-zero exit instead of the documented # note: line. The CLI now pre-compiles patterns after auto-promote and surfaces a pointed # note: invalid regex … + # hint: … (suggesting \( / \) or --regex), returning 0. |
| 2026-05-23 |
v1.0.0: First stable release — HTML adapter (.html, .htm) added. HTML pages (landing pages, templates, documentation sites, form-heavy admin UIs) get the same outline / digest / show / grep surface as code. Elements render as CSS-selector tokens — section#hero, header.site-nav, input[name=email type=email required], [import] link[rel=stylesheet href=/css/main.css] — so the outline line and the show argument share a vocabulary, and one selector grammar covers HTML, CSS, and SCSS. Headings (<h1>–<h6>) carry a 60-char text preview (h1: Pull exactly the context you need). Bare wrappers <div> / <span> / <p> / <li> / <tr> / etc. without id/class/significant attributes are dropped but their meaningful descendants float up to the parent's depth — real-world templates have 5-10 wrapping containers per visible block. Inline text-styling tags (<em>, <strong>, <code>, …) are skipped entirely. <svg> / <math> render the root only — inline icons would otherwise dominate with 30-50 unaddressable paths. Three or more consecutive sibling <details> collapse to one synthetic details ×N line so FAQ pages don't crowd the outline. Imports — <link rel=stylesheet|preload|prefetch|modulepreload|icon|manifest> and <script src=…> surface three ways: signature gets an [import] prefix, --imports lists them, grep classifies inner matches as [import] via import_regions. Inline <script> / <style> bodies and <!-- comments --> ride in noise_regions so grep filters them by default. ERROR-node recovery for templated HTML — when Jinja {% if %} at root, Vue/Svelte raw templates, Handlebars, or PHP <?php cause tree-sitter to wrap the document in a single ERROR, a one-pass recovery walks the ERROR subtree to surface any well-formed elements inside; templated files get a partial outline instead of a blank one. Hardening protections present from day one: class de-duplication (class="btn btn primary" → tag.btn.primary); duplicate same-name attribute last-wins (DOM semantics); attribute-value quoting for values containing whitespace, ], or " so the bracket grammar stays unambiguous; srcset / sizes whitelisted on <img> and <source> for responsive images; UTF-8 BOM, CRLF, Cyrillic / Chinese / emoji content survive intact; data-URI / base64 attribute values truncate at 40 chars; empty / doctype-only / comment-only files don't crash; long XHTML doctypes parse cleanly. A new core._split_query short-circuit recognises HTML compound selectors (section#hero.primary[disabled]) as a single token — same family as the CSS-sigil short-circuit. A new elements counter in the file-header summary mirrors the existing types/methods/fields (code), headings/code blocks (markdown), and per-doc count (YAML). Out of scope (deliberate, may revisit): <base href> not classified as import (sets base URL, not a resource pull); inline <script> and <script type="module"> are content, not imports; data-* / aria-* / role not promoted (would over-inflate the bracketed selector); SVG / Math children not recursed. The release is also a stability marker — 1976 regression tests pass, the IR + CLI contract is stable enough that consumers can pin a major version. |
| 2026-05-20 |
v0.9.6: --json reworked into a pure encoding switch. The JSON mode added in v0.9.3 shipped as "lossless" — it ignored every content-filtering flag and always emitted the complete IR, so --json behaved differently from the same command without it. That is the minority design; rg --json, kubectl -o json, and eslint --format json all treat --json as an encoding switch that changes only how output is serialized, not what it contains. v0.9.6 aligns with that convention. Content-filtering flags now apply to JSON exactly as they apply to the text output: outline --no-private / --no-fields / --no-docs / --no-attrs prune the declarations tree; digest --include-private / --include-fields prune it; show --view / --no-doc trim each match's source. The JSON schema is unchanged — these filters thin arrays and empty sub-lists, they never alter the shape, so JSON-Schema validation still passes. Behavior change: because digest itself defaults to a public-API map (private members and fields hidden), digest --json now does too — pass --include-private / --include-fields or --format=wide for the complete tree (in v0.9.3 digest --json was always complete). Layout / output-mode flags stay text-only — they have no JSON equivalent because JSON has no layout: --no-lines and --imports (outline), the --format preset's layout dimension and the --max-members readability cap (digest), and -l / -c (grep). A --format preset's content settings still apply, so --format=wide --json differs from --format=default --json, while --format=names/compact/default — sharing the same content — produce identical JSON. Internally a new pure core.filter_declarations IR→IR transform gives the JSON serializer and the text renderers one shared definition of the content filter, so the two cannot drift. |
| 2026-05-20 |
v0.9.5: .swift added to the LLM-agent prompt. Patch release — v0.9.4 shipped the Swift adapter and the CLI help with Swift, but the AGENT_PROMPT snippet (ast-outline prompt, the text wired into AGENTS.md / CLAUDE.md) was missed, so an agent running ast-outline prompt on a Swift project never learned the tool applies to .swift. The snippet now lists .swift alongside the other supported extensions. The regression test meant to guard exactly this drift was itself the reason it slipped: it checked a hardcoded extension list rather than the registered adapters, so it kept passing while .swift was absent. It now derives from ADAPTERS — adding a future adapter without naming one of its extensions in the prompt fails the suite. |
| 2026-05-20 |
v0.9.4: Swift adapter (.swift) — parses Swift through tree-sitter-swift; merged from PR #5. Full outline / digest / grep / show coverage of classes, structs, enums (raw-valued + CaseIterable), protocols, extension blocks, actors, methods, top-level functions, initialisers / deinitialisers, subscripts, stored and computed properties, enum cases, typealiases, associated types. init / deinit classify as constructor / destructor and subscript as an indexer; an extension surfaces as a type group so its members stay attached to the type they augment, and an actor is its own type kind. Visibility follows the language — default internal, private / fileprivate honoured, protocol members implicitly public. Attributes (@MainActor, @available, @Published, @objc) ride along in the signature, generics and protocol-conformance clauses are preserved, and /// doc comments attach to the following declaration. import directives register as static imports — Swift has no in-body imports, so conditional_imports_count stays 0. Structural grep classifies Swift // / /// comment lines as noise and tags import lines as [import], consistent with every other //-comment language. |
| 2026-05-20 |
v0.9.3: --json machine-readable output mode — the four structural commands (outline, digest, grep, show) gain an opt-in --json flag that swaps the text format for a single JSON document. The text digest stays the default for humans and LLM agents — it is deliberately token-dense and free to change every release; --json exists for programmatic consumers — editor plugins, CI gates, scripts — that need a stable, parseable contract instead of regexing the evolving text format. Every document is wrapped in a fixed envelope ({tool, schema_version, command, …}) carrying a schema_version integer so a consumer can detect a breaking change; every field is always present (empty lists as [], empty strings as "") so no defensive .get() is needed. Lossless by design — in --json mode every content-filtering flag is ignored (--no-private / --no-fields / --no-docs / --no-attrs / --no-lines for outline; --format / --include-private / --include-fields / --max-members / --oneline for digest; --no-doc / --view for show; -l / -c for grep); the complete IR is always emitted and the consumer filters itself. Flags that select which files to process (--no-ignore, --exclude, --glob) still apply — they shape input, not output. Errors stay valid JSON — a user-facing failure (path not found, bad argument, unsupported extension) is emitted as an {"error": {"notes": […], "hint": …}} object on stdout rather than a # note: text line, so the output is always parseable; the exit-0 contract is preserved. A zero-result search (grep with no matches, a file with no declarations) is a valid empty document, not an error. Unicode identifiers are emitted unescaped (ensure_ascii=False). The LLM-agent prompt (ast-outline prompt) is intentionally unchanged — agents keep using the token-dense text digest. New json_output module holds the IR-to-JSON serialization, kept separate from the text renderers so the JSON schema evolves on its own cadence as a contract. |
| 2026-05-20 |
v0.9.2: Ruby callback-DSL blocks — extends the KIND_BLOCK recognition added for TypeScript/JavaScript in v0.9.1 to the Ruby adapter. RSpec suites (describe / context / it / feature / scenario / shared_examples), Rake tasks (task :build do), route maps (namespace :admin do / resources :users do), ActiveSupport::Concern's concerning, Sinatra routes, and any in-house DSL of the same shape were invisible to the outline — an RSpec spec file digested as "no declarations". They are now recognised as block declarations and descended into. The rule is structural — no hard-coded list of framework names: a block-bearing call (do...end or {...}) becomes a block when (1) its callee is a plain identifier and (2) its first argument is a string or symbol label. A constant first argument is intentionally NOT a label — assert_raises(ArgumentError) { ... } is structurally identical to describe User do, so promoting on a constant produces false positives; a constant-named bare container still has its body surfaced by transparent descent, it simply isn't given its own heading. Transparent descent is the Ruby-specific addition: a block-bearing call that is not itself a named container — a member call such as RSpec.describe User do (the modern RSpec entry point) or Rails.application.routes.draw do — still has its body walked, so the plain-identifier blocks nested inside it surface. Without it a modern RSpec.describe spec file would still outline as empty. Works across all four commands (outline / digest / show / grep) exactly as the TypeScript blocks do. Validated on ~9,000 real-world Ruby files (Rails, RuboCop, Mastodon, Jekyll, Devise, Sidekiq, RSpec, Rake, Sinatra): no crashes, no signature body-leaks. |
| 2026-05-20 |
v0.9.1: TypeScript / JavaScript callback-DSL blocks — closes issue #3. The TS adapter was blind to structure expressed through callback arguments: a test file whose suites and cases live inside describe(...) / it(...) outlined as empty, and a Pinia setup-store (const s = defineStore('id', () => {...})) dumped its entire callback body onto one signature line. Both are now recognised as a new KIND_BLOCK container kind and descended into. The recognition rule is structural — no hard-coded list of framework names: a call_expression becomes a block when (1) its callee is a plain identifier, (2) its last argument is a function literal, and (3) its first argument is a string-literal label. Clause 3 is the discriminator between a named container (describe('suite', fn), defineStore('id', fn) — the label is its reason to exist) and a bare function wrapper (action(fn), memoize(fn)) or a property-definition wrapper (defineGetter(obj, 'name', fn) — string not first); the rule therefore covers vitest / jest / mocha / jasmine / ava / tape / node:test, Pinia defineStore, and any in-house DSL of the same name('label', callback) shape, and rejects setTimeout(fn, 1000) / useEffect(fn, deps) / el.on('x', fn). Blocks nest (describe → it → inner declarations) and work across all four commands: outline (indented tree), digest (a block <label> header with member tokens for the nested cases, like a type), show (extract by label — including labels with spaces, colons, or a literal dot, via a whole-string fallback in find_symbols when the dotted-path walk finds nothing), grep (blocks join the scope chain, and a test description matches as [def] rather than vanishing as string-literal noise). Same release: function bodies no longer leak into field signatures — a field whose value embeds a function/method body (const inc = action((d) => { ...stmts... })) renders with the body elided to {…}, so an outline shows structure, never implementation code; and commented-out code no longer renders as documentation — a leading run of // comments that is disabled code (braces, arrow bodies, declaration keywords, by majority vote) is dropped instead of being shown as doc lines on the next declaration, while genuine prose and /** ... */ JSDoc blocks are kept. Known blind spots (a pure shape rule cannot avoid them): a member-expression callee (QUnit.test(...), Playwright test.describe(...)) is excluded the same way arr.map / el.on are — bare-global frameworks are unaffected; a bare addEventListener('x', fn) is structurally identical to it('x', fn) and becomes a block. Validated on 326 real-world files (Pinia, Express, Lodash) with zero crashes; ~40 new regression tests. |
| 2026-05-17 |
v0.9.0: Lua adapter (.lua, .wlua) — first new language since the Ruby drop. Vanilla Lua 5.1–5.4 via tree-sitter-lua; full outline / digest / grep coverage. Maps Lua's flat-file convention-based shape onto the existing IR without inventing synthetic kinds. Decisions worth flagging: function M.foo() (dot) is KIND_FUNCTION; function M:bar() (colon) is KIND_METHOD — the colon is Lua's source-true marker for implicit self, the cheapest and most honest signal for "instance method", avoiding the brittle "detect a class from setmetatable" path. Metamethods (__add, __sub, __mul, __div, __mod, __pow, __unm, __idiv, __band, __bor, __bxor, __bnot, __shl, __shr, __eq, __lt, __le, __concat, __len, __call, __index, __newindex, __tostring, __metatable, __pairs, __name, __close, __gc, __mode) → KIND_OPERATOR regardless of shape (method-style function C:__tostring(), assignment-style C.__add = function() end, non-function value C.__index = C); --kind operator isolates every protocol declaration in one query. Visibility: local scope → private (Lua's actual private), metamethods → public protocol (underscore prefix is part of the language contract, NOT a private convention — Python-dunder analogue), M._helper → private (underscore convention universal in Neovim / LÖVE / Roblox-Lua). Lua 5.4 attributes (<const>, <close>) ride in Declaration.attrs. require imports: bare (require "x"), parenthesised (require("x")), AND local Y = require("x") all register as static imports; the local X = require(...) shape pushes the WHOLE statement byte range into import_regions so grep promotes it to [import] despite the line prefix being local (too broad to whitelist). require calls inside function bodies / branches / loops bump conditional_imports_count and never appear in the static list. Long-bracket comments (--[[ ]], --[==[ ]==] at any = level) and long-bracket strings ([[ ]], [=[ ]=]) ride in noise_regions so grep filters matches inside them. Direct-return-table module shape (return { foo = function() end, V = 1 }) walks the returned table fields and surfaces each as a top-level decl — the second canonical Lua module pattern besides local M = {} ... return M. Grep classifier — Lua chain skipping and sugar calls (opt-in via new language kwarg to _next_call_paren_after): obj:method(x) matched on obj classifies as KIND_CALL (chain skipping over :method); same for deep dot chains (a.b.c.d(x)). Sugar-call shapes f"x" / f'x' / f{...} / f[[...]] classify as KIND_CALL (Lua treats them as syntactic sugar). Bare [ is intentionally NOT a call marker (f[key] is a subscript). Strictly opt-in — in TypeScript / Rust the same shapes still classify as KIND_REF. Out of scope (v1): Luau (.luau) — vanilla tree-sitter-lua produces ERROR nodes on Luau type annotations; symbol semantics are the same so the v0.9.1+ path is a suffix-dispatch branch on tree-sitter-luau. setmetatable-based inheritance — no syntactic anchor in vanilla Lua, only convention; v1 leaves bases = [] and agents read the __index assignment (which lands as KIND_OPERATOR) themselves. Module-shape detection — each decl emitted at the file's top level with the qualifier baked into name (M.foo, M:bar), flat like the Python adapter; wrapping under a synthetic KIND_NAMESPACE would need a heuristic that fails on the many real files holding several module-shaped tables. 42 regression tests, 10 fixtures (module_pattern, mt_class, direct_return, neovim_plugin, love_callbacks, requires_and_attrs, nested_names, strings_and_comments, broken_syntax, empty). 1493 tests green. |
| 2026-05-17 |
v0.8.13: ast-outline grep — match classification on Unicode source lines. The classifier resolved a match's line column as a 1-based byte offset, then indexed line_content (a Python str) with it. For ASCII-only lines the two coincide, so the bug was silent in the 99% case; for any line containing a multi-byte UTF-8 character — Cyrillic (2 bytes/codepoint), CJK (3 bytes), emoji / supplementary plane (4 bytes), accented Latin / math symbols — the byte offset over-shoots the codepoint index for everything after the first multi-byte char. Three downstream classifiers consumed the wrong column: _next_call_paren_after (the call/ref walker) started past the intended position and either landed on a later character or ran off the end of the line; _column_inside_string counted unescaped quotes against a string-length cap that wasn't its bound, returning even/odd parity for the wrong span; _classify_match's inline-comment check compared # index against column - 1 so a comment match could be mis-claimed as code on Cyrillic-prefixed lines. Net effect: locale-sensitive false negatives across --kind filtering, leaked string/comment content into the default code view, and degraded behavior for any project with non-ASCII identifiers, comments, or string literals — Russian / Ukrainian / Chinese / Japanese / Korean projects, math-heavy code with ≤ / α / β, emoji in test names. Fix: the loop that converts per-match byte spans to columns decodes the line-prefix bytes once per side and uses the codepoint length, so every downstream check receives a codepoint-correct cursor regardless of multi-byte content ahead of the match. No-op for ASCII-only lines (len(decoded_prefix) == len(prefix_bytes)), so no performance regression in the common path. Companion fix in _next_call_paren_after's rest-of-identifier skip (introduced in v0.8.12 for ASCII-only [A-Za-z0-9_]): now uses str.isalpha() / str.isdecimal() / _, covering Unicode letter and decimal-digit categories — matches what Python / Rust / TypeScript / Swift accept as identifier chars beyond the first. Deliberately narrower than isalnum(), which would also accept Unicode No-category numerics like ² / ¼ that aren't identifier chars in any supported language. The other walker skip cases (< / > / [ / ] / ?. / :: / !) stay ASCII: they're language-syntax markers, not identifier content. Both fixes are necessary together — boundary conversion lands the cursor at the right codepoint, Unicode-aware skip walks the rest of the identifier to the (. Behavior change: GrepMatch.column is now documented as a 1-based codepoint offset (was "1-based byte offset on its line, +1"). On ASCII-only lines the values are identical; on multi-byte lines the reported column is now the codepoint index — what an editor / language server / IDE reports. 7 new regression tests covering Cyrillic / CJK / accented-Latin walker-unit cases, Cyrillic and CJK call classification end-to-end, ASCII call after a Unicode prefix, in-string detection on a Cyrillic-prefixed line, inline-comment detection past a Cyrillic-prefixed comment marker, and the codepoint-offset contract. 1451 tests green. |
| 2026-05-17 |
v0.8.12: ast-outline grep — match ending mid-identifier no longer demotes calls to [ref]. Repro that surfaced the gap: agent runs ast-outline grep "TryAssembleFragments\|TryAssembleFragmentsNear" Assets/Scripts/App --kind def,call against a C# Unity codebase to find every definition + caller of a method; the three [def] matches surface correctly, but the caller at ThingDragNDropController.cs:1208 — controller.TryAssembleFragmentsNear(thingData, currentPosition); — is missing. Plain grep -rn confirms the call exists. Cause: Python re resolves alternation by picking the leftmost alternative that matches at a given position (NOT the longest), so TryAssembleFragments\|TryAssembleFragmentsNear matches the shorter TryAssembleFragments against TryAssembleFragmentsNear(...) and the match ends on N mid-identifier. The call/ref classifier in _next_call_paren_after (grep.py) walks the line right of the match-end looking for (, already skipping whitespace, generic blocks <…> / […], bare closers > / ] (v0.8.8), turbofish ::, TS ?., TS !, but never the trailing bytes of an identifier — N was treated as "other significant char", returned False, classified as [ref], filtered out by --kind def,call. Bug was language-agnostic — the walker is shared across all adapters, so any language with name(args) call syntax was affected. Verified on Python, TypeScript, Go, Rust, and C#; same regression on all. Fix is a one-shot rest-of-identifier skip at the entry of the walker: if start lands on [A-Za-z0-9_], advance past the remaining word bytes, then run the existing skip loop. Applied only at entry (not mid-walk) since legitimate intermediate identifiers in real-language line shapes don't arise — after the existing generic-block balance / :: / ?. skips, an identifier char mid-walk indicates a different identifier whose call shape shouldn't be attributed to the original match. Pinned by test_next_call_paren_after_identifier_skip_one_shot_not_recursive: foo<T>bar() matched at foo returns False, not True. Symmetric to the bare-closer skip from v0.8.8 — both cover the same root cause (match end position doesn't align with a token boundary), just on different sides of the syntactic boundary. Behavior change worth flagging: the fix also affects literal (non-regex) substring searches by the same path — foo against fooBar(x) now classifies as [call] (was [ref]). Consistent with the walker's documented "bias toward call" — agents searching for a substring inside a called identifier almost always want the call site surfaced. Non-call sites (foo against fooBar = 1) still classify as [ref] because the trailing ( check fails. Identifier-skip uses ASCII [A-Za-z0-9_] only — non-ASCII identifiers (Cyrillic / CJK) are bypassed by a separate pre-existing byte-vs-char index mismatch higher in the pipeline; flagged as a follow-up. Workaround for users on older versions: write the longer alternative first (TryAssembleFragmentsNear\|TryAssembleFragments) or anchor the shorter one (TryAssembleFragments\b\|TryAssembleFragmentsNear). 10 new regression tests across 5 languages and the walker unit layer. 1444 tests green. |
| 2026-05-16 |
v0.8.11: ast-outline outline / digest / grep gain --exclude <glob> — repeatable, gitwildmatch (.gitignore syntax), so agents can narrow the walk inline without editing .gitignore or .ignore files. Repro that surfaced the gap: agent runs ast-outline digest src/ against a Unity-shaped repo where Tests/ and *.gen.cs swamp the public-API map; today the only escape hatches are (a) edit .gitignore (touches version control for a one-shot exploration filter), (b) edit .ignore (still a file write, agent has to know the convention), (c) pre-filter paths via shell ls \| grep -v then pass globs (loses directory recursion + the project-root anchor of .gitignore). rg --glob '!pattern', fd --exclude, tree -I, tar --exclude all offer the same shape inline; ast-outline was the only structural-search tool without it. Flag is repeatable, anchored at the project root (same anchor as the root .gitignore frame — --exclude src/generated/ resolves identically regardless of cwd), supports !pattern negation. Layered ABOVE the auto-filter frame in collect_files_with_stats, so a bare --exclude '!node_modules/' re-includes a default-filtered dir on a single line — the three-line git escape idiom (!node_modules/ + node_modules/* + !node_modules/our-fork/) required inside .gitignore isn't needed because the --exclude frame sits one level higher and its negation wins. Survives --no-ignore: that flag silences the AUTO-filter (.gitignore + hardcoded defaults), --exclude is the user's explicit voice and keeps applying; two semantic axes, both useful together (--no-ignore --exclude secret/ = walk every dir EXCEPT the named one). Explicit single-file inputs continue to bypass filtering — same rule as the .gitignore precedent. When the flag contributes to ignored-dir pruning, the existing # note: ignored N dirs (…) via … line widens its source list from .gitignore/.ignore + defaults to .gitignore/.ignore + defaults + --exclude, so an agent debugging "where did my folder go" sees its own flag named — visibility over magic. Malformed patterns (! alone, trailing backslash — GitWildMatchPatternError) surface as # note: invalid --exclude pattern: … with rc=0, honoring the CLI batch-friendly invariant. Implemented as one extra GitIgnoreSpec frame in collect_files_with_stats, reusing the existing pathspec>=0.12 dependency; no new deps. The agent prompt snippet (ast-outline prompt) gains ONE universal sentence under the 4-step menu so any LLM picks the flag up on first read without studying --help. What's NOT added (intentional): --exclude-from=FILE — YAGNI, .gitignore / .ignore already serve that role for any pattern stable enough to be worth a file; [tool.ast-outline] exclude = […] in pyproject.toml — the tool is principally stateless / zero-config and reading project config would break that invariant; per-command divergence in flag shape — all three subcommands take the same --exclude GLOB, keeping the agent-facing surface symmetric. |
| 2026-05-16 |
v0.8.10: ast-outline show resolves markdown headings whose query drops inline-markdown decoration. outline prints headings verbatim, so an H2 with inline code surfaces as ## `useState` — when to reach for it; agents (and humans) copying the title routinely strip the backticks as if they were rendering hints, and show then returned # note: symbol not found because the substring matcher compared raw heading text — backticks broke continuity. Same shape with *emphasis*, _emphasis_, ~~strike~~. The matcher now strips `, *, _, ~ from BOTH the heading title and the query before the case-insensitive in test; symmetric, so passing the decorated form verbatim still resolves the same heading. Scoped to the substring=True branch of _trail_matches, which only fires for KIND_HEADING (markdown-only) — code-symbol matching keeps strict equality, so a Python _foo or a Rust *const T is never silently broadened. Composes with the existing numbered-prefix short-circuit (2. \Bar` setupresolves fromBar setup). Structural markup — linkstext, autolinks`, raw HTML — is intentionally NOT stripped: char-wise removal would corrupt the visible label rather than peel decoration. Fixes #4. |
| 2026-05-13 |
v0.8.9: ast-outline digest gains a --format= preset selector with four levels (names, compact, default, wide) plus the --oneline alias. Repeated agent traces showed that digest's default per-file detail (line ranges, per-class counters, blank-line paragraph breaks) is exactly right when the next step is Read --offset N, and exactly wrong when the next step is "pick a file to drill into" — multiple skills reached for head -N digest or piped output through awk to throw away detail they didn't need, truncating the tail instead of compressing per-file. Preset solves the underlying problem: let the caller name the level of detail that matches the task. names for repo orientation = one line per file (name.py [large]: ClassA, ClassB, func_c), top-level symbols only, no methods / no () / no : Base / no L<a>-<b> / no legend; files with no public top-level symbols hidden. compact for module-structure questions = full hierarchy minus per-file , X types, Y methods, Z fields counters, L<a>-<b> line ranges, blank paragraph breaks between types-with-members, and # no declarations markers (declaration-less files hidden entirely); inheritance, decorators, modifiers, and size labels all survive — they carry semantic weight. default unchanged (byte-identical back-compat — every existing skill parsing v0.8.x output keeps working). wide for one-file deep-dives = CLI-side preset that cranks --include-private + --include-fields + lifts --max-members to 10**9; no rendering branch, the existing default-format renderer already shows everything when those toggles are on. Names format extends to non-code languages — top-level H1 headings for markdown, top-level keys for single-doc YAML / doc-separator captions for multi-doc YAML (--- doc 1 of 3 — ConfigMap …), flat selector list for CSS/SCSS. [huge] files (>100k tokens) collapse to a header-only line with no symbol list in names; [broken] parse-error marker preserved across all formats. Preset overrides follow kubectl-style silent override: explicit --include-private / --include-fields / --max-members win over the format's defaults (--format=wide --max-members 5 = wide's private+fields with a small cap; --oneline --include-private = names with private symbols added). --imports composes with every format including names, adding an indented imports: … line per file — an earlier draft silently dropped imports under --oneline, which would have misled agents into thinking "this file has no imports" and broken the prompt's stated invariant. Design boundaries — a fifth grep-shape flat format (path:line:qualname per symbol, pipe-ready) was considered and dropped: it would have collided with the existing ast-outline grep subcommand's output shape, and no concrete agent task was unmet by the four-level set. No new --no-X flags either (point YAGNI — in-between cases are reachable by picking the adjacent preset plus existing override flags). : Base stays the inheritance notation: a Python-native Class(Base) alternative would actively mislead on C# 12 (primary constructor) and isn't language-native for the OOP-heavy languages digest most cares about. The canonical agent prompt (ast-outline prompt) gets a single 2-line addition under the digest entry — obvious-by-convention preset names (kubectl -o name/-o wide, jq -c, git --oneline) chosen so an LLM reads the flag without --help. |
| 2026-05-12 |
v0.8.8: Generic-call invocations now classify as [call] across all languages. ast-outline grep "Bind.*SaveSystem" --regex --kind call against a C# Unity codebase previously returned 0 hits even though c.Bind<SaveSystem>() is clearly an invocation — the call-vs-ref walker (_next_call_paren_after) only knew how to skip an opener (< / [) preceding the cursor, balancing the block to the matching closer before resuming the ( search. It didn't know how to skip a bare closer (> / ]) left over from a greedy regex match (Bind.*SaveSystem ending on >) or from a literal-with-type pattern agents type to disambiguate generic overloads (parse::<i32, Map[String, Int). With no closer-skip the walker hit the closer, fell through return ch == "(" → False, and classified the whole invocation as [ref] — making --kind call return 0 across C# / Java / Kotlin / Scala / TypeScript / Rust-turbofish-with-explicit-type / Go (1.18+ Foo[int]()) / C++ uniformly. The walker now skips bare leading > / ] the same way it already skipped whitespace, ?., !, and :: — symmetric to the existing opener skip, consistent with the walker's existing bias-toward-call policy (when the line shape is ambiguous, classify as call; ref false positives in code-search are more painful). 8-language test matrix pins the classification. Foo.*Bar patterns now surface the --regex hint on zero results. Companion gap to the above: even before the agent thought to add --regex, the warn-on-no-match hint that would have suggested it didn't fire on .* patterns — the ambiguous-regex fingerprint required either an escaped metachar (\., \(), a letter-or-)/] followed by a quantifier (d*, )*), or an edge anchor (^, $). The .<quantifier> shape — where . is the char before * / + / ? — slipped through both the strict auto-promote fingerprint (intentionally excluded — . and * individually appear in literal code as qualified names and array types) AND the warn-on-no-match fingerprint. The ambiguous-regex fingerprint now treats .[*+?] as unambiguous regex intent: bare . in qualified names (User.save) is still skipped, but the pair .* / .+ / .? has no literal-code interpretation worth protecting. Extends the same hint-coverage principle as v0.8.4 to the regex-mode blind spot. |
| 2026-05-12 |
v0.8.7: HTML block comments (<!-- ... -->) in markdown classify as [comment] in grep. A multi-line <!-- TODO: revisit useState patterns --> block in README.md previously surfaced under ast-outline grep useState as a regular [ref] match alongside real prose mentions — agents reading the result couldn't tell signal from author-private annotation. The markdown adapter now appends (start, end, "comment") noise regions for every block-level html_block whose first four bytes are <!--, which the existing noise filter handles identically to source-language comments. Surfaces with [comment] under --include-noise so the agent can opt back in. Inline <!-- --> inside a paragraph is NOT covered — tree-sitter-markdown fragments those into single-character punctuation nodes with no clean byte range; block-level is the 95% case for hidden TODO / NOTE / draft annotations. Other html_block content (raw <div>, <table>) intentionally stays visible — embedded HTML carries searchable signal (component names, data attrs) that an agent may legitimately grep for. |
| 2026-05-12 |
v0.8.6: YAML block scalars (|, >) noise-filtered in grep. ast-outline grep npm .github/workflows/ previously returned every npm install, npm run lint, npm run build line lifted from run: | step bodies — the structural mentions (job names, step names) drowned in the shell-line noise. The YAML adapter now populates ParseResult.noise_regions with the byte ranges of every block_scalar node (kind string, so the existing --noise-filter / --include-noise path handles it without a new flag). Plain scalars stay visible — image: registry.example.com/api, replicas: 3, single-line run: npm publish are exactly what agents grep YAML for; only the multi-line block forms (\| literal, > folded) — which YAML authors reach for specifically to embed opaque scripts / templates — get masked. Mirrors the v0.8.5 fenced-code-block treatment in markdown. |
| 2026-05-12 |
v0.8.5: Fenced code block bodies noise-filtered in markdown grep. ast-outline grep useState docs/ against a tutorial site previously surfaced every example-code occurrence of useState alongside the prose mentions, making the result useless on docs-heavy repositories. The markdown adapter now populates ParseResult.noise_regions with the byte ranges of each fenced block's code_fence_content (kind string, so the existing --noise-filter / --include-noise path handles it without a new flag). Fence delimiters and the info string itself (```python) stay searchable, so language-by-fence queries still work. Indented (4-space) code blocks are intentionally not masked yet — rare in modern markdown and almost always paired with a fenced equivalent. Repro that surfaced the gap: ast-outline grep useState README.md returning identical hits from prose and from the JSX example below it. |
| 2026-05-11 |
v0.8.4: Kind-filter hint on empty grep results. When grep --kind X returns zero matches but the pattern would have matched under other kinds, the CLI now appends a # hint: line under the bare # note: no matches with a count breakdown and a retry suggestion: # hint: --kind call excluded 4 matches (4 ref) — retry with --kind call,ref or drop --kind. Motivation: agents reading EditorPrefs.GetString(...) see a call, pass --kind call, and get nothing — because the match lands on the type name EditorPrefs (a ref, since . follows it), not on the called method GetString. The bare "no matches" hid the fact that the symbol was present in a different role; one extra line collapses what was previously a binary-search through --kind values into a single retry. Universal across all six kinds (def / call / ref / import / comment / string) and works with multi-kind filters (--kind def,import); covers the noise-filter blind spot too — a pattern living only in a comment / string under a non-noise --kind narrow now counts toward the breakdown, and the suggested retry (--kind def,comment) works because --kind comment auto-enables --include-noise in the CLI. Suppressed when the regex-syntax hint fires for the same empty result — one hint per call keeps the output scannable. |
| 2026-05-11 |
v0.8.3: show "3. Foo" / show "4.2 Foo" now resolve. Markdown headings like ## 3. Numbered Heading are stored with their numeric prefix intact and outline prints them that way, but the find_symbols query tokenizer was reading the dot in 3. as a path separator — splitting the query into ["3", " Foo"] and demanding a two-segment trail that never exists (markdown headings are single declarations whose name carries the prefix). Result: a silent symbol not found for the exact text outline had just printed, breaking the round-trip workflow. _split_query now short-circuits queries shaped like a numbered-heading prefix (\d+(\.\d+)*\.?\s+<text>) into a single opaque token, letting the existing substring-matching path resolve them. Bare dotted-numeric queries without trailing text ("1.2", "1.foo") keep the previous split behaviour so non-markdown lookups are unaffected. Fixes #2. |
| 2026-05-10 |
v0.8.2: Agent-prompt --signature scope disambiguated. The canonical agent prompt previously read "Add --signature to any of the above" inside the show section — literal models (Opus 4.7, GPT-5.5) read the menu globally and reached for --signature on outline and digest, where the flag does not exist. Reworded to "Add --signature to show (only there)" — preserves the workflow hint, adds an explicit anchor. Cross-command flag hint on unrecognized arguments. When an unknown flag passed to one subcommand is recognized by another, the # note: unrecognized arguments: --flag line now appends (hint: \--flag` is a flag of ``, not ``)`. Multi-owner flags list every owning command. Truly unknown flags get no hint. Agents self-correct in one retry instead of guessing or asking the user. |
| 2026-05-08 |
v0.8.1: grep -e PAT PATHS... now works without a positional pattern. Previously ast-outline grep -e foo src/ failed with the following arguments are required: paths because argparse couldn't disambiguate the trailing string as path-vs-pattern. Patch fix: a pre-argparse rewrite promotes the first -e PATTERN value into the positional slot when no positional pattern appears before the first -e. Aligns with POSIX grep -e and rg -e muscle memory. Long form --expression PAT and equals form --expression=PAT covered too. Existing call shapes (grep PAT PATH, grep PAT -e PAT2 PATH) unchanged. |
| 2026-05-08 |
v0.8.0: ast-outline grep — AST-aware structural code search. New subcommand returning matches annotated with their enclosing class/function and a kind classification ([def] for definitions, [import] for import statements; calls and refs render untagged because identifier-followed-by-( makes them obvious). Comments and string literals are filtered by default; --include-noise opts in. The intended consumer is an LLM agent that today does grep symbol → 20 hits → read 5 files to understand which class / function contains each match; this collapses that into one call by placing scope and kind inline in the output, eliminating the cascade of follow-up Read calls. Built for code-symbol queries — not a replacement for rg on TODO comments / log strings / free-text. POSIX-style flags: -e/--expression PATTERN (repeatable, multi-pattern via alternation — one walk for N symbols), -w/--word (whole-word \b...\b), -l/--files-with-matches (paths only), -c/--count (path:N per file), -m/--max-count NUM (per-file cap with explicit # truncated — N more... footer so partial results are never silent), -i/--case-insensitive, --regex. Regex auto-detect — patterns with unambiguous regex syntax (\|, \d, \w, (?:, bare |) auto-promote with a # note:; \| normalizes to | (BRE→ERE). Ambiguous metachars (., *, [, ^) never auto-promote but emit # hint: on zero matches suggesting --regex. --kind def\|call\|ref\|import\|comment\|string filter eliminates the most common post-filter step ("show me only definitions of X" / "only call sites"); accepts repeated (--kind def --kind call) or comma-separated (--kind def,call) forms. Multi-line / block-form imports classify correctly via tree-sitter: Go import (...), Python from X import (...), TypeScript multi-line import {...} from, Rust use foo::{...}, PHP use App\{...}, Scala import foo.{...} — inner symbols get [import], not [ref] or [string]. Python lazy imports inside function/class bodies also covered (e.g. def foo(): from x import (\n a,\n)). For C++, AST node distinction means using namespace std; and using std::vector; classify as [import] while using my_int = int; correctly stays as a type alias declaration. C# 10+ global using recognized too. Implementation uses a new ParseResult.import_regions field populated via piggyback on existing tree walks — zero measurable overhead vs not collecting them. --kind filter, regex auto-detect, and multi-line import handling are all part of the public surface from the start — grep is stable, no longer experimental. Bumped to a minor version (0.7.x → 0.8.0) to reflect the new command. |
| 2026-05-06 |
v0.7.7: Setup-prompt — Claude-Code-only Explore-shadow sub-step. Claude Code's built-in Explore subagent runs in an isolated context — it does not inherit CLAUDE.md / AGENTS.md, so the snippet written in Step 2 of setup-prompt does not reach Explore invocations on its own. Setup-prompt now detects when the user has Claude Code (~/.claude/ exists) AND no .claude/agents/Explore.md shadow file exists yet, and asks once whether to create the shadow. If approved, writes a ready-to-go Explore definition that embeds the full fresh canonical from Step 2.1 verbatim (not a short pointer — the shadow is a brand-new file and embedding avoids forcing every Explore invocation to re-run ast-outline prompt), wrapped in the standard markers; offers project-local vs global scope (.claude/agents/Explore.md / ~/.claude/agents/Explore.md). Codex and Gemini subagents are user-defined files only — they fall under the existing diff-aware patch logic, no shadow concept needed. Skipped in headless mode and when a shadow already exists. Closes the gap where ast-outline integration via AGENTS.md silently failed to reach the most-used Claude Code subagent. Step 2 also detects user-written ast-outline content outside markers — previously the "markers absent" branch silently appended a fresh marker block at the end of an existing AGENTS.md / CLAUDE.md / GEMINI.md, leaving two competing references when the user had hand-written content (e.g. from an old ast-outline prompt >> AGENTS.md run that they later edited). Setup-prompt now scans for ast-outline mentions in the target file before any write; if found outside markers, shows the offending lines and asks the user which path to take — (1) wrap the existing content in markers verbatim (preserves their version, future re-runs go diff-aware), (2) replace with the fresh canonical, (3) append anyway with duplication, or (4) skip Step 2 entirely. Default is ask — silent append on top of user content is no longer reachable. Site homepage now also surfaces an Install with AI call-to-action linking to a dedicated section that walks through the setup-prompt flow before the manual install paths. |
| 2026-05-06 |
v0.7.6: setup-prompt subcommand — ast-outline setup-prompt prints an install-time checklist meant for one-shot consumption by a coding agent (Claude Code, Codex CLI, Gemini CLI, Cursor). Tell the agent "run ast-outline setup-prompt and follow its instructions" and it walks the user through three idempotent steps: (1) verify the CLI (offer uv tool install / pipx / pip if missing, run on user's behalf with explicit consent — never silently); best-effort PyPI version check, surface the matching upgrade command (uv tool upgrade / pipx upgrade / pip install -U) without auto-upgrading; (2) write the canonical ast-outline prompt snippet to the right persistent-context file — ./AGENTS.md cross-tool default (covers Codex CLI, Claude Code via @AGENTS.md import, Gemini CLI with settings.json config, and Cursor) or the native single-vendor file (./CLAUDE.md / ./GEMINI.md); offers project-local vs global scope (~/.claude/CLAUDE.md / ~/.codex/AGENTS.md / ~/.gemini/GEMINI.md); checks for Codex AGENTS.override.md so writes are not silently shadowed; wraps the snippet in <!-- ast-outline:start --> ... <!-- ast-outline:end --> markers and runs diff-aware on re-run — if existing block content differs from the fresh canonical, the agent shows the diff and offers replace / keep / show-full-diff, never overwriting customizations silently; (3) optionally patch existing exploration-oriented subagent files in .claude/agents/ / .codex/agents/ / .gemini/agents/ with per-agent permission. Cross-vendor universal — outcome-first markdown structure, no persona, no chain-of-thought, no aggressive emphasis; calibrated for Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5, GPT-5.x, and Gemini 3.x. Cross-OS — agent translates which / $VIRTUAL_ENV / curl to PowerShell / cmd.exe equivalents on Windows. Adapts to whichever human language the surrounding conversation uses for spoken replies and any free-form prose written into a freshly-created AGENTS.md / CLAUDE.md, while keeping the marker block content and subagent files in English. Headless harnesses (codex exec, claude -p, Gemini non-interactive, scheduled CI) restrict execution to read-only checks plus AGENTS.md write at project-local scope. The CLI itself is print(SETUP_PROMPT, end="") — file IO is delegated to the agent's native edit tools, so encoding / permission / merge-conflict edge cases are handled in agent context, not in Python. |
| 2026-05-06 |
v0.7.5: SQL adapter (.sql) — DerekStride tree-sitter-sql plus a regex fallback for grammar-unsupported constructs. Tables surface as KIND_TABLE with each column as a KIND_FIELD child carrying the full source-true column line as signature (email TEXT NOT NULL UNIQUE); views, materialized views, composite types, enums, functions, triggers, indexes, sequences, schemas, extensions all parse natively. The regex fallback recovers six constructs the upstream grammar can't parse: CREATE [OR REPLACE] PROCEDURE, CREATE DOMAIN, CREATE TABLE … PARTITION OF parent (modern PG declarative partitioning — child tables vanish without this), CREATE FUNCTION … SECURITY DEFINER and similar exotic modifier orderings, LOAD 'lib', and IMPORT FOREIGN SCHEMA … FROM SERVER … INTO …. Fallback is line-anchored, gated by AST-derived skip ranges (comment / marginalia / literal / block subtrees) so red herrings inside comments / string literals / PL/pgSQL bodies don't surface as spurious declarations, short-circuited by a bytes-level fingerprint check so the typical schema-dump file with no fallback constructs pays nothing, and uses a byte-range guard against double-extracting AST-parsed functions. PL/pgSQL bodies inside AS $$ … $$ parse as opaque dollar-quoted strings — header (name, parameters, return type) extracts cleanly, and parse errors inside function bodies are excluded from error_count so files using := or IF…THEN…END IF don't get mis-reported as broken. PostgreSQL is the primary target; MySQL and SQLite extract tables / columns / indexes / views cleanly with some error_count > 0 noise on dialect-specific syntax (ENGINE=InnoDB, AUTOINCREMENT, inline KEY constraints). MSSQL, T-SQL, Oracle PL/SQL, and BigQuery have partial coverage — bracketed identifiers, GO separators, VARCHAR2, and STRUCT<> / backtick names degrade. Same release adds TypeScript / JavaScript conditional dynamic imports — import('...') calls inside a function / method / control-flow scope contribute to conditional_imports_count, rendered as [+ N conditional includes] next to the imports line, mirroring Python / Ruby / PHP. Top-level dynamic imports are NOT counted (they execute unconditionally on module load). |
| 2026-05-06 |
v0.7.4: CSS adapter (.css) and SCSS adapter (.scss). Rules carry simple-selector tokens in match_names — find_symbols(".btn-primary") returns every cascade-relevant rule (top-level, inside @media, themed, descendant in .modal) with the wrapping at-rule visible in the breadcrumb. Pseudo-classes and attribute filters are stripped for matching, so .btn-primary finds the rule whether it carries :hover, [disabled], or sits in .modal .btn-primary. :is(.a, .b) / :where(.a, .b) recurse (additive); :not(...) / :has(...) don't. Native CSS nesting and SCSS & resolve against each parent simple selector — .card { &__header { } } is findable as .card__header; multi-selector parents propagate (a, .link { &:hover { } } is findable as both a and .link). SCSS additionally surfaces @mixin name($args) (callable, gets () in digest), @function, top-level $variable: value, and %placeholder; Sass privacy convention applied — names with leading _ / - marked private. New optional Declaration.match_names field for declarations reachable under several identifiers. [huge] size label ships in the same release — fourth bucket alongside [tiny] / [medium] / [large] with a behavioral twist: files at or above 100 000 estimated tokens collapse to header-only in digest mode, leaving the agg counters intact so the agent can still size the file up. A directory of 50 huge generated/vendored mega-files goes from 1 602 lines of digest output to 52 — measured 31× reduction. outline and show are unaffected. The legend gets one extra clause whenever a huge file appears in the batch: [huge]=body omitted (use \ast-outline outline `). Default ignore list also expanded with.min.js/.min.mjs/.min.cjs/.min.css/.min.html/.map`. |
| 2026-05-06 |
v0.7.3: outline header now carries the [tiny] / [medium] / [large] size label — the same categorical bucket digest stamps next to each filename. An agent calling outline directly (skipping digest) gets the at-a-glance size signal in plain English alongside the precise ~N tokens count, instead of having to map the raw token number onto a bucket from memory. Header reads # /abs/path.py [medium] (95 lines, ~1,200 tokens, 5 types, 12 methods). Digest output is unchanged. |
| 2026-05-06 |
v0.7.2: Ruby language adapter (.rb, .rake, .gemspec, .ru, plus Rakefile / Gemfile resolved by exact basename — first adapter to ship basename-matching) covering modules with qualified-form (module Foo::Bar) + old-style nested-module collapse to A::B::C (HIGH-fix from C++ applied: comments don't count as structural children when deciding whether to collapse), classes with < Super superclass + include / extend / prepend mixins surfaced as : Base, include Mod, extend Mod2 on the digest type header (signature stays Ruby-true class Foo < Bar, mixins live only in the digest's MRO clause), methods, def self.foo singleton methods + entire class << self blocks (both render [static]), full operator coverage as KIND_OPERATOR (+ / - / * / / / % / **, == / != / < / > / <= / >= / <=> / ===, & / | / ^ / ~ / << / >>, [] / []=, unary -@ / +@ / !), attr_accessor / attr_reader / attr_writer (one KIND_FIELD per symbol with [accessor] / [reader] / [writer] marker — multi-symbol calls split so each name stays grep-able), alias / alias_method (with [alias] marker and new → old signature), constants (MAX_NAME_LENGTH = 64) as fields, visibility tracked as a state machine across the class body — bare private / public / protected flips subsequent decls; targeted private :foo, :bar and private_class_method :baz retroactively mark named methods (forward + back references both supported); private() with explicit empty parens parses as a call node and still flips state. require / require_relative / load / autoload collected as imports; lazy loads inside method / block / lambda bodies counted into [+ N conditional includes]. Rails associations recognised by default — has_many, has_one, belongs_to, has_and_belongs_to_many surface as KIND_FIELD with [has_many] / [has_one] / [belongs_to] / [habtm] markers, by direct analogy to how the C++ adapter recognises Unreal Engine UPROPERTY macros. Other Rails DSL (validates, scope, before_action) intentionally NOT recognised — line drawn at relations because they describe model-to-model edges, not behaviour. |
| 2026-05-05 |
v0.7.1: agent-prompt snippet (printed by ast-outline prompt, copied verbatim into the EN/RU/ZH READMEs and docs/agents.md) now lists C++ extensions (.cpp/.cc/.cxx/.h/.hpp/.hh) alongside the rest of the supported set. v0.7.0 shipped the C++ adapter but the snippet still claimed the tool only handled the pre-C++ language list, so an LLM agent reading the snippet for the first time on a UE or general C++ project wouldn't learn ast-outline applies. RU and ZH READMEs also got the C++ row in their supported-languages table that was previously only in the English copy. |
| 2026-05-05 |
v0.7.0: C++ language adapter (.cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++, .ipp, .tpp, .inl, .cppm, .ixx) covering classes / structs / unions / enums (classic + enum class), namespaces with single-chain collapse to a::b::c (anonymous + inline preserved), templates with full / partial specialisation / variadic / template template params, operator overloads incl. conversion operators and the C++20 spaceship <=>, ctors / dtors with classification, public: / protected: / private: access blocks with C++-correct defaults (class → private, struct/union → public), base-class clauses with access + virtual markers, out-of-class member definitions (Widget::draw), #include directives as imports, C++20 concepts. Unreal Engine reflection macros recognised by default: UCLASS() / USTRUCT() / UENUM() / UINTERFACE() attach to the next type, UPROPERTY() / UFUNCTION() to the next member; GENERATED_BODY() family is stripped from the source pre-parse (length-preserving — line numbers stay aligned) so UHT's missing-semicolon convention no longer breaks tree-sitter on UE headers. Synthetic MISSING-; parse errors that tree-sitter emits after every UE macro are subtracted from the reported error count, so valid UE files no longer surface as [broken] in the digest. |
| 2026-05-05 |
v0.6.8: directory walks now respect .gitignore and .ignore (root + nested, with deepest-wins override semantics including ! negation) and prune a hardcoded list of universally non-source dirs out of the box (.git, node_modules, __pycache__, .venv, .tox, .next, .gradle, .idea, .vscode, .cursor, .zed, .fleet, .vs, *.egg-info/, …). Conflict-prone names (build/, bin/, dist/, target/, vendor/, out/, obj/) are intentionally NOT in the hardcoded list — those go through .gitignore per-repo. When the walker filters anything, output starts with # note: ignored N dirs (basename1, basename2, …) via .gitignore/.ignore + defaults — pass --no-ignore to disable so the agent sees what got skipped and learns the escape hatch. New --no-ignore flag disables the entire filter pipeline (one switch — no per-rule combinatorics like ripgrep's six). New pathspec>=0.12 dependency (MPL-2.0, Apache-2.0 compatible). |
| 2026-05-04 |
v0.6.7: ast-outline digest legend is now dynamic — only entries whose token shape actually appears in the rendered body are listed. YAML- and markdown-only digests (whose body contains no callables, kinds, markers, or inheritance) emit no legend at all; code batches keep a legend pruned to the subset of tokens that actually surface. The omission rule also drops the legend in the rare case where only L<a>-<b> would fire (e.g. a code batch of pure marker classes with no members) — a one-entry legend documenting line ranges is more overhead than insight when the suffix shape is already obvious from the body. Drops ~200 bytes of noise from yaml/md digests and saves prompt budget when digest output is piped into LLM context. |
| 2026-05-04 |
v0.6.6: ast-outline show gains a depth dial — --signature (and the long form --view signature) returns header only (docs + attributes + the signature line, no method body), with the mutex-grouped --full / --view full aliases preserving the existing body-extraction behavior as the default. Closes the gap between digest (just symbol names) and show (full body) for the post-digest "I have the name, I want the contract, not the implementation" workflow — and removes the temptation for agents to pipe show through head -80 to peek at signatures of large methods. Doc placement matches outline: /// / JSDoc / Rust before the signature; Python docstrings after with +1 indent. Composes with --no-doc for a bare contract line. New public helper core.render_signature_view(match) and SymbolMatch.decl back-reference. Agent-prompt snippet (English canonical + RU/ZH README copies + docs-site agents.md) gets a closing line on step 3 documenting the flag as a modifier across every show form. |
| 2026-05-03 |
v0.6.5: ast-outline prompt snippet (and its README copies) reworded to be explicitly cross-vendor — Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5 and OpenAI GPT-5.x (5.3-codex / 5.4 / 5.5). The broad-to-narrow command list lead-in changed from "Stop at the step that answers the question" to a menu/decision-tree framing, avoiding GPT-5.5's process-prescription antipattern while keeping the same scope guard for Claude's literal models. Module docstring of _prompt.py now codifies the cross-vendor invariants future edits must preserve. |
| 2026-05-03 |
v0.6.4: outline and digest no longer produce empty stdout when every file in a batch fails to parse. Per-file # note: parse error in <path>: <err> lines now go to stdout (matching the existing convention for user-facing failures), so an LLM agent reading stdout sees what happened instead of (no output). Detailed # WARN lines still go to stderr for humans; partial-failure batches keep their existing behavior. |
| 2026-05-03 |
v0.6.3: conditional_imports_count extended to Python, Rust, Scala — same [+ N conditional includes] marker now flags lazy import inside fn / class bodies (Python), use inside fn / closures (Rust), and method-scoped import (Scala). |
| 2026-05-03 |
v0.6.2: PHP language adapter (.php, .phtml, .phps, .php8) targeting modern PHP 8.x and the 7.4 LTS line — namespaces, classes (abstract / final / readonly), interfaces, traits, PHP 8.1 enums, magic ctor / dtor, PHP 8.0 ctor property promotion, multi-variable properties, PHP 8.3 typed class constants, PHP 8.0 #[Attr] attributes, use (incl. grouped) and top-level include / require. Verified on real WordPress core (no parse errors on files up to 291 KB). New common-IR field ParseResult.conditional_imports_count — renderers append [+ N conditional includes] to the imports line when the file has dependencies that aren't statically listed. |
| 2026-05-03 |
v0.6.1: PyPI metadata refresh after the GitHub Organization transfer (no code changes). |
| 2026-05-03 |
Repository transferred from dim-s/ast-outline to the ast-outline GitHub Organization. Old dim-s/ast-outline URLs continue to redirect. Copyright remains with Dmitrii Zaitsev (dim-s); the org is hosting infrastructure, not a new copyright holder. |
| 2026-05-03 |
v0.6.0: relicense from MIT to Apache License 2.0. Documentation separately licensed under CC BY 4.0. The previous MIT text is retained in LICENSE-MIT for compatibility with downstream forks of the 0.5.x tree. |
| 2026-05-02 |
First publish to PyPI as ast-outline. v0.4.2 / v0.4.3 / v0.5.0 (code-outline CLI alias dropped) / v0.5.1 (implements command dropped — outline/digest already render : Base) / v0.5.2 (--imports flag) / v0.5.3 (--version flag). |
| 2026-05-01 |
v0.4.0: digest method markers ([async] / [unsafe] / [const] / [suspend] / [static] / [abstract] / [override] / [classmethod] / [property]); type modifiers, attrs, and [deprecated] tag. |