pi-fovea 0.18.2 → 0.18.4
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 +1 -1
- package/dist/cli.mjs +14 -1
- package/package.json +3 -3
- package/src/core/build.ts +28 -1
- package/src/core/provenance.ts +7 -1
package/README.md
CHANGED
|
@@ -205,7 +205,7 @@ next user prompt instead; it never restarts an idle agent. The compact update
|
|
|
205
205
|
names changed files, route deltas, and newly relevant files. Shell commands,
|
|
206
206
|
external editors, and agents without Fovea remain `unattributed` rather than
|
|
207
207
|
being guessed, while the path scope still keeps unrelated sibling sandboxes
|
|
208
|
-
quiet. Provenance journals accept either intercepted mutations or explicit trusted SHA-1 transitions, write one bounded replacement per event batch, expire after seven days, and live in
|
|
208
|
+
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
|
|
209
209
|
`$TMPDIR`; repository content remains the drift oracle. Updates list causal
|
|
210
210
|
channels such as calls, imports, shared literals, tests, or co-change history.
|
|
211
211
|
By default Fovea also embeds the refreshed focus context of the top drift target
|
package/dist/cli.mjs
CHANGED
|
@@ -1769,6 +1769,7 @@ import { join as join3 } from "node:path";
|
|
|
1769
1769
|
import { createHash as createHash3, randomUUID } from "node:crypto";
|
|
1770
1770
|
import { createReadStream } from "node:fs";
|
|
1771
1771
|
import { mkdir as mkdir2, open, readFile as readFile4, readdir, rename, rm, stat } from "node:fs/promises";
|
|
1772
|
+
import { performance } from "node:perf_hooks";
|
|
1772
1773
|
import { createInterface } from "node:readline";
|
|
1773
1774
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
1774
1775
|
import { dirname as dirname2, join as joinPath3 } from "node:path";
|
|
@@ -2585,6 +2586,8 @@ var promote = (sigs, pack = []) => {
|
|
|
2585
2586
|
};
|
|
2586
2587
|
|
|
2587
2588
|
// src/core/build.ts
|
|
2589
|
+
var buildObserver;
|
|
2590
|
+
var observe = (event) => buildObserver?.(event);
|
|
2588
2591
|
var CACHE_VERSION = 11;
|
|
2589
2592
|
var IGNORE_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "vendor", ".venv", "venv", "target", "coverage", ".next", "build", "__pycache__", ".pi", ".pi-fovea", "deps", "_build", ".tox", "Pods", ".cargo"]);
|
|
2590
2593
|
var MAX_FILES = envInt("FOVEA_MAX_FILES", 8e3, 100, 1e5);
|
|
@@ -2896,6 +2899,7 @@ var extractInto = async (root, store, files, contents, packRules) => {
|
|
|
2896
2899
|
batches.push(files.slice(start, start + EXTRACTION_BATCH));
|
|
2897
2900
|
}
|
|
2898
2901
|
const extracted = await mapLimit(batches, SPAWN_CONCURRENCY, async (batch) => {
|
|
2902
|
+
const tBatch = performance.now();
|
|
2899
2903
|
const code = batch.filter((f) => !isConfigFile(f));
|
|
2900
2904
|
const anchorPlan = anchorScanPlan(code, packRules.pack);
|
|
2901
2905
|
const rules = [...coreScanRules(code), ...anchorPlan.rules];
|
|
@@ -2933,6 +2937,7 @@ var extractInto = async (root, store, files, contents, packRules) => {
|
|
|
2933
2937
|
extractAnchors(code, root, enclosingId, packRules.pack)
|
|
2934
2938
|
]);
|
|
2935
2939
|
}
|
|
2940
|
+
observe({ phase: "extract", files: batch.length, ms: performance.now() - tBatch, at: tBatch });
|
|
2936
2941
|
return { batch, symbols, imports, calls, literals, anchors, fileRouteAnchors };
|
|
2937
2942
|
});
|
|
2938
2943
|
for (const result of extracted) {
|
|
@@ -3043,7 +3048,9 @@ var loadFacts = async (root, files) => {
|
|
|
3043
3048
|
cacheDirty = true;
|
|
3044
3049
|
}
|
|
3045
3050
|
}
|
|
3051
|
+
const tStat = performance.now();
|
|
3046
3052
|
const stats = await statMany(root, files);
|
|
3053
|
+
observe({ phase: "stat", files: files.length, ms: performance.now() - tStat, at: tStat });
|
|
3047
3054
|
const needHashed = [];
|
|
3048
3055
|
const unreadable = [];
|
|
3049
3056
|
const oversized = [];
|
|
@@ -3078,6 +3085,7 @@ var loadFacts = async (root, files) => {
|
|
|
3078
3085
|
const contents = /* @__PURE__ */ new Map();
|
|
3079
3086
|
const dirty = [];
|
|
3080
3087
|
const spend = makeTextBudget();
|
|
3088
|
+
const tHash = performance.now();
|
|
3081
3089
|
await mapLimit(needHashed, IO_CONCURRENCY, async (rel) => {
|
|
3082
3090
|
const got = await sha1Of(root, rel);
|
|
3083
3091
|
const meta = stats.get(rel);
|
|
@@ -3113,6 +3121,7 @@ var loadFacts = async (root, files) => {
|
|
|
3113
3121
|
store.meta.set(rel, meta);
|
|
3114
3122
|
store.facts.set(rel, pendingFileFacts(rel, got.sha1, got.text));
|
|
3115
3123
|
});
|
|
3124
|
+
observe({ phase: "hash", files: needHashed.length, ms: performance.now() - tHash, at: tHash });
|
|
3116
3125
|
const active = resolveRulePack(store, await loadRepoRules(root));
|
|
3117
3126
|
clearTaint(store, dirty);
|
|
3118
3127
|
await extractInto(root, store, dirty, contents, active);
|
|
@@ -3135,7 +3144,11 @@ var loadFacts = async (root, files) => {
|
|
|
3135
3144
|
store.oversized.forEach((f) => {
|
|
3136
3145
|
if (!oversized.includes(f) && files.includes(f)) store.oversized.delete(f);
|
|
3137
3146
|
});
|
|
3138
|
-
if (cacheDirty)
|
|
3147
|
+
if (cacheDirty) {
|
|
3148
|
+
const tPersist = performance.now();
|
|
3149
|
+
await persistFacts(store);
|
|
3150
|
+
observe({ phase: "persist", files: store.facts.size, ms: performance.now() - tPersist, at: tPersist });
|
|
3151
|
+
}
|
|
3139
3152
|
return {
|
|
3140
3153
|
store,
|
|
3141
3154
|
report: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-fovea",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.4",
|
|
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",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"fovea": "dist/cli.mjs"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
56
|
-
"@earendil-works/pi-tui": "0.84.
|
|
55
|
+
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
56
|
+
"@earendil-works/pi-tui": "0.84.3",
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"esbuild": "^0.28.1",
|
|
59
59
|
"knip": "^6.32.0",
|
package/src/core/build.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { createHash, randomUUID } from "node:crypto";
|
|
17
17
|
import { createReadStream } from "node:fs";
|
|
18
18
|
import { mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
|
|
19
|
+
import { performance } from "node:perf_hooks";
|
|
19
20
|
import { createInterface } from "node:readline";
|
|
20
21
|
import { tmpdir } from "node:os";
|
|
21
22
|
import { dirname, join as joinPath } from "node:path";
|
|
@@ -65,6 +66,22 @@ export interface FileFacts {
|
|
|
65
66
|
sigs?: FileSigs;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/** Opt-in build-phase timing for benchmarks and profiling. Unset observers
|
|
70
|
+
* cost one optional call per checkpoint; no allocations, no clock reads. */
|
|
71
|
+
export interface BuildPhaseEvent {
|
|
72
|
+
phase: "stat" | "hash" | "extract" | "persist";
|
|
73
|
+
/** Files the phase covered (batch size for extract events). */
|
|
74
|
+
files: number;
|
|
75
|
+
ms: number;
|
|
76
|
+
/** performance.now() at phase start, for wall-window reconstruction. */
|
|
77
|
+
at: number;
|
|
78
|
+
}
|
|
79
|
+
let buildObserver: ((event: BuildPhaseEvent) => void) | undefined;
|
|
80
|
+
export const setBuildObserver = (observer: typeof buildObserver): void => {
|
|
81
|
+
buildObserver = observer;
|
|
82
|
+
};
|
|
83
|
+
const observe = (event: BuildPhaseEvent): void => buildObserver?.(event);
|
|
84
|
+
|
|
68
85
|
const CACHE_VERSION = 11; // bump when extractor semantics or the cache schema change (v11: generated-source skip for minified bundles)
|
|
69
86
|
|
|
70
87
|
// Honest coverage: what the extractor could NOT see. Tools and status render
|
|
@@ -517,6 +534,7 @@ const extractInto = async (
|
|
|
517
534
|
batches.push(files.slice(start, start + EXTRACTION_BATCH));
|
|
518
535
|
}
|
|
519
536
|
const extracted = await mapLimit(batches, SPAWN_CONCURRENCY, async (batch) => {
|
|
537
|
+
const tBatch = performance.now();
|
|
520
538
|
const code = batch.filter((f) => !isConfigFile(f));
|
|
521
539
|
const anchorPlan = anchorScanPlan(code, packRules.pack);
|
|
522
540
|
const rules = [...coreScanRules(code), ...anchorPlan.rules];
|
|
@@ -556,6 +574,7 @@ const extractInto = async (
|
|
|
556
574
|
]);
|
|
557
575
|
}
|
|
558
576
|
|
|
577
|
+
observe({ phase: "extract", files: batch.length, ms: performance.now() - tBatch, at: tBatch });
|
|
559
578
|
return { batch, symbols, imports, calls, literals, anchors, fileRouteAnchors };
|
|
560
579
|
});
|
|
561
580
|
|
|
@@ -718,7 +737,9 @@ export const loadFacts = async (root: string, files: string[]): Promise<FactsOut
|
|
|
718
737
|
}
|
|
719
738
|
}
|
|
720
739
|
|
|
740
|
+
const tStat = performance.now();
|
|
721
741
|
const stats = await statMany(root, files);
|
|
742
|
+
observe({ phase: "stat", files: files.length, ms: performance.now() - tStat, at: tStat });
|
|
722
743
|
const needHashed: string[] = [];
|
|
723
744
|
const unreadable: string[] = [];
|
|
724
745
|
const oversized: string[] = [];
|
|
@@ -754,6 +775,7 @@ export const loadFacts = async (root: string, files: string[]): Promise<FactsOut
|
|
|
754
775
|
const contents = new Map<string, string>();
|
|
755
776
|
const dirty: string[] = [];
|
|
756
777
|
const spend = makeTextBudget();
|
|
778
|
+
const tHash = performance.now();
|
|
757
779
|
await mapLimit(needHashed, IO_CONCURRENCY, async (rel) => {
|
|
758
780
|
const got = await sha1Of(root, rel);
|
|
759
781
|
const meta = stats.get(rel)!;
|
|
@@ -790,6 +812,7 @@ export const loadFacts = async (root: string, files: string[]): Promise<FactsOut
|
|
|
790
812
|
store.meta.set(rel, meta);
|
|
791
813
|
store.facts.set(rel, pendingFileFacts(rel, got.sha1, got.text));
|
|
792
814
|
});
|
|
815
|
+
observe({ phase: "hash", files: needHashed.length, ms: performance.now() - tHash, at: tHash });
|
|
793
816
|
|
|
794
817
|
const active = resolveRulePack(store, await loadRepoRules(root));
|
|
795
818
|
clearTaint(store, dirty);
|
|
@@ -812,7 +835,11 @@ export const loadFacts = async (root: string, files: string[]): Promise<FactsOut
|
|
|
812
835
|
store.unreadable.forEach((f) => { if (!unreadable.includes(f) && files.includes(f)) store.unreadable.delete(f); });
|
|
813
836
|
store.oversized.forEach((f) => { if (!oversized.includes(f) && files.includes(f)) store.oversized.delete(f); });
|
|
814
837
|
|
|
815
|
-
if (cacheDirty)
|
|
838
|
+
if (cacheDirty) {
|
|
839
|
+
const tPersist = performance.now();
|
|
840
|
+
await persistFacts(store);
|
|
841
|
+
observe({ phase: "persist", files: store.facts.size, ms: performance.now() - tPersist, at: tPersist });
|
|
842
|
+
}
|
|
816
843
|
return {
|
|
817
844
|
store,
|
|
818
845
|
report: {
|
package/src/core/provenance.ts
CHANGED
|
@@ -13,6 +13,7 @@ interface MutationRecord {
|
|
|
13
13
|
afterSha?: string;
|
|
14
14
|
owner: string;
|
|
15
15
|
toolCallId: string;
|
|
16
|
+
commitOrder?: number;
|
|
16
17
|
at: number;
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -34,6 +35,7 @@ export interface MutationTransition {
|
|
|
34
35
|
path: string;
|
|
35
36
|
beforeSha?: string | undefined;
|
|
36
37
|
afterSha?: string | undefined;
|
|
38
|
+
commitOrder?: number | undefined;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
type ProvenanceKind = "current-session" | "other-session" | "mixed" | "unattributed";
|
|
@@ -114,6 +116,7 @@ export const recordMutationTransitions = async (
|
|
|
114
116
|
afterSha: transition.afterSha,
|
|
115
117
|
owner,
|
|
116
118
|
toolCallId,
|
|
119
|
+
...(transition.commitOrder === undefined ? {} : { commitOrder: transition.commitOrder }),
|
|
117
120
|
at,
|
|
118
121
|
}];
|
|
119
122
|
});
|
|
@@ -179,7 +182,10 @@ const readRecords = async (root: string, since: number): Promise<MutationRecord[
|
|
|
179
182
|
// A concurrent atomic replacement or malformed journal is unattributed.
|
|
180
183
|
}
|
|
181
184
|
}));
|
|
182
|
-
return records.sort((a, b) => a.at - b.at
|
|
185
|
+
return records.sort((a, b) => a.at - b.at
|
|
186
|
+
|| a.owner.localeCompare(b.owner)
|
|
187
|
+
|| (a.commitOrder ?? Number.MAX_SAFE_INTEGER) - (b.commitOrder ?? Number.MAX_SAFE_INTEGER)
|
|
188
|
+
|| a.toolCallId.localeCompare(b.toolCallId));
|
|
183
189
|
};
|
|
184
190
|
|
|
185
191
|
const kindForOwners = (owners: Set<string>, currentOwner: string): ProvenanceKind => {
|