pi-fovea 0.22.0 → 0.22.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 +77 -2
- package/dist/cli.mjs +12 -8
- package/package.json +1 -1
- package/skills/pi-fovea/SKILL.md +18 -0
- package/src/core/heat.ts +16 -9
- package/src/core/roots.ts +47 -0
- package/src/index.ts +138 -108
package/README.md
CHANGED
|
@@ -177,7 +177,7 @@ limits accept environment overrides:
|
|
|
177
177
|
| --- | :---: | --- |
|
|
178
178
|
| `FOVEA_MAX_FILES` | `8000` | maximum indexed files in one graph |
|
|
179
179
|
| `FOVEA_MAX_FILE_BYTES` | `1048576` | maximum bytes extracted from one source file |
|
|
180
|
-
| `FOVEA_MAX_ROOTS` | `2` | resident graph, fact, session, sync, and root-metadata caches |
|
|
180
|
+
| `FOVEA_MAX_ROOTS` | `2` | observed execution roots and resident graph, fact, session, sync, and root-metadata caches |
|
|
181
181
|
| `FOVEA_SPAWN_CONCURRENCY` | `3` | concurrent ast-grep/git child processes (ast-grep parallelizes parsing inside each process; values above ~4 rarely help) |
|
|
182
182
|
| `FOVEA_MEMORY_HALF_LIFE_HOURS` | `48` | wall-clock half-life of the per-node sync memory (charged cascade warmth) |
|
|
183
183
|
| `FOVEA_IO_CONCURRENCY` | `32` | concurrent file stat/read operations |
|
|
@@ -187,6 +187,62 @@ Files over the size cap keep their place in the model's view of the repo.
|
|
|
187
187
|
Failed extractions do the same. You find both in `/fovea status` and in tool
|
|
188
188
|
details.
|
|
189
189
|
|
|
190
|
+
## Explicit project/worktree continuity (Rakazo)
|
|
191
|
+
|
|
192
|
+
The extension's existing four graph tools are the headless binding API; no new
|
|
193
|
+
host event or trust flag is required:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
await extensions.fovea_focus({ root: "../project-b", query: "src/handler.ts", maxTokens: 1024 });
|
|
197
|
+
// Baseline is ready before this resolves (unless target sync is disabled).
|
|
198
|
+
// Native tools still use the host cwd: use absolute paths or cwd-relative paths.
|
|
199
|
+
await pi.edit({ path: "../project-b/src/handler.ts", old: "before", new: "after" });
|
|
200
|
+
await extensions.fovea_dwell({ maxTokens: 512 }); // last bound root
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
- `root` resolves against **the tool context's cwd**, not process cwd or the last
|
|
204
|
+
binding. Real paths unify symlink aliases; linked Git worktrees remain distinct
|
|
205
|
+
even when they share HEAD and a common Git directory. Roots are exact directory
|
|
206
|
+
scopes, not automatically promoted to Git toplevels.
|
|
207
|
+
- Cwd is the startup/fallback observation target. After a graph call binds a root,
|
|
208
|
+
hooks inspect only the bound set. Bind each project you want observed; an
|
|
209
|
+
alternate binding does not keep an otherwise-unselected umbrella cwd active.
|
|
210
|
+
Calls without `root` use the last binding. Parallel coordinators should always
|
|
211
|
+
supply `root`; enrollment is serialized in invocation order. Results include
|
|
212
|
+
canonical `details.root` and sorted `details.observedRoots`.
|
|
213
|
+
- A first binding establishes that target's semantic baseline before edits can
|
|
214
|
+
follow. Subsequent focus calls do not consume pending drift. Before-prompt and
|
|
215
|
+
post-turn sync compare every bound target, including hintless shell/Fabric/editor
|
|
216
|
+
changes. Attention remains target-local: focus a file/symbol or use
|
|
217
|
+
`fovea_impact({ root, files: ["src/file.ts"], includeUncommitted: false })` before
|
|
218
|
+
a headless mutation to enter its scope. Ordinary path events never enroll a new
|
|
219
|
+
root; they route to the most specific enrolled physical owner.
|
|
220
|
+
- Native read/edit/write/search paths **do not change cwd**. Augment-mode grep
|
|
221
|
+
attaches only the graph of the enrolled owner of its actual cwd-relative or
|
|
222
|
+
absolute search path. No-path native grep still searches cwd; it never appends
|
|
223
|
+
an unrelated alternate graph. In legacy replace mode, bare graph queries and
|
|
224
|
+
their miss fallback use the bound root; explicit native options retain cwd
|
|
225
|
+
semantics. Each target's grep mode is checked independently.
|
|
226
|
+
- Binding is an explicit request to index a directory, **not a sandbox or a trust
|
|
227
|
+
grant**. Rakazo must authorize tool arguments itself. `.pi/fovea.json` is loaded
|
|
228
|
+
only when that exact canonical target equals `ctx.cwd` and the context is
|
|
229
|
+
trusted. Parent/sibling trust never authorizes it; alternate targets use global
|
|
230
|
+
defaults. To honor a target's project config, run it in its own trusted Pi
|
|
231
|
+
context. Config cache keys include trust and agent directory. Existing
|
|
232
|
+
target-local declarative `.fovea/rules.json` extraction rules are unchanged.
|
|
233
|
+
- All roots share one per-hook `sync.budget` allowance from the session cwd's
|
|
234
|
+
effective config, divided evenly and capped again by each target's budget;
|
|
235
|
+
root labels count toward that allowance. Hidden targets remain hidden (a mixed
|
|
236
|
+
pre-prompt aggregate is hidden). Graph calls keep their individual `maxTokens`.
|
|
237
|
+
Enrollment is capped by `FOVEA_MAX_ROOTS` (default 2); excess roots fail before
|
|
238
|
+
indexing rather than silently losing a baseline. Set this before startup for
|
|
239
|
+
larger coordinated runs.
|
|
240
|
+
- `/fovea status` reports the bound root and observed count. `/fovea reset`,
|
|
241
|
+
shutdown, new/resume/fork/reload clear bindings and conversation baselines;
|
|
242
|
+
reusable content facts remain cached. Rebind after session replacement.
|
|
243
|
+
The standalone CLI remains stateless; these continuity semantics belong to
|
|
244
|
+
the Pi extension lifecycle.
|
|
245
|
+
|
|
190
246
|
## Turn sync
|
|
191
247
|
|
|
192
248
|
Continuous sync is enabled and visible by default. Before an agent starts,
|
|
@@ -496,9 +552,28 @@ Exact contract topology: **Protocol Buffers (`.proto`) and GraphQL (`.graphql`,
|
|
|
496
552
|
```sh
|
|
497
553
|
bun install
|
|
498
554
|
bun run check # typecheck + full vitest suite
|
|
499
|
-
bun run bench # rate–distortion bench against ../pi-fabric
|
|
555
|
+
bun run bench # rate–distortion and refresh bench against ../pi-fabric
|
|
556
|
+
bun run bench tests/fixtures/mini # self-contained smoke run
|
|
500
557
|
```
|
|
501
558
|
|
|
559
|
+
The developer benchmark gates timings on semantic equivalence: cold versus
|
|
560
|
+
cached builds, and forced refresh versus clean rebuild after unchanged,
|
|
561
|
+
location-only, semantic, added-file, and deleted-file scenarios in disposable
|
|
562
|
+
copies of the cross-language fixture. It checks facts, weighted edges and
|
|
563
|
+
evidence, coverage, operator contents, and fixture navigation; extraction-order
|
|
564
|
+
permutations and opaque rebuild hashes are not semantic differences. Runtime
|
|
565
|
+
ordering and generation invalidation are unchanged.
|
|
566
|
+
|
|
567
|
+
Reports include focus/dwell and refresh median/p95 samples, actual estimated
|
|
568
|
+
output tokens, and process peak RSS (including the validation work). Cold and
|
|
569
|
+
disk-warm target builds are single samples; three-sample refresh p95s are only
|
|
570
|
+
smoke diagnostics. The outline gets no more tokens than Fovea actually used.
|
|
571
|
+
`fidelity@16k` measures disclosed node IDs against a finite larger Fovea response,
|
|
572
|
+
**not independently labeled relevance**. Timing results are informational, never
|
|
573
|
+
a flaky CI gate; deterministic equivalence tests run in `bun run check`.
|
|
574
|
+
The bench clears the target's disposable facts cache to measure cold loading,
|
|
575
|
+
but edits only temporary fixture copies.
|
|
576
|
+
|
|
502
577
|
pi loads the extension straight from `src/` through jiti, so nothing needs
|
|
503
578
|
building. Per-repo JSONL caches live in `$TMPDIR`, guarded by per-file content
|
|
504
579
|
sha1 values and stat manifests. Cache I/O streams. Only dirty files re-run
|
package/dist/cli.mjs
CHANGED
|
@@ -403,10 +403,13 @@ var buildCsr = (g) => {
|
|
|
403
403
|
}
|
|
404
404
|
return { n, rowPtr, col, w, deg };
|
|
405
405
|
};
|
|
406
|
-
var
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
406
|
+
var inverseDegrees = ({ n, deg }) => {
|
|
407
|
+
const out = new Float64Array(n);
|
|
408
|
+
for (let i = 0; i < n; i++) out[i] = deg[i] > 0 ? 1 / Math.sqrt(deg[i]) : 0;
|
|
409
|
+
return out;
|
|
410
|
+
};
|
|
411
|
+
var applyNegP = (csr, x, invSqrt = inverseDegrees(csr)) => {
|
|
412
|
+
const { n, rowPtr, col, w } = csr;
|
|
410
413
|
const y = new Float64Array(n);
|
|
411
414
|
for (let i = 0; i < n; i++) {
|
|
412
415
|
let acc = 0;
|
|
@@ -464,13 +467,14 @@ var heatCoeff = (k, t) => {
|
|
|
464
467
|
var chebyshevVectors = (csr, s, K) => {
|
|
465
468
|
const tk = new Array(K + 1);
|
|
466
469
|
tk[0] = Float64Array.from(s);
|
|
467
|
-
if (K
|
|
470
|
+
if (K === 0) return tk;
|
|
471
|
+
const invSqrt = inverseDegrees(csr);
|
|
472
|
+
if (K >= 1) tk[1] = applyNegP(csr, tk[0], invSqrt);
|
|
468
473
|
for (let k = 2; k <= K; k++) {
|
|
469
474
|
const prev = tk[k - 1];
|
|
470
|
-
const
|
|
471
|
-
const out = new Float64Array(csr.n);
|
|
475
|
+
const out = applyNegP(csr, prev, invSqrt);
|
|
472
476
|
const p2 = tk[k - 2];
|
|
473
|
-
for (let i = 0; i < csr.n; i++) out[i] = 2 *
|
|
477
|
+
for (let i = 0; i < csr.n; i++) out[i] = 2 * out[i] - p2[i];
|
|
474
478
|
tk[k] = out;
|
|
475
479
|
}
|
|
476
480
|
return tk;
|
package/package.json
CHANGED
package/skills/pi-fovea/SKILL.md
CHANGED
|
@@ -23,6 +23,24 @@ All four accept `maxTokens` (256–16000). Budget is roughly 4 chars per token.
|
|
|
23
23
|
- **Sketch is the safe opening bid.** If unsure, pay for a sketch; it almost never exceeds a few hundred tokens.
|
|
24
24
|
- **Skip the map when the repo is tiny.** A few dozen files are cheaper to bulk-read than to sketch; Fovea's value grows with repos larger than context. Either way it points at windows to read — the project's own format/lint/typecheck/test commands and CI remain the final verification layer.
|
|
25
25
|
|
|
26
|
+
## Multiple projects/worktrees
|
|
27
|
+
|
|
28
|
+
Use an explicit graph-tool `root` before editing an alternate authorized project.
|
|
29
|
+
Roots resolve relative to `ctx.cwd`; symlink aliases share identity, linked Git
|
|
30
|
+
worktrees do not. The first call establishes its sync baseline before returning.
|
|
31
|
+
Graph results expose `details.root` and `details.observedRoots`; omitted roots use
|
|
32
|
+
the last binding. Bind every target to observe; after binding, hooks stop using
|
|
33
|
+
an unselected umbrella cwd. Parallel calls should specify roots explicitly.
|
|
34
|
+
|
|
35
|
+
Native paths remain cwd-relative: use absolute paths or `../project/file` for
|
|
36
|
+
edits and grep. Augment grep follows its actual search path's enrolled owner,
|
|
37
|
+
not the active graph binding. Focus or file-seeded impact establishes attention
|
|
38
|
+
for headless shell edits; path events alone never enroll siblings. Alternate
|
|
39
|
+
roots do not inherit cwd project-config trust. Shared sync context and observed
|
|
40
|
+
roots are bounded (`FOVEA_MAX_ROOTS`, default 2; excess enrollment errors).
|
|
41
|
+
Reset/reload/session replacement clears bindings, not reusable extraction facts.
|
|
42
|
+
See README's “Explicit project/worktree continuity (Rakazo)” for the full contract.
|
|
43
|
+
|
|
26
44
|
## Turn sync
|
|
27
45
|
|
|
28
46
|
Before an agent starts, pi-fovea establishes its baseline or injects relevant out-of-band semantic drift into that run. After each assistant turn it compares again. The default `sync.scope: "session"` indexes the whole root but steers only for top-level directories/root files this conversation entered through path-bearing tools or focus. Sibling-directory drift advances the index and baseline silently. Current, mixed, and unattributed changes in scope may trigger a continuation; changes owned solely by another Fovea session wait for the next user prompt and cannot restart an idle agent. Comment- and formatting-only edits stay silent. Set `sync.scope: "repository"` only when root-wide steering is intentional.
|
package/src/core/heat.ts
CHANGED
|
@@ -64,11 +64,15 @@ export const buildCsr = (g: Graph): Csr => {
|
|
|
64
64
|
return { n, rowPtr, col, w, deg };
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
const inverseDegrees = ({ n, deg }: Csr): Float64Array => {
|
|
68
|
+
const out = new Float64Array(n);
|
|
69
|
+
for (let i = 0; i < n; i++) out[i] = deg[i]! > 0 ? 1 / Math.sqrt(deg[i]!) : 0;
|
|
70
|
+
return out;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// y = -P~ x where P~ = D^{-1/2} W D^{-1/2}.
|
|
74
|
+
const applyNegP = (csr: Csr, x: Float64Array, invSqrt = inverseDegrees(csr)): Float64Array => {
|
|
75
|
+
const { n, rowPtr, col, w } = csr;
|
|
72
76
|
const y = new Float64Array(n);
|
|
73
77
|
for (let i = 0; i < n; i++) {
|
|
74
78
|
let acc = 0;
|
|
@@ -130,13 +134,16 @@ const heatCoeff = (k: number, t: number): number => {
|
|
|
130
134
|
export const chebyshevVectors = (csr: Csr, s: Float64Array, K: number): Float64Array[] => {
|
|
131
135
|
const tk: Float64Array[] = new Array(K + 1);
|
|
132
136
|
tk[0] = Float64Array.from(s);
|
|
133
|
-
|
|
137
|
+
// Invocation-local normalization preserves compatibility with mutable/synthetic CSR
|
|
138
|
+
// callers without adding a persistent cache or a new invalidation boundary.
|
|
139
|
+
if (K === 0) return tk;
|
|
140
|
+
const invSqrt = inverseDegrees(csr);
|
|
141
|
+
if (K >= 1) tk[1] = applyNegP(csr, tk[0]!, invSqrt);
|
|
134
142
|
for (let k = 2; k <= K; k++) {
|
|
135
143
|
const prev = tk[k - 1]!;
|
|
136
|
-
const
|
|
137
|
-
const out = new Float64Array(csr.n);
|
|
144
|
+
const out = applyNegP(csr, prev, invSqrt);
|
|
138
145
|
const p2 = tk[k - 2]!;
|
|
139
|
-
for (let i = 0; i < csr.n; i++) out[i] = 2 *
|
|
146
|
+
for (let i = 0; i < csr.n; i++) out[i] = 2 * out[i]! - p2[i]!;
|
|
140
147
|
tk[k] = out;
|
|
141
148
|
}
|
|
142
149
|
return tk;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, basename, join, relative, resolve, sep, isAbsolute } from "node:path";
|
|
3
|
+
import { ROOT_CACHE_LIMIT } from "./asyncutil.js";
|
|
4
|
+
|
|
5
|
+
// Resolve aliases without collapsing linked worktrees to their common git dir.
|
|
6
|
+
export const canonicalPath = (cwd: string, input = "."): string => {
|
|
7
|
+
const absolute = resolve(cwd, input);
|
|
8
|
+
try { return realpathSync(absolute); } catch {
|
|
9
|
+
const parent = dirname(absolute);
|
|
10
|
+
return parent === absolute ? absolute : join(canonicalPath(parent), basename(absolute));
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const contains = (root: string, path: string): boolean => {
|
|
15
|
+
const rel = relative(root, path);
|
|
16
|
+
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Explicit session enrollment only; never discovers parents or siblings. */
|
|
20
|
+
export class ExecutionRoots {
|
|
21
|
+
private observed = new Set<string>();
|
|
22
|
+
private active?: string;
|
|
23
|
+
|
|
24
|
+
clear(): void { this.observed.clear(); this.active = undefined; }
|
|
25
|
+
list(cwd: string): string[] {
|
|
26
|
+
return this.observed.size ? [...this.observed].sort() : [canonicalPath(cwd)];
|
|
27
|
+
}
|
|
28
|
+
target(cwd: string, root?: string): string {
|
|
29
|
+
return root === undefined ? this.active ?? canonicalPath(cwd) : canonicalPath(cwd, root);
|
|
30
|
+
}
|
|
31
|
+
check(root: string): void {
|
|
32
|
+
if (!statSync(root).isDirectory()) throw new Error(`Fovea root is not a directory: ${root}`);
|
|
33
|
+
if (!this.observed.has(root) && this.observed.size >= ROOT_CACHE_LIMIT) {
|
|
34
|
+
throw new Error(`Fovea observed-root limit (${ROOT_CACHE_LIMIT}) reached; use /fovea reset or set FOVEA_MAX_ROOTS before startup.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
bind(root: string): void {
|
|
38
|
+
this.check(root);
|
|
39
|
+
this.observed.add(root);
|
|
40
|
+
this.active = root;
|
|
41
|
+
}
|
|
42
|
+
owner(cwd: string, path: string): { root: string; path: string } | undefined {
|
|
43
|
+
const absolute = canonicalPath(cwd, path);
|
|
44
|
+
const root = this.list(cwd).filter((r) => contains(r, absolute)).sort((a, b) => b.length - a.length || a.localeCompare(b))[0];
|
|
45
|
+
return root ? { root, path: relative(root, absolute).split(sep).join("/") } : undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
// incremental across sessions.
|
|
5
5
|
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Type } from "typebox";
|
|
9
|
+
import { canonicalPath, ExecutionRoots } from "./core/roots.js";
|
|
10
|
+
import { ensureState } from "./core/state.js";
|
|
9
11
|
import { resolveAgentDir } from "./core/agent-dir.js";
|
|
10
12
|
import { loadFoveaConfig, type FoveaConfig } from "./core/config.js";
|
|
11
13
|
import { hasAstGrep } from "./core/astgrep.js";
|
|
@@ -13,7 +15,7 @@ import { ROOT_CACHE_LIMIT } from "./core/asyncutil.js";
|
|
|
13
15
|
import { coverageSummary, dwell, ensureStateBackground, focus, impact, sketch } from "./core/ops.js";
|
|
14
16
|
import { observeSessionPaths, resetSessions } from "./core/session.js";
|
|
15
17
|
import { captureMutation, finishMutation, type MutationCapture } from "./core/provenance.js";
|
|
16
|
-
import { resetSyncBaselines, sync, warmSync } from "./core/sync.js";
|
|
18
|
+
import { resetSyncBaselines, sync, syncBaselineStore, warmSync } from "./core/sync.js";
|
|
17
19
|
import type { NodeKind } from "./core/types.js";
|
|
18
20
|
|
|
19
21
|
const PACKAGE_VERSION = (() => {
|
|
@@ -29,7 +31,7 @@ const BudgetParam = Type.Optional(
|
|
|
29
31
|
Type.Number({ description: "Max tokens for the response (256..16000). Estimate: 4 chars/token.", minimum: 256, maximum: 16000 }),
|
|
30
32
|
);
|
|
31
33
|
const RootParam = Type.Optional(
|
|
32
|
-
Type.String({ description: "
|
|
34
|
+
Type.String({ description: "Explicit root to observe and bind. Relative to session cwd; omitted uses the last bound root. Does not change native tool cwd or grant project trust." }),
|
|
33
35
|
);
|
|
34
36
|
const GrepParams = Type.Object({
|
|
35
37
|
pattern: Type.String({ description: "Graph query for a bare identifier/path; exact text or regex pattern when search options are present." }),
|
|
@@ -80,21 +82,46 @@ const focusKind = (value: string | undefined): NodeKind | undefined =>
|
|
|
80
82
|
value && NODE_KINDS.has(value as NodeKind) ? value as NodeKind : undefined;
|
|
81
83
|
|
|
82
84
|
export default function fovea(pi: ExtensionAPI) {
|
|
85
|
+
const roots = new ExecutionRoots();
|
|
86
|
+
const targetConfig = (root: string, ctx: ExtensionContext): FoveaConfig =>
|
|
87
|
+
configFor(root, canonicalPath(ctx.cwd) === root && ctx.isProjectTrusted());
|
|
83
88
|
// Per-root config cache; invalidated by settings saves (/fovea settings).
|
|
84
89
|
const configs = new Map<string, FoveaConfig>();
|
|
85
90
|
const configFor = (root: string, trusted = false, agentDir?: string): FoveaConfig => {
|
|
86
|
-
const
|
|
91
|
+
const key = `${root}\0${trusted}\0${agentDir ?? resolveAgentDir()}`;
|
|
92
|
+
const hit = configs.get(key);
|
|
87
93
|
if (hit) {
|
|
88
|
-
configs.delete(
|
|
89
|
-
configs.set(
|
|
94
|
+
configs.delete(key);
|
|
95
|
+
configs.set(key, hit);
|
|
90
96
|
return hit;
|
|
91
97
|
}
|
|
92
98
|
const cfg = loadFoveaConfig({ cwd: root, agentDir: agentDir ?? resolveAgentDir(), projectTrusted: trusted });
|
|
93
|
-
configs.set(
|
|
99
|
+
configs.set(key, cfg);
|
|
94
100
|
while (configs.size > ROOT_CACHE_LIMIT) configs.delete(configs.keys().next().value!);
|
|
95
101
|
return cfg;
|
|
96
102
|
};
|
|
97
103
|
|
|
104
|
+
// Serialize enrollment in invocation order, including parallel graph calls.
|
|
105
|
+
let bindingTail: Promise<unknown> = Promise.resolve();
|
|
106
|
+
const bindRoot = (root: string, ctx: ExtensionContext): Promise<void> => {
|
|
107
|
+
const epoch = lifecycleEpoch;
|
|
108
|
+
const pending = bindingTail.catch(() => {}).then(async () => {
|
|
109
|
+
if (epoch !== lifecycleEpoch) throw new Error("Fovea session changed during root binding");
|
|
110
|
+
roots.check(root);
|
|
111
|
+
const cfg = targetConfig(root, ctx);
|
|
112
|
+
if (syncRuns(cfg) && !syncBaselineStore().has(root)) {
|
|
113
|
+
const state = await ensureState(root);
|
|
114
|
+
if (epoch !== lifecycleEpoch) throw new Error("Fovea session changed during root binding");
|
|
115
|
+
await sync(root, { files: [], budget: cfg.sync.budget, steerThreshold: cfg.sync.steerThreshold, scope: cfg.sync.scope,
|
|
116
|
+
sessionId: ctx.sessionManager.getSessionId() }, state);
|
|
117
|
+
}
|
|
118
|
+
if (epoch !== lifecycleEpoch) throw new Error("Fovea session changed during root binding");
|
|
119
|
+
roots.bind(root);
|
|
120
|
+
});
|
|
121
|
+
bindingTail = pending;
|
|
122
|
+
return pending;
|
|
123
|
+
};
|
|
124
|
+
|
|
98
125
|
// A missing ast-grep throws the full install guidance on the first
|
|
99
126
|
// failure; subsequent calls answer with a short "proceed natively" result
|
|
100
127
|
// instead of burning turns on identical hard errors. Self-healing: once
|
|
@@ -125,14 +152,16 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
125
152
|
// a broken or seedless graph simply yields native grep unchanged.
|
|
126
153
|
pi.on("tool_result", async (event, ctx) => {
|
|
127
154
|
if (event.toolName !== "grep" || event.isError) return undefined;
|
|
128
|
-
const cfg = configFor(ctx.cwd, ctx.isProjectTrusted());
|
|
129
|
-
if (cfg.tools.grepMode !== "augment") return undefined;
|
|
130
155
|
const input = (event.input ?? {}) as { pattern?: unknown; path?: unknown };
|
|
156
|
+
const owner = roots.owner(ctx.cwd, typeof input.path === "string" ? input.path : ".");
|
|
157
|
+
if (!owner) return undefined;
|
|
158
|
+
const cfg = targetConfig(owner.root, ctx);
|
|
159
|
+
if (cfg.tools.grepMode !== "augment") return undefined;
|
|
131
160
|
const pattern = typeof input.pattern === "string" ? input.pattern.trim() : "";
|
|
132
161
|
if (!pattern || !isSymbolLikeGrepQuery(pattern)) return undefined;
|
|
133
162
|
try {
|
|
134
|
-
const result = await focus(
|
|
135
|
-
path:
|
|
163
|
+
const result = await focus(owner.root, pattern, cfg.tools.grepAugmentBudget, {
|
|
164
|
+
path: owner.path || undefined,
|
|
136
165
|
fresh: true,
|
|
137
166
|
});
|
|
138
167
|
if (Number(result.details.seeds ?? 0) === 0) return undefined;
|
|
@@ -153,7 +182,7 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
153
182
|
...event.content,
|
|
154
183
|
text(gap + result.text.replace(/^fovea focus/, "fovea graph")),
|
|
155
184
|
],
|
|
156
|
-
details: { ...details, backend: "hybrid", foveaAppended: true, query: pattern },
|
|
185
|
+
details: { ...details, backend: "hybrid", foveaAppended: true, query: pattern, root: owner.root },
|
|
157
186
|
};
|
|
158
187
|
} catch {
|
|
159
188
|
return undefined;
|
|
@@ -177,12 +206,12 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
177
206
|
],
|
|
178
207
|
parameters: GrepParams,
|
|
179
208
|
async execute(id, params, signal, onUpdate, ctx) {
|
|
180
|
-
const root = ctx.cwd;
|
|
181
|
-
if (requestsNativeGrep(params)) {
|
|
182
|
-
const native = await loadNativeGrepTool(
|
|
209
|
+
const root = roots.target(ctx.cwd);
|
|
210
|
+
if (requestsNativeGrep(params) || targetConfig(root, ctx).tools.grepMode !== "replace") {
|
|
211
|
+
const native = await loadNativeGrepTool(ctx.cwd);
|
|
183
212
|
return native.execute(id, params, signal, onUpdate);
|
|
184
213
|
}
|
|
185
|
-
const budget =
|
|
214
|
+
const budget = targetConfig(root, ctx).tools.defaultBudget;
|
|
186
215
|
const query = params.pattern.trim() || params.pattern;
|
|
187
216
|
try {
|
|
188
217
|
const result = await focus(root, query, budget, { fresh: true });
|
|
@@ -192,7 +221,7 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
192
221
|
}
|
|
193
222
|
return {
|
|
194
223
|
content: [text(result.text.replace(/^fovea focus/, "fovea grep"))],
|
|
195
|
-
details: { ...result.details, backend: "fovea", query },
|
|
224
|
+
details: { ...result.details, backend: "fovea", query, root },
|
|
196
225
|
};
|
|
197
226
|
} catch (error) {
|
|
198
227
|
// A broken graph backend must not break text search: degrade to
|
|
@@ -216,6 +245,7 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
216
245
|
// pi 0.84 replaces extension runtimes on resume/fork/new/reload. Core
|
|
217
246
|
// module caches may outlive one factory instance, but disclosure and sync
|
|
218
247
|
// baselines are session-local and must never cross that boundary.
|
|
248
|
+
roots.clear();
|
|
219
249
|
resetSessions();
|
|
220
250
|
resetSyncBaselines();
|
|
221
251
|
if (configFor(ctx.cwd, ctx.isProjectTrusted()).tools.grepMode === "replace") registerGrepOverride();
|
|
@@ -223,7 +253,8 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
223
253
|
// wait on hashing/ast-grep. Slow cold builds surface a ready notice so
|
|
224
254
|
// the freeze feels like progress instead of a hang.
|
|
225
255
|
try {
|
|
226
|
-
const
|
|
256
|
+
const startupRoot = canonicalPath(ctx.cwd);
|
|
257
|
+
const kick = ensureStateBackground(startupRoot);
|
|
227
258
|
const t0 = Date.now();
|
|
228
259
|
// Always attach a rejection handler; headless sessions must not leak an
|
|
229
260
|
// unhandled rejection when ast-grep is unavailable.
|
|
@@ -237,9 +268,10 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
237
268
|
// Pre-establish the sync baseline in the background so the very
|
|
238
269
|
// first prompt fast-paths instead of paying the snapshot on the
|
|
239
270
|
// send path. A /new, /fork, or reload bumps the epoch and clears it.
|
|
240
|
-
const pre =
|
|
271
|
+
const pre = targetConfig(startupRoot, ctx);
|
|
272
|
+
if (!roots.list(ctx.cwd).includes(startupRoot) || !syncRuns(pre) || syncBaselineStore().has(startupRoot)) return;
|
|
241
273
|
void sync(
|
|
242
|
-
|
|
274
|
+
startupRoot,
|
|
243
275
|
{ files: [], budget: pre.sync.budget, steerThreshold: pre.sync.steerThreshold, pushFocus: pre.sync.pushFocus, scope: pre.sync.scope, sessionId },
|
|
244
276
|
st,
|
|
245
277
|
{ probe: "full" },
|
|
@@ -290,6 +322,7 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
290
322
|
lifecycleEpoch++;
|
|
291
323
|
turnFiles = [];
|
|
292
324
|
lastSyncError = undefined;
|
|
325
|
+
roots.clear();
|
|
293
326
|
resetSessions();
|
|
294
327
|
resetSyncBaselines();
|
|
295
328
|
});
|
|
@@ -299,12 +332,15 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
299
332
|
pi.on("tool_execution_start", async (event, ctx) => {
|
|
300
333
|
const args = event.args as { path?: unknown };
|
|
301
334
|
if (ATTENTION_PATH_TOOLS.has(event.toolName) && typeof args.path === "string") {
|
|
302
|
-
|
|
335
|
+
const owner = roots.owner(ctx.cwd, args.path);
|
|
336
|
+
if (owner) observeSessionPaths(owner.root, [owner.path]);
|
|
303
337
|
}
|
|
304
338
|
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
305
339
|
if (typeof args.path !== "string") return;
|
|
306
|
-
|
|
307
|
-
|
|
340
|
+
const owner = roots.owner(ctx.cwd, args.path);
|
|
341
|
+
if (!owner) return;
|
|
342
|
+
turnFiles.push(canonicalPath(owner.root, owner.path));
|
|
343
|
+
const capture = await captureMutation(owner.root, owner.path);
|
|
308
344
|
if (capture) pendingMutations.set(event.toolCallId, capture);
|
|
309
345
|
});
|
|
310
346
|
// Warm once the file is actually on disk (tool_execution_start fires during
|
|
@@ -316,79 +352,63 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
316
352
|
if (!event.isError && capture) {
|
|
317
353
|
await finishMutation(capture, ctx.sessionManager.getSessionId(), event.toolCallId).catch(() => false);
|
|
318
354
|
}
|
|
319
|
-
warmAfterEdit(
|
|
355
|
+
if (capture) warmAfterEdit(capture.root, targetConfig(capture.root, ctx));
|
|
320
356
|
});
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
357
|
+
// One session-level context allowance, shared across only explicit roots.
|
|
358
|
+
const pollRoots = async (ctx: ExtensionContext, probe: "cheap" | "defer", files: string[]) => {
|
|
359
|
+
const targets = roots.list(ctx.cwd);
|
|
360
|
+
const allowance = configFor(canonicalPath(ctx.cwd), ctx.isProjectTrusted()).sync.budget;
|
|
361
|
+
const share = Math.floor(allowance / targets.length);
|
|
362
|
+
const notices: Array<{ content: string; display: boolean; details: Record<string, unknown>; nextPrompt: boolean }> = [];
|
|
363
|
+
for (const root of targets) {
|
|
364
|
+
try {
|
|
365
|
+
const cfg = targetConfig(root, ctx);
|
|
366
|
+
if (!syncRuns(cfg)) continue;
|
|
367
|
+
const budget = Math.min(cfg.sync.budget, share);
|
|
368
|
+
const rels = files.flatMap((file) => {
|
|
369
|
+
const owner = roots.owner(ctx.cwd, file);
|
|
370
|
+
return owner?.root === root ? [owner.path] : [];
|
|
371
|
+
});
|
|
372
|
+
const outcome = await sync(root, {
|
|
373
|
+
files: rels, budget, steerThreshold: cfg.sync.steerThreshold,
|
|
374
|
+
pushFocus: cfg.sync.pushFocus, scope: cfg.sync.scope,
|
|
375
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
376
|
+
}, undefined, { probe });
|
|
377
|
+
lastSyncError = undefined;
|
|
378
|
+
if (outcome.red && outcome.text) {
|
|
379
|
+
const content = (targets.length > 1 ? `Root: ${root}\n` : "") + outcome.text;
|
|
380
|
+
notices.push({ content: content.slice(0, budget * 4 - 1) + "\n", display: syncDisplays(cfg),
|
|
381
|
+
details: { ...outcome.details, root }, nextPrompt: outcome.delivery === "next-prompt" });
|
|
382
|
+
} else if (outcome.structural && !outcome.details.baseline && !outcome.details.deferred &&
|
|
383
|
+
!outcome.details.outsideAttention && cfg.sync.ackClean && ctx.hasUI) {
|
|
384
|
+
ctx.ui.notify(`fovea: checked ${root}; no new action is needed.`, "info");
|
|
385
|
+
}
|
|
386
|
+
} catch (error) {
|
|
387
|
+
const message = `${root}: ${error instanceof Error ? error.message : error}`;
|
|
388
|
+
if (ctx.hasUI && message !== lastSyncError) ctx.ui.notify(`fovea: sync paused: ${message}`, "warning");
|
|
389
|
+
lastSyncError = message;
|
|
347
390
|
}
|
|
348
|
-
} catch (error) {
|
|
349
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
350
|
-
if (ctx.hasUI && message !== lastSyncError) ctx.ui.notify(`fovea: sync paused: ${message}`, "warning");
|
|
351
|
-
lastSyncError = message;
|
|
352
391
|
}
|
|
392
|
+
return notices;
|
|
393
|
+
};
|
|
394
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
395
|
+
const notices = await pollRoots(ctx, "defer", []);
|
|
396
|
+
if (!notices.length) return;
|
|
397
|
+
return { message: {
|
|
398
|
+
customType: "pi-fovea-sync",
|
|
399
|
+
content: notices.map((n) => n.content).join(""),
|
|
400
|
+
// A combined pre-prompt notice must not expose a hidden target.
|
|
401
|
+
display: notices.every((n) => n.display),
|
|
402
|
+
details: notices.length === 1 ? notices[0]!.details : { roots: notices.map((n) => n.details) },
|
|
403
|
+
} };
|
|
353
404
|
});
|
|
354
|
-
|
|
355
405
|
pi.on("turn_end", async (_event, ctx) => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
.
|
|
361
|
-
|
|
362
|
-
if (!syncRuns(cfg)) return;
|
|
363
|
-
const outcome = await sync(
|
|
364
|
-
ctx.cwd,
|
|
365
|
-
{ files: rels, budget: cfg.sync.budget, steerThreshold: cfg.sync.steerThreshold, pushFocus: cfg.sync.pushFocus, scope: cfg.sync.scope, sessionId: ctx.sessionManager.getSessionId() },
|
|
366
|
-
undefined,
|
|
367
|
-
{ probe: "cheap" },
|
|
368
|
-
);
|
|
369
|
-
lastSyncError = undefined;
|
|
370
|
-
if (!outcome.structural) return;
|
|
371
|
-
if (outcome.red && outcome.text) {
|
|
372
|
-
// Self/mixed/unattributed work can still receive an immediate
|
|
373
|
-
// consequence steer. Another session's relevant update waits for the
|
|
374
|
-
// next user prompt and therefore cannot restart an idle agent.
|
|
375
|
-
pi.sendMessage({
|
|
376
|
-
customType: "pi-fovea-sync",
|
|
377
|
-
content: outcome.text,
|
|
378
|
-
display: syncDisplays(cfg),
|
|
379
|
-
details: outcome.details,
|
|
380
|
-
}, outcome.delivery === "next-prompt"
|
|
381
|
-
? { deliverAs: "nextTurn" }
|
|
382
|
-
: { deliverAs: "steer", triggerTurn: true });
|
|
383
|
-
} else if (!outcome.details.baseline && !outcome.details.outsideAttention && cfg.sync.ackClean && ctx.hasUI) {
|
|
384
|
-
ctx.ui.notify("fovea: checked repository changes; no new action is needed.", "info");
|
|
385
|
-
}
|
|
386
|
-
} catch (error) {
|
|
387
|
-
// Turn-sync stays nonfatal, but a persistent index failure must not look
|
|
388
|
-
// like a clean repository. Notify once until a successful sync clears it.
|
|
389
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
390
|
-
if (ctx.hasUI && message !== lastSyncError) ctx.ui.notify(`fovea: sync paused: ${message}`, "warning");
|
|
391
|
-
lastSyncError = message;
|
|
406
|
+
const files = turnFiles;
|
|
407
|
+
turnFiles = [];
|
|
408
|
+
for (const notice of await pollRoots(ctx, "cheap", files)) {
|
|
409
|
+
pi.sendMessage({ customType: "pi-fovea-sync", content: notice.content,
|
|
410
|
+
display: notice.display, details: notice.details }, notice.nextPrompt
|
|
411
|
+
? { deliverAs: "nextTurn" } : { deliverAs: "steer", triggerTurn: true });
|
|
392
412
|
}
|
|
393
413
|
});
|
|
394
414
|
|
|
@@ -404,12 +424,13 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
404
424
|
],
|
|
405
425
|
parameters: Type.Object({ root: RootParam, maxTokens: BudgetParam }),
|
|
406
426
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
407
|
-
const root =
|
|
427
|
+
const root = roots.target(ctx.cwd, params.root);
|
|
408
428
|
try {
|
|
409
429
|
if (signal?.aborted) throw new Error("Fovea sketch cancelled");
|
|
430
|
+
await bindRoot(root, ctx);
|
|
410
431
|
onUpdate?.({ content: [text("Surveying production architecture…")], details: { phase: "sketch" } });
|
|
411
|
-
const r = await sketch(root, params.maxTokens ??
|
|
412
|
-
return { content: [text(r.text)], details: r.details };
|
|
432
|
+
const r = await sketch(root, params.maxTokens ?? targetConfig(root, ctx).tools.defaultBudget);
|
|
433
|
+
return { content: [text(r.text)], details: { ...r.details, root, observedRoots: roots.list(ctx.cwd) } };
|
|
413
434
|
} catch (error) {
|
|
414
435
|
return rethrowOrDegrade(error);
|
|
415
436
|
}
|
|
@@ -439,14 +460,15 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
439
460
|
maxTokens: BudgetParam,
|
|
440
461
|
}),
|
|
441
462
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
442
|
-
const root =
|
|
463
|
+
const root = roots.target(ctx.cwd, params.root);
|
|
443
464
|
try {
|
|
444
465
|
if (signal?.aborted) throw new Error("Fovea focus cancelled");
|
|
466
|
+
await bindRoot(root, ctx);
|
|
445
467
|
onUpdate?.({ content: [text("Resolving focused repository context…")], details: { phase: "focus" } });
|
|
446
468
|
const r = await focus(
|
|
447
469
|
root,
|
|
448
470
|
params.query,
|
|
449
|
-
params.maxTokens ??
|
|
471
|
+
params.maxTokens ?? targetConfig(root, ctx).tools.defaultBudget,
|
|
450
472
|
{
|
|
451
473
|
path: params.path,
|
|
452
474
|
language: params.language,
|
|
@@ -454,7 +476,7 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
454
476
|
fresh: params.fresh,
|
|
455
477
|
},
|
|
456
478
|
);
|
|
457
|
-
return { content: [text(r.text)], details: r.details };
|
|
479
|
+
return { content: [text(r.text)], details: { ...r.details, root, observedRoots: roots.list(ctx.cwd) } };
|
|
458
480
|
} catch (error) {
|
|
459
481
|
return rethrowOrDegrade(error);
|
|
460
482
|
}
|
|
@@ -474,12 +496,13 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
474
496
|
maxTokens: BudgetParam,
|
|
475
497
|
}),
|
|
476
498
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
477
|
-
const root =
|
|
499
|
+
const root = roots.target(ctx.cwd, params.root);
|
|
478
500
|
try {
|
|
479
501
|
if (signal?.aborted) throw new Error("Fovea dwell cancelled");
|
|
502
|
+
await bindRoot(root, ctx);
|
|
480
503
|
onUpdate?.({ content: [text("Widening the current graph context…")], details: { phase: "diffuse" } });
|
|
481
|
-
const r = await dwell(root, params.factor, params.maxTokens ??
|
|
482
|
-
return { content: [text(r.text)], details: r.details };
|
|
504
|
+
const r = await dwell(root, params.factor, params.maxTokens ?? targetConfig(root, ctx).tools.defaultBudget);
|
|
505
|
+
return { content: [text(r.text)], details: { ...r.details, root, observedRoots: roots.list(ctx.cwd) } };
|
|
483
506
|
} catch (error) {
|
|
484
507
|
return rethrowOrDegrade(error);
|
|
485
508
|
}
|
|
@@ -502,21 +525,22 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
502
525
|
maxTokens: BudgetParam,
|
|
503
526
|
}),
|
|
504
527
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
505
|
-
const root =
|
|
528
|
+
const root = roots.target(ctx.cwd, params.root);
|
|
506
529
|
try {
|
|
507
530
|
if (signal?.aborted) throw new Error("Fovea impact cancelled");
|
|
531
|
+
await bindRoot(root, ctx);
|
|
508
532
|
onUpdate?.({ content: [text("Tracing likely change impact…")], details: { phase: "impact" } });
|
|
509
|
-
if (
|
|
510
|
-
observeSessionPaths(
|
|
533
|
+
if (params.files?.length) {
|
|
534
|
+
observeSessionPaths(root, params.files);
|
|
511
535
|
}
|
|
512
536
|
const r = await impact(root, {
|
|
513
537
|
files: params.files,
|
|
514
538
|
symbols: params.symbols,
|
|
515
539
|
includeUncommitted: params.includeUncommitted,
|
|
516
540
|
base: params.base,
|
|
517
|
-
budget: params.maxTokens ??
|
|
541
|
+
budget: params.maxTokens ?? targetConfig(root, ctx).tools.defaultBudget,
|
|
518
542
|
});
|
|
519
|
-
return { content: [text(r.text)], details: r.details };
|
|
543
|
+
return { content: [text(r.text)], details: { ...r.details, root, observedRoots: roots.list(ctx.cwd) } };
|
|
520
544
|
} catch (error) {
|
|
521
545
|
return rethrowOrDegrade(error);
|
|
522
546
|
}
|
|
@@ -540,6 +564,11 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
540
564
|
}
|
|
541
565
|
if (sub === "reset") {
|
|
542
566
|
lifecycleEpoch++;
|
|
567
|
+
roots.clear();
|
|
568
|
+
for (const timer of warmTimers.values()) clearTimeout(timer);
|
|
569
|
+
warmTimers.clear();
|
|
570
|
+
pendingMutations.clear();
|
|
571
|
+
turnFiles = [];
|
|
543
572
|
resetSessions();
|
|
544
573
|
resetSyncBaselines();
|
|
545
574
|
ctx.ui.notify("Fovea focus history and sync baseline reset.", "info");
|
|
@@ -555,8 +584,9 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
555
584
|
return;
|
|
556
585
|
}
|
|
557
586
|
try {
|
|
587
|
+
const root = roots.target(ctx.cwd);
|
|
558
588
|
const [state, astGrep] = await Promise.all([
|
|
559
|
-
sketch(
|
|
589
|
+
sketch(root, 256),
|
|
560
590
|
pi.exec(process.env.FOVEA_AST_GREP ?? "ast-grep", ["--version"], { timeout: 15_000 })
|
|
561
591
|
.catch(() => ({ code: -1, stdout: "" })),
|
|
562
592
|
]);
|
|
@@ -565,9 +595,9 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
565
595
|
const unreadableCount = Array.isArray(state.details.extractionUnreadable) ? state.details.extractionUnreadable.length : 0;
|
|
566
596
|
const oversizedCount = Array.isArray(state.details.extractionOversized) ? state.details.extractionOversized.length : 0;
|
|
567
597
|
const generatedCount = Array.isArray(state.details.extractionGenerated) ? state.details.extractionGenerated.length : 0;
|
|
568
|
-
const cfg =
|
|
598
|
+
const cfg = targetConfig(root, ctx);
|
|
569
599
|
ctx.ui.notify(
|
|
570
|
-
`pi-fovea ${PACKAGE_VERSION} · ${coverage} · ${state.details.nodes ?? 0} symbols · ` +
|
|
600
|
+
`pi-fovea ${PACKAGE_VERSION} · root ${root} · ${roots.list(ctx.cwd).length} observed · ${coverage} · ${state.details.nodes ?? 0} symbols · ` +
|
|
571
601
|
`${state.details.productionAnchors ?? state.details.anchors ?? 0} production anchors` +
|
|
572
602
|
`${Number(state.details.testAnchors ?? 0) ? ` (${state.details.testAnchors} test/fixture collapsed)` : ""}` +
|
|
573
603
|
`${failedCount ? ` · !${failedCount} files failed extraction` : ""}` +
|