copilot-usage-studio 0.2.0 → 0.2.1
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/CHANGELOG.md +16 -1
- package/README.md +42 -13
- package/data/github-copilot-pricing.json +34 -6
- package/dist/copilot-usage-studio/browser/chunk-2SAR2Q6M.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-5JYQJM5G.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-7FKLWMFH.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-EWN3YOHT.js +1 -0
- package/dist/copilot-usage-studio/browser/{chunk-NXH37FKU.js → chunk-KS5W2HMH.js} +1 -1
- package/dist/copilot-usage-studio/browser/{chunk-TVSYR63W.js → chunk-OS2XPCVW.js} +2 -2
- package/dist/copilot-usage-studio/browser/{chunk-QQOFM3HF.js → chunk-U4H425LY.js} +1 -1
- package/dist/copilot-usage-studio/browser/chunk-VEKXIM47.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-WOIYAEKB.js +4 -0
- package/dist/copilot-usage-studio/browser/{chunk-J47KKACI.js → chunk-XHID4XVE.js} +1 -1
- package/dist/copilot-usage-studio/browser/index.html +2 -2
- package/dist/copilot-usage-studio/browser/main-OZIMQRXC.js +1 -0
- package/dist/copilot-usage-studio/browser/styles-7RDDQXQV.css +1 -0
- package/docs/local-deployment.md +67 -11
- package/docs/pricing.md +3 -3
- package/docs/scanner-api.md +28 -0
- package/lib/cli.mjs +15 -1
- package/lib/local-runtime.mjs +455 -19
- package/lib/scanner-worker.mjs +25 -0
- package/package.json +17 -1
- package/scripts/pricing-utils.mjs +5 -0
- package/scripts/scan-vscode-sessions.mjs +383 -1475
- package/scripts/scanner-customization-evidence.mjs +576 -0
- package/scripts/scanner-customization-inventory.mjs +1021 -0
- package/scripts/scanner-memory.mjs +229 -0
- package/scripts/scanner-session-parser.mjs +1237 -0
- package/scripts/scanner-traversal.mjs +203 -0
- package/scripts/scanner-workspace.mjs +209 -0
- package/dist/copilot-usage-studio/browser/chunk-CUKTUQ6W.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-EYAHOEVS.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-FXNLFSUB.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-HJNYITFH.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-KDAJN6DF.js +0 -4
- package/dist/copilot-usage-studio/browser/main-TL27ZSEQ.js +0 -1
- package/dist/copilot-usage-studio/browser/styles-GZOQ5QIH.css +0 -1
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { basename, isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
function safeJson(text) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(text);
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function timestampForEvent(event) {
|
|
13
|
+
return event?.timestamp ?? new Date(Number(event?.ts ?? 0)).toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeMatchText(value) {
|
|
17
|
+
return String(value ?? '')
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/\\r?\\n/g, '\n')
|
|
20
|
+
.replace(/\\\\/g, '/')
|
|
21
|
+
.replace(/[ \t]+/g, ' ')
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizedWordCount(value) {
|
|
27
|
+
return normalizeMatchText(value).split(/\s+/).filter((word) => /[a-z]{2,}/.test(word)).length;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isDistinctiveCustomizationChunk(chunk) {
|
|
31
|
+
const normalized = normalizeMatchText(chunk);
|
|
32
|
+
if (normalized.length < 56 || !/[a-z]/.test(normalized)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (normalizedWordCount(normalized) < 8) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (/^(applyto|description|trigger|name|title)\s*[:=]/i.test(normalized)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function customizationChunks(content) {
|
|
45
|
+
const lineChunks = String(content ?? '')
|
|
46
|
+
.split(/\r?\n/)
|
|
47
|
+
.map(normalizeMatchText)
|
|
48
|
+
.filter(isDistinctiveCustomizationChunk);
|
|
49
|
+
const normalized = normalizeMatchText(content);
|
|
50
|
+
const chunks = normalized
|
|
51
|
+
.split(/\n{2,}|(?<=\.)\s+/)
|
|
52
|
+
.map((chunk) => chunk.trim())
|
|
53
|
+
.filter(isDistinctiveCustomizationChunk)
|
|
54
|
+
.sort((a, b) => b.length - a.length);
|
|
55
|
+
|
|
56
|
+
if (isDistinctiveCustomizationChunk(normalized)) {
|
|
57
|
+
chunks.unshift(normalized.slice(0, Math.min(normalized.length, 420)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return [...new Set([...lineChunks, ...chunks])].slice(0, 24);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function escapeRegExpLiteral(value) {
|
|
64
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildCustomizationChunkMatchers(matchState) {
|
|
68
|
+
const chunkOwners = new Map();
|
|
69
|
+
for (const [customizationId, state] of matchState.entries()) {
|
|
70
|
+
for (const chunk of state.chunks) {
|
|
71
|
+
if (!chunkOwners.has(chunk)) {
|
|
72
|
+
chunkOwners.set(chunk, []);
|
|
73
|
+
}
|
|
74
|
+
chunkOwners.get(chunk).push(customizationId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const chunks = [...chunkOwners.keys()].sort((a, b) => b.length - a.length);
|
|
79
|
+
const matchers = [];
|
|
80
|
+
let patternParts = [];
|
|
81
|
+
let patternLength = 0;
|
|
82
|
+
const maxPatternLength = 150_000;
|
|
83
|
+
|
|
84
|
+
const flush = () => {
|
|
85
|
+
if (!patternParts.length) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
matchers.push({
|
|
89
|
+
regex: new RegExp(patternParts.join('|'), 'g'),
|
|
90
|
+
chunkOwners,
|
|
91
|
+
});
|
|
92
|
+
patternParts = [];
|
|
93
|
+
patternLength = 0;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
for (const chunk of chunks) {
|
|
97
|
+
const escaped = escapeRegExpLiteral(chunk);
|
|
98
|
+
if (patternParts.length && patternLength + escaped.length > maxPatternLength) {
|
|
99
|
+
flush();
|
|
100
|
+
}
|
|
101
|
+
patternParts.push(escaped);
|
|
102
|
+
patternLength += escaped.length + 1;
|
|
103
|
+
}
|
|
104
|
+
flush();
|
|
105
|
+
|
|
106
|
+
return matchers;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function matchedChunksByCustomization(text, chunkMatchers) {
|
|
110
|
+
if (!text || !chunkMatchers.length) {
|
|
111
|
+
return new Map();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const matchesByCustomization = new Map();
|
|
115
|
+
for (const matcher of chunkMatchers) {
|
|
116
|
+
matcher.regex.lastIndex = 0;
|
|
117
|
+
let match;
|
|
118
|
+
while ((match = matcher.regex.exec(text)) !== null) {
|
|
119
|
+
const chunk = match[0];
|
|
120
|
+
for (const customizationId of matcher.chunkOwners.get(chunk) ?? []) {
|
|
121
|
+
if (!matchesByCustomization.has(customizationId)) {
|
|
122
|
+
matchesByCustomization.set(customizationId, new Set());
|
|
123
|
+
}
|
|
124
|
+
matchesByCustomization.get(customizationId).add(chunk);
|
|
125
|
+
}
|
|
126
|
+
if (match[0] === '') {
|
|
127
|
+
matcher.regex.lastIndex += 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return new Map(
|
|
133
|
+
[...matchesByCustomization.entries()].map(([customizationId, chunks]) => [
|
|
134
|
+
customizationId,
|
|
135
|
+
[...chunks],
|
|
136
|
+
]),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function matchedChunkPreview(chunks) {
|
|
141
|
+
return [...new Set(chunks)]
|
|
142
|
+
.slice(0, 2)
|
|
143
|
+
.map((chunk) => chunk.length > 220 ? `${chunk.slice(0, 217)}...` : chunk);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function hasStrongContentMatch(chunks) {
|
|
147
|
+
return chunks.length > 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function extractPayloadText(value, depth = 0) {
|
|
151
|
+
if (value === null || value === undefined || depth > 8) {
|
|
152
|
+
return '';
|
|
153
|
+
}
|
|
154
|
+
if (typeof value === 'string') {
|
|
155
|
+
const trimmed = value.trim();
|
|
156
|
+
const parsed = trimmed && /^[\[{"]/.test(trimmed) ? safeJson(trimmed) : null;
|
|
157
|
+
return parsed === null || parsed === value
|
|
158
|
+
? value
|
|
159
|
+
: `${value}\n${extractPayloadText(parsed, depth + 1)}`;
|
|
160
|
+
}
|
|
161
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
162
|
+
return String(value);
|
|
163
|
+
}
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
return value.map((item) => extractPayloadText(item, depth + 1)).filter(Boolean).join('\n');
|
|
166
|
+
}
|
|
167
|
+
if (typeof value === 'object') {
|
|
168
|
+
return Object.values(value)
|
|
169
|
+
.map((item) => extractPayloadText(item, depth + 1))
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
.join('\n');
|
|
172
|
+
}
|
|
173
|
+
return '';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sessionSideFilePath(sessionDir, file) {
|
|
177
|
+
const value = String(file ?? '').trim();
|
|
178
|
+
if (!value || isAbsolute(value)) {
|
|
179
|
+
return '';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const root = `${resolve(sessionDir)}${sep}`;
|
|
183
|
+
const candidate = resolve(sessionDir, value);
|
|
184
|
+
return candidate.startsWith(root) ? candidate : '';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function readRequestSideFile(sessionDir, file, sideFileCache = new Map()) {
|
|
188
|
+
const sideFilePath = sessionSideFilePath(sessionDir, String(file ?? '').trim());
|
|
189
|
+
if (!sideFilePath || !existsSync(sideFilePath)) {
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
if (sideFileCache.has(sideFilePath)) {
|
|
193
|
+
return sideFileCache.get(sideFilePath);
|
|
194
|
+
}
|
|
195
|
+
const text = extractPayloadText(readFileSync(sideFilePath, 'utf8'));
|
|
196
|
+
sideFileCache.set(sideFilePath, text);
|
|
197
|
+
return text;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function requestTextParts(sessionDir, event, sideFileCache = new Map()) {
|
|
201
|
+
const parts = [
|
|
202
|
+
{ source: 'inputMessages', text: extractPayloadText(event.attrs?.inputMessages) },
|
|
203
|
+
{ source: 'userRequest', text: extractPayloadText(event.attrs?.userRequest) },
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
for (const fileField of ['systemPromptFile', 'toolsFile']) {
|
|
207
|
+
const file = String(event.attrs?.[fileField] ?? '').trim();
|
|
208
|
+
const text = readRequestSideFile(sessionDir, file, sideFileCache);
|
|
209
|
+
if (text) {
|
|
210
|
+
parts.push({ source: file, text });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return parts.map((part) => ({
|
|
215
|
+
...part,
|
|
216
|
+
normalized: normalizeMatchText(part.text),
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function customizationTerms(customization) {
|
|
221
|
+
return [
|
|
222
|
+
customization.sourcePath,
|
|
223
|
+
customization.relativePath,
|
|
224
|
+
basename(customization.sourcePath),
|
|
225
|
+
customization.name,
|
|
226
|
+
customization.title,
|
|
227
|
+
]
|
|
228
|
+
.map(normalizeMatchText)
|
|
229
|
+
.filter((value) => value.length >= 8);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function basenameCounts(customizations) {
|
|
233
|
+
const counts = new Map();
|
|
234
|
+
for (const customization of customizations) {
|
|
235
|
+
const name = normalizeMatchText(basename(customization.sourcePath));
|
|
236
|
+
if (!name) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
240
|
+
}
|
|
241
|
+
return counts;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readTermsForCustomization(customization, nameCounts) {
|
|
245
|
+
const sourcePath = normalizeMatchText(customization.sourcePath);
|
|
246
|
+
const relativePath = normalizeMatchText(customization.relativePath);
|
|
247
|
+
const fileName = normalizeMatchText(basename(customization.sourcePath));
|
|
248
|
+
const terms = [sourcePath, relativePath].filter((term) => term.length >= 8);
|
|
249
|
+
if (fileName.length >= 8 && nameCounts.get(fileName) === 1) {
|
|
250
|
+
terms.push(fileName);
|
|
251
|
+
}
|
|
252
|
+
return [...new Set(terms)];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function looksLikeFileReadEvidence(text) {
|
|
256
|
+
return (
|
|
257
|
+
/(?:\bread\b|read_file|reviewed|opened|summaris|summariz|loaded)/i.test(text) &&
|
|
258
|
+
/\.(?:md|markdown|json|jsonc|ya?ml)\b/i.test(text)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function evidenceStatus(rank) {
|
|
263
|
+
return ['not_seen', 'discovered', 'listed', 'sent'][rank] ?? 'not_seen';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function statusRank(status) {
|
|
267
|
+
return {
|
|
268
|
+
sent: 3,
|
|
269
|
+
listed: 2,
|
|
270
|
+
discovered: 1,
|
|
271
|
+
not_seen: 0,
|
|
272
|
+
}[status] ?? 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function mergeCustomizationRecords(existing, next) {
|
|
276
|
+
if (!existing) {
|
|
277
|
+
return next;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const status = statusRank(next.evidenceStatus) > statusRank(existing.evidenceStatus)
|
|
281
|
+
? next.evidenceStatus
|
|
282
|
+
: existing.evidenceStatus;
|
|
283
|
+
const matchMap = new Map();
|
|
284
|
+
for (const match of [...(existing.matches ?? []), ...(next.matches ?? [])]) {
|
|
285
|
+
const key = [
|
|
286
|
+
match.sessionId,
|
|
287
|
+
match.eventIndex,
|
|
288
|
+
match.modelCallNumber,
|
|
289
|
+
match.source,
|
|
290
|
+
match.status,
|
|
291
|
+
].join(':');
|
|
292
|
+
matchMap.set(key, match);
|
|
293
|
+
}
|
|
294
|
+
const matches = [...matchMap.values()]
|
|
295
|
+
.sort((a, b) => statusRank(b.status) - statusRank(a.status) || b.timestamp.localeCompare(a.timestamp))
|
|
296
|
+
.slice(0, 60);
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
...existing,
|
|
300
|
+
evidenceStatus: status,
|
|
301
|
+
matches,
|
|
302
|
+
modifiedAt: next.modifiedAt > existing.modifiedAt ? next.modifiedAt : existing.modifiedAt,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function recordCustomizationMatch(state, match) {
|
|
307
|
+
state.matches.push(match);
|
|
308
|
+
state.matches.sort(
|
|
309
|
+
(a, b) => statusRank(b.status) - statusRank(a.status) || b.timestamp.localeCompare(a.timestamp),
|
|
310
|
+
);
|
|
311
|
+
state.matches = state.matches.slice(0, 60);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function customizationEvidenceFromDebugLogs(
|
|
315
|
+
debugRoot,
|
|
316
|
+
customizations,
|
|
317
|
+
workspace = '',
|
|
318
|
+
workspaceDir = '',
|
|
319
|
+
onProgress = () => {},
|
|
320
|
+
context = {},
|
|
321
|
+
) {
|
|
322
|
+
const diagnostics = context.diagnostics ?? {
|
|
323
|
+
customizationEvidenceScannedSessions: 0,
|
|
324
|
+
customizationEvidenceModelCalls: 0,
|
|
325
|
+
customizationEvidenceTextParts: 0,
|
|
326
|
+
customizationEvidenceMatchedCustomizations: 0,
|
|
327
|
+
customizationEvidenceCapReason: '',
|
|
328
|
+
};
|
|
329
|
+
const listDirs = context.listDirs ?? (() => []);
|
|
330
|
+
const readJsonl = context.readJsonl ?? (() => []);
|
|
331
|
+
const maxSessions = optionalPositiveLimit(context.maxSessions, 40);
|
|
332
|
+
const maxModelCalls = optionalPositiveLimit(context.maxModelCalls, 300);
|
|
333
|
+
const maxElapsedMs = optionalPositiveLimit(context.maxElapsedMs, 60_000);
|
|
334
|
+
const maxPartChars = positiveInteger(context.maxPartChars, 250_000);
|
|
335
|
+
const startedAt = Date.now();
|
|
336
|
+
let cappedReason = '';
|
|
337
|
+
|
|
338
|
+
if (!customizations.length || !existsSync(debugRoot)) {
|
|
339
|
+
return customizations.map((customization) => {
|
|
340
|
+
const { _content, ...publicCustomization } = customization;
|
|
341
|
+
return { ...publicCustomization, evidenceStatus: 'not_seen', matches: [] };
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const fileNameCounts = basenameCounts(customizations);
|
|
346
|
+
const matchState = new Map(
|
|
347
|
+
customizations.map((customization) => [
|
|
348
|
+
customization.id,
|
|
349
|
+
{
|
|
350
|
+
rank: 0,
|
|
351
|
+
matches: [],
|
|
352
|
+
chunks: customizationChunks(customization._content),
|
|
353
|
+
terms: customizationTerms(customization),
|
|
354
|
+
readTerms: readTermsForCustomization(customization, fileNameCounts),
|
|
355
|
+
readSessions: new Set(),
|
|
356
|
+
},
|
|
357
|
+
]),
|
|
358
|
+
);
|
|
359
|
+
const chunkMatchers = buildCustomizationChunkMatchers(matchState);
|
|
360
|
+
|
|
361
|
+
const allSessionDirs = listDirs(debugRoot);
|
|
362
|
+
const sessionDirs = Number.isFinite(maxSessions) ? allSessionDirs.slice(0, maxSessions) : allSessionDirs;
|
|
363
|
+
if (allSessionDirs.length > sessionDirs.length) {
|
|
364
|
+
const sessionLimitReason = `limited to ${sessionDirs.length}/${allSessionDirs.length} debug-log folders`;
|
|
365
|
+
diagnostics.warnings?.push?.(
|
|
366
|
+
`Customization evidence scan for ${workspace || workspaceDir || debugRoot} ${sessionLimitReason}.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
onProgress({
|
|
371
|
+
stage: 'customization-evidence',
|
|
372
|
+
message: `Checking customization usage in recent Copilot sessions for ${workspace || 'current workspace'}.`,
|
|
373
|
+
workspace,
|
|
374
|
+
workspaceDir,
|
|
375
|
+
index: 0,
|
|
376
|
+
total: sessionDirs.length,
|
|
377
|
+
sessions: 0,
|
|
378
|
+
modelCalls: diagnostics.customizationEvidenceModelCalls,
|
|
379
|
+
matches: diagnostics.customizationEvidenceMatchedCustomizations,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
for (const [sessionIndex, sessionDir] of sessionDirs.entries()) {
|
|
383
|
+
if (Number.isFinite(maxElapsedMs) && Date.now() - startedAt > maxElapsedMs) {
|
|
384
|
+
cappedReason = `stopped after ${Math.round(maxElapsedMs / 1000)}s`;
|
|
385
|
+
diagnostics.customizationEvidenceCapReason = cappedReason;
|
|
386
|
+
diagnostics.warnings?.push?.(
|
|
387
|
+
`Customization evidence scan for ${workspace || workspaceDir || debugRoot} ${cappedReason}.`,
|
|
388
|
+
);
|
|
389
|
+
onProgress({
|
|
390
|
+
stage: 'customization-evidence',
|
|
391
|
+
message: `Customization usage check reached the configured limit for ${workspace || 'current workspace'}.`,
|
|
392
|
+
workspace,
|
|
393
|
+
workspaceDir,
|
|
394
|
+
index: sessionIndex,
|
|
395
|
+
total: allSessionDirs.length,
|
|
396
|
+
capped: true,
|
|
397
|
+
reason: cappedReason,
|
|
398
|
+
matches: diagnostics.customizationEvidenceMatchedCustomizations,
|
|
399
|
+
});
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
diagnostics.customizationEvidenceScannedSessions += 1;
|
|
404
|
+
onProgress({
|
|
405
|
+
stage: 'customization-evidence',
|
|
406
|
+
message: `Checking customization usage in recent Copilot sessions for ${workspace || 'current workspace'}.`,
|
|
407
|
+
workspace,
|
|
408
|
+
workspaceDir,
|
|
409
|
+
index: sessionIndex + 1,
|
|
410
|
+
total: sessionDirs.length,
|
|
411
|
+
sessions: diagnostics.customizationEvidenceScannedSessions,
|
|
412
|
+
modelCalls: diagnostics.customizationEvidenceModelCalls,
|
|
413
|
+
matches: diagnostics.customizationEvidenceMatchedCustomizations,
|
|
414
|
+
});
|
|
415
|
+
const sessionId = basename(sessionDir);
|
|
416
|
+
const main = readJsonl(join(sessionDir, 'main.jsonl'));
|
|
417
|
+
const sideFileCache = new Map();
|
|
418
|
+
const modelCallNumbers = new Map();
|
|
419
|
+
let modelCallNumber = 0;
|
|
420
|
+
main.forEach((event, index) => {
|
|
421
|
+
if (event.type === 'llm_request') {
|
|
422
|
+
modelCallNumber += 1;
|
|
423
|
+
modelCallNumbers.set(index, modelCallNumber);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
for (const [index, event] of main.entries()) {
|
|
428
|
+
if (Number.isFinite(maxModelCalls) && diagnostics.customizationEvidenceModelCalls >= maxModelCalls) {
|
|
429
|
+
cappedReason = `limited to ${maxModelCalls} model calls`;
|
|
430
|
+
diagnostics.customizationEvidenceCapReason = cappedReason;
|
|
431
|
+
diagnostics.warnings?.push?.(
|
|
432
|
+
`Customization evidence scan for ${workspace || workspaceDir || debugRoot} ${cappedReason}.`,
|
|
433
|
+
);
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
const eventText = normalizeMatchText(`${event.name ?? ''} ${event.attrs?.details ?? ''}`);
|
|
437
|
+
const requestParts =
|
|
438
|
+
event.type === 'llm_request' ? requestTextParts(sessionDir, event, sideFileCache) : [];
|
|
439
|
+
if (event.type === 'llm_request') {
|
|
440
|
+
diagnostics.customizationEvidenceModelCalls += 1;
|
|
441
|
+
diagnostics.customizationEvidenceTextParts += requestParts.filter((part) => part.normalized).length;
|
|
442
|
+
} else {
|
|
443
|
+
const readEvidenceText = normalizeMatchText(
|
|
444
|
+
`${event.type ?? ''} ${event.name ?? ''} ${extractPayloadText(event.attrs)}`,
|
|
445
|
+
);
|
|
446
|
+
if (looksLikeFileReadEvidence(readEvidenceText)) {
|
|
447
|
+
for (const state of matchState.values()) {
|
|
448
|
+
if (!state.readTerms.some((term) => readEvidenceText.includes(term))) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (!state.readSessions.has(sessionId)) {
|
|
452
|
+
state.readSessions.add(sessionId);
|
|
453
|
+
state.rank = Math.max(state.rank, 2);
|
|
454
|
+
recordCustomizationMatch(state, {
|
|
455
|
+
status: 'listed',
|
|
456
|
+
sessionId,
|
|
457
|
+
workspace,
|
|
458
|
+
timestamp: timestampForEvent(event),
|
|
459
|
+
eventIndex: index,
|
|
460
|
+
modelCallNumber: 0,
|
|
461
|
+
source: 'copilotFileRead',
|
|
462
|
+
matchedChunks: 0,
|
|
463
|
+
matchedCharacters: 0,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const matchedChunksForParts = new Map();
|
|
470
|
+
for (const customization of customizations) {
|
|
471
|
+
const state = matchState.get(customization.id);
|
|
472
|
+
if (!state) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (event.type === 'llm_request') {
|
|
477
|
+
if (!state.readSessions.has(sessionId)) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
for (const part of requestParts) {
|
|
481
|
+
if (part.normalized.length > maxPartChars) {
|
|
482
|
+
part.normalized = part.normalized.slice(0, maxPartChars);
|
|
483
|
+
}
|
|
484
|
+
let allPartMatches = matchedChunksForParts.get(part.source);
|
|
485
|
+
if (!allPartMatches) {
|
|
486
|
+
allPartMatches = matchedChunksByCustomization(part.normalized, chunkMatchers);
|
|
487
|
+
matchedChunksForParts.set(part.source, allPartMatches);
|
|
488
|
+
}
|
|
489
|
+
const matchedChunks = allPartMatches.get(customization.id) ?? [];
|
|
490
|
+
if (!matchedChunks.length || !hasStrongContentMatch(matchedChunks)) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const rank = 3;
|
|
495
|
+
if (rank === 3 && state.rank < 3) {
|
|
496
|
+
diagnostics.customizationEvidenceMatchedCustomizations += 1;
|
|
497
|
+
}
|
|
498
|
+
state.rank = Math.max(state.rank, rank);
|
|
499
|
+
recordCustomizationMatch(state, {
|
|
500
|
+
status: evidenceStatus(rank),
|
|
501
|
+
sessionId,
|
|
502
|
+
workspace,
|
|
503
|
+
timestamp: timestampForEvent(event),
|
|
504
|
+
eventIndex: index,
|
|
505
|
+
modelCallNumber: modelCallNumbers.get(index) ?? 0,
|
|
506
|
+
source: part.source,
|
|
507
|
+
matchedChunks: matchedChunks.length,
|
|
508
|
+
matchedCharacters: matchedChunks.reduce((sum, chunk) => sum + chunk.length, 0),
|
|
509
|
+
matchedPreview: matchedChunkPreview(matchedChunks),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (state.rank < 2 && state.terms.some((term) => eventText.includes(term))) {
|
|
516
|
+
state.rank = Math.max(state.rank, 1);
|
|
517
|
+
recordCustomizationMatch(state, {
|
|
518
|
+
status: 'discovered',
|
|
519
|
+
sessionId,
|
|
520
|
+
workspace,
|
|
521
|
+
timestamp: timestampForEvent(event),
|
|
522
|
+
eventIndex: index,
|
|
523
|
+
modelCallNumber: 0,
|
|
524
|
+
source: String(event.name ?? event.type ?? 'event'),
|
|
525
|
+
matchedChunks: 0,
|
|
526
|
+
matchedCharacters: 0,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (cappedReason) {
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (cappedReason) {
|
|
535
|
+
onProgress({
|
|
536
|
+
stage: 'customization-evidence',
|
|
537
|
+
message: `Customization usage check reached the configured limit for ${workspace || 'current workspace'}.`,
|
|
538
|
+
workspace,
|
|
539
|
+
workspaceDir,
|
|
540
|
+
index: sessionIndex + 1,
|
|
541
|
+
total: allSessionDirs.length,
|
|
542
|
+
capped: true,
|
|
543
|
+
reason: cappedReason,
|
|
544
|
+
sessions: diagnostics.customizationEvidenceScannedSessions,
|
|
545
|
+
modelCalls: diagnostics.customizationEvidenceModelCalls,
|
|
546
|
+
matches: diagnostics.customizationEvidenceMatchedCustomizations,
|
|
547
|
+
});
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return customizations.map((customization) => {
|
|
553
|
+
const state = matchState.get(customization.id);
|
|
554
|
+
const matches = (state?.matches ?? []).sort(
|
|
555
|
+
(a, b) => statusRank(b.status) - statusRank(a.status) || b.timestamp.localeCompare(a.timestamp),
|
|
556
|
+
);
|
|
557
|
+
const { _content, ...publicCustomization } = customization;
|
|
558
|
+
return {
|
|
559
|
+
...publicCustomization,
|
|
560
|
+
evidenceStatus: evidenceStatus(state?.rank ?? 0),
|
|
561
|
+
matches,
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function positiveInteger(value, fallback) {
|
|
567
|
+
const number = Number(value);
|
|
568
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function optionalPositiveLimit(value, fallback) {
|
|
572
|
+
if (value === 0 || value === '0' || value === false || value === null) {
|
|
573
|
+
return Number.POSITIVE_INFINITY;
|
|
574
|
+
}
|
|
575
|
+
return positiveInteger(value, fallback);
|
|
576
|
+
}
|