@ryuenn3123/agentic-senior-core 6.5.0 → 6.5.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/.agents/plugins/agentic-senior-core/.codex-plugin/plugin.json +1 -1
- package/.agents/plugins/agentic-senior-core/hooks/dedup-gate.js +95 -7
- package/.agents/plugins/agentic-senior-core/plugin.json +1 -1
- package/.asc/dedup-config.json +5 -1
- package/gemini-extension.json +1 -1
- package/lib/cli/commands/git-hook-generator.mjs +85 -6
- package/package.json +1 -1
- package/plugin.yaml +1 -1
|
@@ -20,6 +20,63 @@ const JSCPD_TIMEOUT_MS = 10000;
|
|
|
20
20
|
// Recognized source root directories — scan scope walks up to the first match
|
|
21
21
|
const SOURCE_ROOTS = ['src', 'lib', 'app'];
|
|
22
22
|
|
|
23
|
+
// Framework-conventional filenames that MUST be identical across directories by design.
|
|
24
|
+
// Matches where both files share one of these basenames in different dirs are not duplication.
|
|
25
|
+
const FRAMEWORK_CONVENTIONAL_BASENAMES = new Set([
|
|
26
|
+
// Next.js App Router
|
|
27
|
+
'page.tsx', 'page.jsx', 'page.ts', 'page.js',
|
|
28
|
+
'layout.tsx', 'layout.jsx', 'layout.ts', 'layout.js',
|
|
29
|
+
'loading.tsx', 'loading.jsx', 'loading.ts', 'loading.js',
|
|
30
|
+
'error.tsx', 'error.jsx', 'error.ts', 'error.js',
|
|
31
|
+
'not-found.tsx', 'not-found.jsx', 'not-found.ts', 'not-found.js',
|
|
32
|
+
'template.tsx', 'template.jsx', 'template.ts', 'template.js',
|
|
33
|
+
'route.tsx', 'route.ts', 'route.js',
|
|
34
|
+
'default.tsx', 'default.jsx', 'default.ts', 'default.js',
|
|
35
|
+
// Remix
|
|
36
|
+
'root.tsx', 'root.jsx', 'root.ts', 'root.js',
|
|
37
|
+
'entry.server.tsx', 'entry.server.ts', 'entry.client.tsx', 'entry.client.ts',
|
|
38
|
+
// Expo Router
|
|
39
|
+
'_layout.tsx', '_layout.jsx', '_layout.ts', '_layout.js',
|
|
40
|
+
// Nuxt
|
|
41
|
+
'index.vue', 'app.vue',
|
|
42
|
+
// SvelteKit
|
|
43
|
+
'+page.svelte', '+layout.svelte', '+page.server.ts', '+page.server.js',
|
|
44
|
+
'+error.svelte', '+layout.server.ts', '+layout.server.js',
|
|
45
|
+
// Common convention / barrel files
|
|
46
|
+
'index.ts', 'index.js', 'index.tsx', 'index.jsx',
|
|
47
|
+
'types.ts', 'types.d.ts',
|
|
48
|
+
// Config files (declarative, structurally similar across projects)
|
|
49
|
+
'tailwind.config.ts', 'tailwind.config.js', 'tailwind.config.mjs',
|
|
50
|
+
'postcss.config.js', 'postcss.config.mjs', 'postcss.config.cjs',
|
|
51
|
+
'next.config.ts', 'next.config.js', 'next.config.mjs',
|
|
52
|
+
'vite.config.ts', 'vite.config.js', 'vite.config.mjs',
|
|
53
|
+
'tsconfig.json', 'jest.config.ts', 'jest.config.js', 'vitest.config.ts',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// Suffix patterns for frameworks that mandate a naming convention (e.g. Angular).
|
|
57
|
+
// Files matching the same suffix in different dirs are structural, not copy-paste.
|
|
58
|
+
const FRAMEWORK_CONVENTIONAL_SUFFIXES = [
|
|
59
|
+
// Angular (*.component.ts, *.module.ts, *.service.ts, *.pipe.ts, *.directive.ts)
|
|
60
|
+
'.component.ts', '.component.js', '.module.ts', '.service.ts',
|
|
61
|
+
'.pipe.ts', '.directive.ts', '.guard.ts', '.resolver.ts',
|
|
62
|
+
// Storybook
|
|
63
|
+
'.stories.tsx', '.stories.jsx', '.stories.ts', '.stories.js',
|
|
64
|
+
// Test / spec (structurally similar boilerplate across suites)
|
|
65
|
+
'.spec.ts', '.spec.tsx', '.spec.js', '.spec.jsx',
|
|
66
|
+
'.test.ts', '.test.tsx', '.test.js', '.test.jsx',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function hasConventionalSuffix(basename) {
|
|
70
|
+
for (var i = 0; i < FRAMEWORK_CONVENTIONAL_SUFFIXES.length; i++) {
|
|
71
|
+
if (basename.endsWith(FRAMEWORK_CONVENTIONAL_SUFFIXES[i])) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isFrameworkConventional(basename) {
|
|
77
|
+
return FRAMEWORK_CONVENTIONAL_BASENAMES.has(basename) || hasConventionalSuffix(basename);
|
|
78
|
+
}
|
|
79
|
+
|
|
23
80
|
let inputBuffer = '';
|
|
24
81
|
process.stdin.setEncoding('utf8');
|
|
25
82
|
process.stdin.on('data', chunk => {
|
|
@@ -201,7 +258,15 @@ function loadDedupConfig() {
|
|
|
201
258
|
}
|
|
202
259
|
} catch (_) {}
|
|
203
260
|
}
|
|
204
|
-
return {
|
|
261
|
+
return {
|
|
262
|
+
mode: 'advisory',
|
|
263
|
+
minTokens: 30,
|
|
264
|
+
ignoreDirs: [
|
|
265
|
+
'tests', 'test', '__tests__', 'migrations', 'generated', 'node_modules',
|
|
266
|
+
'dist', 'build', '.next', '.nuxt', '.expo', 'coverage', '.storybook',
|
|
267
|
+
'prisma/migrations', 'android', 'ios',
|
|
268
|
+
],
|
|
269
|
+
};
|
|
205
270
|
}
|
|
206
271
|
|
|
207
272
|
// minimal: attempt-then-fallback — try scan directly, fall back on failure.
|
|
@@ -238,6 +303,7 @@ function checkForDuplicates(report, filePath) {
|
|
|
238
303
|
if (duplicates.length === 0) return null;
|
|
239
304
|
|
|
240
305
|
var normalizedTarget = path.resolve(filePath).replace(/\\/g, '/').toLowerCase();
|
|
306
|
+
var targetBasename = path.basename(filePath);
|
|
241
307
|
|
|
242
308
|
for (var i = 0; i < duplicates.length; i++) {
|
|
243
309
|
var dup = duplicates[i];
|
|
@@ -245,13 +311,35 @@ function checkForDuplicates(report, filePath) {
|
|
|
245
311
|
var secondName = path.resolve(dup.secondFile.name).replace(/\\/g, '/').toLowerCase();
|
|
246
312
|
|
|
247
313
|
if (firstName === normalizedTarget || secondName === normalizedTarget) {
|
|
248
|
-
var
|
|
249
|
-
|
|
250
|
-
|
|
314
|
+
var otherRaw = firstName === normalizedTarget ? dup.secondFile.name : dup.firstFile.name;
|
|
315
|
+
var otherBasename = path.basename(otherRaw);
|
|
316
|
+
|
|
317
|
+
// Skip framework-conventional filenames in different directories — identical names
|
|
318
|
+
// are mandated by the framework (e.g. Next.js page.tsx, Angular *.component.ts),
|
|
319
|
+
// not copy-paste duplication.
|
|
320
|
+
if (isFrameworkConventional(targetBasename)
|
|
321
|
+
&& isFrameworkConventional(otherBasename)
|
|
322
|
+
&& path.dirname(path.resolve(filePath)) !== path.dirname(path.resolve(otherRaw))) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Show relative path instead of bare basename so the developer knows WHICH file.
|
|
327
|
+
var cwd = process.cwd();
|
|
328
|
+
var matchedFile = path.relative(cwd, path.resolve(otherRaw)).replace(/\\/g, '/');
|
|
251
329
|
var lines = dup.lines || 0;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
330
|
+
var totalFileLines = 1;
|
|
331
|
+
try {
|
|
332
|
+
totalFileLines = fs.readFileSync(path.resolve(filePath), 'utf8').split('\n').length || 1;
|
|
333
|
+
} catch (_) {
|
|
334
|
+
totalFileLines = dup.firstFile.lines || dup.secondFile.lines || lines || 1;
|
|
335
|
+
}
|
|
336
|
+
var percent = Math.round((lines / totalFileLines) * 100);
|
|
337
|
+
|
|
338
|
+
// Skip trivial matches (import boilerplate, small overlaps)
|
|
339
|
+
if (lines < 10 && percent < 10) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
255
343
|
return { matchedFile: matchedFile, lines: lines, percent: percent };
|
|
256
344
|
}
|
|
257
345
|
}
|
package/.asc/dedup-config.json
CHANGED
|
@@ -2,5 +2,9 @@
|
|
|
2
2
|
"mode": "advisory",
|
|
3
3
|
"minTokens": 30,
|
|
4
4
|
"scanRoot": null,
|
|
5
|
-
"ignoreDirs": [
|
|
5
|
+
"ignoreDirs": [
|
|
6
|
+
"tests", "test", "__tests__", "migrations", "generated", "node_modules",
|
|
7
|
+
"dist", "build", ".next", ".nuxt", ".expo", "coverage", ".storybook",
|
|
8
|
+
"prisma/migrations", "android", "ios"
|
|
9
|
+
]
|
|
6
10
|
}
|
package/gemini-extension.json
CHANGED
|
@@ -27,6 +27,51 @@ 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 thresholds — skip trivial matches (import boilerplate, etc.)
|
|
72
|
+
const MIN_LINES_THRESHOLD = 10;
|
|
73
|
+
const MIN_PERCENT_THRESHOLD = 10;
|
|
74
|
+
|
|
30
75
|
function getStagedFiles(cwd) {
|
|
31
76
|
try {
|
|
32
77
|
const output = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8', cwd });
|
|
@@ -48,7 +93,14 @@ function loadDedupConfig(cwd) {
|
|
|
48
93
|
}
|
|
49
94
|
} catch (_) {}
|
|
50
95
|
}
|
|
51
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
minTokens: 30,
|
|
98
|
+
ignoreDirs: [
|
|
99
|
+
'tests', 'test', '__tests__', 'migrations', 'generated', 'node_modules',
|
|
100
|
+
'dist', 'build', '.next', '.nuxt', '.expo', 'coverage', '.storybook',
|
|
101
|
+
'prisma/migrations', 'android', 'ios',
|
|
102
|
+
],
|
|
103
|
+
};
|
|
52
104
|
}
|
|
53
105
|
|
|
54
106
|
function runEslintAutoFix(cwd, stagedFiles) {
|
|
@@ -126,10 +178,34 @@ function checkForDuplicates(report, stagedSourceFiles, cwd) {
|
|
|
126
178
|
|
|
127
179
|
if (isFirstStaged || isSecondStaged) {
|
|
128
180
|
const stagedFile = isFirstStaged ? normalizedStagedMap.get(firstName) : normalizedStagedMap.get(secondName);
|
|
129
|
-
const
|
|
181
|
+
const otherRaw = isFirstStaged ? dup.secondFile.name : dup.firstFile.name;
|
|
182
|
+
const otherBasename = path.basename(otherRaw);
|
|
183
|
+
const stagedBasename = path.basename(stagedFile);
|
|
184
|
+
|
|
185
|
+
// Skip framework-conventional filenames in different directories
|
|
186
|
+
if (isFrameworkConventional(stagedBasename)
|
|
187
|
+
&& isFrameworkConventional(otherBasename)
|
|
188
|
+
&& path.dirname(path.resolve(cwd, stagedFile)) !== path.dirname(path.resolve(cwd, otherRaw))) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
130
192
|
const lines = dup.lines || 0;
|
|
131
|
-
|
|
132
|
-
|
|
193
|
+
// Calculate percent relative to the actual staged file size
|
|
194
|
+
let totalFileLines = 1;
|
|
195
|
+
try {
|
|
196
|
+
totalFileLines = fs.readFileSync(path.resolve(cwd, stagedFile), 'utf8').split('\\n').length || 1;
|
|
197
|
+
} catch (_) {
|
|
198
|
+
totalFileLines = dup.firstFile.lines || dup.secondFile.lines || lines || 1;
|
|
199
|
+
}
|
|
200
|
+
const percent = Math.round((lines / totalFileLines) * 100);
|
|
201
|
+
|
|
202
|
+
// Skip trivial matches (import boilerplate, small overlaps)
|
|
203
|
+
if (lines < MIN_LINES_THRESHOLD && percent < MIN_PERCENT_THRESHOLD) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Show relative path for actionable messages
|
|
208
|
+
const matchedFile = path.relative(cwd, path.resolve(cwd, otherRaw)).replace(/\\\\/g, '/');
|
|
133
209
|
return { stagedFile, matchedFile, lines, percent };
|
|
134
210
|
}
|
|
135
211
|
}
|
|
@@ -185,8 +261,11 @@ function runPreCommitGate() {
|
|
|
185
261
|
})));
|
|
186
262
|
|
|
187
263
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'asc-git-dedup-'));
|
|
188
|
-
const ignoreFlags = (config.ignoreDirs || [
|
|
189
|
-
|
|
264
|
+
const ignoreFlags = (config.ignoreDirs || [
|
|
265
|
+
'tests', 'test', '__tests__', 'migrations', 'generated', 'node_modules',
|
|
266
|
+
'dist', 'build', '.next', '.nuxt', '.expo', 'coverage', '.storybook',
|
|
267
|
+
'prisma/migrations', 'android', 'ios',
|
|
268
|
+
]).map(d => \`--ignore "\${d}"\`).join(' ');
|
|
190
269
|
const minTokens = config.minTokens || 30;
|
|
191
270
|
|
|
192
271
|
let finding = null;
|
package/package.json
CHANGED
package/plugin.yaml
CHANGED