pi-fovea 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,21 +19,21 @@ _See the whole repo on every prompt, sharp where you work and cheap everywhere e
19
19
 
20
20
  pi-fovea hands the model a map of your repo on every prompt. The repo compiles once into a code graph across languages, where symbols, files, and route anchors join into one network. Each question becomes an interest vector that diffuses over the graph as heat. The renderer converts the field into a token-capped view: exact source locations and full signatures near your task, typed one-hop relationships next, and a skeleton of the rest.
21
21
 
22
- After each assistant turn the map re-syncs incrementally. Detection reads content hashes instead of tool events, so edits made by pi's edit/write tools, a fabric_exec inner `pi.edit`, a bash heredoc, a subagent, or an editor save outside the session all land identically. A clean turn stays silent. A turn that moves route anchors or warms files you have not looked at says so.
22
+ At agent start Fovea establishes or checks its semantic baseline, so out-of-band edits made while Pi was idle enter context before the first model call. After each assistant turn it re-syncs again. Detection does not trust tool events: edits made by Pi tools, fabric_exec, bash, subagents, or an editor land identically, while comment- and formatting-only drift stays silent. A meaningful post-turn change is delivered as a **steer**, and Fovea triggers the continuation itself if the agent would otherwise wait.
23
23
 
24
24
  ## What the model gets
25
25
 
26
26
  | Command | Ask | Answer |
27
27
  |---|---|---|
28
- | `fovea_sketch` | where is everything? | the repo as a silhouette, with feature anchors and inferred regions ranked by mass |
29
- | `fovea_focus` | what is this? | centered on a symbol, route path, or env key: exact hot signatures, typed direct relationships, then warm one-liners |
30
- | `fovea_dwell` | what else? | diffuses the field one step further and returns the delta |
28
+ | `fovea_sketch` | where is everything? | production-first silhouette; test and fixture architecture stays collapsed |
29
+ | `fovea_focus` | what is this? | exact matches, typed relationships, suggested reads, optional source scopes, and deterministic `fresh` views |
30
+ | `fovea_dwell` | what else? | widens the current focus and returns newly relevant neighbors |
31
31
  | `fovea_impact` | what does this touch? | warms everything a file, symbol, or PR base reaches across languages |
32
- | `grep` *(default override)* | where does this concept lead? | the same graph-backed focus through grep's familiar `pattern/path/glob/...` signature |
32
+ | `grep` *(default hybrid)* | graph or text? | bare identifiers, qualified symbols, repo paths, and routes use Fovea; search options and obvious regex retain native grep |
33
33
 
34
34
  Focus normalizes camelCase and common inflections, so an approximate name such as `switchServer` can resolve `switchingServers`. If a query is still uncertain, Fovea returns nearby symbols with locations instead of a dead miss. Direct graph edges are labeled (caller, callee, route, shared literal, co-change), while unrelated same-file siblings remain collapsed.
35
35
 
36
- The **Replace grep** toggle makes Fovea own Pi's `grep` tool slot. It is on by default. Familiar calls such as `grep({ pattern: "CreateUser", path: "src" })` navigate the code graph first; use `bash` with `rg` only when you need exact matching lines. Disable the toggle to restore the previous grep implementation. Changing the toggle reloads extensions so pi-fabric captures the same override and `pi.grep(...)` follows it inside `fabric_exec`.
36
+ The **Hybrid grep** toggle is on by default. `grep({ pattern: "CreateUser" })`, `grep({ pattern: "Controller.create" })`, and route paths can navigate the graph. Calls with text-search options and obvious regexes delegate to Pi's native grep unchanged; a graph miss also falls back to native text. Disable the toggle for a purely native slot. Changing it reloads extensions so Pi and pi-fabric capture the same behavior.
37
37
 
38
38
  ### pi-fabric
39
39
 
@@ -54,10 +54,12 @@ return tools.call({ ref: action.ref, args: { query: "CreateUserHandler" } });
54
54
 
55
55
  The stable explicit ref is `extensions.fovea_focus`, not bare `fovea_focus` or `fovea.fovea_focus`.
56
56
 
57
- Two slash commands on top:
57
+ Runtime slash controls:
58
58
 
59
- - `/fovea status` for graph stats and sync state
60
- - `/fovea settings` for an overlay in your TUI, styled after pi-fabric's `/fabric settings`
59
+ - `/fovea status` for loaded versions, graph coverage, and active modes
60
+ - `/fovea settings` for a TUI configuration overlay
61
+ - `/fovea reset` for a fresh focus and sync baseline
62
+ - `/fovea reload` to activate updated extension source
61
63
 
62
64
  ## Install
63
65
 
@@ -99,14 +101,18 @@ fovea status /path/to/repo
99
101
 
100
102
  ## Turn sync
101
103
 
102
- Turn sync is on by default. After every assistant turn the graph is re-synced against your edits: unchanged files cost nothing because every parsed fact sits behind its content hash. The verdict is **green** or **red**:
104
+ Continuous sync is on by default. Before an agent starts, Fovea establishes its baseline or injects any out-of-band drift before the first model call. After every assistant turn it compares extracted symbols, calls, imports, literals, and anchors again. Content hashes keep the unchanged fast path cheap, while comment- and formatting-only edits do not wake the model.
103
105
 
104
- - **green**: silence in the model's context. A clean-toast shows only if you enable `sync.ackClean`.
105
- - **red**: a capped custom message naming route anchors that appeared or disappeared, plus files warmed by the edit cascade that the model has not focused on yet.
106
+ A meaningful change found before agent start is injected directly into that run. A post-turn route or dependency change is sent with `deliverAs: "steer"`; if the agent would otherwise settle, `triggerTurn` starts the continuation automatically. The compact update names directly changed files, route deltas, newly relevant files, and causal channels such as calls, imports, shared literals, tests, or co-change history. Clean turns remain silent unless `sync.ackClean` is enabled.
106
107
 
107
- The first sync seeds the baseline. The first drift after it calibrates the warm neighborhood, so a steady feature cone stays quiet. Sub sequent drift turns red.
108
+ Runtime controls:
108
109
 
109
- Turn it off per repo or globally: `/fovea settings` → Turn sync, or
110
+ - `/fovea status` — loaded package/ast-grep versions, indexed coverage, anchor scopes, sync and grep modes.
111
+ - `/fovea reset` — clear focus disclosure/depth and establish a fresh sync baseline.
112
+ - `/fovea reload` — hot-reload extensions and activate newly installed source.
113
+ - `/fovea settings` — configure sync, budgets, and hybrid grep.
114
+
115
+ Turn sync off per repo or globally through settings, or with:
110
116
 
111
117
  ```sh
112
118
  FOVEA_TURN_SYNC=off pi
@@ -118,12 +124,12 @@ Global settings live in `~/.pi/agent/fovea.json`. A trusted repo-level override
118
124
 
119
125
  | Key | Default | Meaning |
120
126
  | --- | :-----: | ------- |
121
- | `sync.enabled` | `true` | the turn-sync loop |
122
- | `sync.budget` | `1024` | token cap for the red report seen by the model |
127
+ | `sync.enabled` | `true` | pre-agent and post-turn continuous sync |
128
+ | `sync.budget` | `1024` | token cap for proactive steering context |
123
129
  | `sync.ackClean` | `false` | toast after clean structural turns |
124
- | `sync.warmFileThreshold` | `2` | warmed files unseen by the model that justify turning red |
130
+ | `sync.warmFileThreshold` | `2` | newly relevant files that justify proactive model steering |
125
131
  | `tools.defaultBudget` | `2000` | fallback maxTokens for the fovea_* tools |
126
- | `tools.replaceGrep` | `true` | replace Pi's grep slot with graph-backed Fovea navigation |
132
+ | `tools.replaceGrep` | `true` | install hybrid native-text / bare-query graph grep |
127
133
 
128
134
  ## How routes are found
129
135
 
@@ -172,7 +178,7 @@ $$
172
178
  v(t) = e^{-tL} \cdot s \quad \text{with} \quad L = I - D^{-1/2} W D^{-1/2}
173
179
  $$
174
180
 
175
- The four tools are the same operator at four timescales: sketch at $t=16$ with hub and anchor seeds, focus at $t=4$ with your query as seed, dwell doubling $t$ per call with a disclosed-set delta, and impact using the changed files as seed.
181
+ The four tools are the same operator at four timescales: sketch at $t=16$ with production hub and anchor seeds, focus at $t=2$ with your query as seed, dwell doubling $t$ within that focus, and impact using changed files as seeds. Changing focus resets to the sharp timescale and its own disclosure scope.
176
182
 
177
183
  The kernel is evaluated with a Chebyshev expansion. Rescale $M = L - I$ so the spectrum sits in $[-1,1]$; then with $T_k$ the Chebyshev polynomials and $I_k$ the modified Bessel functions:
178
184
 
@@ -190,7 +196,7 @@ $$
190
196
 
191
197
  Measured against eight cloned projects, corpus junk sits below $\hat{p} \approx 0.27$ and real route shapes above $\hat{p} \approx 0.75$. The cutoff stays mid-cliff regardless of repo size.
192
198
 
193
- Lineage: spectral-graph wavelets evaluated by shared Chebyshev recurrence, progressive image coding where the budget is a bitrate over significance-ordered coefficients, and foveated rendering. Aider's PageRank repo map is the fixed-timescale special case of this field. The full walkthrough of conductance tiers, specificity bridges, hub gravity, and basins lives in [docs/heat-diffusion.md](docs/heat-diffusion.md).
199
+ Lineage: spectral-graph wavelets evaluated by shared Chebyshev recurrence, progressive image coding where the budget is a bitrate over significance-ordered coefficients, and foveated rendering. Aider's PageRank repo map is the fixed-timescale special case of this field. The full walkthrough of conductance tiers, specificity bridges, hub gravity, and inferred regions lives in [docs/heat-diffusion.md](docs/heat-diffusion.md).
194
200
 
195
201
  ## Languages
196
202
 
package/cli.ts CHANGED
@@ -56,7 +56,9 @@ try {
56
56
  let out = "";
57
57
  if (cmd === "status") {
58
58
  const s = sketch(rootAt(0), 256);
59
- out = `${s.details.files} files, ${s.details.nodes} nodes, ${s.details.anchors} anchors`;
59
+ const testAnchors = Number(s.details.testAnchors ?? 0);
60
+ out = `${s.details.files} files, ${s.details.nodes} symbols, ${s.details.productionAnchors ?? s.details.anchors} production anchors` +
61
+ (testAnchors ? `, ${testAnchors} test/fixture anchors collapsed` : "");
60
62
  } else if (cmd === "sketch") {
61
63
  const root = rootAt(0);
62
64
  const B = numAt(pos[0] === root && pos.length > 1 ? 1 : 0) ?? 1400;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fovea",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Token-budgeted repo mapping for agent sessions: foveated heat diffusion over a cross-language code graph, with progressive disclosure.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,22 +9,22 @@ pi-fovea maintains a cross-language code graph of the working repository — rou
9
9
 
10
10
  ## The loop
11
11
 
12
- 1. **`fovea_sketch`** — silhouettes only. Route/anchor inventory plus directory blobs ranked by heat. Start here in an unfamiliar repo. ~256–1024 tokens.
13
- 2. **`fovea_focus` `<query>`** — point at a symbol name (close spellings work), route path (`/api/users/{id}`), env key, or file path. Hot nodes carry exact source locations and signatures; direct callers/callees and other typed edges are labeled; the periphery stays collapsed. A true miss suggests nearby symbols. Already-shown nodes are suppressed, so repeated focus calls stay cheap.
14
- 3. **`fovea_dwell`** — optional second look. If a focus footer reports a remaining low-acuity periphery, dwell (diffusion time ×2) returns newly warmed neighbors.
15
- 4. **`fovea_impact`** — blast radius. Seed with explicit repo-relative `files`, symbol names for what-if analysis, or uncommitted changes (`base` works PR-style against a ref). Output is the predicted co-change cascade ordered by warmth.
12
+ 1. **`fovea_sketch`** — production-first silhouette. Shipped routes and source regions lead; test and fixture architecture is collapsed. Start here in an unfamiliar repo. ~256–1024 tokens.
13
+ 2. **`fovea_focus` `<query>`** — point at a symbol name (close spellings work), route, env key, or file. The active seed and direct relationships always remain visible; previously seen periphery is suppressed only within that focus. A different focus resets to sharp context. Use `path`, `language`, or `kind` to scope output and `fresh: true` for a reproducible full view. Structured details include nodes and suggested read windows.
14
+ 3. **`fovea_dwell`** — optional second look. If focus says more results remain, dwell widens only the current focus and returns newly relevant neighbors.
15
+ 4. **`fovea_impact`** — blast radius. Seed with repo-relative `files`, symbols, uncommitted changes, or a PR `base`. Output is likely review order with causal channels (calls, imports, literals, routes, tests, inheritance, co-change).
16
16
 
17
17
  All four accept `maxTokens` (256–16000). Budget is roughly 4 chars per token.
18
18
 
19
19
  ## Working rules
20
20
 
21
- - **Never bulk-read to find things.** Read what focus surfaced; let the graph answer "where is X" and "what uses X" instead of spawning searches.
21
+ - **Do not bulk-read to discover structure.** Focus first, then read its suggested ranges. Native grep semantics remain available whenever grep receives path/glob/literal/context/limit options or an obvious regex; unresolved graph queries fall back to native text.
22
22
  - **Impact before destructive edits.** One `fovea_impact` call is cheaper than rediscovering dependents by breaking them.
23
23
  - **Sketch is the safe opening bid.** If unsure, pay for a sketch; it almost never exceeds a few hundred tokens.
24
24
 
25
25
  ## Turn sync
26
26
 
27
- After each assistant turn, pi-fovea diffs content hashes against its baseline. If edits moved route anchors or warmed files outside the session's disclosed set, a `[fovea turn sync]` message arrives in the next turn with the delta; otherwise everything stays silent. Treat that message as ground truth about mid-session state changes.
27
+ Before an agent starts, pi-fovea establishes its baseline or injects out-of-band semantic drift into that run. After each assistant turn it compares again; meaningful route/dependency drift is delivered as a **steer**, and Fovea triggers a continuation if the agent would otherwise stop. Treat its changed files, route deltas, and causal channels as continuous task context. Comment- and formatting-only edits stay silent.
28
28
 
29
29
  Sync is **mutation-path agnostic**: pi's edit/write tools, a pi-fabric `fabric_exec` program's inner `pi.edit`, a bash heredoc, a subagent, or an editor save outside the session all register identically. Content hashes are the source of truth; tool events are not consulted for detection. In repos with no `.git` directory this is also the only drift signal — do not fall back to `git status` assumptions.
30
30
 
@@ -36,7 +36,7 @@ When writing or editing code **inside a `fabric_exec` program**, the fovea tools
36
36
  - For dynamic discovery, use `const hits = await tools.search({ query: "fovea_focus" })`, then call the returned namespaced ref with `tools.call({ ref: hits[0].ref, args: { query: "CreateUserHandler", maxTokens: 6000 } })`. The stable explicit ref is `extensions.fovea_focus`; bare `fovea_focus` and `fovea.fovea_focus` are invalid.
37
37
  - Prefer a single `extensions.fovea_impact(...)` call over hand-rolled grep fan-outs when computing what an edit touches — the graph already resolved imports/calls across Go, TypeScript, Python, and Java.
38
38
  - Any file mutation performed by the program (including `pi.edit`/`pi.write` calls inside the sandbox) is picked up by turn sync automatically, so post-edit verification does not need a re-sketch.
39
- - The sketch `details` field carries counts (`files`, `nodes`, `anchors`); the hot-node list is the graph's highest-value entry points. On an unfamiliar repo, fetch it once and reuse instead of rediscovering entry points per call.
39
+ - Sketch `details` carries coverage counts; its compact text names the highest-value entry points. On an unfamiliar repo, fetch it once and reuse it instead of rediscovering entry points per call.
40
40
 
41
41
  ## CLI
42
42
 
@@ -44,4 +44,4 @@ The same engine runs headlessly as the `fovea` binary (repo root scan, plus JSON
44
44
 
45
45
  ## Settings
46
46
 
47
- `/fovea settings` in the TUI, or `fovea.json` under `~/.pi/agent/` or a trusted repo's `.pi/` directory. Relevant knobs: `sync.enabled`, `sync.budget`, `sync.warmFileThreshold` (files that must escape before a red sync fires), `tools.defaultBudget`, and `tools.replaceGrep` (default on; installs a grep-compatible Fovea override and reloads extensions).
47
+ Use `/fovea status` for loaded version and index coverage, `/fovea reset` for fresh state, `/fovea reload` after updates, and `/fovea settings` for configuration. Files live under `~/.pi/agent/fovea.json` or trusted `.pi/fovea.json`. `tools.replaceGrep` installs hybrid grep: native text semantics plus bare-query graph navigation.
@@ -15,11 +15,14 @@ const MIN_BASIN_SIZE = 4;
15
15
 
16
16
  // eligible marks nodes that may seed a basin (symbols, not files/anchors: a
17
17
  // file's contains-star has ~zero triangle density and yields useless seeds).
18
+ // include optionally constrains every member, for operation-specific views
19
+ // such as production-first sketching without changing the underlying graph.
18
20
  export const detectBasins = (
19
21
  adjacency: Map<number, Array<{ to: number; kind: string; w: number }>>,
20
22
  conductance: Float64Array,
21
23
  n: number,
22
24
  eligible?: (i: number) => boolean,
25
+ include?: (i: number) => boolean,
23
26
  ): Basin[] => {
24
27
  // Triangle density: fraction of a node's neighbors that are co-neighbors.
25
28
  // Cheap O(deg^2) sampling with degree cap — hubs are star points anyway.
@@ -60,12 +63,14 @@ export const detectBasins = (
60
63
  const order: number[] = [seed];
61
64
  let internal = 0;
62
65
  const boundary = new Map<number, number>();
63
- for (const e of adjacency.get(seed) ?? []) boundary.set(e.to, (boundary.get(e.to) ?? 0) + e.w);
66
+ for (const e of adjacency.get(seed) ?? []) {
67
+ if (!include || include(e.to)) boundary.set(e.to, (boundary.get(e.to) ?? 0) + e.w);
68
+ }
64
69
  while (order.length < MAX_BASIN_SIZE && boundary.size) {
65
70
  let best = -1;
66
71
  let bestRatio = -1;
67
72
  for (const [j, inW] of boundary) {
68
- if (claimed.has(j) || members.has(j)) continue;
73
+ if (claimed.has(j) || members.has(j) || (include && !include(j))) continue;
69
74
  const total = [...(adjacency.get(j) ?? [])].reduce((s, e) => s + e.w, 0);
70
75
  const ratio = total > 0 ? inW / total : 0;
71
76
  if (ratio > bestRatio) { bestRatio = ratio; best = j; }
@@ -79,7 +84,7 @@ export const detectBasins = (
79
84
  internal += boundary.get(best) ?? 0;
80
85
  boundary.delete(best);
81
86
  for (const e of adjacency.get(best) ?? []) {
82
- if (members.has(e.to)) continue;
87
+ if (members.has(e.to) || (include && !include(e.to))) continue;
83
88
  boundary.set(e.to, (boundary.get(e.to) ?? 0) + e.w);
84
89
  }
85
90
  const cut = [...boundary.values()].reduce((a, b2) => a + b2, 0);
@@ -11,18 +11,18 @@ import path from "node:path";
11
11
  interface FoveaSyncConfig {
12
12
  /** turn_end feedback loop on/off (the default-on, opt-out knob). */
13
13
  enabled: boolean;
14
- /** Token budget for the red (issues) message. */
14
+ /** Token budget for proactive model steering context. */
15
15
  budget: number;
16
16
  /** Also send a tiny model-visible ack on clean turns (default false: silent green). */
17
17
  ackClean: boolean;
18
- /** Number of newly-warm undisclosed files that justifies a red message on its own. */
18
+ /** Number of newly relevant files that justifies proactive steering on its own. */
19
19
  warmFileThreshold: number;
20
20
  }
21
21
 
22
22
  interface FoveaToolsConfig {
23
23
  /** Budget applied when a fovea_* tool call omits maxTokens. */
24
24
  defaultBudget: number;
25
- /** Replace Pi's grep slot with a graph-backed fovea_focus adapter. */
25
+ /** Install hybrid grep: native text semantics plus bare-query Fovea navigation. */
26
26
  replaceGrep: boolean;
27
27
  }
28
28