pi-fovea
Token-budgeted repo mapping for agent sessions: foveated heat diffusion over a cross-language code graph, with progressive disclosure.
Package details
Install pi-fovea from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-fovea- Package
pi-fovea- Version
0.29.1- Published
- Sep 18, 2026
- Downloads
- 3,749/mo · 1,129/wk
- Author
- monotykamary
- License
- MIT
- Types
- extension, skill
- Size
- 757.5 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"image": "https://raw.githubusercontent.com/monotykamary/pi-fovea/main/media/cover.svg",
"skills": [
"./skills"
],
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
👁️ pi-fovea
A foveated repo-mapping extension for Pi
See the whole repo on every prompt, sharp where you work and cheap everywhere else.
pi-fovea gives the model a map of your repo on every prompt. The repo compiles once into a cross-language graph of code. Symbols, files, and route anchors share one network. Your question becomes an interest vector that diffuses through the graph as heat. The renderer caps the field inside a token budget. Near the question you get exact source locations and full signatures. One hop out you get typed relationships. Past that the repo collapses to a skeleton.
When a session starts, Fovea records a baseline of the repo. If files changed while Pi was idle, those changes enter context before the first model call. Fovea checks the repo again after each assistant turn. Detection uses file content hashes. An edit from a Pi tool, fabric_exec, bash, a subagent, or an editor looks the same to Fovea. Edits that touch only comments or formatting stay silent. A meaningful change arrives as a steer. When the agent is about to stop, Fovea starts the next turn itself.
Where fovea fits in shipping a feature
Shipping a feature in a large codebase costs time before the first edit. First you find where the feature lives. Then the change needs a map of everything it touches. Other branches keep landing while you work. Reviewers want the blast radius. In long-lived enterprise repos, these steps cost more time than the change itself. Fovea handles these steps. Each step becomes a cheap call against the code graph. Tests, review gates, CI, and rollout keep their own tools.
When not to reach for fovea
Skip Fovea on small repos. A repo of a few dozen files reads faster than it sketches. Fovea earns its cost when the working set outgrows the context window. Cross-language monorepos hit that wall early. A long-lived codebase you have never opened hits it too. Fovea narrows your reading to suggested windows. Open those windows yourself, and keep the project's format, lint, typecheck, and test commands in your loop. CI has the final say.
What the model gets
| Command | Ask | Answer |
|---|---|---|
fovea_sketch |
where is everything? | production-first silhouette plus explicit discovery/extraction coverage; test and fixture architecture stays collapsed |
fovea_focus |
what is this? | exact symbols/routes/protocol ids, evidenced relationships, path-gap reasons, suggested reads, scopes, and deterministic fresh views |
fovea_dwell |
what else? | widens the current focus, or expires safely when its graph generation changed |
fovea_impact |
what does this touch? | hunk-precise seeds, evidenced impact paths, co-change companions, and conserved diffusion heat |
grep (default hybrid) |
graph or text? | bare identifiers, qualified symbols, repo paths, and routes use Fovea; search options and obvious regex retain native grep |
Focus normalizes camelCase and common inflections. An approximate name such as
switchServer can still resolve switchingServers. A query with no certain
match returns the nearest symbols plus their locations. Direct graph edges
carry labels such as caller, callee, route, shared literal, and co-change, plus
deterministic strategy / rule / source evidence. Every tool result includes
a coverage report; an explicit missing path says whether it is unsupported,
ignored, oversized, generated, unreadable, Git-listed but unavailable, beyond
the file cap, behind a closed nested-repository boundary, or absent. Symbols
that merely share a file stay collapsed.
The Hybrid grep toggle is on by default. grep({ pattern: "CreateUser" }),
grep({ pattern: "Controller.create" }), and route paths travel through the
graph. Calls that carry text-search options or obvious regexes go to Pi's
native grep. A graph miss falls back to native text. A graph error, such as a
broken ast-grep, falls back the same way and adds a one-line note marking the
result as native. Turn the toggle off to recover a purely native grep. A toggle
change reloads extensions, so Pi and pi-fabric capture the same behavior.
pi-fabric
Captured extension tools live under Fabric's extensions provider. Use the direct proxy when the action is known:
const result = await extensions.fovea_focus({ query: "CreateUserHandler", maxTokens: 512 });
return result.text;
For dynamic discovery, pass an object to tools.search and keep the returned namespaced ref:
const [action] = await tools.search({ query: "fovea_focus", limit: 5 });
if (!action) return "Fovea is not captured";
return tools.call({ ref: action.ref, args: { query: "CreateUserHandler" } });
The stable explicit ref is extensions.fovea_focus. The bare forms
fovea_focus and fovea.fovea_focus will miss.
Runtime slash controls:
/fovea statusfor loaded versions, graph coverage, and active modes/fovea settingsfor a TUI configuration overlay/fovea resetfor a fresh focus and sync baseline/fovea reloadto activate updated extension source
Install
Requires Node.js 20+. The install provisions ast-grep automatically through the @ast-grep/cli npm optional dependency; an ast-grep found on PATH takes precedence over the packaged copy, and FOVEA_AST_GREP=/path/to/sg overrides both. Bend-only and config/protocol-only roots work without ast-grep; it is required when discovery or refresh includes a language that uses its parser.
pi install npm:pi-fovea
From GitHub:
pi install git:github.com/monotykamary/pi-fovea
From a local checkout:
bun install
pi install /absolute/path/to/pi-fovea
There is also a package for any agent shell or CI:
fovea sketch /path/to/repo 900
fovea focus /path/to/repo "/v1/messages" 800
fovea impact /path/to/repo --base main 1200
fovea rules /path/to/repo
fovea status /path/to/repo
Install the CLI globally — the published bin is a single self-contained bundle, so it runs on plain Node.js (no tsx, no node_modules):
npm i -g pi-fovea # or: bun add -g pi-fovea, bun add -g pi-fovea
From a checkout, bun run fovea runs the live source via tsx, and bun run build:cli rebuilds dist/cli.mjs (the prepack hook keeps the published bundle in sync).
Many projects, one conversation
Your Pi cwd can be a parent folder, a launcher, or unrelated to the work.
Successful native/Fabric pi.* reads, edits, writes, and searches automatically
select their containing project. Discovery walks bounded ancestors—not siblings
or the whole filesystem—and accepts only successful structured/literal access.
Startup and idle hooks do not index an otherwise-unselected launch directory.
32 observed roots; two hot graphs. The recency ring refreshes on access. Project 33 retires the least recently used root instead of failing. Canonical symlink aliases share identity; linked worktrees stay independent. Each root has its own heat, attention, semantic baseline, and provenance. Retirement is an explicit observation gap, never a clean verdict. Re-entry baselines anew.
await pi.read({ path: "/projects/service/src/handler.ts", offset: 1, limit: 40 });
// Access alone enrolls the project; an unrooted call still answers about cwd.
await extensions.fovea_focus({ root: "/projects/service", query: "src/handler.ts", maxTokens: 1024 });
// An explicit root selects any exact directory, including umbrella scopes.
await extensions.fovea_focus({ root: "../other", query: "entry", maxTokens: 512 });
- Omitted analysis roots use the session cwd's own project—the nearest
.gitor manifest, else the cwd itself—never the last selected project, so an unrooted call cannot answer about a sibling repository. Contour keeps the recency ring as a fallback for a coordinator cwd, where a review needs a Git worktree. Explicitrootresolves against the tool context's cwd, not process cwd or the previous root. Parallel coordinators should supply it. Native paths remain cwd-relative; neither Fovea nor Contour changes cwd. Results carrydetails.root,observedRoots,workspace(capacity/retirements), andagentOrigin. - Enrollment happens after success, not before permission checks. Failed or blocked calls enroll nothing. Automatic discovery excludes broad/system, private, dependency, and generated locations. It does not parse arbitrary programs or trust output text. Literal shell cwd forms are supported; opaque programs and remote filesystems need explicit coordination.
- Indexing expands a successful path to a project scope. This is not a sandbox
or a project-trust grant.
.pi/fovea.jsonis honored only for the exact canonical trustedctx.cwd; parent/sibling trust does not propagate. Existing declarative.fovea/rules.jsonextraction rules are unchanged. Hosts with finer-grained analysis permissions must enforce them separately. - The first access is a new observation boundary. A first write may already be included in it; Fovea does not invent its prior delta or authorship. Contour still compares that project's patch with Git. Subsequent focus calls do not consume pending drift. Focus/file-seeded impact establishes attention before opaque shell edits; hintless changes in enrolled roots remain detectable.
- Native augment grep follows the physical owner of its actual search path, never an unrelated active graph. No-path native grep still searches cwd. Legacy replace-mode bare graph queries use the cwd project; native options and fallbacks retain cwd semantics.
- Sync spends one shared context allowance on relevant messages, including root labels and retirement notices—not an equal slice for every quiet root. Cold unchanged Git roots do not rebuild graphs, and cold probes stay off the blocking before-agent hook. Numerical paging retains logical focus/attention.
- Branch-local bounded root snapshots survive compaction, reload, resume, fork,
and tree navigation. Semantic baselines and trust are not restored.
/fovea resetclears the ring;/fovea statusreports the ring, capacity, and the manual default. The CLI stays stateless. Fovea and Contour exchange session-qualified target hints, not heat, source content, trust, or mutation authorship.
Bounded indexing
Explicit umbrella graphs retain progressive nested-repository/submodule
boundaries: projects join that graph as their contents are worked in, subject to
FOVEA_MAX_FILES. Automatic project selection does not create an umbrella graph
just because unrelated projects share a parent directory. Cold extraction keeps
streamed JSONL caches, 64-file batches, adaptive ast-grep chunk splitting, and
bounded I/O/process concurrency.
| Variable | Default | Meaning |
|---|---|---|
FOVEA_MAX_ROOTS |
32 |
observed recency ring, clamped to 1–32 |
FOVEA_CACHE_ROOTS |
2 |
hot graph/fact/vector cache residency, separate from observation |
FOVEA_MAX_FILES |
8000 |
maximum indexed files in one graph |
FOVEA_MAX_FILE_BYTES |
1048576 |
maximum bytes extracted from one source file |
FOVEA_SPAWN_CONCURRENCY |
3 |
concurrent ast-grep/Git subprocesses |
FOVEA_IO_CONCURRENCY |
32 |
concurrent file stat/read operations |
FOVEA_MEMORY_HALF_LIFE_HOURS |
48 |
wall-clock half-life of charged cascade memory |
FOVEA_GIT_UNTRACKED_CACHE |
enabled | set 0 to disable Git's native untracked-directory cache |
FOVEA_MAX_SUBMODULE_DEPTH |
4 |
recursion cap for nested submodules |
Root-discovery metadata is coalesced and TTL-cached; it never certifies source
freshness. Git status/content hashes and bounded manifest/boundary sweeps remain
the drift oracle, including dirty-to-clean reverts. Local Git analysis disables
fsmonitor and remote/lazy fetching. Native untracked caching can still update
Git index metadata, not Git configuration; opt out with
FOVEA_GIT_UNTRACKED_CACHE=0 or GIT_OPTIONAL_LOCKS=0.
Oversized files and failed extractions remain visible coverage gaps in status and tool details. See the full workspace contract and shared API for boundaries, persistence, scheduling, and remaining limitations.
Turn sync
Continuous sync is enabled and visible by default. Before an agent starts, Fovea establishes its baseline or injects relevant drift ahead of the first model call. After every assistant turn it compares symbols, calls, imports, literals, and anchors again. Content hashes keep the unchanged fast path cheap. Edits that touch only comments or formatting raise no signal.
The default sync.scope is "session": path-bearing read/search/edit tools,
explicit focus/dwell results, and file-seeded impact calls add the top-level
logical directory (or exact root file) to that conversation's attention. Fovea
still indexes and baselines the whole root. Drift solely in sibling directories
is absorbed silently, so broad umbrella coverage does not become broad model
context. Set sync.scope to "repository" to restore root-wide steering.
A meaningful current, mixed, or unattributed change inside the attention scope
can still ship post-turn with deliverAs: "steer" and triggerTurn. If
pi-queue-steer still
holds rows (globalThis.__tmustierPiQueueSteerState.pending > 0), that same
notice rides nextTurn instead so a queued user row stays first in Pi's native
lane and Fovea cannot triggerTurn ahead of it. A relevant
change attributed solely to another Fovea-enabled session is queued for the
next user prompt instead; it never restarts an idle agent. The compact update
names changed files, route deltas, and newly relevant files. Shell commands,
external editors, and agents without Fovea remain unattributed rather than
being guessed, while the path scope still keeps unrelated sibling sandboxes
quiet. Provenance journals accept either intercepted mutations or explicit trusted SHA-1 transitions, preserve supplied commit order, write one bounded replacement per event batch, expire after seven days, and live in
$TMPDIR; repository content remains the drift oracle. Updates list causal
channels such as calls, imports, shared literals, tests, or co-change history.
By default Fovea also embeds the refreshed focus context of the top drift target
(push). With sync.pushFocus off, the update ends with a suggested focus probe
for the next call (pull). Switching
branches re-baselines silently instead of steering: a git checkout
re-materializes the worktree, but the branch diff is not authored drift —
commits, pulls, and rebases still report. Clean turns stay silent. Enable sync.ackClean if you want an ack for those. Set sync.mode to
"hidden" to keep red sync context working behind the scenes without rendering it
in the transcript, or to "disabled" to turn continuous sync off.
Surprise is measured per graph node, not per file: a disclosed cascade
charges the symbols, literals, and anchors it warmed, and a later verdict
only counts mass exceeding that ledger. Re-editing the same spot re-seeds
the same charged nodes and stays silent — flip-flopped work cannot wake the
model twice, no matter how many times it flips within a session. A novel
hunk still fires, damped only by the charged nodes it overlaps. The ledger
cools by wall clock (48h half-life, FOVEA_MEMORY_HALF_LIFE_HOURS), so a
structurally re-heated neighborhood can earn a fresh verdict on a later
day. Anchor deltas follow the same evidence rule: a route add/remove
escalates only when its carrier file drifted, which makes transient
extraction artifacts quiet by construction.
Runtime controls:
/fovea status: loaded package and ast-grep versions, indexed coverage, anchor scopes, sync mode/attention scope, and grep mode/fovea reset: clear focus disclosure and depth, then establish a fresh sync baseline/fovea reload: hot-reload extensions and activate newly installed source; sync baselines (the verdict ledger) ride through on a global slot, so a reload no longer replays charged cascades as first disclosures/fovea settings: configure sync, budgets, and hybrid grep
Choose enabled, hidden, or disabled per repo or globally through settings. The environment override still turns sync off with:
FOVEA_TURN_SYNC=off pi
Change impact as heat
fovea_impact ranks relevant context under a strict token budget. It does not track inspection or certify feature completeness.
Seeds come from the diff. Hunk parsing maps each change to the symbols that contain it. One unit of mass goes to each changed file: 0.2 on the file node, 0.8 over the touched symbols in proportion to sqrt(changed lines). Heat then spreads along static relationships, with historical co-change partners added as decaying seed heat. Edits that resist symbol-level location fall back to the file-node seed. New files, deletions, renames, untracked paths, and oversized diffs all take that path.
Historical co-change is a decaying heat prior, never a permanent graph edge. It counts up to 400 first-parent integration boundaries: merge net changes count once, not again through their constituent commits. Explicit fixup!/squash! followups join only uniquely resolved older subjects in that window. Time, author, shared issue numbers, and unlabeled "forgot this" do not group work. Boundaries are not proof of a semantic feature (release merges can mix work). Aggregates above 24 tracked files emit no pairs but retain directional touch counts; two distinct retained units are needed for a pair, three for expectations. Raw history caching includes shallow-state identity; recency still applies at use. Focus, sketch, and the structural diffusion operator remain unchanged.
Impact details describe the current cascade:
warmedMass,warmedReasons, andwarmedEvidence: per-file heat and the causal channels supporting the ranking.expectedButUnchanged: likely companions inferred from directional co-change history, not required edits.conservedMass: degree-corrected random-walk heat, reported separately from the raw scale used by sync.
Repeated impact calls do not accumulate pending work. The result and its overflow list describe the current seeds, graph, and historical heat prior; a seedless query does not carry earlier suggestions forward. Focus and dwell retain only their own progressive-disclosure context, while graph refresh and turn sync keep navigation current after source changes.
docs/heat-diffusion.md has the full mechanics.
Configuration
Global settings live in ~/.pi/agent/fovea.json. A trusted repo-level override
sits in <repo>/.pi/fovea.json. These are the same two scopes pi-fabric uses
with fabric.json. In /fovea settings, the configured external-editor key
(Ctrl+G by default) switches both the displayed values and save destination
between project overrides and global defaults. A project override can remain
effective while its global default is being edited.
| Key | Default | Meaning |
|---|---|---|
sync.mode |
"enabled" |
"enabled" shows model-visible sync messages, "hidden" keeps them model-visible but out of the transcript, and "disabled" turns sync off. Legacy sync.enabled booleans still parse. |
sync.scope |
"session" |
"session" steers only for top-level directories/root files this conversation entered while indexing the whole root; "repository" restores root-wide steering. |
sync.budget |
512 |
token cap for proactive steering context |
sync.ackClean |
false |
toast after clean structural turns |
sync.steerThreshold |
0.15 |
total surprise (channel-weighted heat above the session sync memory) that justifies proactive model steering |
sync.pushFocus |
true |
embed a budgeted focus preview of the top drift target in red syncs |
tools.defaultBudget |
512 |
fallback maxTokens for the fovea_* tools |
tools.grepMode |
"augment" |
"augment"\u0020keeps native grep and appends a Fovea graph section to symbol-query results (works with pi.grepinside fabric_exec too);"replace"keeps the legacy takeover where bare symbol queries navigate the graph instead of returning lines;"off"is native grep only. The legacy booleantools.replaceGrep still parses (true\u2192"replace", false\u2192"off") and loses to an explicit grepMode`. |
tools.grepAugmentBudget |
512 |
token cap for the appended graph section |
Budgets cap the rendered view, not the map. Focus and dwell keep every eligible candidate recoverable beyond both the token budget and the 400-candidate display cap. Their details.lit counts the full eligible set, while details.candidateOmitted counts display-cap exclusions. Whenever a view truncates, the full list spills to $TMPDIR/pi-fovea-<op>-<hash>.txt if writable, and the footer names the path. Read or grep that artifact for the remainder; a failed write never produces a false path. Scope filters, already-disclosed nodes, and nodes below the heat cutoff are not added to overflow. fovea_dwell remains the semantic widen.
How routes are found
Route anchors come from five port shapes. Together they cover most of the ecosystem:
| Port shape | Examples |
|---|---|
recv.verb("path", handlers…) |
express, koa, fastify, hono, gin, echo, chi, net/http |
| annotation + optional class prefix | NestJS @Controller + @Get, Flask and FastAPI decorators, Spring @RequestMapping + @GetMapping |
| verb embedded in the path | Go 1.22 mux.HandleFunc("GET /x", h) |
| verb as first string argument | chi r.Method("GET", path, h), aiohttp router.add_route("GET", path, h) |
| receiver-less DSL macros | Rails routes.rb, Phoenix router.ex, Django path(), Ktor routing { get("/x") {} } |
File-convention routers keep their paths in the file tree. Next.js App Router, Pages Router, SvelteKit, Nuxt, and Astro work this way. Fovea derives their anchors from file paths. The verb comes from exported handler names or filename suffixes.
Discovery mode
A repo may write routes in a shape fovea has never seen. The literal pass
harvests every call shape in it. Shapes with solid statistics get promoted into
implicit rules. A discovered anchor carries half the conductance of a declared
one and shows a △ sigil. Turn sync reports the churn. An unconfirmed
hypothesis cannot turn the verdict red. Once a known rule matches any site of a
hub, that hub upgrades to first-class.
fovea anchors <root> --discovered # the △ hypothesis hubs only
fovea rules <root> # promoted rules with evidence
fovea rules <root> --sigs # every path-touching signature, by precision
fovea rules <root> --adopt # persist promotions into .fovea/rules.json
.fovea/rules.json pins community or project rules in the repo:
{
"rules": [
{ "id": "fiber", "langs": ["Go"], "pattern": "$R.$M(\"$P\", $$H)", "methods": "^(get|post)$", "kind": "route" }
]
}
A rule may declare prefixPattern. A class-level prefix such as
@Controller('api/airports') then composes with per-method paths. A change to
the rules file invalidates the anchor extraction cache. The parsed facts above
the cache carry over.
Non-HTTP protocol topology
Fovea parses protocol grammar locally and deterministically; it does not run a
language server, descriptor compiler, broker, or model. GraphQL documents emit
named operations, root fields, schema types/type references, and fragments.
Protocol Buffer documents emit package-qualified services, methods, messages,
and request/response/field references. Exact generated gRPC method paths join
back to their method and service. Declared tRPC and oRPC procedures join
router members, split-file route constants, and client calls. Hono routes
register through verb methods and app.on, and hc() RPC client calls join
the server-declared hub. Literal publish/subscribe calls join on their channel
while preserving the producer/consumer rule in edge evidence.
Router object members anchor at every position: the first matching slot is
captured exactly and the remaining members are enumerated from the sibling
capture, each at its own line. tRPC receivers must root at the t builder or
a *Procedure factory, oRPC at the os builder, so trpc.post.list.query()
client proxies and oRPC oc.* contract-only shapes stay unlinked rather
than guessing a nested name.
Validated against 36 open-source repositories (hono, trpc, unnoq/orpc, documenso, cal.com, unkey, googleapis, protobuf, saleor, the published GitHub schema, mqtt.js, nats, ably, and more): Hono yields 1.2k route anchors plus 72 RPC-client joins, documenso 214 tRPC procedures, the proto corpus 9.7k anchors with zero keyword false-positives, and a 1.2 MB GitHub schema parses through the raised protocol byte cap.
Canonical ids can be focused directly:
RPC users.v1.Users/GetUser
RPC SERVICE users.v1.Users
RPC MESSAGE users.v1.User
GRAPHQL QUERY user
GRAPHQL TYPE User
TRPC loadUser
ORPC ping
CHANNEL users.changed
Blind spots are logged in src/core/anchors.ts. The remaining list covers
Rust proc-macro attributes (actix #[get("/x")]), constructor-assigned
prefixes (Flask Blueprint, FastAPI APIRouter(prefix=…), chi Mount, Express
Router mounts, Hono basePath/route sub-apps), scope and namespace
nesting in Phoenix, Rails, or Django include(), router members behind
spreads or beyond the first twelve positions, trpc.post.list.query() client
proxies, oRPC dynamic clients, Hono app.on with non-standard verbs or path
arrays and hc() chains deeper than one segment, GraphQL embedded inside
host-language strings, generated gRPC clients with no literal method path,
and other computed protocol names. Ambiguous strings remain unlinked rather
than guessed.
Coverage and completeness
File discovery records its source (git or bounded filesystem walk), recording
state (complete, partial, or truncated), supported/unsupported/excluded
counts, exact Git-listing cap omissions, closed nested repositories, unavailable
Git worktree entries, and unreadable traversal boundaries. Extraction separately
records partial failures, unreadable files, oversized files, and generated files.
Protocol documents (.proto, .graphql, .gql) use their own larger byte cap
(FOVEA_MAX_PROTO_FILE_BYTES, 8 MB default) because their exact readers never
reach ast-grep; a real-world schema larger than the code cap still parses.
Lists in tool details are bounded examples; the counters are not. A truncated
filesystem walk reports an unknown omission count instead of inventing one.
/fovea status and fovea status use this same ledger rather than comparing
unlike tracked and supported file counts.
Import diagnostics live in details.coverage.imports: captured sites are classified as resolved, possible, or unresolved, with bounded examples, cap counts, and languages lacking import readers. Unresolved includes external packages and unmodeled resolution—not necessarily broken code. JS/TS literal import() and require() targets resolve normally. A relative one-hole expression such as import('./plugins/' + name + '.js') or require(`./plugins/${name}.js`) can connect up to 32 matching in-scope files through explicitly possible import edges. A family scans at most 4,096 files; exceeding either limit emits no partial family and reports the cap. Unknown expressions remain unresolved. Possible imports do not become exact call or test edges.
This is a finite candidate approximation, not a bound on runtime values: variables can contain traversal segments or name generated, external, or excluded modules. Conductance encodes a prior, not a calibrated probability. Selected files have file nodes by construction; relationship completeness, future edits, runtime behavior, and business requirements do not follow from that membership.
The 39-repository paired corpus checks this boundary without repository execution or useful co-change history. It distinguishes ordinary diffused probes from uniform-field display-cap stress, records unsupported/excluded files, and retains failures rather than replacing repositories. The dynamic-family observation is limited to one Vue build utility; regression fixtures separately exercise runtime plugin loading and capped/unknown cases.
How it works
The repo compiles to a typed graph. Your question becomes a source vector $s$ over the nodes. The field the model receives is the heat kernel at time $t$ over the Laplacian $L$:
$$ v(t) = e^{-tL} \cdot s \quad \text{with} \quad L = I - D^{-1/2} W D^{-1/2} $$
The four tools run one operator at four timescales. Sketch runs at $t=16$ with production hubs and anchors as seeds. Focus drops to $t=2$, seeded by your query. Dwell doubles $t$ inside the current focus. Impact takes changed files as its seed. A change of focus resets the sharp timescale and the disclosure scope.
A Chebyshev expansion evaluates the kernel. Rescale $M = L - I$ so the spectrum sits in $[-1,1]$. With $T_k$ as the Chebyshev polynomials and $I_k$ as the modified Bessel functions:
$$ e^{-tL} = e^{-t} \left[ I_0(t) T_0(M) + 2 \sum_{k\ge 1} (-1)^k I_k(t) T_k(M) \right] $$
The vectors $T_k(M) s$ stay cached in the session. A new timescale reuses those
vectors and pays only for fresh coefficients. They are bound to a hash of the
ordered graph generation (node identities/signatures and weighted, evidenced
edges). A refresh that changes that generation clears focus/disclosure state;
dwell fails closed and asks for a new focus rather than applying stale node
indices. The graph walk happens once per generation.
Discovery measures how often the argument at one slot of a call shape carries a route path. Shapes earn promotion past a Jeffreys-smoothed posterior:
$$ \hat{p} = \frac{\mathrm{pathN} + \frac{1}{2}}{\mathrm{n} + 1} \ge 0.55 \quad \text{with} \quad \mathrm{n} \ge 4 \text{ sites and} \ge 2 \text{ files} $$
Tests on eight cloned projects put corpus junk below $\hat{p} \approx 0.27$. Real route shapes land above $\hat{p} \approx 0.75$. The cutoff sits mid-cliff at any repo size.
The method draws on spectral-graph wavelets evaluated by shared Chebyshev recurrence. Progressive image coding contributes the budget-as-bitrate view over significance-ordered coefficients. Foveated rendering supplies the sharp center and the coarse rim. Aider's PageRank repo map is the fixed-timescale special case of this field. docs/heat-diffusion.md walks through conductance tiers, specificity bridges, hub gravity, and inferred regions.
Snapshot substrate API
pi-fovea/substrate exposes a versioned, session-free graph and heat interface for
consumers such as pi-contour. It assembles caller-supplied immutable facts without
discovering files or reading a live worktree. See docs/substrate.md.
Languages
Full symbol and call extraction: TypeScript, TSX, JavaScript, Python, Go, and Rust.
Outline-based symbols: Elixir, Ruby, C, C++, Java, Kotlin, Lua, PHP, Swift, Scala, Haskell, and Bash.
Config joins through literals: YAML, JSON, TOML, env, Markdown, and OpenAPI.
Exact contract topology: Protocol Buffers (.proto) and GraphQL (.graphql, .gql), joined to gRPC, tRPC, oRPC, Hono, and producer/consumer call sites.
Bend 2 (.bend) uses a native source reader (no ast-grep binary or grammar required):
definitions, laws, datatypes/constructors, explicit calls (including GPU !),
strings, and local/foreign imports. Module aliases resolve qualified calls.
A law implemented in the same file shares its definition's symbol. Built-in
Base, hub packages, and out-of-root imports remain unresolved. Operator/desugared
calls and proof checking are not modeled; Bend 1/HVM syntax is not supported.
Development
bun install
bun run check:fast # typecheck + tests your working tree affects
bun run test:smoke # curated scan-to-render floor, seconds
bun run bench # rate–distortion and refresh bench against ../pi-fabric
bun run bench tests/fixtures/mini # self-contained smoke run
The warm-path performance report compares the coverage-complete implementation with the optimized version. bun run corpus:performance <coverage-work> <before-source> <after-source> [rounds] runs alternating, isolated comparisons with a shared extraction snapshot, independent Git indexes, and exact graph, focus/dwell/sketch, and overflow-content checks. It does not trade fewer candidates or weaker freshness checks for speed. For Node, run NODE_OPTIONS='--import tsx' node scripts/performance-corpus.mjs ...; development dependencies are required.
Warm matching derives query terms once and caches normalized symbol names by weak node identity, checking renames before reuse. Focus builds only the heat orders currently needed; dwell appends missing orders without replacing its existing basis. No new kernel or review state is introduced. Independent cold extraction can select different literal-join locations when capture order varies, so this paired benchmark deliberately makes no cold-extraction speedup claim.
The developer benchmark gates timings on semantic equivalence: cold versus cached builds, and forced refresh versus clean rebuild after unchanged, location-only, semantic, added-file, and deleted-file scenarios in disposable copies of the cross-language fixture. It checks facts, weighted edges and evidence, coverage, operator contents, and fixture navigation; extraction-order permutations and opaque rebuild hashes are not semantic differences. Runtime ordering and generation invalidation are unchanged.
Reports include focus/dwell and refresh median/p95 samples, actual estimated
output tokens, and process peak RSS (including the validation work). Cold and
disk-warm target builds are single samples; three-sample refresh p95s are only
smoke diagnostics. The outline gets no more tokens than Fovea actually used.
fidelity@16k measures disclosed node IDs against a finite larger Fovea response,
not independently labeled relevance. Timing results are informational, never
a flaky CI gate; deterministic equivalence tests run in the change-scoped selection (bun run test:changed).
The bench clears the target's disposable facts cache to measure cold loading,
but edits only temporary fixture copies.
pi loads the extension straight from src/ through jiti, so nothing needs
building. Per-repo JSONL caches live in $TMPDIR, guarded by per-file content
sha1 values and stat manifests. Cache I/O streams. Only dirty files re-run
ast-grep. Failed extractions keep fact-free hash markers that stay visible
across launches. Those files skip the retry on each start. Bump CACHE_VERSION
in src/core/build.ts whenever extractor semantics change.
Temporary-storage retention
Retention runs asynchronously on actual facts/cochange, journal, spill, or scan
activity—not extension registration or idle lifecycle hooks. Sweeps are coalesced
and throttled to once per five minutes per process. No background timer is kept.
Policies apply across roots in the OS tmpdir() ($TMPDIR where supported):
| Artifacts | Retention |
|---|---|
Facts + cochange (pi-fovea-<16hex>.json, pi-fovea-cochange-<16hex>.json) |
Combined 128 MiB / 128 files; expire after 7 days; oldest modification first; 5-minute fresh-write grace |
Focus/dwell/impact/sketch spills (pi-fovea-<op>-<8hex>.txt) |
Combined 32 MiB / 128 files; expire after 24 hours; oldest modification first; 1-hour grace for reading advertised paths |
Provenance journals (pi-fovea-provenance-<16hex>-<16hex>.json) |
Only the existing 7-day record TTL; never pressure-evict fresh attribution |
Partial atomic writes (<recognized-name>.tmp-<PID>-<UUID>) and new scan rules (pi-fovea-scan-<PID>-<UUID>.yml) |
Clean in finally; recover abandoned files only after 1 hour and only when their PID is definitely dead |
These are best-effort, eventual limits, not global quotas: grace periods, active writers, concurrent processes, permission failures, and no subsequent activity can leave totals temporarily above budget. Reads do not refresh modification times. Cache reads/writes are capped at 64 MiB per file. Oversized facts persistence is skipped without changing in-memory extraction, and the prior cache remains valid through normal content/stat checks; oversized disk caches are misses. Spills are capped at 8 MiB; rejected writes omit the artifact pointer, never advertise a truncated full list. All cache/spill replacements use exclusive 0600 staging files and atomic rename. Journals retain their existing 256-record cap without a new byte cap; malformed or oversized journals are conservatively left alone by housekeeping.
Cleanup recognizes exact names only, checks lstat for regular, singly linked,
current-UID files, and rechecks device/inode/size/mtime/ctime immediately before
unlink. Symlinks, directories, unrelated names and other users' files are never
cleanup candidates; unknown ownership is a reason to skip. Platforms without UID
verification do not create persistent caches, spills or attribution journals;
analysis continues without disk reuse and cross-session attribution is unavailable.
Exclusively created per-invocation scan files still clean up by inode identity.
Descriptor reads remain byte-bounded even if a file grows after validation. Filesystem APIs do not
provide atomic unlink-by-inode, so this is best-effort race detection, not a
security boundary against a hostile same-UID process. New scans have independent
rule files kept until every chunk finishes, with no unbounded rule-file map.
Legacy pi-fovea-scan-* directories have no trustworthy PID metadata and are left
for explicit, separately reviewed reclamation—there is no recursive prefix purge.
Adjacent configuration staging is also cleaned on write/rename failure, but
configuration files and developer-owned reports are not retention candidates.
The performance-corpus benchmark retains raw.json/summary.json reports under
tmpdir(), but removes its uniquely allocated per-worker scratch (facts and Git
index) after worker success or failure. Existing reports, coverage work directories
and history-corpus data are not swept. For explicit maintenance, the internal
pruneTempStorage({ directory }) helper returns eligible paths without mutation;
only { directory, dryRun: false } applies the same rechecked policy.
Acknowledgments
Thanks to Alp, the original user whose request for a better LSP extension started this project.
MIT.