pi-fovea 0.3.0 → 0.3.2
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 +73 -75
- package/package.json +5 -1
- package/skills/pi-fovea/SKILL.md +46 -0
- package/src/core/sync.ts +24 -9
- package/src/index.ts +5 -5
package/README.md
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
**A foveated repo-mapping extension for [Pi](https://github.com/earendil-works/pi-coding-agent)**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
_See the whole repo on every prompt, sharp where you work and cheap everywhere else._
|
|
8
8
|
|
|
9
9
|
<p>
|
|
10
|
-
<img src="https://raw.githubusercontent.com/monotykamary/pi-fovea/main/media/cover.svg" alt="pi-fovea: a code graph seen through a fovea
|
|
10
|
+
<img src="https://raw.githubusercontent.com/monotykamary/pi-fovea/main/media/cover.svg" alt="pi-fovea: a code graph seen through a fovea, hot at the center and collapsed at the rim" width="1100">
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/pi-fovea)
|
|
@@ -17,35 +17,23 @@ _Survey, focus, dwell, impact — a budget-capped field of view instead of a fol
|
|
|
17
17
|
|
|
18
18
|
</div>
|
|
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: full signatures near your task, one-liners a hop away, a skeleton of the rest.
|
|
21
21
|
|
|
22
|
-
|
|
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.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## What the model gets
|
|
25
25
|
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
| 🩸 | **Turn sync** | After every edit turn the graph re-syncs for free. Anchor shifts and unwatched warmings surface as red flags; stable turns stay silent. |
|
|
33
|
-
| 🪙 | **Token truth** | Budgets are hard caps, not hopes: the renderer fits a monotonic prefix and never exceeds `maxTokens`. |
|
|
26
|
+
| Command | Ask | Answer |
|
|
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: hot nodes as signatures, neighbors as one-liners |
|
|
30
|
+
| `fovea_dwell` | what else? | diffuses the field one step further and returns the delta |
|
|
31
|
+
| `fovea_impact` | what does this touch? | warms everything a file, symbol, or PR base reaches across languages |
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
The repo compiles to a typed graph whose edges carry **conductance**: imports are bridges, calls are solid, and shared literals — route paths, env keys — are the cross-language welds, weighted by specificity. Your interest is a heat source `s`; the map the model receives is the heat kernel run for time `t` over the graph Laplacian:
|
|
38
|
-
|
|
39
|
-
```text
|
|
40
|
-
v(t) = e^{−tL} · s
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
- **sketch** — large `t`, hub + anchor seeds: the whole repo in one blurry-but-valid silhouette.
|
|
44
|
-
- **focus** — small `t`, your query as the seed: the fovea on exactly that feature.
|
|
45
|
-
- **dwell** — `t` ×2 per call; only newly-luminous nodes are returned.
|
|
46
|
-
- **impact** — changed files as the seed; warmth = predicted blast radius.
|
|
33
|
+
Two slash commands on top:
|
|
47
34
|
|
|
48
|
-
|
|
35
|
+
- `/fovea status` for graph stats and sync state
|
|
36
|
+
- `/fovea settings` for an overlay in your TUI, styled after pi-fabric's `/fabric settings`
|
|
49
37
|
|
|
50
38
|
## Install
|
|
51
39
|
|
|
@@ -73,34 +61,28 @@ pi install /absolute/path/to/pi-fovea
|
|
|
73
61
|
|
|
74
62
|
</details>
|
|
75
63
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- `/fovea status` — graph stats, sync on/off
|
|
79
|
-
- `/fovea settings` — an overlay built from the same SettingsList idiom as pi-fabric's `/fabric settings`
|
|
80
|
-
|
|
81
|
-
### CLI
|
|
82
|
-
|
|
83
|
-
The same ops, stateless and pipe-friendly — for agent shells, CI, and `llmc`-style uses:
|
|
64
|
+
There is also a package for any agent shell or CI:
|
|
84
65
|
|
|
85
66
|
```sh
|
|
86
67
|
fovea sketch /path/to/repo 900
|
|
87
68
|
fovea focus /path/to/repo "/v1/messages" 800
|
|
88
69
|
fovea impact /path/to/repo --base main 1200
|
|
70
|
+
fovea rules /path/to/repo
|
|
89
71
|
fovea status /path/to/repo
|
|
90
72
|
```
|
|
91
73
|
|
|
92
|
-
|
|
74
|
+
`fovea` runs `cli.ts` via `tsx`. Install `tsx` globally, or use `pnpm fovea` from a checkout.
|
|
93
75
|
|
|
94
|
-
## Turn
|
|
76
|
+
## Turn sync
|
|
95
77
|
|
|
96
|
-
After every assistant turn
|
|
78
|
+
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**:
|
|
97
79
|
|
|
98
|
-
- **green
|
|
99
|
-
- **red
|
|
80
|
+
- **green**: silence in the model's context. A clean-toast shows only if you enable `sync.ackClean`.
|
|
81
|
+
- **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.
|
|
100
82
|
|
|
101
|
-
The first sync
|
|
83
|
+
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.
|
|
102
84
|
|
|
103
|
-
|
|
85
|
+
Turn it off per repo or globally: `/fovea settings` → Turn sync, or
|
|
104
86
|
|
|
105
87
|
```sh
|
|
106
88
|
FOVEA_TURN_SYNC=off pi
|
|
@@ -108,46 +90,42 @@ FOVEA_TURN_SYNC=off pi
|
|
|
108
90
|
|
|
109
91
|
## Configuration
|
|
110
92
|
|
|
111
|
-
Global `~/.pi/agent/fovea.json
|
|
93
|
+
Global settings live in `~/.pi/agent/fovea.json`. A trusted repo-level override sits in `<repo>/.pi/fovea.json`. Two scopes, the same model pi-fabric uses with `fabric.json`.
|
|
112
94
|
|
|
113
95
|
| Key | Default | Meaning |
|
|
114
96
|
| --- | :-----: | ------- |
|
|
115
97
|
| `sync.enabled` | `true` | the turn-sync loop |
|
|
116
|
-
| `sync.budget` | `1024` |
|
|
117
|
-
| `sync.ackClean` | `false` | toast
|
|
118
|
-
| `sync.warmFileThreshold` | `2` |
|
|
119
|
-
| `tools.defaultBudget` | `2000` | fallback maxTokens for fovea_*
|
|
98
|
+
| `sync.budget` | `1024` | token cap for the red report seen by the model |
|
|
99
|
+
| `sync.ackClean` | `false` | toast after clean structural turns |
|
|
100
|
+
| `sync.warmFileThreshold` | `2` | warmed files unseen by the model that justify turning red |
|
|
101
|
+
| `tools.defaultBudget` | `2000` | fallback maxTokens for the fovea_* tools |
|
|
120
102
|
|
|
121
|
-
##
|
|
103
|
+
## How routes are found
|
|
122
104
|
|
|
123
|
-
|
|
105
|
+
Route anchors come from port shapes, and five shapes cover almost the whole ecosystem:
|
|
124
106
|
|
|
125
107
|
| Port shape | Examples |
|
|
126
108
|
|---|---|
|
|
127
|
-
| `recv.verb("path", handlers…)` | express, koa, fastify, hono, gin, echo, chi, net/http
|
|
128
|
-
|
|
|
109
|
+
| `recv.verb("path", handlers…)` | express, koa, fastify, hono, gin, echo, chi, net/http |
|
|
110
|
+
| annotation + optional class prefix | NestJS `@Controller + @Get`, Flask and FastAPI decorators, Spring `@RequestMapping + @GetMapping` |
|
|
129
111
|
| verb embedded in the path | Go 1.22 `mux.HandleFunc("GET /x", h)` |
|
|
130
112
|
| verb as first string argument | chi `r.Method("GET", path, h)`, aiohttp `router.add_route("GET", path, h)` |
|
|
131
113
|
| receiver-less DSL macros | Rails `routes.rb`, Phoenix `router.ex`, Django `path()`, Ktor `routing { get("/x") {} }` |
|
|
132
114
|
|
|
133
|
-
File-convention routers
|
|
134
|
-
|
|
135
|
-
**Known blind spots** (deliberate, logged in `src/core/anchors.ts`): Rust proc-macro attribute routers (actix `#[get("/x")]`, rocket) — ast-grep patterns can't parameterize attribute paths; frameworks with constructor-assigned prefixes (Flask Blueprint, FastAPI `APIRouter(prefix=…)`, chi `Mount`, Express `Router` mounts) — variable binding tracking is out of band; `scope`/`namespace` nesting in Phoenix/Rails/Django `include()` — prefixes across blocks aren't composed; tRPC/GraphQL/gRPC — no path token exists to anchor on.
|
|
136
|
-
|
|
137
|
-
### Tier 3: discovery mode
|
|
115
|
+
File-convention routers declare paths nowhere in code. Next.js App Router, Pages Router, SvelteKit, Nuxt, and Astro anchors therefore derive from file paths, with the verb read off exported handler names or filename suffixes.
|
|
138
116
|
|
|
139
|
-
|
|
117
|
+
### Discovery mode
|
|
140
118
|
|
|
141
|
-
|
|
119
|
+
When a repo writes routes in a shape fovea has never seen, the literal pass harvests every call shape and promotes statistically solid ones into implicit rules. Discovered anchors carry half the conductance of declared ones and appear with a `△` sigil. Turn sync reports their churn without letting an unconfirmed hypothesis turn the verdict red. A hub upgrades to first-class the moment a known rule matches any of its sites.
|
|
142
120
|
|
|
143
121
|
```sh
|
|
144
|
-
fovea anchors <root> --discovered #
|
|
145
|
-
fovea rules <root> # promoted rules
|
|
146
|
-
fovea rules <root> --sigs # every path-touching signature, by precision
|
|
147
|
-
fovea rules <root> --adopt #
|
|
122
|
+
fovea anchors <root> --discovered # the △ hypothesis hubs only
|
|
123
|
+
fovea rules <root> # promoted rules with evidence
|
|
124
|
+
fovea rules <root> --sigs # every path-touching signature, by precision
|
|
125
|
+
fovea rules <root> --adopt # persist promotions into .fovea/rules.json
|
|
148
126
|
```
|
|
149
127
|
|
|
150
|
-
|
|
128
|
+
`.fovea/rules.json` pins community or project rules in the repo:
|
|
151
129
|
|
|
152
130
|
```json
|
|
153
131
|
{
|
|
@@ -157,23 +135,43 @@ Drop `.fovea/rules.json` in a repo to extend anchor detection beyond the built-i
|
|
|
157
135
|
}
|
|
158
136
|
```
|
|
159
137
|
|
|
160
|
-
Changing the rules file invalidates
|
|
138
|
+
A rule may declare `prefixPattern` so a class-level prefix like `@Controller('api/airports')` composes with per-method paths. Changing the rules file invalidates the anchor extraction cache alone; parsed facts above it carry over.
|
|
139
|
+
|
|
140
|
+
**Blind spots**, logged in `src/core/anchors.ts`: Rust proc-macro attributes (actix `#[get("/x")]`), constructor-assigned prefixes (Flask Blueprint, FastAPI `APIRouter(prefix=…)`, chi `Mount`, Express `Router` mounts), `scope` and `namespace` nesting in Phoenix, Rails, or Django `include()`, and tRPC/GraphQL/gRPC (no path token exists to anchor on).
|
|
141
|
+
|
|
142
|
+
## How it works
|
|
143
|
+
|
|
144
|
+
The repo compiles to a typed graph. Your question is a source vector $s$ over its nodes, and the field the model receives is the heat kernel at time $t$ over the Laplacian $L$:
|
|
145
|
+
|
|
146
|
+
$$
|
|
147
|
+
v(t) = e^{-tL} \cdot s \quad \text{with} \quad L = I - D^{-1/2} W D^{-1/2}
|
|
148
|
+
$$
|
|
149
|
+
|
|
150
|
+
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.
|
|
151
|
+
|
|
152
|
+
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:
|
|
153
|
+
|
|
154
|
+
$$
|
|
155
|
+
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]
|
|
156
|
+
$$
|
|
157
|
+
|
|
158
|
+
The vectors $T_k(M) s$ are cached in the session. A new timescale costs coefficient recombination, never a second graph walk.
|
|
159
|
+
|
|
160
|
+
Discovery asks how often the argument at one slot of one call shape carries a route path, and promotes the shape past a Jeffreys-smoothed posterior:
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
$$
|
|
163
|
+
\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}
|
|
164
|
+
$$
|
|
163
165
|
|
|
164
|
-
|
|
166
|
+
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.
|
|
165
167
|
|
|
166
|
-
-
|
|
167
|
-
- **literal joins** — route paths, env keys, OpenAPI operation paths; document-frequency-gated cliques so rare literals bridge strongly and ubiquitous ones don't become gravity wells
|
|
168
|
-
- **co-change** — mined from recent git history (Jaccard-tilted, per-file capped, cached by HEAD), so files that commute together warm each other even without a static edge
|
|
169
|
-
- **feature hubs** — route declarations and every client call of the same normalized path collapse to ONE anchor node: where client, server, and spec meet
|
|
170
|
-
- **basins** — where there are no routes at all (CLIs, kernels), sketch infers implicit features as conductance-cut regions around triangle-dense seeds
|
|
168
|
+
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).
|
|
171
169
|
|
|
172
|
-
##
|
|
170
|
+
## Languages
|
|
173
171
|
|
|
174
|
-
Full symbol
|
|
175
|
-
Outline-based symbols
|
|
176
|
-
Config joins through literals: **YAML
|
|
172
|
+
Full symbol and call extraction: **TypeScript, TSX, JavaScript, Python, Go, and Rust**.
|
|
173
|
+
Outline-based symbols: **Elixir, Ruby, C, C++, Java, Kotlin, Lua, PHP, Swift, Scala, Haskell, and Bash**.
|
|
174
|
+
Config joins through literals: **YAML, JSON, TOML, env, Markdown, and OpenAPI**.
|
|
177
175
|
|
|
178
176
|
## Development
|
|
179
177
|
|
|
@@ -183,6 +181,6 @@ pnpm run check # typecheck + full vitest suite
|
|
|
183
181
|
pnpm run bench # rate–distortion bench against ../pi-fabric
|
|
184
182
|
```
|
|
185
183
|
|
|
186
|
-
pi loads the extension straight from `src/` via jiti
|
|
184
|
+
pi loads the extension straight from `src/` via jiti; there is no build step. Per-repo caches live in `$TMPDIR` behind per-file content sha1 hashes, and only dirty files re-run ast-grep. Bump `CACHE_VERSION` in `src/core/build.ts` whenever extractor semantics change.
|
|
187
185
|
|
|
188
186
|
[MIT](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-fovea",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
"extensions": [
|
|
29
29
|
"./src/index.ts"
|
|
30
30
|
],
|
|
31
|
+
"skills": [
|
|
32
|
+
"./skills"
|
|
33
|
+
],
|
|
31
34
|
"image": "https://raw.githubusercontent.com/monotykamary/pi-fovea/main/media/cover.svg"
|
|
32
35
|
},
|
|
33
36
|
"exports": {
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
"files": [
|
|
37
40
|
"src",
|
|
38
41
|
"cli.ts",
|
|
42
|
+
"skills",
|
|
39
43
|
"media",
|
|
40
44
|
"README.md",
|
|
41
45
|
"LICENSE"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pi-fovea
|
|
3
|
+
description: Token-efficient repo navigation with the pi-fovea code graph. Use when you need to survey an unfamiliar repository, trace where a symbol or route lives and what depends on it, assess the blast radius of a change before editing, or re-orient after files have been edited mid-session (by any tool path, including bash and pi-fabric fabric_exec programs).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# pi-fovea
|
|
7
|
+
|
|
8
|
+
pi-fovea maintains a cross-language code graph of the working repository — routes, symbols, imports, calls, string/env literals — and exposes it through progressive disclosure: cheap silhouettes first, detail only where you point it. It costs almost nothing until you ask, and it re-syncs automatically whenever file content drifts, no matter which tool made the edit.
|
|
9
|
+
|
|
10
|
+
## The loop
|
|
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, route path (`/api/users/{id}`), env key, or file path. Hot nodes come back with full signatures; warm neighbors as one-liners; the periphery stays collapsed. Already-shown nodes are suppressed, so repeated focus calls stay cheap.
|
|
14
|
+
3. **`fovea_dwell`** — optional second look. If a focus footer says more nodes are lit below the token threshold, dwell (diffusion time ×2) surfaces exactly those newcomers.
|
|
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.
|
|
16
|
+
|
|
17
|
+
All four accept `maxTokens` (256–16000). Budget is roughly 4 chars per token.
|
|
18
|
+
|
|
19
|
+
## Working rules
|
|
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.
|
|
22
|
+
- **Impact before destructive edits.** One `fovea_impact` call is cheaper than rediscovering dependents by breaking them.
|
|
23
|
+
- **Sketch is the safe opening bid.** If unsure, pay for a sketch; it almost never exceeds a few hundred tokens.
|
|
24
|
+
|
|
25
|
+
## Turn sync
|
|
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.
|
|
28
|
+
|
|
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
|
+
|
|
31
|
+
## Using with pi-fabric (fabric_exec)
|
|
32
|
+
|
|
33
|
+
When writing or editing code **inside a `fabric_exec` program**, the fovea tools exist but the fabric sandbox has no built-in knowledge of them (it lazy-loads tools). Key points:
|
|
34
|
+
|
|
35
|
+
- Inside `fabric_exec`, discover them once with `await tools.search("fovea")` and call them through `tools.call({ ref, args })` — e.g. `{ ref: "fovea_focus", args: { query: "CreateUserHandler" } }`. They are ordinary pi tools; there is no fabric-specific wrapper.
|
|
36
|
+
- Prefer a single `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.
|
|
37
|
+
- 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.
|
|
38
|
+
- 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
|
+
|
|
40
|
+
## CLI
|
|
41
|
+
|
|
42
|
+
The same engine runs headlessly as the `fovea` binary (repo root scan, plus JSON and TSV modes). Prefer the in-session tools unless you need scripting or a second opinion outside the extension's session state.
|
|
43
|
+
|
|
44
|
+
## Settings
|
|
45
|
+
|
|
46
|
+
`/fovea settings` in the TUI, or `fovea.config.json` at repo or user level. Relevant knobs: `sync.enabled`, `sync.budget`, `sync.warmFileThreshold` (files that must escape before a red sync fires), `tools.defaultBudget`.
|
package/src/core/sync.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Turn-sync: the default-on feedback loop the extension was built around.
|
|
2
2
|
// After each assistant turn, if the repo's facts version drifted ANY edit
|
|
3
|
-
// re-syncs, regardless of
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// re-syncs, regardless of the mutation path — pi's edit/write tools, a
|
|
4
|
+
// fabric_exec inner pi.edit, a bash heredoc, a subagent run, or an editor
|
|
5
|
+
// save outside the session all land the same way, because drift is measured
|
|
6
|
+
// by diffing the baseline's content hashes against the current facts instead
|
|
7
|
+
// of trusting tool events or git.
|
|
6
8
|
//
|
|
7
9
|
// red = route anchors appeared/vanished (structural feature churn)
|
|
8
10
|
// OR warm undisclosed files >= warmFileThreshold (unseen blast radius)
|
|
@@ -11,13 +13,17 @@
|
|
|
11
13
|
// The first sync of a session only establishes the baseline (never red).
|
|
12
14
|
// Baselines reset on /new and /fork alongside fovea sessions.
|
|
13
15
|
|
|
14
|
-
import { ensureState, impact
|
|
16
|
+
import { ensureState, impact } from "./ops.js";
|
|
15
17
|
import type { RepoState } from "./ops.js";
|
|
16
18
|
import { getSession } from "./session.js";
|
|
17
19
|
|
|
18
20
|
interface SyncBaseline {
|
|
19
21
|
version: string;
|
|
20
22
|
anchors: Set<string>;
|
|
23
|
+
/** file -> content sha1 at baseline. Diffing this against the current facts
|
|
24
|
+
* yields the exact changed-file set for any mutation path — no dependence
|
|
25
|
+
* on which tool executed the write, nor on git. */
|
|
26
|
+
shas: Map<string, string>;
|
|
21
27
|
/** Steady-state warmth recorded on the most recent sync. undefined = "the
|
|
22
28
|
* first drift after baseline calibrates the neighborhood instead of
|
|
23
29
|
* escalating" — a file list appears after that calibration sync. */
|
|
@@ -29,7 +35,8 @@ const baselines = new Map<string, SyncBaseline>();
|
|
|
29
35
|
export const resetSyncBaselines = (): void => baselines.clear();
|
|
30
36
|
|
|
31
37
|
export interface SyncParams {
|
|
32
|
-
/**
|
|
38
|
+
/** Optional drift hints (e.g. files touched by pi's edit/write tools this
|
|
39
|
+
* turn). Unioned into the warmth seeds; never the source of truth. */
|
|
33
40
|
files?: string[];
|
|
34
41
|
budget: number;
|
|
35
42
|
warmFileThreshold: number;
|
|
@@ -48,6 +55,7 @@ export interface SyncOutcome {
|
|
|
48
55
|
const snapshot = (state: RepoState): SyncBaseline => ({
|
|
49
56
|
version: state.version,
|
|
50
57
|
anchors: new Set(state.graph.anchors.map((a) => a.id)),
|
|
58
|
+
shas: new Map(Object.entries(state.facts).map(([f, x]) => [f, x.sha1])),
|
|
51
59
|
});
|
|
52
60
|
|
|
53
61
|
export const sync = (root: string, params: SyncParams, now?: RepoState): SyncOutcome => {
|
|
@@ -80,8 +88,15 @@ export const sync = (root: string, params: SyncParams, now?: RepoState): SyncOut
|
|
|
80
88
|
if (at >= 0) disclosedFiles.add(id.slice(at + 1));
|
|
81
89
|
}
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
// Exact change set: facts whose content hash moved since the baseline.
|
|
92
|
+
// Deleted files can't warm anything (absent from the graph) but ride along
|
|
93
|
+
// in details for observability.
|
|
94
|
+
const hinted = (params.files ?? []).filter((f) => state.graph.byFile.has(f));
|
|
95
|
+
const changed = Object.keys(state.facts).filter(
|
|
96
|
+
(f) => prev.shas.get(f) !== state.facts[f]!.sha1 && state.graph.byFile.has(f),
|
|
97
|
+
);
|
|
98
|
+
const deleted = [...prev.shas.keys()].filter((f) => !(f in state.facts));
|
|
99
|
+
const files = [...new Set([...hinted, ...changed])];
|
|
85
100
|
|
|
86
101
|
let warmNow: Set<string> = new Set();
|
|
87
102
|
let warmNew: string[] = [];
|
|
@@ -100,7 +115,7 @@ export const sync = (root: string, params: SyncParams, now?: RepoState): SyncOut
|
|
|
100
115
|
if (!red) {
|
|
101
116
|
return {
|
|
102
117
|
structural: true, red: false, tokens: 0,
|
|
103
|
-
details: { version: state.version, anchorsDelta: added.length - removed.length, warmNew: warmNew.length },
|
|
118
|
+
details: { version: state.version, anchorsDelta: added.length - removed.length, warmNew: warmNew.length, deletedFiles: deleted },
|
|
104
119
|
};
|
|
105
120
|
}
|
|
106
121
|
|
|
@@ -116,6 +131,6 @@ export const sync = (root: string, params: SyncParams, now?: RepoState): SyncOut
|
|
|
116
131
|
const text = lines.join("\n");
|
|
117
132
|
return {
|
|
118
133
|
structural: true, red: true, text, tokens: Math.ceil(text.length / 4),
|
|
119
|
-
details: { version: state.version, added, removed, warmNew },
|
|
134
|
+
details: { version: state.version, added, removed, warmNew, deletedFiles: deleted },
|
|
120
135
|
};
|
|
121
136
|
};
|
package/src/index.ts
CHANGED
|
@@ -38,11 +38,11 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
// Turn-sync loop.
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
41
|
+
// Turn-sync loop. The tracker below is a hint accumulator only: pi's
|
|
42
|
+
// edit/write tool starts give the warmth pass a head start, but sync relies
|
|
43
|
+
// on content-hash drift, so identical detection covers fabric_exec inner
|
|
44
|
+
// pi.edit calls, bash mutations, subagents, and out-of-band editor saves.
|
|
45
|
+
// Pure conversation turns exit at zero cost through the version fast path.
|
|
46
46
|
let turnFiles: string[] = [];
|
|
47
47
|
pi.on("turn_start", () => {
|
|
48
48
|
turnFiles = [];
|