omp-plugin-duplicate-detector 0.2.0 → 0.2.2
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/dist/detector-worker.js +94 -9
- package/package.json +1 -1
- package/src/detector-worker.ts +95 -6
- package/src/disk-cache.ts +19 -1
- package/src/index.ts +6 -5
- package/src/repo-context.ts +14 -5
package/dist/detector-worker.js
CHANGED
|
@@ -971,6 +971,7 @@ var require_ignore = __commonJS(function(exports, module) {
|
|
|
971
971
|
|
|
972
972
|
// src/detector-worker.ts
|
|
973
973
|
import * as crypto3 from "crypto";
|
|
974
|
+
import * as fsSync3 from "fs";
|
|
974
975
|
import * as fs3 from "fs/promises";
|
|
975
976
|
import * as path5 from "path";
|
|
976
977
|
|
|
@@ -13074,6 +13075,7 @@ class DiskCacheManager {
|
|
|
13074
13075
|
#getStmt = null;
|
|
13075
13076
|
#saveStmt = null;
|
|
13076
13077
|
#deleteStmt = null;
|
|
13078
|
+
#deleteByRelPathStmt = null;
|
|
13077
13079
|
#totalSizeStmt = null;
|
|
13078
13080
|
#oldestShardsStmt = null;
|
|
13079
13081
|
#deleteAllStmt = null;
|
|
@@ -13135,6 +13137,7 @@ class DiskCacheManager {
|
|
|
13135
13137
|
mtime = excluded.mtime
|
|
13136
13138
|
`);
|
|
13137
13139
|
this.#deleteStmt = db.prepare("DELETE FROM shards WHERE rel_path = ?1 AND content_hash = ?2");
|
|
13140
|
+
this.#deleteByRelPathStmt = db.prepare("DELETE FROM shards WHERE rel_path = ?1");
|
|
13138
13141
|
this.#totalSizeStmt = db.prepare("SELECT COALESCE(SUM(LENGTH(payload)), 0) as total FROM shards");
|
|
13139
13142
|
this.#oldestShardsStmt = db.prepare("SELECT rowid, LENGTH(payload) as size FROM shards ORDER BY mtime ASC LIMIT ?1");
|
|
13140
13143
|
this.#deleteAllStmt = db.prepare("DELETE FROM shards");
|
|
@@ -13189,6 +13192,15 @@ class DiskCacheManager {
|
|
|
13189
13192
|
this.#deleteStmt.run(normalizedRelPath, contentHash);
|
|
13190
13193
|
} catch {}
|
|
13191
13194
|
}
|
|
13195
|
+
async deleteByRelPath(relPath) {
|
|
13196
|
+
try {
|
|
13197
|
+
const db = this.#getDb();
|
|
13198
|
+
if (!db || !this.#deleteByRelPathStmt)
|
|
13199
|
+
return;
|
|
13200
|
+
const normalizedRelPath = relPath.replace(/\\/g, "/");
|
|
13201
|
+
this.#deleteByRelPathStmt.run(normalizedRelPath);
|
|
13202
|
+
} catch {}
|
|
13203
|
+
}
|
|
13192
13204
|
async saveShards(items) {
|
|
13193
13205
|
if (items.length === 0)
|
|
13194
13206
|
return;
|
|
@@ -13273,6 +13285,7 @@ class DiskCacheManager {
|
|
|
13273
13285
|
this.#getStmt = null;
|
|
13274
13286
|
this.#saveStmt = null;
|
|
13275
13287
|
this.#deleteStmt = null;
|
|
13288
|
+
this.#deleteByRelPathStmt = null;
|
|
13276
13289
|
this.#totalSizeStmt = null;
|
|
13277
13290
|
this.#oldestShardsStmt = null;
|
|
13278
13291
|
this.#deleteAllStmt = null;
|
|
@@ -13302,11 +13315,18 @@ import * as fs2 from "fs/promises";
|
|
|
13302
13315
|
import * as path4 from "path";
|
|
13303
13316
|
function canonicalizePath(targetPath) {
|
|
13304
13317
|
const resolved = path4.resolve(targetPath);
|
|
13305
|
-
|
|
13306
|
-
|
|
13307
|
-
|
|
13308
|
-
|
|
13309
|
-
|
|
13318
|
+
let current = resolved;
|
|
13319
|
+
let suffix = "";
|
|
13320
|
+
while (current && current !== path4.dirname(current)) {
|
|
13321
|
+
try {
|
|
13322
|
+
if (fsSync2.existsSync(current)) {
|
|
13323
|
+
const real = fsSync2.realpathSync(current);
|
|
13324
|
+
return suffix ? path4.join(real, suffix) : real;
|
|
13325
|
+
}
|
|
13326
|
+
} catch {}
|
|
13327
|
+
suffix = suffix ? path4.join(path4.basename(current), suffix) : path4.basename(current);
|
|
13328
|
+
current = path4.dirname(current);
|
|
13329
|
+
}
|
|
13310
13330
|
return resolved;
|
|
13311
13331
|
}
|
|
13312
13332
|
function isOmpWorktreePath(targetPath) {
|
|
@@ -13481,6 +13501,56 @@ function areOptionsEqual(a, b) {
|
|
|
13481
13501
|
const bFormats = b.formatsExts ? JSON.stringify(b.formatsExts) : "";
|
|
13482
13502
|
return aFormats === bFormats;
|
|
13483
13503
|
}
|
|
13504
|
+
function filterAndEvictStaleClones(clones, activeFilePath) {
|
|
13505
|
+
if (!currentRootDir || !fsSync3.existsSync(currentRootDir)) {
|
|
13506
|
+
return clones;
|
|
13507
|
+
}
|
|
13508
|
+
const validClones = [];
|
|
13509
|
+
const evictedSources = new Set;
|
|
13510
|
+
const activeRes = activeFilePath ? path5.resolve(activeFilePath) : null;
|
|
13511
|
+
const activeCan = activeFilePath ? canonicalizePath(activeFilePath) : null;
|
|
13512
|
+
const isActive = (src) => {
|
|
13513
|
+
if (!activeFilePath)
|
|
13514
|
+
return false;
|
|
13515
|
+
if (src === activeFilePath)
|
|
13516
|
+
return true;
|
|
13517
|
+
if (activeRes && path5.resolve(src) === activeRes)
|
|
13518
|
+
return true;
|
|
13519
|
+
if (activeCan && canonicalizePath(src) === activeCan)
|
|
13520
|
+
return true;
|
|
13521
|
+
return false;
|
|
13522
|
+
};
|
|
13523
|
+
const isStaleSource = (src) => {
|
|
13524
|
+
if (isActive(src))
|
|
13525
|
+
return false;
|
|
13526
|
+
if (evictedSources.has(src))
|
|
13527
|
+
return true;
|
|
13528
|
+
const can = canonicalizePath(path5.isAbsolute(src) ? src : path5.resolve(currentRootDir, src));
|
|
13529
|
+
const rel = path5.relative(currentRootDir, can);
|
|
13530
|
+
const isInside = !rel.startsWith("..") && !path5.isAbsolute(rel);
|
|
13531
|
+
if (isInside && !fsSync3.existsSync(can)) {
|
|
13532
|
+
evictedSources.add(src);
|
|
13533
|
+
currentIndex.removeSource(src);
|
|
13534
|
+
watchedRevisions.delete(src);
|
|
13535
|
+
watchedRevisions.delete(can);
|
|
13536
|
+
watchedRevisions.delete(path5.resolve(src));
|
|
13537
|
+
if (currentDiskCache) {
|
|
13538
|
+
currentDiskCache.deleteByRelPath(rel).catch(() => {});
|
|
13539
|
+
}
|
|
13540
|
+
return true;
|
|
13541
|
+
}
|
|
13542
|
+
return false;
|
|
13543
|
+
};
|
|
13544
|
+
for (const clone2 of clones) {
|
|
13545
|
+
const srcA = clone2.duplicationA.sourceId;
|
|
13546
|
+
const srcB = clone2.duplicationB.sourceId;
|
|
13547
|
+
if (isStaleSource(srcA) || isStaleSource(srcB)) {
|
|
13548
|
+
continue;
|
|
13549
|
+
}
|
|
13550
|
+
validClones.push(clone2);
|
|
13551
|
+
}
|
|
13552
|
+
return validClones;
|
|
13553
|
+
}
|
|
13484
13554
|
function notifyLateFindings(clones) {
|
|
13485
13555
|
for (const clone2 of clones) {
|
|
13486
13556
|
const srcA = clone2.duplicationA.sourceId;
|
|
@@ -13888,16 +13958,18 @@ async function handleWorkerRequest(msg) {
|
|
|
13888
13958
|
}
|
|
13889
13959
|
case "checkSnippet": {
|
|
13890
13960
|
const { filePath, content, format } = msg.payload;
|
|
13891
|
-
const
|
|
13961
|
+
const rawClones = currentIndex.checkSnippet(filePath, content, format);
|
|
13962
|
+
const clones = filterAndEvictStaleClones(rawClones, filePath);
|
|
13892
13963
|
self.postMessage(createSuccessResponse(msg.id, clones));
|
|
13893
13964
|
break;
|
|
13894
13965
|
}
|
|
13895
13966
|
case "checkAndUpdate": {
|
|
13896
13967
|
const { filePath, content, format, revision = 1 } = msg.payload;
|
|
13897
|
-
const
|
|
13968
|
+
const rawClones = currentIndex.updateSource(filePath, content, format);
|
|
13898
13969
|
const canonicalFilePath = canonicalizePath(filePath);
|
|
13899
13970
|
const resolvedPath = path5.resolve(filePath);
|
|
13900
13971
|
cacheSourceShard(filePath, content);
|
|
13972
|
+
const clones = filterAndEvictStaleClones(rawClones, filePath);
|
|
13901
13973
|
const fileClones = clones.filter((c) => c.duplicationA.sourceId === filePath || c.duplicationB.sourceId === filePath || path5.resolve(c.duplicationA.sourceId) === resolvedPath || path5.resolve(c.duplicationB.sourceId) === resolvedPath || canonicalizePath(c.duplicationA.sourceId) === canonicalFilePath || canonicalizePath(c.duplicationB.sourceId) === canonicalFilePath);
|
|
13902
13974
|
const watchEntry = {
|
|
13903
13975
|
revision,
|
|
@@ -13914,14 +13986,19 @@ async function handleWorkerRequest(msg) {
|
|
|
13914
13986
|
}
|
|
13915
13987
|
case "updateFile": {
|
|
13916
13988
|
const { filePath, content, format } = msg.payload;
|
|
13917
|
-
const
|
|
13989
|
+
const rawClones = currentIndex.updateSource(filePath, content, format);
|
|
13918
13990
|
cacheSourceShard(filePath, content);
|
|
13991
|
+
const clones = filterAndEvictStaleClones(rawClones, filePath);
|
|
13919
13992
|
self.postMessage(createSuccessResponse(msg.id, { clones }));
|
|
13920
13993
|
break;
|
|
13921
13994
|
}
|
|
13922
13995
|
case "removeFile": {
|
|
13923
13996
|
const { filePath } = msg.payload;
|
|
13924
13997
|
currentIndex.removeSource(filePath);
|
|
13998
|
+
if (currentDiskCache) {
|
|
13999
|
+
const relPath = path5.isAbsolute(filePath) && currentRootDir ? path5.relative(currentRootDir, filePath) : filePath;
|
|
14000
|
+
currentDiskCache.deleteByRelPath(relPath).catch(() => {});
|
|
14001
|
+
}
|
|
13925
14002
|
self.postMessage(createSuccessResponse(msg.id, { removed: true }));
|
|
13926
14003
|
break;
|
|
13927
14004
|
}
|
|
@@ -13937,6 +14014,10 @@ async function handleWorkerRequest(msg) {
|
|
|
13937
14014
|
} else {
|
|
13938
14015
|
if (!getSupportedCodeFormat(fileEntry.filePath, currentOptions?.formatsExts)) {
|
|
13939
14016
|
currentIndex.removeSource(fileEntry.filePath);
|
|
14017
|
+
if (currentDiskCache) {
|
|
14018
|
+
const relPath = path5.isAbsolute(fileEntry.filePath) && currentRootDir ? path5.relative(currentRootDir, fileEntry.filePath) : fileEntry.filePath;
|
|
14019
|
+
currentDiskCache.deleteByRelPath(relPath).catch(() => {});
|
|
14020
|
+
}
|
|
13940
14021
|
continue;
|
|
13941
14022
|
}
|
|
13942
14023
|
const stat2 = await fs3.stat(fileEntry.filePath);
|
|
@@ -13951,6 +14032,10 @@ async function handleWorkerRequest(msg) {
|
|
|
13951
14032
|
}
|
|
13952
14033
|
} catch {
|
|
13953
14034
|
currentIndex.removeSource(fileEntry.filePath);
|
|
14035
|
+
if (currentDiskCache) {
|
|
14036
|
+
const relPath = path5.isAbsolute(fileEntry.filePath) && currentRootDir ? path5.relative(currentRootDir, fileEntry.filePath) : fileEntry.filePath;
|
|
14037
|
+
currentDiskCache.deleteByRelPath(relPath).catch(() => {});
|
|
14038
|
+
}
|
|
13954
14039
|
}
|
|
13955
14040
|
}
|
|
13956
14041
|
self.postMessage(createSuccessResponse(msg.id, { reconciledCount }));
|
|
@@ -13999,7 +14084,7 @@ async function handleWorkerRequest(msg) {
|
|
|
13999
14084
|
clones = currentIndex.getClones();
|
|
14000
14085
|
}
|
|
14001
14086
|
} else {
|
|
14002
|
-
clones = currentIndex.getClones();
|
|
14087
|
+
clones = filterAndEvictStaleClones(currentIndex.getClones());
|
|
14003
14088
|
}
|
|
14004
14089
|
self.postMessage(createSuccessResponse(msg.id, clones));
|
|
14005
14090
|
break;
|
package/package.json
CHANGED
package/src/detector-worker.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as crypto from "node:crypto";
|
|
8
|
+
import * as fsSync from "node:fs";
|
|
8
9
|
import * as fs from "node:fs/promises";
|
|
9
10
|
import * as path from "node:path";
|
|
10
11
|
import type { IClone } from "@jscpd/core";
|
|
@@ -88,6 +89,70 @@ function areOptionsEqual(a?: WorkspaceOptions, b?: WorkspaceOptions): boolean {
|
|
|
88
89
|
const bFormats = b.formatsExts ? JSON.stringify(b.formatsExts) : "";
|
|
89
90
|
return aFormats === bFormats;
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Verifies that source files involved in detected clones exist on disk.
|
|
94
|
+
* If an indexed repository source has been deleted or moved, it is evicted from currentIndex,
|
|
95
|
+
* disk cache, and watchedRevisions, and the stale clone is filtered out.
|
|
96
|
+
*
|
|
97
|
+
* activeFilePath: the file currently being queried or updated in memory (exempt from disk check).
|
|
98
|
+
*/
|
|
99
|
+
function filterAndEvictStaleClones(
|
|
100
|
+
clones: IClone[],
|
|
101
|
+
activeFilePath?: string,
|
|
102
|
+
): IClone[] {
|
|
103
|
+
if (!currentRootDir || !fsSync.existsSync(currentRootDir)) {
|
|
104
|
+
return clones;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const validClones: IClone[] = [];
|
|
108
|
+
const evictedSources = new Set<string>();
|
|
109
|
+
const activeRes = activeFilePath ? path.resolve(activeFilePath) : null;
|
|
110
|
+
const activeCan = activeFilePath ? canonicalizePath(activeFilePath) : null;
|
|
111
|
+
|
|
112
|
+
const isActive = (src: string): boolean => {
|
|
113
|
+
if (!activeFilePath) return false;
|
|
114
|
+
if (src === activeFilePath) return true;
|
|
115
|
+
if (activeRes && path.resolve(src) === activeRes) return true;
|
|
116
|
+
if (activeCan && canonicalizePath(src) === activeCan) return true;
|
|
117
|
+
return false;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const isStaleSource = (src: string): boolean => {
|
|
121
|
+
if (isActive(src)) return false;
|
|
122
|
+
if (evictedSources.has(src)) return true;
|
|
123
|
+
|
|
124
|
+
const can = canonicalizePath(
|
|
125
|
+
path.isAbsolute(src) ? src : path.resolve(currentRootDir, src),
|
|
126
|
+
);
|
|
127
|
+
const rel = path.relative(currentRootDir, can);
|
|
128
|
+
const isInside = !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
129
|
+
if (isInside && !fsSync.existsSync(can)) {
|
|
130
|
+
evictedSources.add(src);
|
|
131
|
+
currentIndex.removeSource(src);
|
|
132
|
+
watchedRevisions.delete(src);
|
|
133
|
+
watchedRevisions.delete(can);
|
|
134
|
+
watchedRevisions.delete(path.resolve(src));
|
|
135
|
+
if (currentDiskCache) {
|
|
136
|
+
currentDiskCache.deleteByRelPath(rel).catch(() => {});
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
for (const clone of clones) {
|
|
144
|
+
const srcA = clone.duplicationA.sourceId;
|
|
145
|
+
const srcB = clone.duplicationB.sourceId;
|
|
146
|
+
|
|
147
|
+
if (isStaleSource(srcA) || isStaleSource(srcB)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
validClones.push(clone);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return validClones;
|
|
155
|
+
}
|
|
91
156
|
function notifyLateFindings(clones: IClone[]): void {
|
|
92
157
|
for (const clone of clones) {
|
|
93
158
|
const srcA = clone.duplicationA.sourceId;
|
|
@@ -96,7 +161,6 @@ function notifyLateFindings(clones: IClone[]): void {
|
|
|
96
161
|
const resB = path.resolve(srcB);
|
|
97
162
|
const canA = canonicalizePath(srcA);
|
|
98
163
|
const canB = canonicalizePath(srcB);
|
|
99
|
-
|
|
100
164
|
const isWatchedA =
|
|
101
165
|
watchedRevisions.has(srcA) ||
|
|
102
166
|
watchedRevisions.has(resA) ||
|
|
@@ -659,19 +723,22 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
659
723
|
|
|
660
724
|
case "checkSnippet": {
|
|
661
725
|
const { filePath, content, format } = msg.payload;
|
|
662
|
-
const
|
|
726
|
+
const rawClones = currentIndex.checkSnippet(filePath, content, format);
|
|
727
|
+
const clones = filterAndEvictStaleClones(rawClones, filePath);
|
|
663
728
|
self.postMessage(createSuccessResponse(msg.id, clones));
|
|
664
729
|
break;
|
|
665
730
|
}
|
|
666
731
|
|
|
667
732
|
case "checkAndUpdate": {
|
|
668
733
|
const { filePath, content, format, revision = 1 } = msg.payload;
|
|
669
|
-
const
|
|
734
|
+
const rawClones = currentIndex.updateSource(filePath, content, format);
|
|
670
735
|
const canonicalFilePath = canonicalizePath(filePath);
|
|
671
736
|
const resolvedPath = path.resolve(filePath);
|
|
672
737
|
|
|
673
738
|
cacheSourceShard(filePath, content);
|
|
674
739
|
|
|
740
|
+
const clones = filterAndEvictStaleClones(rawClones, filePath);
|
|
741
|
+
|
|
675
742
|
const fileClones = clones.filter(
|
|
676
743
|
(c) =>
|
|
677
744
|
c.duplicationA.sourceId === filePath ||
|
|
@@ -701,8 +768,9 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
701
768
|
|
|
702
769
|
case "updateFile": {
|
|
703
770
|
const { filePath, content, format } = msg.payload;
|
|
704
|
-
const
|
|
771
|
+
const rawClones = currentIndex.updateSource(filePath, content, format);
|
|
705
772
|
cacheSourceShard(filePath, content);
|
|
773
|
+
const clones = filterAndEvictStaleClones(rawClones, filePath);
|
|
706
774
|
self.postMessage(createSuccessResponse(msg.id, { clones }));
|
|
707
775
|
break;
|
|
708
776
|
}
|
|
@@ -710,6 +778,13 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
710
778
|
case "removeFile": {
|
|
711
779
|
const { filePath } = msg.payload;
|
|
712
780
|
currentIndex.removeSource(filePath);
|
|
781
|
+
if (currentDiskCache) {
|
|
782
|
+
const relPath =
|
|
783
|
+
path.isAbsolute(filePath) && currentRootDir
|
|
784
|
+
? path.relative(currentRootDir, filePath)
|
|
785
|
+
: filePath;
|
|
786
|
+
currentDiskCache.deleteByRelPath(relPath).catch(() => {});
|
|
787
|
+
}
|
|
713
788
|
self.postMessage(createSuccessResponse(msg.id, { removed: true }));
|
|
714
789
|
break;
|
|
715
790
|
}
|
|
@@ -732,6 +807,13 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
732
807
|
)
|
|
733
808
|
) {
|
|
734
809
|
currentIndex.removeSource(fileEntry.filePath);
|
|
810
|
+
if (currentDiskCache) {
|
|
811
|
+
const relPath =
|
|
812
|
+
path.isAbsolute(fileEntry.filePath) && currentRootDir
|
|
813
|
+
? path.relative(currentRootDir, fileEntry.filePath)
|
|
814
|
+
: fileEntry.filePath;
|
|
815
|
+
currentDiskCache.deleteByRelPath(relPath).catch(() => {});
|
|
816
|
+
}
|
|
735
817
|
continue;
|
|
736
818
|
}
|
|
737
819
|
const stat = await fs.stat(fileEntry.filePath);
|
|
@@ -745,8 +827,15 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
745
827
|
}
|
|
746
828
|
}
|
|
747
829
|
} catch {
|
|
748
|
-
// If file cannot be read or no longer exists, remove from index
|
|
830
|
+
// If file cannot be read or no longer exists, remove from index and disk cache
|
|
749
831
|
currentIndex.removeSource(fileEntry.filePath);
|
|
832
|
+
if (currentDiskCache) {
|
|
833
|
+
const relPath =
|
|
834
|
+
path.isAbsolute(fileEntry.filePath) && currentRootDir
|
|
835
|
+
? path.relative(currentRootDir, fileEntry.filePath)
|
|
836
|
+
: fileEntry.filePath;
|
|
837
|
+
currentDiskCache.deleteByRelPath(relPath).catch(() => {});
|
|
838
|
+
}
|
|
750
839
|
}
|
|
751
840
|
}
|
|
752
841
|
|
|
@@ -803,7 +892,7 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
803
892
|
clones = currentIndex.getClones();
|
|
804
893
|
}
|
|
805
894
|
} else {
|
|
806
|
-
clones = currentIndex.getClones();
|
|
895
|
+
clones = filterAndEvictStaleClones(currentIndex.getClones());
|
|
807
896
|
}
|
|
808
897
|
|
|
809
898
|
self.postMessage(createSuccessResponse(msg.id, clones));
|
package/src/disk-cache.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import type { WorkspaceOptions } from "./worker-protocol";
|
|
22
22
|
|
|
23
23
|
const DEFAULT_MAX_CACHE_BYTES = 250 * 1024 * 1024; // 250 MB
|
|
24
|
-
|
|
24
|
+
const TOKENIZER_CACHE_VERSION = "4.0";
|
|
25
25
|
|
|
26
26
|
export interface DiskCacheOptions {
|
|
27
27
|
/** Root directory of the workspace */
|
|
@@ -389,6 +389,7 @@ export class DiskCacheManager {
|
|
|
389
389
|
#getStmt: Statement | null = null;
|
|
390
390
|
#saveStmt: Statement | null = null;
|
|
391
391
|
#deleteStmt: Statement | null = null;
|
|
392
|
+
#deleteByRelPathStmt: Statement | null = null;
|
|
392
393
|
#totalSizeStmt: Statement | null = null;
|
|
393
394
|
#oldestShardsStmt: Statement | null = null;
|
|
394
395
|
#deleteAllStmt: Statement | null = null;
|
|
@@ -473,6 +474,9 @@ export class DiskCacheManager {
|
|
|
473
474
|
this.#deleteStmt = db.prepare(
|
|
474
475
|
"DELETE FROM shards WHERE rel_path = ?1 AND content_hash = ?2",
|
|
475
476
|
);
|
|
477
|
+
this.#deleteByRelPathStmt = db.prepare(
|
|
478
|
+
"DELETE FROM shards WHERE rel_path = ?1",
|
|
479
|
+
);
|
|
476
480
|
this.#totalSizeStmt = db.prepare(
|
|
477
481
|
"SELECT COALESCE(SUM(LENGTH(payload)), 0) as total FROM shards",
|
|
478
482
|
);
|
|
@@ -587,6 +591,19 @@ export class DiskCacheManager {
|
|
|
587
591
|
// Fail open
|
|
588
592
|
}
|
|
589
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* Deletes all cached shards for a given relPath regardless of contentHash.
|
|
596
|
+
*/
|
|
597
|
+
async deleteByRelPath(relPath: string): Promise<void> {
|
|
598
|
+
try {
|
|
599
|
+
const db = this.#getDb();
|
|
600
|
+
if (!db || !this.#deleteByRelPathStmt) return;
|
|
601
|
+
const normalizedRelPath = relPath.replace(/\\/g, "/");
|
|
602
|
+
this.#deleteByRelPathStmt.run(normalizedRelPath);
|
|
603
|
+
} catch {
|
|
604
|
+
// Fail open
|
|
605
|
+
}
|
|
606
|
+
}
|
|
590
607
|
|
|
591
608
|
/**
|
|
592
609
|
* Atomically saves a batch of pre-tokenized shards inside a single SQLite transaction.
|
|
@@ -721,6 +738,7 @@ export class DiskCacheManager {
|
|
|
721
738
|
this.#getStmt = null;
|
|
722
739
|
this.#saveStmt = null;
|
|
723
740
|
this.#deleteStmt = null;
|
|
741
|
+
this.#deleteByRelPathStmt = null;
|
|
724
742
|
this.#totalSizeStmt = null;
|
|
725
743
|
this.#oldestShardsStmt = null;
|
|
726
744
|
this.#deleteAllStmt = null;
|
package/src/index.ts
CHANGED
|
@@ -302,15 +302,16 @@ function notifyBaselineStatus(
|
|
|
302
302
|
? "warning"
|
|
303
303
|
: "info";
|
|
304
304
|
|
|
305
|
-
// 1. Send
|
|
305
|
+
// 1. Send TUI notification if UI is active (presents directly without wiping chat container)
|
|
306
306
|
ctx?.ui?.notify?.(message, level);
|
|
307
307
|
|
|
308
|
-
// 2. Send
|
|
308
|
+
// 2. Send status message into session with display: false to prevent triggering
|
|
309
|
+
// an invasive chat rebuild that clears ephemeral startup banners (e.g. omp update notification)
|
|
309
310
|
pi.sendMessage(
|
|
310
311
|
{
|
|
311
312
|
customType: "duplicate-detector-status",
|
|
312
313
|
content: message,
|
|
313
|
-
display:
|
|
314
|
+
display: false,
|
|
314
315
|
attribution: "user",
|
|
315
316
|
details: {
|
|
316
317
|
status,
|
|
@@ -394,7 +395,7 @@ export default function duplicateDetectorExtension(pi: ExtensionAPI): void {
|
|
|
394
395
|
}
|
|
395
396
|
}
|
|
396
397
|
const { stdout: statusOut } = await execGit(
|
|
397
|
-
["status", "--porcelain", "-z", "
|
|
398
|
+
["status", "--porcelain", "-z", "-uall", "--", "."],
|
|
398
399
|
cwd,
|
|
399
400
|
).catch(() => ({ stdout: "" }));
|
|
400
401
|
|
|
@@ -447,7 +448,7 @@ export default function duplicateDetectorExtension(pi: ExtensionAPI): void {
|
|
|
447
448
|
{
|
|
448
449
|
customType: "duplicate-detector-status",
|
|
449
450
|
content: message,
|
|
450
|
-
display:
|
|
451
|
+
display: false,
|
|
451
452
|
attribution: "user",
|
|
452
453
|
details: {
|
|
453
454
|
status: "failed",
|
package/src/repo-context.ts
CHANGED
|
@@ -26,12 +26,21 @@ export interface RepositoryContext {
|
|
|
26
26
|
*/
|
|
27
27
|
export function canonicalizePath(targetPath: string): string {
|
|
28
28
|
const resolved = path.resolve(targetPath);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
let current = resolved;
|
|
30
|
+
let suffix = "";
|
|
31
|
+
while (current && current !== path.dirname(current)) {
|
|
32
|
+
try {
|
|
33
|
+
if (fsSync.existsSync(current)) {
|
|
34
|
+
const real = fsSync.realpathSync(current);
|
|
35
|
+
return suffix ? path.join(real, suffix) : real;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Fall back to walking up
|
|
32
39
|
}
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
suffix = suffix
|
|
41
|
+
? path.join(path.basename(current), suffix)
|
|
42
|
+
: path.basename(current);
|
|
43
|
+
current = path.dirname(current);
|
|
35
44
|
}
|
|
36
45
|
return resolved;
|
|
37
46
|
}
|