1+ import path from "node:path" ;
12import { isEligibleDuplicateGroup } from "./eligibility.js" ;
23
3- export function findIrrelevanceFindings ( { duplicates = [ ] , configuredRules = [ ] } = { } ) {
4+ export function findIrrelevanceFindings ( { duplicates = [ ] , files = [ ] , directories = [ ] , configuredRules = [ ] } = { } ) {
45 const normalizedRules = configuredRules . map ( ( rule ) => rule . toLowerCase ( ) ) ;
56 const findings = [ ] ;
67
78 if ( normalizedRules . includes ( "exact duplicate files by content hash" ) ) {
89 findings . push ( ...duplicates . filter ( isEligibleDuplicateGroup ) . map ( buildDuplicateFinding ) ) ;
910 }
1011
12+ // New rule: redundant archives
13+ findings . push ( ...findExtractedArchives ( { files, directories } ) ) ;
14+
1115 return findings . sort ( ( left , right ) => left . id . localeCompare ( right . id ) ) ;
1216}
1317
18+ function findExtractedArchives ( { files, directories } ) {
19+ const findings = [ ] ;
20+ const ARCHIVE_EXTENSIONS = [ ".zip" , ".7z" , ".rar" , ".tar" , ".gz" , ".tgz" ] ;
21+ const directoryPaths = new Set ( directories . map ( d => d . absolutePath . toLowerCase ( ) ) ) ;
22+
23+ for ( const file of files ) {
24+ const baseName = file . baseName || path . basename ( file . absolutePath || "" ) ;
25+ if ( ! baseName ) continue ;
26+
27+ const ext = path . extname ( baseName ) . toLowerCase ( ) ;
28+ if ( ! ARCHIVE_EXTENSIONS . includes ( ext ) ) continue ;
29+
30+ const baseNameWithoutExt = baseName . slice ( 0 , - ext . length ) ;
31+ const possibleDirPath = path . join ( path . dirname ( file . absolutePath ) , baseNameWithoutExt ) ;
32+
33+ if ( directoryPaths . has ( possibleDirPath . toLowerCase ( ) ) ) {
34+ findings . push ( {
35+ id : `irrelevance:extracted_archive:${ file . sha256 } :${ file . relativePath } ` ,
36+ type : "irrelevance_finding" ,
37+ action : "review_archive_cleanup" ,
38+ status : "pending_user_approval" ,
39+ approvalGate : "deleting irrelevant files" ,
40+ risk : "destructive" ,
41+ subjectPath : file . absolutePath ,
42+ matchedRule : "archive exists alongside extracted folder" ,
43+ confidence : "medium" ,
44+ reviewOnly : true ,
45+ evidence : {
46+ archivePath : file . absolutePath ,
47+ extractedFolderPath : possibleDirPath ,
48+ reasons : [ "This archive appears to have been extracted into a folder in the same location." ]
49+ }
50+ } ) ;
51+ }
52+ }
53+
54+ return findings ;
55+ }
56+
1457function buildDuplicateFinding ( group ) {
15- const [ proposedKeepFile , ...proposedDeleteFiles ] = group . files ;
58+ const proposedKeepFile = identifyProposedKeepFile ( group . files ) ;
59+ const proposedDeleteFiles = group . files . filter ( ( file ) => file . absolutePath !== proposedKeepFile . absolutePath ) ;
1660
1761 return {
1862 id : `irrelevance:duplicate:${ group . sha256 } ` ,
@@ -37,3 +81,27 @@ function buildDuplicateFinding(group) {
3781 }
3882 } ;
3983}
84+
85+ function identifyProposedKeepFile ( files ) {
86+ // Score files based on "originality" (lower score is better/more original)
87+ const scoredFiles = files . map ( ( file ) => {
88+ let score = 0 ;
89+ const name = file . baseName || path . basename ( file . absolutePath || "" ) ;
90+
91+ // Penalize common download suffixes
92+ if ( name ) {
93+ const baseNameOnly = path . parse ( name . toLowerCase ( ) ) . name ;
94+ if ( / \( \d + \) $ / . test ( baseNameOnly ) ) score += 10 ;
95+ if ( name . toLowerCase ( ) . includes ( "copy" ) ) score += 5 ;
96+ if ( name . toLowerCase ( ) . includes ( "duplicate" ) ) score += 5 ;
97+ }
98+
99+ // Prefer shorter paths (usually more root-level/intentional)
100+ score += ( file . relativePath || "" ) . split ( "/" ) . length ;
101+
102+ return { file, score } ;
103+ } ) ;
104+
105+ scoredFiles . sort ( ( a , b ) => a . score - b . score ) ;
106+ return scoredFiles [ 0 ] . file ;
107+ }
0 commit comments