@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-senior-core",
3
- "version": "6.5.0",
3
+ "version": "6.5.2",
4
4
  "description": "Universal AI coding rules. Write code like a staff engineer.",
5
5
  "skills": "./skills/",
6
6
  "hooks": "./hooks/hooks.json",
@@ -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 { mode: 'advisory', minTokens: 30, ignoreDirs: ['tests', 'migrations', 'generated', 'node_modules'] };
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 matchedFile = firstName === normalizedTarget
249
- ? path.basename(dup.secondFile.name)
250
- : path.basename(dup.firstFile.name);
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
- // jscpd v5 reports fragments; estimate overlap percentage from line count
253
- var totalLines = (report.statistics && report.statistics.total && report.statistics.total.lines) || 1;
254
- var percent = Math.round((lines / totalLines) * 100);
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-senior-core",
3
- "version": "6.5.0",
3
+ "version": "6.5.2",
4
4
  "description": "Universal AI coding rules. Because your AI writes code like it gets paid by the line.",
5
5
  "contextFileName": "rules/agentic-senior-core.md",
6
6
  "rules": [
@@ -2,5 +2,9 @@
2
2
  "mode": "advisory",
3
3
  "minTokens": 30,
4
4
  "scanRoot": null,
5
- "ignoreDirs": ["tests", "migrations", "generated", "node_modules"]
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-senior-core",
3
- "version": "6.5.0",
3
+ "version": "6.5.2",
4
4
  "description": "Universal AI coding rules. Write code like a staff engineer.",
5
5
  "author": "fatidaprilian",
6
6
  "license": "MIT",
@@ -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 { minTokens: 30, ignoreDirs: ['tests', 'migrations', 'generated', 'node_modules'] };
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 matchedFile = isFirstStaged ? path.basename(dup.secondFile.name) : path.basename(dup.firstFile.name);
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
- const totalLines = (report.statistics && report.statistics.total && report.statistics.total.lines) || 1;
132
- const percent = Math.round((lines / totalLines) * 100);
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 || ['tests', 'migrations', 'generated', 'node_modules'])
189
- .map(d => \`--ignore "\${d}"\`).join(' ');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuenn3123/agentic-senior-core",
3
- "version": "6.5.0",
3
+ "version": "6.5.2",
4
4
  "type": "module",
5
5
  "description": "Agentic Senior Core: Universal AI coding rules and workflows. Write code like a staff engineer, not a junior.",
6
6
  "bin": {
package/plugin.yaml CHANGED
@@ -1,5 +1,5 @@
1
1
  name: agentic-senior-core
2
- version: 6.5.0
2
+ version: 6.5.2
3
3
  description: Universal AI coding rules. Write code like a staff engineer.
4
4
  author: fatidaprilian
5
5
  provides_hooks: