copilot-usage-studio 0.2.1 → 0.2.3
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 +26 -1
- package/README.md +15 -11
- package/dist/copilot-usage-studio/browser/{chunk-2SAR2Q6M.js → chunk-23NLHIOR.js} +1 -1
- package/dist/copilot-usage-studio/browser/{chunk-EWN3YOHT.js → chunk-7I2HDBC6.js} +1 -1
- package/dist/copilot-usage-studio/browser/{chunk-XHID4XVE.js → chunk-QMNOUQ2X.js} +1 -1
- package/dist/copilot-usage-studio/browser/{chunk-KS5W2HMH.js → chunk-RTOXDOOI.js} +1 -1
- package/dist/copilot-usage-studio/browser/{chunk-VEKXIM47.js → chunk-TICIZRSM.js} +1 -1
- package/dist/copilot-usage-studio/browser/{chunk-U4H425LY.js → chunk-WWU6QMJC.js} +1 -1
- package/dist/copilot-usage-studio/browser/chunk-XMVGLQQH.js +1 -0
- package/dist/copilot-usage-studio/browser/index.html +1 -1
- package/dist/copilot-usage-studio/browser/main-XFPP45VE.js +1 -0
- package/docs/local-deployment.md +32 -13
- package/docs/pricing.md +2 -0
- package/docs/scanner-api.md +12 -0
- package/lib/local-runtime.mjs +54 -10
- package/package.json +4 -2
- package/scripts/scan-vscode-sessions.mjs +4 -0
- package/scripts/scanner-customization-evidence.mjs +50 -13
- package/scripts/scanner-customization-inventory.mjs +1 -0
- package/scripts/scanner-workspace.mjs +38 -10
- package/dist/copilot-usage-studio/browser/chunk-7FKLWMFH.js +0 -1
- package/dist/copilot-usage-studio/browser/main-OZIMQRXC.js +0 -1
package/docs/local-deployment.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The app is intended to run locally, near the VS Code data it reads. It should not require a hosted SaaS backend for the core workflow.
|
|
4
4
|
|
|
5
|
-
Current release posture:
|
|
5
|
+
Current release posture: the VS Code Marketplace extension is the product path. The npm package remains a development and fallback host.
|
|
6
6
|
|
|
7
7
|
Packaging foundation now available: the scanner exposes an in-memory Node API through `lib/scanner-api.mjs`. The existing scan command, local runtime, npm host, and VS Code extension preview use the same scanner/runtime core so behavior stays aligned across hosts.
|
|
8
8
|
|
|
@@ -94,10 +94,10 @@ GitHub Actions is the release control plane:
|
|
|
94
94
|
|
|
95
95
|
- `.github/workflows/ci.yml` runs the full release gate and packages a VSIX artifact for every pushed branch, plus pull requests to `main`.
|
|
96
96
|
- `.github/workflows/release.yml` runs automatically for version tags such as `v0.1.1`, with a manual existing-tag mode for repair and backfill.
|
|
97
|
-
- The release workflow verifies that the tag matches
|
|
98
|
-
-
|
|
97
|
+
- The release workflow verifies that the tag matches both package versions, runs the release gate, packages one VSIX, publishes that artifact to the VS Code Marketplace, and attaches the exact VSIX to the matching GitHub Release.
|
|
98
|
+
- Marketplace publication uses GitHub OIDC and Microsoft Entra workload identity federation. No Marketplace token or client secret is stored in GitHub.
|
|
99
99
|
- A failed workflow can be rerun safely: an existing npm version is accepted only when its published `gitHead` matches the exact tagged commit. Conflicting or unverifiable versions are refused.
|
|
100
|
-
-
|
|
100
|
+
- Every release and repair run passes the current release gate before publication.
|
|
101
101
|
|
|
102
102
|
This keeps GitHub, release artifacts, release notes, and the source tag tied to the same commit. Ordinary pushes never publish.
|
|
103
103
|
|
|
@@ -120,10 +120,28 @@ Commit and push the workflow files first, then configure npm Trusted Publishing
|
|
|
120
120
|
3. Set repository to `copilot-usage-studio`.
|
|
121
121
|
4. Set workflow filename to `release.yml`.
|
|
122
122
|
5. Allow the `npm publish` action.
|
|
123
|
-
6.
|
|
123
|
+
6. Set the trusted-publisher environment to `marketplace`, matching the protected environment used by the release job.
|
|
124
124
|
|
|
125
125
|
The workflow uses GitHub's OIDC identity, so no long-lived `NPM_TOKEN` repository secret is required. npm automatically adds provenance for a public package published from a public repository through Trusted Publishing.
|
|
126
126
|
|
|
127
|
+
### One-Time VS Code Marketplace Setup
|
|
128
|
+
|
|
129
|
+
1. In Microsoft Entra ID, create an app registration for Marketplace publishing. Record its application (client) ID and directory (tenant) ID. Do not create a client secret.
|
|
130
|
+
2. Add a federated credential to that app registration using the GitHub Actions scenario:
|
|
131
|
+
- organization: `fortunes-guardian`
|
|
132
|
+
- repository: `copilot-usage-studio`
|
|
133
|
+
- entity type: **Environment**
|
|
134
|
+
- environment: `marketplace`
|
|
135
|
+
3. In GitHub, create an environment named `marketplace`. Restrict deployment to protected version tags when practical.
|
|
136
|
+
4. Add environment variables `AZURE_CLIENT_ID` and `AZURE_TENANT_ID`. These identifiers are not credentials; GitHub's short-lived OIDC assertion is the credential.
|
|
137
|
+
5. After this workflow is merged to `main`, run **Marketplace Identity Check** from GitHub Actions.
|
|
138
|
+
6. Copy the profile ID from the workflow summary. In the Visual Studio Marketplace publisher management page, add that identity ID as a member of publisher `fortunes-guardian` with the **Contributor** role.
|
|
139
|
+
7. Rerun **Marketplace Identity Check** if publisher membership or federation changes.
|
|
140
|
+
|
|
141
|
+
The release job uses `azure/login` with GitHub OIDC, then publishes with `vsce --azure-credential`. No PAT, client secret, or certificate is created. The workflow publishes the already packaged `tmp/*.vsix`, and that exact file is uploaded to the GitHub Release.
|
|
142
|
+
|
|
143
|
+
References: [VS Code secure automated publishing](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#secure-automated-publishing-to-visual-studio-marketplace) and [GitHub OIDC with Azure](https://docs.github.com/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-azure).
|
|
144
|
+
|
|
127
145
|
### Publishing a Version
|
|
128
146
|
|
|
129
147
|
Start from an up-to-date, clean `main` branch. Choose the semantic-version bump that matches the change:
|
|
@@ -138,20 +156,21 @@ npm version patch
|
|
|
138
156
|
git push origin main --follow-tags
|
|
139
157
|
```
|
|
140
158
|
|
|
141
|
-
`npm version patch` updates
|
|
159
|
+
`npm version patch` updates `package.json` and `package-lock.json`; the repository's `version` lifecycle hook synchronizes `vscode-extension/package.json` before npm creates the version commit and annotated `vX.Y.Z` tag. Use `npm version minor` for a backward-compatible feature release or `npm version major` for a breaking release.
|
|
142
160
|
|
|
143
161
|
After the tag is pushed:
|
|
144
162
|
|
|
145
163
|
1. Watch the **Release** workflow in GitHub Actions.
|
|
146
|
-
2. Confirm the new version appears
|
|
147
|
-
3. Confirm
|
|
148
|
-
4. Confirm
|
|
149
|
-
5.
|
|
150
|
-
6.
|
|
164
|
+
2. Confirm the new version appears in the VS Code Marketplace.
|
|
165
|
+
3. Confirm the new version appears on npm.
|
|
166
|
+
4. Confirm GitHub created a Release for the same tag and attached the same-version VSIX.
|
|
167
|
+
5. Confirm the release notes read well and include the curated highlights.
|
|
168
|
+
6. Install or update from the Marketplace and run one final extension smoke test.
|
|
169
|
+
7. Run `npx copilot-usage-studio@X.Y.Z --version` from a clean directory as a fallback-host smoke test.
|
|
151
170
|
|
|
152
171
|
If CI or release validation fails, fix the issue on `main` and publish a new version. Do not move or reuse a public release tag, and do not overwrite an npm version; both are immutable release coordinates.
|
|
153
172
|
|
|
154
|
-
The workflow also has a manual **Run workflow** action with a required existing tag. Use
|
|
173
|
+
The workflow also has a manual **Run workflow** action with a required existing tag. Use it to repair a release from that immutable tag. Duplicate npm and Marketplace publication is skipped safely, while the release gate reruns and the GitHub Release notes/artifact are repaired.
|
|
155
174
|
|
|
156
175
|
### Manual Emergency Fallback
|
|
157
176
|
|
|
@@ -286,7 +305,7 @@ npm run vscode:package
|
|
|
286
305
|
Install it locally:
|
|
287
306
|
|
|
288
307
|
```bash
|
|
289
|
-
code --install-extension tmp/copilot-usage-studio-vscode-
|
|
308
|
+
code --install-extension tmp/copilot-usage-studio-vscode-X.Y.Z.vsix --force
|
|
290
309
|
```
|
|
291
310
|
|
|
292
311
|
The extension exposes the full app inside VS Code: Usage, Sessions, Memory, Customizations preview, Compare, Insights, and Prices. The npm/browser host remains useful for development and fallback testing, but the extension is the primary product surface.
|
package/docs/pricing.md
CHANGED
|
@@ -169,6 +169,8 @@ The user should be able to inspect the cost inputs directly. If a session looks
|
|
|
169
169
|
- cached input or cache-write accounting
|
|
170
170
|
- a model fallback because the raw model id was not in the known GitHub table
|
|
171
171
|
|
|
172
|
+
Some VS Code logs can contain internal raw model ids that are not published as GitHub pricing rows. For example, a raw id such as `oswe-vscode-prime` stays visible in the UI and is estimated with the configured fallback pricing row. The app should not silently rename that id unless a reliable pricing alias is added.
|
|
173
|
+
|
|
172
174
|
The `GitHub prices` view therefore shows every rate row the app knows about, where it came from, and whether any imported session currently uses it.
|
|
173
175
|
|
|
174
176
|
The same view also shows the selected Business/Enterprise AI-credit allowance and how the current imported sessions compare with it.
|
package/docs/scanner-api.md
CHANGED
|
@@ -81,6 +81,18 @@ Next decomposition targets:
|
|
|
81
81
|
- pricing/token normalization
|
|
82
82
|
- SQLite/state enrichment helpers
|
|
83
83
|
|
|
84
|
+
## Incremental scans
|
|
85
|
+
|
|
86
|
+
The runtime passes the previous snapshot's `generatedAt` value as `incrementalSince` during a normal quick refresh. The scanner still discovers known VS Code storage entries, but it parses only debug-session `main.jsonl` files and chat snapshots modified after that cursor. The runtime merges changed sessions by stable session ID and retains unchanged sessions.
|
|
87
|
+
|
|
88
|
+
This provides two explicit modes: quick/incremental for normal startup, visibility refresh, and `Refresh Data`; full for recovery/debugging through `Copilot Usage Studio: Full Rescan`.
|
|
89
|
+
|
|
90
|
+
Incremental scans intentionally do not infer deletions. A full rescan rebuilds the snapshot and is the recovery path for deleted or externally rewritten sources. Memory roots remain indexed during quick scans because memory files are small and can be independently added or removed.
|
|
91
|
+
|
|
92
|
+
Customization analysis has its own cache boundary. Inventory records include a SHA-256 content fingerprint. If all current fingerprints match the previous analysis, only changed session logs are checked and prior evidence is retained. Any customization content change invalidates that shortcut.
|
|
93
|
+
|
|
94
|
+
The current delta cursor is timestamp-based because VS Code does not publish a stable scanner API or change journal for Agent Debug Logs. The full-rescan command exists specifically to recover from timestamp anomalies or external file restoration.
|
|
95
|
+
|
|
84
96
|
## Local Runtime Host
|
|
85
97
|
|
|
86
98
|
`lib/local-runtime.mjs` is the first shared API host. It keeps the last valid `SessionData` snapshot in memory, serves it while scans run, and persists successful refreshes.
|
package/lib/local-runtime.mjs
CHANGED
|
@@ -62,6 +62,7 @@ export function createLocalRuntime(options = {}) {
|
|
|
62
62
|
let activeScanController = null;
|
|
63
63
|
let activeScanId = 0;
|
|
64
64
|
let activeScanMode = '';
|
|
65
|
+
let lastScanMode = '';
|
|
65
66
|
let scanSequence = 0;
|
|
66
67
|
|
|
67
68
|
function status() {
|
|
@@ -78,6 +79,7 @@ export function createLocalRuntime(options = {}) {
|
|
|
78
79
|
lastError,
|
|
79
80
|
activeScanId,
|
|
80
81
|
activeScanMode,
|
|
82
|
+
lastScanMode,
|
|
81
83
|
scanProgress,
|
|
82
84
|
progressHistory: progressHistory.slice(-40),
|
|
83
85
|
workspaceProgress: [...workspaceProgress.values()],
|
|
@@ -107,7 +109,7 @@ export function createLocalRuntime(options = {}) {
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
const scanMode = normalizeScanMode(refreshOptions.mode ?? 'quick');
|
|
110
|
-
const effectiveScanOptions = scanOptionsForMode(scanMode, scanOptions);
|
|
112
|
+
const effectiveScanOptions = scanOptionsForMode(scanMode, scanOptions, sessionData);
|
|
111
113
|
const started = Date.now();
|
|
112
114
|
const scanId = ++scanSequence;
|
|
113
115
|
activeScanId = scanId;
|
|
@@ -138,28 +140,28 @@ export function createLocalRuntime(options = {}) {
|
|
|
138
140
|
}),
|
|
139
141
|
)
|
|
140
142
|
.then((nextSessionData) => {
|
|
143
|
+
const changedSessionCount = nextSessionData.sessions?.length ?? 0;
|
|
141
144
|
const finalSessionData = sessionData
|
|
142
145
|
? scanMode === 'customizations'
|
|
143
146
|
? mergeCustomizationScan(sessionData, nextSessionData)
|
|
144
|
-
:
|
|
147
|
+
: scanMode === 'quick'
|
|
148
|
+
? mergeIncrementalScan(sessionData, nextSessionData)
|
|
149
|
+
: preserveCustomizationSnapshot(sessionData, nextSessionData)
|
|
145
150
|
: nextSessionData;
|
|
146
151
|
writer(finalSessionData, dataFile);
|
|
147
152
|
sessionData = finalSessionData;
|
|
148
153
|
phase = 'ready';
|
|
149
154
|
lastScanCompletedAt = new Date().toISOString();
|
|
150
155
|
lastScanDurationMs = Date.now() - started;
|
|
156
|
+
lastScanMode = scanMode;
|
|
151
157
|
scanProgress = rememberProgress({
|
|
152
158
|
stage: 'complete',
|
|
153
|
-
message: scanMode
|
|
154
|
-
? `Customization usage check complete.`
|
|
155
|
-
: `Scan #${scanId} imported ${finalSessionData.sessions.length} sessions.`,
|
|
159
|
+
message: scanResultMessage(scanMode, changedSessionCount, finalSessionData.sessions.length),
|
|
156
160
|
sessions: finalSessionData.sessions.length,
|
|
157
161
|
}, scanId, reason, lastScanCompletedAt);
|
|
158
162
|
emitLog(
|
|
159
163
|
'log',
|
|
160
|
-
scanMode
|
|
161
|
-
? `Customization usage check #${scanId} (${reason}) completed in ${lastScanDurationMs}ms.`
|
|
162
|
-
: `Local data refresh #${scanId} (${reason}) imported ${finalSessionData.sessions.length} sessions in ${lastScanDurationMs}ms.`,
|
|
164
|
+
`${scanResultMessage(scanMode, changedSessionCount, finalSessionData.sessions.length)} (${lastScanDurationMs}ms).`,
|
|
163
165
|
);
|
|
164
166
|
return finalSessionData;
|
|
165
167
|
})
|
|
@@ -398,12 +400,20 @@ function summarizeScanOptions(scanOptions = {}) {
|
|
|
398
400
|
};
|
|
399
401
|
}
|
|
400
402
|
|
|
403
|
+
function scanResultMessage(mode, changedSessions, totalSessions) {
|
|
404
|
+
if (mode === 'customizations') return 'Customization analysis complete';
|
|
405
|
+
if (mode === 'full') return `Full rescan complete: ${totalSessions} sessions imported`;
|
|
406
|
+
return changedSessions
|
|
407
|
+
? `${changedSessions} session${changedSessions === 1 ? '' : 's'} added or updated`
|
|
408
|
+
: 'Copilot data is up to date';
|
|
409
|
+
}
|
|
410
|
+
|
|
401
411
|
function normalizeScanMode(mode) {
|
|
402
412
|
return ['quick', 'full', 'customizations'].includes(String(mode)) ? String(mode) : 'quick';
|
|
403
413
|
}
|
|
404
414
|
|
|
405
|
-
function scanOptionsForMode(mode, baseScanOptions = {}) {
|
|
406
|
-
if (mode === '
|
|
415
|
+
function scanOptionsForMode(mode, baseScanOptions = {}, previousData = null) {
|
|
416
|
+
if (mode === 'customizations') {
|
|
407
417
|
const { customizationWorkspaceFolders, ...scanOptions } = baseScanOptions;
|
|
408
418
|
if (mode === 'customizations' && Array.isArray(customizationWorkspaceFolders)) {
|
|
409
419
|
return {
|
|
@@ -411,11 +421,14 @@ function scanOptionsForMode(mode, baseScanOptions = {}) {
|
|
|
411
421
|
includeCustomizations: true,
|
|
412
422
|
requireWorkspaceFolders: true,
|
|
413
423
|
workspaceFolders: customizationWorkspaceFolders,
|
|
424
|
+
incrementalSince: previousData?.ingestion?.customizationEvidenceAnalyzedAt ?? '',
|
|
414
425
|
customizationEvidence: {
|
|
415
426
|
maxSessions: 0,
|
|
416
427
|
maxModelCalls: 0,
|
|
417
428
|
maxElapsedMs: 0,
|
|
418
429
|
maxPartChars: 250_000,
|
|
430
|
+
previousEvidence: previousData?.customizations ?? [],
|
|
431
|
+
incrementalSince: previousData?.ingestion?.customizationEvidenceAnalyzedAt ?? '',
|
|
419
432
|
...(scanOptions.customizationEvidence ?? {}),
|
|
420
433
|
},
|
|
421
434
|
};
|
|
@@ -434,12 +447,40 @@ function scanOptionsForMode(mode, baseScanOptions = {}) {
|
|
|
434
447
|
};
|
|
435
448
|
}
|
|
436
449
|
|
|
450
|
+
if (mode === 'full') {
|
|
451
|
+
return {
|
|
452
|
+
...baseScanOptions,
|
|
453
|
+
includeCustomizations: false,
|
|
454
|
+
incrementalSince: '',
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
437
458
|
return {
|
|
438
459
|
...baseScanOptions,
|
|
439
460
|
includeCustomizations: false,
|
|
461
|
+
incrementalSince: previousData?.generatedAt ?? '',
|
|
440
462
|
};
|
|
441
463
|
}
|
|
442
464
|
|
|
465
|
+
function mergeIncrementalScan(previousData, deltaData) {
|
|
466
|
+
const changedSessions = deltaData.sessions ?? [];
|
|
467
|
+
const changedIds = new Set(changedSessions.map((session) => session.id));
|
|
468
|
+
const sessions = [
|
|
469
|
+
...changedSessions,
|
|
470
|
+
...(previousData.sessions ?? []).filter((session) => !changedIds.has(session.id)),
|
|
471
|
+
]
|
|
472
|
+
.sort((a, b) => String(b.startedAt ?? '').localeCompare(String(a.startedAt ?? '')));
|
|
473
|
+
return preserveCustomizationSnapshot(previousData, {
|
|
474
|
+
...deltaData,
|
|
475
|
+
ingestion: {
|
|
476
|
+
...deltaData.ingestion,
|
|
477
|
+
importedSessions: sessions.length,
|
|
478
|
+
incrementalSessionsImported: changedSessions.length,
|
|
479
|
+
},
|
|
480
|
+
sessions,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
443
484
|
function mergeCustomizationScan(previousData, currentWorkspaceData) {
|
|
444
485
|
const scannedWorkspaces = new Set(
|
|
445
486
|
(currentWorkspaceData.ingestion?.workspaceScans ?? [])
|
|
@@ -502,6 +543,9 @@ function preserveCustomizationSnapshot(previousData, nextData) {
|
|
|
502
543
|
customizationEvidenceScannedSessions:
|
|
503
544
|
nextData.ingestion?.customizationEvidenceScannedSessions ??
|
|
504
545
|
previousData.ingestion?.customizationEvidenceScannedSessions,
|
|
546
|
+
customizationEvidenceAnalyzedAt:
|
|
547
|
+
nextData.ingestion?.customizationEvidenceAnalyzedAt ||
|
|
548
|
+
previousData.ingestion?.customizationEvidenceAnalyzedAt,
|
|
505
549
|
customizationEvidenceModelCalls:
|
|
506
550
|
nextData.ingestion?.customizationEvidenceModelCalls ??
|
|
507
551
|
previousData.ingestion?.customizationEvidenceModelCalls,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-usage-studio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Local VS Code GitHub Copilot usage, cost, and session insights.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,13 +46,15 @@
|
|
|
46
46
|
"schema:accept": "node scripts/audit-vscode-schema.mjs --accept",
|
|
47
47
|
"vscode:typecheck": "tsc -p vscode-extension/tsconfig.json --noEmit",
|
|
48
48
|
"vscode:compile": "npm run vscode:typecheck && node scripts/build-vscode-extension.mjs --extension-only",
|
|
49
|
+
"verify:versions": "node scripts/verify-release-versions.mjs",
|
|
49
50
|
"vscode:build": "npm run build && npm run vscode:typecheck && node scripts/build-vscode-extension.mjs",
|
|
50
51
|
"vscode:verify": "npm run vscode:build && node scripts/verify-vscode-extension.mjs",
|
|
51
52
|
"vscode:vsix:dry-run": "npm run vscode:verify && cd vscode-extension && vsce ls --tree",
|
|
52
53
|
"vscode:package": "npm run vscode:verify && node scripts/package-vscode-extension.mjs",
|
|
53
54
|
"verify:package": "node scripts/verify-package.mjs",
|
|
54
55
|
"release:notes": "node scripts/generate-release-notes.mjs",
|
|
55
|
-
"
|
|
56
|
+
"version": "node scripts/sync-release-version.mjs",
|
|
57
|
+
"release:check": "npm run verify:versions && npm run test:scripts && npm test -- --watch=false && npm run build && npm run verify:package",
|
|
56
58
|
"prepack": "npm run build"
|
|
57
59
|
},
|
|
58
60
|
"exports": {
|
|
@@ -627,6 +627,7 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
627
627
|
includeSystemCustomizations: options.includeSystemCustomizations === true,
|
|
628
628
|
customizationDiscovery: options.customizationDiscovery ?? null,
|
|
629
629
|
customizationEvidence: options.customizationEvidence ?? {},
|
|
630
|
+
incrementalSince: options.incrementalSince ?? '',
|
|
630
631
|
};
|
|
631
632
|
|
|
632
633
|
try {
|
|
@@ -748,6 +749,9 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
748
749
|
ingestion: {
|
|
749
750
|
...diagnostics,
|
|
750
751
|
importedSessions: sessions.length,
|
|
752
|
+
scanKind: options.incrementalSince ? 'incremental' : 'full',
|
|
753
|
+
incrementalSince: options.incrementalSince ?? '',
|
|
754
|
+
customizationEvidenceAnalyzedAt: options.includeCustomizations !== false ? generatedAt : '',
|
|
751
755
|
cacheTokenAudit: mergeCacheTokenAudits(
|
|
752
756
|
sessions.map((session) => session.cacheTokenAudit).filter(Boolean),
|
|
753
757
|
),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename, isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
3
|
|
|
4
4
|
function safeJson(text) {
|
|
@@ -144,7 +144,13 @@ function matchedChunkPreview(chunks) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
function hasStrongContentMatch(chunks) {
|
|
147
|
-
|
|
147
|
+
const unique = [...new Set(chunks.map(normalizeMatchText).filter(Boolean))];
|
|
148
|
+
const totalCharacters = unique.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
149
|
+
const totalWords = unique.reduce((sum, chunk) => sum + normalizedWordCount(chunk), 0);
|
|
150
|
+
return (
|
|
151
|
+
unique.some((chunk) => chunk.length >= 120 && normalizedWordCount(chunk) >= 16) ||
|
|
152
|
+
(unique.length >= 2 && totalCharacters >= 140 && totalWords >= 20)
|
|
153
|
+
);
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
function extractPayloadText(value, depth = 0) {
|
|
@@ -311,6 +317,30 @@ function recordCustomizationMatch(state, match) {
|
|
|
311
317
|
state.matches = state.matches.slice(0, 60);
|
|
312
318
|
}
|
|
313
319
|
|
|
320
|
+
function initialMatchState(customization, fileNameCounts, previous) {
|
|
321
|
+
const matches = previous?.matches ?? [];
|
|
322
|
+
return {
|
|
323
|
+
rank: statusRank(previous?.evidenceStatus),
|
|
324
|
+
matches: [...matches],
|
|
325
|
+
chunks: customizationChunks(customization._content),
|
|
326
|
+
terms: customizationTerms(customization),
|
|
327
|
+
readTerms: readTermsForCustomization(customization, fileNameCounts),
|
|
328
|
+
readSessions: new Set(
|
|
329
|
+
matches.filter((match) => statusRank(match.status) >= 2).map((match) => match.sessionId),
|
|
330
|
+
),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function sourceChangedSince(path, since) {
|
|
335
|
+
const sinceMs = Date.parse(String(since ?? ''));
|
|
336
|
+
if (!Number.isFinite(sinceMs)) return true;
|
|
337
|
+
try {
|
|
338
|
+
return statSync(path).mtimeMs > sinceMs;
|
|
339
|
+
} catch {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
314
344
|
export function customizationEvidenceFromDebugLogs(
|
|
315
345
|
debugRoot,
|
|
316
346
|
customizations,
|
|
@@ -343,25 +373,32 @@ export function customizationEvidenceFromDebugLogs(
|
|
|
343
373
|
}
|
|
344
374
|
|
|
345
375
|
const fileNameCounts = basenameCounts(customizations);
|
|
376
|
+
const previousById = new Map((context.previousEvidence ?? []).map((item) => [item.id, item]));
|
|
377
|
+
const canIncrement = Boolean(context.incrementalSince) && customizations.every((customization) =>
|
|
378
|
+
previousById.get(customization.id)?.contentHash === customization.contentHash);
|
|
379
|
+
|
|
346
380
|
const matchState = new Map(
|
|
347
381
|
customizations.map((customization) => [
|
|
348
382
|
customization.id,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
readTerms: readTermsForCustomization(customization, fileNameCounts),
|
|
355
|
-
readSessions: new Set(),
|
|
356
|
-
},
|
|
383
|
+
initialMatchState(
|
|
384
|
+
customization,
|
|
385
|
+
fileNameCounts,
|
|
386
|
+
canIncrement ? previousById.get(customization.id) : null,
|
|
387
|
+
),
|
|
357
388
|
]),
|
|
358
389
|
);
|
|
359
390
|
const chunkMatchers = buildCustomizationChunkMatchers(matchState);
|
|
360
391
|
|
|
361
392
|
const allSessionDirs = listDirs(debugRoot);
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
393
|
+
const candidateSessionDirs = canIncrement
|
|
394
|
+
? allSessionDirs.filter((sessionDir) =>
|
|
395
|
+
sourceChangedSince(join(sessionDir, 'main.jsonl'), context.incrementalSince))
|
|
396
|
+
: allSessionDirs;
|
|
397
|
+
const sessionDirs = Number.isFinite(maxSessions)
|
|
398
|
+
? candidateSessionDirs.slice(0, maxSessions)
|
|
399
|
+
: candidateSessionDirs;
|
|
400
|
+
if (candidateSessionDirs.length > sessionDirs.length) {
|
|
401
|
+
const sessionLimitReason = `limited to ${sessionDirs.length}/${candidateSessionDirs.length} changed debug-log folders`;
|
|
365
402
|
diagnostics.warnings?.push?.(
|
|
366
403
|
`Customization evidence scan for ${workspace || workspaceDir || debugRoot} ${sessionLimitReason}.`,
|
|
367
404
|
);
|
|
@@ -343,6 +343,7 @@ function parseSimpleFrontmatter(content) {
|
|
|
343
343
|
sourcePath: resolve(file),
|
|
344
344
|
relativePath,
|
|
345
345
|
createdAt: stats.birthtimeMs > 0 ? stats.birthtime.toISOString() : '',
|
|
346
|
+
contentHash: createHash('sha256').update(content).digest('hex'),
|
|
346
347
|
modifiedAt: stats.mtime.toISOString(),
|
|
347
348
|
sizeBytes: stats.size,
|
|
348
349
|
characterCount: content.length,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
export function parseWorkspace(workspaceDir, options = {}, onProgress = () => {}, dependencies = {}) {
|
|
5
5
|
const {
|
|
@@ -64,15 +64,25 @@ export function parseWorkspace(workspaceDir, options = {}, onProgress = () => {}
|
|
|
64
64
|
});
|
|
65
65
|
};
|
|
66
66
|
const debugRoot = join(workspaceDir, 'GitHub.copilot-chat', 'debug-logs');
|
|
67
|
-
const
|
|
68
|
-
const
|
|
67
|
+
const allDebugSessionDirs = listDirs(debugRoot);
|
|
68
|
+
const allChatSessionFiles = listFiles(join(workspaceDir, 'chatSessions'), '.jsonl');
|
|
69
|
+
const incrementalSinceMs = timestampMs(options.incrementalSince);
|
|
70
|
+
const debugSessionDirs = incrementalSinceMs
|
|
71
|
+
? allDebugSessionDirs.filter((sessionDir) =>
|
|
72
|
+
sourceChangedSince(join(sessionDir, 'main.jsonl'), incrementalSinceMs))
|
|
73
|
+
: allDebugSessionDirs;
|
|
74
|
+
const chatSessionFiles = incrementalSinceMs
|
|
75
|
+
? allChatSessionFiles.filter((file) => sourceChangedSince(file, incrementalSinceMs))
|
|
76
|
+
: allChatSessionFiles;
|
|
69
77
|
const memoryRoot = join(workspaceDir, 'GitHub.copilot-chat', 'memory-tool', 'memories');
|
|
70
78
|
const hasMemoryRoot = existsSync(memoryRoot);
|
|
71
|
-
workspaceScan.debugLogFolders =
|
|
72
|
-
workspaceScan.chatSnapshots =
|
|
79
|
+
workspaceScan.debugLogFolders = allDebugSessionDirs.length;
|
|
80
|
+
workspaceScan.chatSnapshots = allChatSessionFiles.length;
|
|
81
|
+
workspaceScan.changedDebugLogFolders = debugSessionDirs.length;
|
|
82
|
+
workspaceScan.changedChatSnapshots = chatSessionFiles.length;
|
|
73
83
|
workspaceScan.hasMemoryRoot = hasMemoryRoot;
|
|
74
84
|
|
|
75
|
-
if (!
|
|
85
|
+
if (!allDebugSessionDirs.length && !allChatSessionFiles.length && !hasMemoryRoot) {
|
|
76
86
|
workspaceScan.completedAt = new Date().toISOString();
|
|
77
87
|
workspaceScan.durationMs = Date.now() - workspaceStartedAt;
|
|
78
88
|
workspaceScan.lastStage = 'empty';
|
|
@@ -84,8 +94,10 @@ export function parseWorkspace(workspaceDir, options = {}, onProgress = () => {}
|
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
progress('workspace', `Checking VS Code storage entry for ${workspace}.`, {
|
|
87
|
-
debugLogFolders:
|
|
88
|
-
chatSnapshots:
|
|
97
|
+
debugLogFolders: allDebugSessionDirs.length,
|
|
98
|
+
chatSnapshots: allChatSessionFiles.length,
|
|
99
|
+
changedDebugLogFolders: debugSessionDirs.length,
|
|
100
|
+
changedChatSnapshots: chatSessionFiles.length,
|
|
89
101
|
hasMemoryRoot,
|
|
90
102
|
});
|
|
91
103
|
progress('workspace-state', `Reading workspace metadata for ${workspace}.`);
|
|
@@ -146,7 +158,7 @@ export function parseWorkspace(workspaceDir, options = {}, onProgress = () => {}
|
|
|
146
158
|
debugSessions.push(enrichSessionFromWorkspaceState(session, stateBySessionId));
|
|
147
159
|
}
|
|
148
160
|
}
|
|
149
|
-
const debugIds = new Set(
|
|
161
|
+
const debugIds = new Set(allDebugSessionDirs.map((sessionDir) => basename(sessionDir)));
|
|
150
162
|
diagnostics.importedDebugLogSessions += debugSessions.length;
|
|
151
163
|
|
|
152
164
|
if (chatSessionFiles.length) {
|
|
@@ -186,6 +198,22 @@ export function parseWorkspace(workspaceDir, options = {}, onProgress = () => {}
|
|
|
186
198
|
};
|
|
187
199
|
}
|
|
188
200
|
|
|
201
|
+
|
|
202
|
+
function timestampMs(value) {
|
|
203
|
+
if (!value) {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
const parsed = Date.parse(String(value));
|
|
207
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function sourceChangedSince(path, sinceMs) {
|
|
211
|
+
try {
|
|
212
|
+
return statSync(path).mtimeMs > sinceMs;
|
|
213
|
+
} catch {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
189
217
|
function assertWorkspaceDependencies(dependencies) {
|
|
190
218
|
for (const name of [
|
|
191
219
|
'customizationsFromDebugReferences',
|