@ryuenn3123/agentic-senior-core 6.5.1 → 6.5.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/.agents/plugins/agentic-senior-core/.codex-plugin/plugin.json +1 -1
- package/.agents/plugins/agentic-senior-core/hooks/dedup-gate.js +57 -7
- package/.agents/plugins/agentic-senior-core/plugin.json +1 -1
- package/.asc/dedup-config.json +2 -1
- package/gemini-extension.json +1 -1
- package/lib/cli/commands/git-hook-generator.mjs +132 -8
- package/package.json +1 -1
- package/plugin.yaml +1 -1
|
@@ -116,18 +116,20 @@ process.stdin.on('data', chunk => {
|
|
|
116
116
|
const config = loadDedupConfig();
|
|
117
117
|
if (!isQualifyingEdit(toolName, toolInput, config)) { process.exit(0); return; }
|
|
118
118
|
|
|
119
|
+
if (hasValidInlineIgnore(filePath)) { process.exit(0); return; }
|
|
120
|
+
|
|
119
121
|
const scanDir = resolveScanDir(filePath, config);
|
|
120
122
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'asc-dedup-'));
|
|
121
123
|
|
|
122
124
|
const ignoreFlags = (config.ignoreDirs || []).map(function (d) { return '--ignore "' + d + '"'; }).join(' ');
|
|
123
|
-
const minTokens = config.minTokens ||
|
|
125
|
+
const minTokens = config.minTokens || 30;
|
|
124
126
|
const scanCmd = ' "' + scanDir + '" --min-tokens ' + minTokens
|
|
125
127
|
+ ' --reporters json --silent --output "' + tmpDir + '" ' + ignoreFlags;
|
|
126
128
|
|
|
127
129
|
const report = runJscpdScan(scanCmd, tmpDir);
|
|
128
130
|
if (!report) { cleanup(tmpDir); process.exit(0); return; }
|
|
129
131
|
|
|
130
|
-
const finding = checkForDuplicates(report, filePath);
|
|
132
|
+
const finding = checkForDuplicates(report, filePath, config);
|
|
131
133
|
if (!finding) { cleanup(tmpDir); process.exit(0); return; }
|
|
132
134
|
|
|
133
135
|
const nudge = '[ASC Dedup] ' + path.basename(filePath) + ' looks similar to '
|
|
@@ -260,7 +262,7 @@ function loadDedupConfig() {
|
|
|
260
262
|
}
|
|
261
263
|
return {
|
|
262
264
|
mode: 'advisory',
|
|
263
|
-
minTokens:
|
|
265
|
+
minTokens: 30,
|
|
264
266
|
ignoreDirs: [
|
|
265
267
|
'tests', 'test', '__tests__', 'migrations', 'generated', 'node_modules',
|
|
266
268
|
'dist', 'build', '.next', '.nuxt', '.expo', 'coverage', '.storybook',
|
|
@@ -298,10 +300,44 @@ function loadReport(tmpDir) {
|
|
|
298
300
|
}
|
|
299
301
|
}
|
|
300
302
|
|
|
301
|
-
function
|
|
303
|
+
function hasValidInlineIgnore(filePath) {
|
|
304
|
+
try {
|
|
305
|
+
if (!fs.existsSync(filePath)) return false;
|
|
306
|
+
var content = fs.readFileSync(filePath, 'utf8');
|
|
307
|
+
if (content.indexOf('jscpd:ignore-start') !== -1) return true;
|
|
308
|
+
var match = content.match(/asc-dedup:ignore\s*--\s*(.+)/i);
|
|
309
|
+
return Boolean(match && match[1] && match[1].trim().length > 0);
|
|
310
|
+
} catch (_) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isAllowedDuplicate(fileA, fileB, allowedDuplicates) {
|
|
316
|
+
if (!Array.isArray(allowedDuplicates) || allowedDuplicates.length === 0) return false;
|
|
317
|
+
var normA = path.resolve(fileA).replace(/\\/g, '/').toLowerCase();
|
|
318
|
+
var normB = path.resolve(fileB).replace(/\\/g, '/').toLowerCase();
|
|
319
|
+
var baseA = path.basename(fileA).toLowerCase();
|
|
320
|
+
var baseB = path.basename(fileB).toLowerCase();
|
|
321
|
+
|
|
322
|
+
for (var i = 0; i < allowedDuplicates.length; i++) {
|
|
323
|
+
var pair = allowedDuplicates[i];
|
|
324
|
+
if (!Array.isArray(pair) || pair.length < 2) continue;
|
|
325
|
+
var p0Norm = path.resolve(pair[0]).replace(/\\/g, '/').toLowerCase();
|
|
326
|
+
var p1Norm = path.resolve(pair[1]).replace(/\\/g, '/').toLowerCase();
|
|
327
|
+
var p0Base = path.basename(pair[0]).toLowerCase();
|
|
328
|
+
var p1Base = path.basename(pair[1]).toLowerCase();
|
|
329
|
+
|
|
330
|
+
if ((normA === p0Norm && normB === p1Norm) || (normA === p1Norm && normB === p0Norm)) return true;
|
|
331
|
+
if ((baseA === p0Base && baseB === p1Base) || (baseA === p1Base && baseB === p0Base)) return true;
|
|
332
|
+
}
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function checkForDuplicates(report, filePath, config) {
|
|
302
337
|
var duplicates = report.duplicates || [];
|
|
303
338
|
if (duplicates.length === 0) return null;
|
|
304
339
|
|
|
340
|
+
var allowedDuplicates = config && config.allowedDuplicates;
|
|
305
341
|
var normalizedTarget = path.resolve(filePath).replace(/\\/g, '/').toLowerCase();
|
|
306
342
|
var targetBasename = path.basename(filePath);
|
|
307
343
|
|
|
@@ -314,6 +350,10 @@ function checkForDuplicates(report, filePath) {
|
|
|
314
350
|
var otherRaw = firstName === normalizedTarget ? dup.secondFile.name : dup.firstFile.name;
|
|
315
351
|
var otherBasename = path.basename(otherRaw);
|
|
316
352
|
|
|
353
|
+
if (isAllowedDuplicate(filePath, otherRaw, allowedDuplicates)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
317
357
|
// Skip framework-conventional filenames in different directories — identical names
|
|
318
358
|
// are mandated by the framework (e.g. Next.js page.tsx, Angular *.component.ts),
|
|
319
359
|
// not copy-paste duplication.
|
|
@@ -327,9 +367,19 @@ function checkForDuplicates(report, filePath) {
|
|
|
327
367
|
var cwd = process.cwd();
|
|
328
368
|
var matchedFile = path.relative(cwd, path.resolve(otherRaw)).replace(/\\/g, '/');
|
|
329
369
|
var lines = dup.lines || 0;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
370
|
+
var totalFileLines = 1;
|
|
371
|
+
try {
|
|
372
|
+
totalFileLines = fs.readFileSync(path.resolve(filePath), 'utf8').split('\n').length || 1;
|
|
373
|
+
} catch (_) {
|
|
374
|
+
totalFileLines = dup.firstFile.lines || dup.secondFile.lines || lines || 1;
|
|
375
|
+
}
|
|
376
|
+
var percent = Math.round((lines / totalFileLines) * 100);
|
|
377
|
+
|
|
378
|
+
// Skip trivial matches (import boilerplate, small overlaps)
|
|
379
|
+
if (lines < 10 && percent < 10) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
333
383
|
return { matchedFile: matchedFile, lines: lines, percent: percent };
|
|
334
384
|
}
|
|
335
385
|
}
|
package/.asc/dedup-config.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"mode": "advisory",
|
|
3
|
-
"minTokens":
|
|
3
|
+
"minTokens": 30,
|
|
4
4
|
"scanRoot": null,
|
|
5
|
+
"allowedDuplicates": [],
|
|
5
6
|
"ignoreDirs": [
|
|
6
7
|
"tests", "test", "__tests__", "migrations", "generated", "node_modules",
|
|
7
8
|
"dist", "build", ".next", ".nuxt", ".expo", "coverage", ".storybook",
|
package/gemini-extension.json
CHANGED
|
@@ -27,6 +27,84 @@ const SOURCE_EXTENSIONS = new Set([${extensionsList}]);
|
|
|
27
27
|
|
|
28
28
|
const JSCPD_TIMEOUT_MS = 10000;
|
|
29
29
|
|
|
30
|
+
// Framework-conventional filenames that MUST be identical across directories by design.
|
|
31
|
+
const FRAMEWORK_CONVENTIONAL_BASENAMES = new Set([
|
|
32
|
+
'page.tsx', 'page.jsx', 'page.ts', 'page.js',
|
|
33
|
+
'layout.tsx', 'layout.jsx', 'layout.ts', 'layout.js',
|
|
34
|
+
'loading.tsx', 'loading.jsx', 'loading.ts', 'loading.js',
|
|
35
|
+
'error.tsx', 'error.jsx', 'error.ts', 'error.js',
|
|
36
|
+
'not-found.tsx', 'not-found.jsx', 'not-found.ts', 'not-found.js',
|
|
37
|
+
'template.tsx', 'template.jsx', 'template.ts', 'template.js',
|
|
38
|
+
'route.tsx', 'route.ts', 'route.js',
|
|
39
|
+
'default.tsx', 'default.jsx', 'default.ts', 'default.js',
|
|
40
|
+
'root.tsx', 'root.jsx', 'root.ts', 'root.js',
|
|
41
|
+
'entry.server.tsx', 'entry.server.ts', 'entry.client.tsx', 'entry.client.ts',
|
|
42
|
+
'_layout.tsx', '_layout.jsx', '_layout.ts', '_layout.js',
|
|
43
|
+
'index.vue', 'app.vue',
|
|
44
|
+
'+page.svelte', '+layout.svelte', '+page.server.ts', '+page.server.js',
|
|
45
|
+
'+error.svelte', '+layout.server.ts', '+layout.server.js',
|
|
46
|
+
'index.ts', 'index.js', 'index.tsx', 'index.jsx',
|
|
47
|
+
'types.ts', 'types.d.ts',
|
|
48
|
+
'tailwind.config.ts', 'tailwind.config.js', 'tailwind.config.mjs',
|
|
49
|
+
'postcss.config.js', 'postcss.config.mjs', 'postcss.config.cjs',
|
|
50
|
+
'next.config.ts', 'next.config.js', 'next.config.mjs',
|
|
51
|
+
'vite.config.ts', 'vite.config.js', 'vite.config.mjs',
|
|
52
|
+
'tsconfig.json', 'jest.config.ts', 'jest.config.js', 'vitest.config.ts',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const FRAMEWORK_CONVENTIONAL_SUFFIXES = [
|
|
56
|
+
'.component.ts', '.component.js', '.module.ts', '.service.ts',
|
|
57
|
+
'.pipe.ts', '.directive.ts', '.guard.ts', '.resolver.ts',
|
|
58
|
+
'.stories.tsx', '.stories.jsx', '.stories.ts', '.stories.js',
|
|
59
|
+
'.spec.ts', '.spec.tsx', '.spec.js', '.spec.jsx',
|
|
60
|
+
'.test.ts', '.test.tsx', '.test.js', '.test.jsx',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function isFrameworkConventional(basename) {
|
|
64
|
+
if (FRAMEWORK_CONVENTIONAL_BASENAMES.has(basename)) return true;
|
|
65
|
+
for (var i = 0; i < FRAMEWORK_CONVENTIONAL_SUFFIXES.length; i++) {
|
|
66
|
+
if (basename.endsWith(FRAMEWORK_CONVENTIONAL_SUFFIXES[i])) return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Minimum blocking thresholds for Git pre-commit gate
|
|
72
|
+
const MIN_BLOCKING_LINES = 15;
|
|
73
|
+
const MIN_BLOCKING_PERCENT = 25;
|
|
74
|
+
|
|
75
|
+
function hasValidInlineIgnore(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
if (!fs.existsSync(filePath)) return false;
|
|
78
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
79
|
+
if (content.includes('jscpd:ignore-start')) return true;
|
|
80
|
+
const match = content.match(/asc-dedup:ignore\\s*--\\s*(.+)/i);
|
|
81
|
+
return Boolean(match && match[1] && match[1].trim().length > 0);
|
|
82
|
+
} catch (_) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isAllowedDuplicate(fileA, fileB, allowedDuplicates) {
|
|
88
|
+
if (!Array.isArray(allowedDuplicates) || allowedDuplicates.length === 0) return false;
|
|
89
|
+
const normA = path.resolve(fileA).replace(/\\\\/g, '/').toLowerCase();
|
|
90
|
+
const normB = path.resolve(fileB).replace(/\\\\/g, '/').toLowerCase();
|
|
91
|
+
const baseA = path.basename(fileA).toLowerCase();
|
|
92
|
+
const baseB = path.basename(fileB).toLowerCase();
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < allowedDuplicates.length; i++) {
|
|
95
|
+
const pair = allowedDuplicates[i];
|
|
96
|
+
if (!Array.isArray(pair) || pair.length < 2) continue;
|
|
97
|
+
const p0Norm = path.resolve(pair[0]).replace(/\\\\/g, '/').toLowerCase();
|
|
98
|
+
const p1Norm = path.resolve(pair[1]).replace(/\\\\/g, '/').toLowerCase();
|
|
99
|
+
const p0Base = path.basename(pair[0]).toLowerCase();
|
|
100
|
+
const p1Base = path.basename(pair[1]).toLowerCase();
|
|
101
|
+
|
|
102
|
+
if ((normA === p0Norm && normB === p1Norm) || (normA === p1Norm && normB === p0Norm)) return true;
|
|
103
|
+
if ((baseA === p0Base && baseB === p1Base) || (baseA === p1Base && baseB === p0Base)) return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
30
108
|
function getStagedFiles(cwd) {
|
|
31
109
|
try {
|
|
32
110
|
const output = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8', cwd });
|
|
@@ -48,7 +126,14 @@ function loadDedupConfig(cwd) {
|
|
|
48
126
|
}
|
|
49
127
|
} catch (_) {}
|
|
50
128
|
}
|
|
51
|
-
return {
|
|
129
|
+
return {
|
|
130
|
+
minTokens: 30,
|
|
131
|
+
ignoreDirs: [
|
|
132
|
+
'tests', 'test', '__tests__', 'migrations', 'generated', 'node_modules',
|
|
133
|
+
'dist', 'build', '.next', '.nuxt', '.expo', 'coverage', '.storybook',
|
|
134
|
+
'prisma/migrations', 'android', 'ios',
|
|
135
|
+
],
|
|
136
|
+
};
|
|
52
137
|
}
|
|
53
138
|
|
|
54
139
|
function runEslintAutoFix(cwd, stagedFiles) {
|
|
@@ -106,10 +191,11 @@ function loadReport(tmpDir) {
|
|
|
106
191
|
}
|
|
107
192
|
}
|
|
108
193
|
|
|
109
|
-
function checkForDuplicates(report, stagedSourceFiles, cwd) {
|
|
194
|
+
function checkForDuplicates(report, stagedSourceFiles, cwd, config) {
|
|
110
195
|
const duplicates = report.duplicates || [];
|
|
111
196
|
if (duplicates.length === 0) return null;
|
|
112
197
|
|
|
198
|
+
const allowedDuplicates = config && config.allowedDuplicates;
|
|
113
199
|
const normalizedStagedMap = new Map();
|
|
114
200
|
for (const f of stagedSourceFiles) {
|
|
115
201
|
const norm = path.resolve(cwd, f).replace(/\\\\/g, '/').toLowerCase();
|
|
@@ -126,10 +212,45 @@ function checkForDuplicates(report, stagedSourceFiles, cwd) {
|
|
|
126
212
|
|
|
127
213
|
if (isFirstStaged || isSecondStaged) {
|
|
128
214
|
const stagedFile = isFirstStaged ? normalizedStagedMap.get(firstName) : normalizedStagedMap.get(secondName);
|
|
129
|
-
const
|
|
215
|
+
const otherRaw = isFirstStaged ? dup.secondFile.name : dup.firstFile.name;
|
|
216
|
+
const stagedPath = path.resolve(cwd, stagedFile);
|
|
217
|
+
const otherPath = path.resolve(cwd, otherRaw);
|
|
218
|
+
|
|
219
|
+
if (hasValidInlineIgnore(stagedPath) || hasValidInlineIgnore(otherPath)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (isAllowedDuplicate(stagedPath, otherPath, allowedDuplicates)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const otherBasename = path.basename(otherRaw);
|
|
228
|
+
const stagedBasename = path.basename(stagedFile);
|
|
229
|
+
|
|
230
|
+
// Skip framework-conventional filenames in different directories
|
|
231
|
+
if (isFrameworkConventional(stagedBasename)
|
|
232
|
+
&& isFrameworkConventional(otherBasename)
|
|
233
|
+
&& path.dirname(stagedPath) !== path.dirname(otherPath)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
130
237
|
const lines = dup.lines || 0;
|
|
131
|
-
|
|
132
|
-
|
|
238
|
+
// Calculate percent relative to the actual staged file size
|
|
239
|
+
let totalFileLines = 1;
|
|
240
|
+
try {
|
|
241
|
+
totalFileLines = fs.readFileSync(stagedPath, 'utf8').split('\\n').length || 1;
|
|
242
|
+
} catch (_) {
|
|
243
|
+
totalFileLines = dup.firstFile.lines || dup.secondFile.lines || lines || 1;
|
|
244
|
+
}
|
|
245
|
+
const percent = Math.round((lines / totalFileLines) * 100);
|
|
246
|
+
|
|
247
|
+
// Skip matches below blocking threshold
|
|
248
|
+
if (lines < MIN_BLOCKING_LINES || percent < MIN_BLOCKING_PERCENT) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Show relative path for actionable messages
|
|
253
|
+
const matchedFile = path.relative(cwd, otherPath).replace(/\\\\/g, '/');
|
|
133
254
|
return { stagedFile, matchedFile, lines, percent };
|
|
134
255
|
}
|
|
135
256
|
}
|
|
@@ -185,8 +306,11 @@ function runPreCommitGate() {
|
|
|
185
306
|
})));
|
|
186
307
|
|
|
187
308
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'asc-git-dedup-'));
|
|
188
|
-
const ignoreFlags = (config.ignoreDirs || [
|
|
189
|
-
|
|
309
|
+
const ignoreFlags = (config.ignoreDirs || [
|
|
310
|
+
'tests', 'test', '__tests__', 'migrations', 'generated', 'node_modules',
|
|
311
|
+
'dist', 'build', '.next', '.nuxt', '.expo', 'coverage', '.storybook',
|
|
312
|
+
'prisma/migrations', 'android', 'ios',
|
|
313
|
+
]).map(d => \`--ignore "\${d}"\`).join(' ');
|
|
190
314
|
const minTokens = config.minTokens || 30;
|
|
191
315
|
|
|
192
316
|
let finding = null;
|
|
@@ -195,7 +319,7 @@ function runPreCommitGate() {
|
|
|
195
319
|
const scanCmd = \` "\${scanDir}" --min-tokens \${minTokens} --reporters json --silent --output "\${tmpDir}" \${ignoreFlags}\`;
|
|
196
320
|
const report = runJscpdScan(scanCmd, cwd, tmpDir);
|
|
197
321
|
if (report) {
|
|
198
|
-
finding = checkForDuplicates(report, currentStagedSource, cwd);
|
|
322
|
+
finding = checkForDuplicates(report, currentStagedSource, cwd, config);
|
|
199
323
|
if (finding) break;
|
|
200
324
|
}
|
|
201
325
|
}
|
package/package.json
CHANGED
package/plugin.yaml
CHANGED