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 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) await persistFacts(store);
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.2",
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.2",
56
- "@earendil-works/pi-tui": "0.84.2",
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) await persistFacts(store);
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: {
@@ -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 || a.toolCallId.localeCompare(b.toolCallId));
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 => {