@monotykamary/pi-vcc 0.3.16
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 +688 -0
- package/__tests__/helpers.ts +135 -0
- package/__tests__/root-causes.test.ts +858 -0
- package/demo.gif +0 -0
- package/index.ts +21 -0
- package/knip.json +4 -0
- package/npm-shrinkwrap.json +3001 -0
- package/package.json +36 -0
- package/scripts/audit-sessions.ts +88 -0
- package/scripts/benchmark-real-sessions.ts +25 -0
- package/scripts/compare-before-after.ts +36 -0
- package/scripts/dump-branch-output.ts +20 -0
- package/src/commands/pi-vcc.ts +36 -0
- package/src/commands/vcc-recall.ts +65 -0
- package/src/core/brief.ts +497 -0
- package/src/core/build-sections.ts +431 -0
- package/src/core/content.ts +71 -0
- package/src/core/filter-noise.ts +42 -0
- package/src/core/format-recall.ts +177 -0
- package/src/core/format.ts +98 -0
- package/src/core/invisible-continue.ts +172 -0
- package/src/core/lineage.ts +26 -0
- package/src/core/load-messages.ts +40 -0
- package/src/core/normalize.ts +73 -0
- package/src/core/recall-scope.ts +49 -0
- package/src/core/render-entries.ts +58 -0
- package/src/core/report.ts +237 -0
- package/src/core/sanitize.ts +5 -0
- package/src/core/search-entries.ts +375 -0
- package/src/core/settings.ts +190 -0
- package/src/core/skill-collapse.ts +35 -0
- package/src/core/summarize.ts +179 -0
- package/src/core/tool-args.ts +14 -0
- package/src/details.ts +19 -0
- package/src/extract/commits.ts +69 -0
- package/src/extract/goals.ts +79 -0
- package/src/extract/preferences.ts +55 -0
- package/src/extract/shared-symbols.ts +357 -0
- package/src/hooks/before-compact.ts +564 -0
- package/src/hooks/proactive-threshold.ts +131 -0
- package/src/sections.ts +23 -0
- package/src/tools/recall.ts +204 -0
- package/src/types.ts +20 -0
- package/tests/australian-government.test.ts +479 -0
- package/tests/before-compact-hook.test.ts +185 -0
- package/tests/before-compact.test.ts +227 -0
- package/tests/brief.test.ts +215 -0
- package/tests/build-sections.test.ts +59 -0
- package/tests/compile.test.ts +89 -0
- package/tests/content.test.ts +66 -0
- package/tests/deep-extraction.test.ts +313 -0
- package/tests/extract-goals.test.ts +86 -0
- package/tests/extract-preferences.test.ts +30 -0
- package/tests/filter-noise.test.ts +61 -0
- package/tests/fixtures.ts +61 -0
- package/tests/format-recall.test.ts +87 -0
- package/tests/format.test.ts +73 -0
- package/tests/government-domain.test.ts +542 -0
- package/tests/lineage.test.ts +33 -0
- package/tests/load-messages.test.ts +49 -0
- package/tests/model-threshold-hook.test.ts +377 -0
- package/tests/model-threshold.test.ts +223 -0
- package/tests/normalize.test.ts +104 -0
- package/tests/proactive-threshold.test.ts +490 -0
- package/tests/real-sessions.test.ts +38 -0
- package/tests/recall-expand.test.ts +15 -0
- package/tests/recall-scope.test.ts +32 -0
- package/tests/recall-tool-scope.test.ts +67 -0
- package/tests/render-entries.test.ts +92 -0
- package/tests/report.test.ts +58 -0
- package/tests/sanitize.test.ts +24 -0
- package/tests/search-entries.test.ts +233 -0
- package/tests/support/load-session.ts +23 -0
- package/tests/support/real-sessions.ts +51 -0
- package/tests/threshold-integration.test.ts +359 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import type { NormalizedBlock, ToolResultIndex } from "../types";
|
|
2
|
+
import { extractPath } from "../core/tool-args";
|
|
3
|
+
|
|
4
|
+
const FILE_WRITE_TOOLS = new Set([
|
|
5
|
+
"Edit", "Write", "edit", "write", "edit_file", "write_file",
|
|
6
|
+
"MultiEdit",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
const FILE_READ_TOOLS = new Set([
|
|
10
|
+
"Read", "read", "read_file", "View",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const FILE_CREATE_TOOLS = new Set([
|
|
14
|
+
"Write", "write", "write_file",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
// Declaration regexes — consolidated from files.ts, symbol-changes.ts, type-catalog.ts
|
|
18
|
+
// Order: more specific patterns first, generic fallbacks last.
|
|
19
|
+
|
|
20
|
+
const TS_EXPORT_DECL_RE =
|
|
21
|
+
/^\s*export\s+(?:default\s+)?(?:async\s+)?(?:function|class|type|interface|const|let|enum)\s+(\w+)/;
|
|
22
|
+
const TS_TYPE_DECL_RE =
|
|
23
|
+
/^\s*(?:export\s+)?(?:type|interface)\s+(\w+)/;
|
|
24
|
+
const TS_EXPORT_SIG_RE =
|
|
25
|
+
/^\s*export\s+(?:default\s+)?(?:async\s+)?(?:function|class|type|interface|const|let|enum)\s+\w+[^;{]*[;{]?/;
|
|
26
|
+
|
|
27
|
+
const RUST_DECL_RE =
|
|
28
|
+
/^\s*(?:pub(?:\s*\([^)]*\))?\s+)?(?:fn|struct|enum|trait|type|const|union|var)\s+(\w+)/;
|
|
29
|
+
const RUST_IMPL_RE =
|
|
30
|
+
/^\s*(?:pub(?:\s*\([^)]*\))?\s+)?impl\s+(?:<[^>]+>\s+)?(\w+)(?:\s+for\s+(\w+))?/;
|
|
31
|
+
const RUST_SIG_RE =
|
|
32
|
+
/^\s*pub\s+(?:async\s+)?(?:fn|struct|enum|trait|type)\s+\w+/;
|
|
33
|
+
|
|
34
|
+
const ELIXIR_DEF_RE =
|
|
35
|
+
/^\s*def(?:p|macro|macrop|guard|guardp)?\s+(\w+)/;
|
|
36
|
+
const ELIXIR_MODULE_RE =
|
|
37
|
+
/^\s*defmodule\s+(\w+)/;
|
|
38
|
+
const ELIXIR_SPECIAL_RE =
|
|
39
|
+
/^\s*def(?:struct|protocol|impl)\s+(\w+)/;
|
|
40
|
+
|
|
41
|
+
const JAVA_TYPE_RE =
|
|
42
|
+
/^\s*(?:(?:public|private|protected)\s+)?(?:abstract\s+|static\s+|final\s+|sealed\s+)?(?:class|interface|enum|@interface|record)\s+(\w+)/;
|
|
43
|
+
const JAVA_METHOD_RE =
|
|
44
|
+
/^\s*(?:public|protected)\s+(?:static\s+|abstract\s+|final\s+)?(?:\S+(?:\s*\[\])?\s+)(\w+)\s*\(/;
|
|
45
|
+
|
|
46
|
+
const C_TYPE_RE =
|
|
47
|
+
/^\s*(?:typedef\s+)?(?:struct|class|enum|union)\s+(\w+)/;
|
|
48
|
+
const C_FUNC_RE =
|
|
49
|
+
/^\s*(?!func\b)(?:(?:static|extern|inline|virtual)\s+)?[\w][\w:*&\s]*?(\b\w+)\s*\(/;
|
|
50
|
+
|
|
51
|
+
const RUBY_DEF_RE =
|
|
52
|
+
/^\s*def\s+(?:self\.)?(\w+)/;
|
|
53
|
+
const RUBY_TYPE_RE =
|
|
54
|
+
/^\s*(?:class|module)\s+(\w+)/;
|
|
55
|
+
|
|
56
|
+
const PY_DECL_RE =
|
|
57
|
+
/^\s*(?:async\s+)?def\s+(\w+)|^\s*class\s+(\w+)/;
|
|
58
|
+
const PY_SIG_RE =
|
|
59
|
+
/^\s*(?:async\s+)?(?:def|class)\s+\w+\s*(?:\([^)]*\))?/;
|
|
60
|
+
|
|
61
|
+
const GO_DECL_RE =
|
|
62
|
+
/^\s*func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)/;
|
|
63
|
+
const GO_SIG_RE =
|
|
64
|
+
/^\s*func\s+(?:\(\w+\s+\*?\w+\)\s+)?\w+\s*(?:\([^)]*\))?\s*(?:\([^)]*\))?/;
|
|
65
|
+
|
|
66
|
+
interface SymbolInfo {
|
|
67
|
+
/** Simple declaration name (used by files.ts, symbol-changes.ts) */
|
|
68
|
+
name: string;
|
|
69
|
+
/** Declaration kind */
|
|
70
|
+
kind: "function" | "type" | "class" | "variable" | "unknown";
|
|
71
|
+
/** Full signature line (used by type-catalog.ts) */
|
|
72
|
+
signature?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ToolCallSymbols {
|
|
76
|
+
/** Symbols found in the tool_result text */
|
|
77
|
+
resultSymbols: SymbolInfo[];
|
|
78
|
+
/** Symbols found in Edit/Write args (newText/content) */
|
|
79
|
+
argSymbols: SymbolInfo[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fast screening regex: rejects lines that can't start any declaration.
|
|
83
|
+
// Avoids running the full 15-regex cascade on body code / comments / blank lines.
|
|
84
|
+
const DECL_SCREEN_RE =
|
|
85
|
+
/^\s*(?:export|pub|func|def|class|type|interface|async|abstract|static|public|private|protected|struct|enum|trait|impl|module|const|fn|sealed|record|typedef|union|virtual|extern|inline)/;
|
|
86
|
+
|
|
87
|
+
const parseDeclName = (line: string): { name: string; kind: SymbolInfo["kind"] } | null => {
|
|
88
|
+
// Quick reject: lines that can't start any declaration keyword
|
|
89
|
+
if (!DECL_SCREEN_RE.test(line)) return null;
|
|
90
|
+
|
|
91
|
+
let m = line.match(TS_EXPORT_DECL_RE);
|
|
92
|
+
if (m) {
|
|
93
|
+
const kind = line.includes("function") ? "function"
|
|
94
|
+
: line.includes("class") ? "class"
|
|
95
|
+
: line.includes("type") ? "type"
|
|
96
|
+
: line.includes("interface") ? "type"
|
|
97
|
+
: line.includes("enum") ? "variable"
|
|
98
|
+
: "variable";
|
|
99
|
+
return { name: m[1], kind };
|
|
100
|
+
}
|
|
101
|
+
m = line.match(TS_TYPE_DECL_RE);
|
|
102
|
+
if (m) return { name: m[1], kind: "type" };
|
|
103
|
+
m = line.match(RUST_DECL_RE);
|
|
104
|
+
if (m) return { name: m[1], kind: "function" };
|
|
105
|
+
m = line.match(RUST_IMPL_RE);
|
|
106
|
+
if (m) return { name: m[1], kind: "class" };
|
|
107
|
+
m = line.match(ELIXIR_MODULE_RE);
|
|
108
|
+
if (m) return { name: m[1], kind: "class" };
|
|
109
|
+
m = line.match(ELIXIR_SPECIAL_RE);
|
|
110
|
+
if (m) return { name: m[1], kind: "class" };
|
|
111
|
+
m = line.match(ELIXIR_DEF_RE);
|
|
112
|
+
if (m) return { name: m[1], kind: "function" };
|
|
113
|
+
m = line.match(JAVA_TYPE_RE);
|
|
114
|
+
if (m) return { name: m[1], kind: "class" };
|
|
115
|
+
m = line.match(JAVA_METHOD_RE);
|
|
116
|
+
if (m) return { name: m[1], kind: "function" };
|
|
117
|
+
m = line.match(C_TYPE_RE);
|
|
118
|
+
if (m) return { name: m[1], kind: "class" };
|
|
119
|
+
m = line.match(C_FUNC_RE);
|
|
120
|
+
if (m) return { name: m[1], kind: "function" };
|
|
121
|
+
m = line.match(RUBY_TYPE_RE);
|
|
122
|
+
if (m) return { name: m[1], kind: "class" };
|
|
123
|
+
m = line.match(RUBY_DEF_RE);
|
|
124
|
+
if (m) return { name: m[1], kind: "function" };
|
|
125
|
+
m = line.match(PY_DECL_RE);
|
|
126
|
+
if (m) return { name: m[1] || m[2], kind: m[2] ? "class" : "function" };
|
|
127
|
+
m = line.match(GO_DECL_RE);
|
|
128
|
+
if (m && m[1][0] === m[1][0].toUpperCase()) return { name: m[1], kind: "function" };
|
|
129
|
+
return null;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const parseSignature = (line: string): string | null => {
|
|
133
|
+
if (TS_EXPORT_SIG_RE.test(line)) return line.trim();
|
|
134
|
+
if (PY_SIG_RE.test(line) && !line.trim().startsWith("def _") && !line.trim().startsWith("class _")) return line.trim();
|
|
135
|
+
if (GO_SIG_RE.test(line)) {
|
|
136
|
+
const nameMatch = line.match(/func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)/);
|
|
137
|
+
if (nameMatch && nameMatch[1] && nameMatch[1][0] === nameMatch[1][0].toUpperCase()) return line.trim();
|
|
138
|
+
}
|
|
139
|
+
if (RUST_SIG_RE.test(line)) return line.trim();
|
|
140
|
+
return null;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Line-by-line iteration over text using indexOf("\n") instead of split().
|
|
145
|
+
* Avoids allocating an intermediate string array — for a 300-line, 12KB
|
|
146
|
+
* tool result this saves ~30μs/scan vs split(). Over 600 tool results
|
|
147
|
+
* in a large session, that's ~18ms reclaimed.
|
|
148
|
+
*/
|
|
149
|
+
const eachLine = function* (text: string, maxLines: number): Generator<string> {
|
|
150
|
+
let pos = 0;
|
|
151
|
+
let count = 0;
|
|
152
|
+
const len = text.length;
|
|
153
|
+
while (pos < len && count < maxLines) {
|
|
154
|
+
const nl = text.indexOf("\n", pos);
|
|
155
|
+
if (nl === -1) {
|
|
156
|
+
yield text.slice(pos);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
yield text.slice(pos, nl);
|
|
160
|
+
pos = nl + 1;
|
|
161
|
+
count++;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const extractSymbolsFromText = (text: string, maxLines: number, includeSigs: boolean): SymbolInfo[] => {
|
|
166
|
+
const names: SymbolInfo[] = [];
|
|
167
|
+
const seen = new Set<string>();
|
|
168
|
+
for (const line of eachLine(text, maxLines)) {
|
|
169
|
+
const decl = parseDeclName(line);
|
|
170
|
+
if (decl && !seen.has(decl.name)) {
|
|
171
|
+
seen.add(decl.name);
|
|
172
|
+
const sig = includeSigs ? parseSignature(line) : undefined;
|
|
173
|
+
names.push({ name: decl.name, kind: decl.kind, signature: sig ?? undefined });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return names;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
interface FileActivity {
|
|
180
|
+
read: Set<string>;
|
|
181
|
+
modified: Set<string>;
|
|
182
|
+
created: Set<string>;
|
|
183
|
+
symbols: Map<string, string[]>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface ExportSig {
|
|
187
|
+
file: string;
|
|
188
|
+
signatures: string[];
|
|
189
|
+
modified: boolean;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface SymbolRef {
|
|
193
|
+
name: string;
|
|
194
|
+
file: string;
|
|
195
|
+
kind: "function" | "type" | "class" | "variable" | "unknown";
|
|
196
|
+
access: "modified" | "read";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface UnifiedExtractResult {
|
|
200
|
+
fileActivity: FileActivity;
|
|
201
|
+
typeCatalog: ExportSig[];
|
|
202
|
+
symbolChanges: SymbolRef[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Single-pass extraction of all file/symbol information from tool calls.
|
|
207
|
+
*
|
|
208
|
+
* Replaces the triple-redundant scan performed by:
|
|
209
|
+
* - extractFiles() — path collection + 200-line symbol scan
|
|
210
|
+
* - extractSymbolChanges() — 300-line symbol scan
|
|
211
|
+
* - extractTypeCatalog() — 150-line signature scan
|
|
212
|
+
*
|
|
213
|
+
* All three scanned the same tool_result content with overlapping regex
|
|
214
|
+
* patterns. This unified pass does it once and returns all three datasets.
|
|
215
|
+
*/
|
|
216
|
+
export const extractFileAndSymbolData = (
|
|
217
|
+
blocks: NormalizedBlock[],
|
|
218
|
+
tri?: ToolResultIndex,
|
|
219
|
+
): UnifiedExtractResult => {
|
|
220
|
+
const read = new Set<string>();
|
|
221
|
+
const modified = new Set<string>();
|
|
222
|
+
const created = new Set<string>();
|
|
223
|
+
// Parallel dedup set for symbols Map — avoids O(n) Array.includes()
|
|
224
|
+
// on symbol arrays that can grow to 200+ entries per file.
|
|
225
|
+
const symbols = new Map<string, string[]>();
|
|
226
|
+
const symbolsSeen = new Map<string, Set<string>>();
|
|
227
|
+
const symbolRefs: SymbolRef[] = [];
|
|
228
|
+
const refSeen = new Set<string>();
|
|
229
|
+
|
|
230
|
+
// Type catalog state
|
|
231
|
+
const fileSigs = new Map<string, { sigs: string[]; modified: boolean }>();
|
|
232
|
+
const fileOrder: string[] = [];
|
|
233
|
+
|
|
234
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
235
|
+
const b = blocks[i];
|
|
236
|
+
if (b.kind !== "tool_call") continue;
|
|
237
|
+
const p = extractPath(b.args);
|
|
238
|
+
if (!p) continue;
|
|
239
|
+
|
|
240
|
+
const isRead = FILE_READ_TOOLS.has(b.name);
|
|
241
|
+
const isWrite = FILE_WRITE_TOOLS.has(b.name);
|
|
242
|
+
const isCreate = FILE_CREATE_TOOLS.has(b.name);
|
|
243
|
+
|
|
244
|
+
if (isRead) read.add(p);
|
|
245
|
+
if (isWrite) modified.add(p);
|
|
246
|
+
if (isCreate) created.add(p);
|
|
247
|
+
|
|
248
|
+
// Extract symbols from Edit/Write args
|
|
249
|
+
if (isWrite) {
|
|
250
|
+
const newText = (b.args.newText ?? b.args.new_text ?? b.args.content ?? "") as string;
|
|
251
|
+
if (newText && typeof newText === "string") {
|
|
252
|
+
const syms = extractSymbolsFromText(newText, 100, true);
|
|
253
|
+
|
|
254
|
+
// File activity symbols
|
|
255
|
+
let seen = symbolsSeen.get(p);
|
|
256
|
+
if (!seen) { seen = new Set(); symbolsSeen.set(p, seen); }
|
|
257
|
+
if (!symbols.has(p)) symbols.set(p, []);
|
|
258
|
+
const existing = symbols.get(p)!;
|
|
259
|
+
for (const s of syms) {
|
|
260
|
+
if (!seen.has(s.name)) { seen.add(s.name); existing.push(s.name); }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Type catalog
|
|
264
|
+
if (!fileSigs.has(p)) {
|
|
265
|
+
fileSigs.set(p, { sigs: [], modified: true });
|
|
266
|
+
fileOrder.push(p);
|
|
267
|
+
} else {
|
|
268
|
+
fileSigs.get(p)!.modified = true;
|
|
269
|
+
}
|
|
270
|
+
const sigs = fileSigs.get(p)!.sigs;
|
|
271
|
+
for (const s of syms) {
|
|
272
|
+
if (s.signature && !sigs.includes(s.signature)) sigs.push(s.signature);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Symbol changes
|
|
276
|
+
const access = "modified" as const;
|
|
277
|
+
for (const s of syms) {
|
|
278
|
+
const key = `${s.name}@${p}`;
|
|
279
|
+
if (!refSeen.has(key)) {
|
|
280
|
+
refSeen.add(key);
|
|
281
|
+
symbolRefs.push({ name: s.name, file: p, kind: s.kind, access });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Extract from tool_result (shared look-ahead via index)
|
|
288
|
+
if (isRead || isWrite) {
|
|
289
|
+
const r = tri ? tri.get(i) : (() => {
|
|
290
|
+
for (let j = i + 1; j < Math.min(blocks.length, i + 4); j++) {
|
|
291
|
+
const b2 = blocks[j];
|
|
292
|
+
if (b2.kind === "tool_result") return b2 as Extract<NormalizedBlock, { kind: "tool_result" }>;
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
})();
|
|
296
|
+
|
|
297
|
+
if (r && r.text && !r.isError) {
|
|
298
|
+
const resultText = r.text;
|
|
299
|
+
|
|
300
|
+
// Parse symbols once — 200 lines covers all three extractors' needs
|
|
301
|
+
const syms = extractSymbolsFromText(resultText, 200, true);
|
|
302
|
+
|
|
303
|
+
// File activity symbols
|
|
304
|
+
let seen = symbolsSeen.get(p);
|
|
305
|
+
if (!seen) { seen = new Set(); symbolsSeen.set(p, seen); }
|
|
306
|
+
if (!symbols.has(p)) symbols.set(p, []);
|
|
307
|
+
const existing = symbols.get(p)!;
|
|
308
|
+
for (const s of syms) {
|
|
309
|
+
if (!seen.has(s.name)) { seen.add(s.name); existing.push(s.name); }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Type catalog (Read results)
|
|
313
|
+
if (isRead) {
|
|
314
|
+
if (!fileSigs.has(p)) {
|
|
315
|
+
fileSigs.set(p, { sigs: [], modified: false });
|
|
316
|
+
fileOrder.push(p);
|
|
317
|
+
}
|
|
318
|
+
const sigs = fileSigs.get(p)!.sigs;
|
|
319
|
+
for (const s of syms) {
|
|
320
|
+
if (s.signature && !sigs.includes(s.signature)) sigs.push(s.signature);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Symbol changes
|
|
325
|
+
const access = isWrite ? "modified" as const : "read" as const;
|
|
326
|
+
for (const s of syms) {
|
|
327
|
+
const key = `${s.name}@${p}`;
|
|
328
|
+
if (!refSeen.has(key)) {
|
|
329
|
+
refSeen.add(key);
|
|
330
|
+
symbolRefs.push({ name: s.name, file: p, kind: s.kind, access });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Dedup: if already Modified, drop from Created
|
|
338
|
+
for (const p of modified) created.delete(p);
|
|
339
|
+
|
|
340
|
+
// Build type catalog — modified files first, then read files
|
|
341
|
+
const modifiedSigs: ExportSig[] = [];
|
|
342
|
+
const readSigs: ExportSig[] = [];
|
|
343
|
+
for (const file of fileOrder) {
|
|
344
|
+
const entry = fileSigs.get(file)!;
|
|
345
|
+
if (entry.sigs.length === 0) continue;
|
|
346
|
+
const esig: ExportSig = { file, signatures: entry.sigs.slice(0, 8), modified: entry.modified };
|
|
347
|
+
if (esig.modified) modifiedSigs.push(esig);
|
|
348
|
+
else readSigs.push(esig);
|
|
349
|
+
}
|
|
350
|
+
const typeCatalog = [...modifiedSigs, ...readSigs].slice(0, 12);
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
fileActivity: { read, modified, created, symbols },
|
|
354
|
+
typeCatalog,
|
|
355
|
+
symbolChanges: symbolRefs,
|
|
356
|
+
};
|
|
357
|
+
};
|