seo-gravity-mcp 1.0.2 → 1.2.0

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.
Files changed (67) hide show
  1. package/README.md +118 -105
  2. package/dist/adapters/adapterRegistry.d.ts +7 -0
  3. package/dist/adapters/adapterRegistry.js +30 -0
  4. package/dist/adapters/astroAdapter.d.ts +17 -0
  5. package/dist/adapters/astroAdapter.js +191 -0
  6. package/dist/adapters/nextAppAdapter.d.ts +17 -0
  7. package/dist/adapters/nextAppAdapter.js +265 -0
  8. package/dist/adapters/nextPagesAdapter.d.ts +17 -0
  9. package/dist/adapters/nextPagesAdapter.js +209 -0
  10. package/dist/adapters/remixAdapter.d.ts +16 -0
  11. package/dist/adapters/remixAdapter.js +118 -0
  12. package/dist/adapters/staticAdapter.d.ts +16 -0
  13. package/dist/adapters/staticAdapter.js +117 -0
  14. package/dist/adapters/svelteKitAdapter.d.ts +16 -0
  15. package/dist/adapters/svelteKitAdapter.js +122 -0
  16. package/dist/adapters/types.d.ts +39 -0
  17. package/dist/adapters/types.js +1 -0
  18. package/dist/adapters/viteReactAdapter.d.ts +16 -0
  19. package/dist/adapters/viteReactAdapter.js +189 -0
  20. package/dist/benchmark/correlationBenchmark.d.ts +1 -0
  21. package/dist/benchmark/correlationBenchmark.js +96 -0
  22. package/dist/benchmark/invariantBenchmark.d.ts +1 -0
  23. package/dist/benchmark/invariantBenchmark.js +47 -0
  24. package/dist/cli.d.ts +8 -0
  25. package/dist/cli.js +162 -0
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.js +8 -646
  28. package/dist/invariants/registry.d.ts +12 -0
  29. package/dist/invariants/registry.js +178 -0
  30. package/dist/invariants/types.d.ts +38 -0
  31. package/dist/invariants/types.js +1 -0
  32. package/dist/providers/providerRegistry.d.ts +65 -0
  33. package/dist/providers/providerRegistry.js +64 -0
  34. package/dist/providers/types.d.ts +22 -0
  35. package/dist/providers/types.js +1 -0
  36. package/dist/server/registry.d.ts +3 -0
  37. package/dist/server/registry.js +559 -0
  38. package/dist/server/server.d.ts +70 -0
  39. package/dist/server/server.js +53 -0
  40. package/dist/server/types.d.ts +7 -0
  41. package/dist/server/types.js +1 -0
  42. package/dist/test.js +108 -138
  43. package/dist/tools/orchestration.js +23 -21
  44. package/dist/types/canonical.d.ts +64 -0
  45. package/dist/types/canonical.js +1 -0
  46. package/dist/types/findings.d.ts +22 -0
  47. package/dist/types/findings.js +1 -1
  48. package/dist/types/seo.d.ts +24 -0
  49. package/dist/utils/astLocator.d.ts +14 -0
  50. package/dist/utils/astLocator.js +137 -0
  51. package/dist/utils/cacheManager.d.ts +29 -0
  52. package/dist/utils/cacheManager.js +60 -0
  53. package/dist/utils/experimentEngine.d.ts +26 -0
  54. package/dist/utils/experimentEngine.js +41 -0
  55. package/dist/utils/findingEngine.d.ts +2 -1
  56. package/dist/utils/findingEngine.js +1 -0
  57. package/dist/utils/gitDiffEngine.d.ts +28 -0
  58. package/dist/utils/gitDiffEngine.js +109 -0
  59. package/dist/utils/opportunityEngine.d.ts +19 -0
  60. package/dist/utils/opportunityEngine.js +61 -0
  61. package/dist/utils/projectScanner.d.ts +4 -2
  62. package/dist/utils/projectScanner.js +14 -346
  63. package/dist/utils/sarifExporter.d.ts +64 -0
  64. package/dist/utils/sarifExporter.js +81 -0
  65. package/dist/utils/snapshotEngine.d.ts +3 -1
  66. package/dist/utils/snapshotEngine.js +232 -94
  67. package/package.json +8 -5
@@ -0,0 +1,117 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ export class StaticAdapter {
4
+ id = 'static-html';
5
+ name = 'Static HTML / Web Application';
6
+ detect(_projectDir) {
7
+ return true; // Fallback
8
+ }
9
+ getProjectInfo(projectDir) {
10
+ return {
11
+ framework: 'static-html',
12
+ name: path.basename(projectDir),
13
+ hasTypeScript: fs.existsSync(path.join(projectDir, 'tsconfig.json')),
14
+ hasSitemapConfig: Boolean(this.findSitemapConfig(projectDir)),
15
+ hasRobotsConfig: Boolean(this.findRobotsConfig(projectDir)),
16
+ hasLlmsTxt: Boolean(this.findLlmsTxt(projectDir)),
17
+ rootDir: path.resolve(projectDir),
18
+ routesDir: '.',
19
+ defaultDevPort: 8080
20
+ };
21
+ }
22
+ discoverRoutes(projectDir) {
23
+ const discovered = [];
24
+ const scan = (dir, prefix) => {
25
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
26
+ for (const e of entries) {
27
+ if (['node_modules', '.git', 'dist', 'build', '.next'].includes(e.name))
28
+ continue;
29
+ const full = path.join(dir, e.name);
30
+ if (e.isDirectory()) {
31
+ const next = prefix === '/' ? `/${e.name}` : `${prefix}/${e.name}`;
32
+ scan(full, next);
33
+ }
34
+ else if (e.isFile() && e.name.endsWith('.html')) {
35
+ const base = e.name.replace(/\.html$/, '');
36
+ let routePath = prefix;
37
+ if (base !== 'index') {
38
+ routePath = prefix === '/' ? `/${base}` : `${prefix}/${base}`;
39
+ }
40
+ if (routePath === '')
41
+ routePath = '/';
42
+ const rel = path.relative(projectDir, full).replace(/\\/g, '/');
43
+ const content = fs.readFileSync(full, 'utf-8');
44
+ discovered.push({
45
+ routePath,
46
+ sourceFilePath: rel,
47
+ isDynamic: false,
48
+ dynamicParams: [],
49
+ routeType: 'page',
50
+ hasMetadataExport: /<title\b|<meta\s+name=["']description["']/i.test(content),
51
+ hasGenerateMetadata: false,
52
+ hasSchemaMarkup: /application\/ld\+json/.test(content),
53
+ hasHeadComponent: /<head\b|<title\b/i.test(content),
54
+ isClientComponent: false
55
+ });
56
+ }
57
+ }
58
+ };
59
+ scan(projectDir, '/');
60
+ return discovered;
61
+ }
62
+ mapRouteToSource(targetUrl, routes) {
63
+ let urlPath = targetUrl.startsWith('http') ? new URL(targetUrl).pathname : targetUrl;
64
+ urlPath = urlPath.replace(/\/$/, '') || '/';
65
+ const match = routes.find(r => r.routePath === urlPath);
66
+ return {
67
+ urlPath,
68
+ matchedRoute: match,
69
+ sourceFilePath: match?.sourceFilePath,
70
+ confidence: match ? 1.0 : 0.0,
71
+ resolutionMethod: match ? 'exact_match' : 'unmapped'
72
+ };
73
+ }
74
+ async findMetadataImplementation(projectDir, route) {
75
+ const full = path.join(projectDir, route.sourceFilePath);
76
+ if (!fs.existsSync(full))
77
+ return { hasMetadata: false, type: 'none' };
78
+ const content = fs.readFileSync(full, 'utf-8');
79
+ const has = /<title\b|<meta\s+name=["']description["']/i.test(content);
80
+ return { hasMetadata: has, type: has ? 'jsx_head' : 'none' };
81
+ }
82
+ async findCanonicalDeclaration(projectDir, route) {
83
+ const full = path.join(projectDir, route.sourceFilePath);
84
+ if (!fs.existsSync(full))
85
+ return { hasCanonical: false, type: 'none' };
86
+ const content = fs.readFileSync(full, 'utf-8');
87
+ return { hasCanonical: /rel=["']canonical["']/i.test(content), type: 'link_jsx_tag' };
88
+ }
89
+ async findSchemaDeclaration(projectDir, route) {
90
+ const full = path.join(projectDir, route.sourceFilePath);
91
+ if (!fs.existsSync(full))
92
+ return { hasSchema: false, typesFound: [] };
93
+ const content = fs.readFileSync(full, 'utf-8');
94
+ return { hasSchema: /application\/ld\+json/i.test(content), typesFound: ['JSON-LD'] };
95
+ }
96
+ findRobotsConfig(projectDir) {
97
+ const paths = ['robots.txt', 'public/robots.txt'];
98
+ for (const p of paths)
99
+ if (fs.existsSync(path.join(projectDir, p)))
100
+ return p;
101
+ return null;
102
+ }
103
+ findSitemapConfig(projectDir) {
104
+ const paths = ['sitemap.xml', 'public/sitemap.xml'];
105
+ for (const p of paths)
106
+ if (fs.existsSync(path.join(projectDir, p)))
107
+ return p;
108
+ return null;
109
+ }
110
+ findLlmsTxt(projectDir) {
111
+ const paths = ['llms.txt', 'public/llms.txt'];
112
+ for (const p of paths)
113
+ if (fs.existsSync(path.join(projectDir, p)))
114
+ return p;
115
+ return null;
116
+ }
117
+ }
@@ -0,0 +1,16 @@
1
+ import { FrameworkAdapter, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
2
+ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
3
+ export declare class SvelteKitAdapter implements FrameworkAdapter {
4
+ id: string;
5
+ name: string;
6
+ detect(projectDir: string): boolean;
7
+ getProjectInfo(projectDir: string): ProjectFrameworkInfo;
8
+ discoverRoutes(projectDir: string): DiscoveredRoute[];
9
+ mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
10
+ findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
11
+ findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
12
+ findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
13
+ findRobotsConfig(projectDir: string): string | null;
14
+ findSitemapConfig(projectDir: string): string | null;
15
+ findLlmsTxt(projectDir: string): string | null;
16
+ }
@@ -0,0 +1,122 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ export class SvelteKitAdapter {
4
+ id = 'sveltekit';
5
+ name = 'SvelteKit';
6
+ detect(projectDir) {
7
+ const pkgPath = path.join(projectDir, 'package.json');
8
+ if (!fs.existsSync(pkgPath))
9
+ return false;
10
+ try {
11
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
12
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
13
+ return Boolean(deps['@sveltejs/kit']);
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ getProjectInfo(projectDir) {
20
+ return {
21
+ framework: 'sveltekit',
22
+ name: path.basename(projectDir),
23
+ hasTypeScript: fs.existsSync(path.join(projectDir, 'tsconfig.json')),
24
+ hasSitemapConfig: Boolean(this.findSitemapConfig(projectDir)),
25
+ hasRobotsConfig: Boolean(this.findRobotsConfig(projectDir)),
26
+ hasLlmsTxt: Boolean(this.findLlmsTxt(projectDir)),
27
+ rootDir: path.resolve(projectDir),
28
+ routesDir: 'src/routes',
29
+ devCommand: 'npm run dev',
30
+ defaultDevPort: 5173
31
+ };
32
+ }
33
+ discoverRoutes(projectDir) {
34
+ const routesDir = path.join(projectDir, 'src/routes');
35
+ const discovered = [];
36
+ if (!fs.existsSync(routesDir))
37
+ return discovered;
38
+ const scan = (dir, prefix) => {
39
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
40
+ for (const e of entries) {
41
+ const full = path.join(dir, e.name);
42
+ if (e.isDirectory()) {
43
+ const next = prefix === '/' ? `/${e.name}` : `${prefix}/${e.name}`;
44
+ scan(full, next);
45
+ }
46
+ else if (e.isFile() && e.name.startsWith('+page.svelte')) {
47
+ const rel = path.relative(projectDir, full).replace(/\\/g, '/');
48
+ const content = fs.readFileSync(full, 'utf-8');
49
+ discovered.push({
50
+ routePath: prefix,
51
+ sourceFilePath: rel,
52
+ isDynamic: prefix.includes('['),
53
+ dynamicParams: (prefix.match(/\[([a-zA-Z0-9_-]+)\]/g) || []).map(p => p.slice(1, -1)),
54
+ routeType: 'page',
55
+ hasMetadataExport: /<title\b|<svelte:head\b/i.test(content),
56
+ hasGenerateMetadata: false,
57
+ hasSchemaMarkup: /application\/ld\+json/.test(content),
58
+ hasHeadComponent: /<svelte:head\b/i.test(content),
59
+ isClientComponent: false
60
+ });
61
+ }
62
+ }
63
+ };
64
+ scan(routesDir, '/');
65
+ return discovered;
66
+ }
67
+ mapRouteToSource(targetUrl, routes) {
68
+ let urlPath = targetUrl.startsWith('http') ? new URL(targetUrl).pathname : targetUrl;
69
+ urlPath = urlPath.replace(/\/$/, '') || '/';
70
+ const match = routes.find(r => r.routePath === urlPath);
71
+ return {
72
+ urlPath,
73
+ matchedRoute: match,
74
+ sourceFilePath: match?.sourceFilePath,
75
+ confidence: match ? 1.0 : 0.0,
76
+ resolutionMethod: match ? 'exact_match' : 'unmapped'
77
+ };
78
+ }
79
+ async findMetadataImplementation(projectDir, route) {
80
+ const full = path.join(projectDir, route.sourceFilePath);
81
+ if (!fs.existsSync(full))
82
+ return { hasMetadata: false, type: 'none' };
83
+ const content = fs.readFileSync(full, 'utf-8');
84
+ const has = /<svelte:head\b|<title\b/i.test(content);
85
+ return { hasMetadata: has, type: has ? 'jsx_head' : 'none' };
86
+ }
87
+ async findCanonicalDeclaration(projectDir, route) {
88
+ const full = path.join(projectDir, route.sourceFilePath);
89
+ if (!fs.existsSync(full))
90
+ return { hasCanonical: false, type: 'none' };
91
+ const content = fs.readFileSync(full, 'utf-8');
92
+ return { hasCanonical: /rel=["']canonical["']/i.test(content), type: 'link_jsx_tag' };
93
+ }
94
+ async findSchemaDeclaration(projectDir, route) {
95
+ const full = path.join(projectDir, route.sourceFilePath);
96
+ if (!fs.existsSync(full))
97
+ return { hasSchema: false, typesFound: [] };
98
+ const content = fs.readFileSync(full, 'utf-8');
99
+ return { hasSchema: /application\/ld\+json/i.test(content), typesFound: ['JSON-LD'] };
100
+ }
101
+ findRobotsConfig(projectDir) {
102
+ const paths = ['static/robots.txt', 'robots.txt'];
103
+ for (const p of paths)
104
+ if (fs.existsSync(path.join(projectDir, p)))
105
+ return p;
106
+ return null;
107
+ }
108
+ findSitemapConfig(projectDir) {
109
+ const paths = ['static/sitemap.xml', 'sitemap.xml'];
110
+ for (const p of paths)
111
+ if (fs.existsSync(path.join(projectDir, p)))
112
+ return p;
113
+ return null;
114
+ }
115
+ findLlmsTxt(projectDir) {
116
+ const paths = ['static/llms.txt', 'llms.txt'];
117
+ for (const p of paths)
118
+ if (fs.existsSync(path.join(projectDir, p)))
119
+ return p;
120
+ return null;
121
+ }
122
+ }
@@ -0,0 +1,39 @@
1
+ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping, ASTSourceRange } from '../types/findings.js';
2
+ export interface FrameworkDetectionResult {
3
+ matched: boolean;
4
+ confidence: number;
5
+ evidence: string[];
6
+ }
7
+ export interface MetadataLocationInfo {
8
+ hasMetadata: boolean;
9
+ type: 'static_export' | 'dynamic_function' | 'jsx_head' | 'none';
10
+ sourceRange?: ASTSourceRange;
11
+ titleSnippet?: string;
12
+ descriptionSnippet?: string;
13
+ }
14
+ export interface CanonicalLocationInfo {
15
+ hasCanonical: boolean;
16
+ type: 'metadata_property' | 'link_jsx_tag' | 'header_config' | 'none';
17
+ canonicalUrl?: string;
18
+ sourceRange?: ASTSourceRange;
19
+ }
20
+ export interface SchemaLocationInfo {
21
+ hasSchema: boolean;
22
+ typesFound: string[];
23
+ sourceRange?: ASTSourceRange;
24
+ }
25
+ export interface FrameworkAdapter {
26
+ id: string;
27
+ name: string;
28
+ detect(projectDir: string): boolean;
29
+ detectDetailed?(projectDir: string): FrameworkDetectionResult;
30
+ getProjectInfo(projectDir: string): ProjectFrameworkInfo;
31
+ discoverRoutes(projectDir: string): DiscoveredRoute[];
32
+ mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
33
+ findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
34
+ findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
35
+ findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
36
+ findRobotsConfig(projectDir: string): string | null;
37
+ findSitemapConfig(projectDir: string): string | null;
38
+ findLlmsTxt(projectDir: string): string | null;
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { FrameworkAdapter, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
2
+ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
3
+ export declare class ViteReactAdapter implements FrameworkAdapter {
4
+ id: string;
5
+ name: string;
6
+ detect(projectDir: string): boolean;
7
+ getProjectInfo(projectDir: string): ProjectFrameworkInfo;
8
+ discoverRoutes(projectDir: string): DiscoveredRoute[];
9
+ mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
10
+ findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
11
+ findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
12
+ findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
13
+ findRobotsConfig(projectDir: string): string | null;
14
+ findSitemapConfig(projectDir: string): string | null;
15
+ findLlmsTxt(projectDir: string): string | null;
16
+ }
@@ -0,0 +1,189 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ export class ViteReactAdapter {
4
+ id = 'vite-react';
5
+ name = 'Vite / React SPA';
6
+ detect(projectDir) {
7
+ const pkgPath = path.join(projectDir, 'package.json');
8
+ if (!fs.existsSync(pkgPath))
9
+ return false;
10
+ try {
11
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
12
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
13
+ return Boolean(deps['vite'] || (deps['react'] && !deps['next'] && !deps['astro'] && !deps['@remix-run/react']));
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ getProjectInfo(projectDir) {
20
+ const pkgPath = path.join(projectDir, 'package.json');
21
+ let pkg = {};
22
+ if (fs.existsSync(pkgPath)) {
23
+ try {
24
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
25
+ }
26
+ catch { }
27
+ }
28
+ return {
29
+ framework: 'vite-react',
30
+ name: pkg.name || path.basename(projectDir),
31
+ version: pkg.dependencies?.vite || pkg.devDependencies?.vite,
32
+ packageManager: 'npm',
33
+ hasTypeScript: fs.existsSync(path.join(projectDir, 'tsconfig.json')),
34
+ hasSitemapConfig: Boolean(this.findSitemapConfig(projectDir)),
35
+ hasRobotsConfig: Boolean(this.findRobotsConfig(projectDir)),
36
+ hasLlmsTxt: Boolean(this.findLlmsTxt(projectDir)),
37
+ rootDir: path.resolve(projectDir),
38
+ routesDir: 'src',
39
+ devCommand: 'npm run dev',
40
+ defaultDevPort: 5173
41
+ };
42
+ }
43
+ discoverRoutes(projectDir) {
44
+ const discovered = [];
45
+ // Check index.html
46
+ if (fs.existsSync(path.join(projectDir, 'index.html'))) {
47
+ const content = fs.readFileSync(path.join(projectDir, 'index.html'), 'utf-8');
48
+ discovered.push({
49
+ routePath: '/',
50
+ sourceFilePath: 'index.html',
51
+ isDynamic: false,
52
+ dynamicParams: [],
53
+ routeType: 'page',
54
+ hasMetadataExport: /<title\b|<meta\s+name=["']description["']/i.test(content),
55
+ hasGenerateMetadata: false,
56
+ hasSchemaMarkup: /application\/ld\+json/.test(content),
57
+ hasHeadComponent: true,
58
+ isClientComponent: true
59
+ });
60
+ }
61
+ // Scan src for router declarations
62
+ const srcDir = path.join(projectDir, 'src');
63
+ if (fs.existsSync(srcDir)) {
64
+ const scanSrc = (dir) => {
65
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
66
+ for (const entry of entries) {
67
+ const fullPath = path.join(dir, entry.name);
68
+ if (entry.isDirectory()) {
69
+ scanSrc(fullPath);
70
+ }
71
+ else if (entry.isFile() && /\.(tsx|jsx|js|ts)$/.test(entry.name)) {
72
+ const content = fs.readFileSync(fullPath, 'utf-8');
73
+ const rel = path.relative(projectDir, fullPath).replace(/\\/g, '/');
74
+ const routeMatches = content.matchAll(/path=["']([^"']+)["']/g);
75
+ for (const m of routeMatches) {
76
+ const rPath = m[1];
77
+ if (rPath && !discovered.some(d => d.routePath === rPath)) {
78
+ discovered.push({
79
+ routePath: rPath,
80
+ sourceFilePath: rel,
81
+ isDynamic: rPath.includes(':'),
82
+ dynamicParams: (rPath.match(/:[a-zA-Z0-9_-]+/g) || []).map(p => p.slice(1)),
83
+ routeType: 'page',
84
+ hasMetadataExport: /<Helmet\b|<title\b/i.test(content),
85
+ hasGenerateMetadata: false,
86
+ hasSchemaMarkup: /application\/ld\+json/.test(content),
87
+ hasHeadComponent: /<Helmet\b/i.test(content),
88
+ isClientComponent: true
89
+ });
90
+ }
91
+ }
92
+ }
93
+ }
94
+ };
95
+ scanSrc(srcDir);
96
+ }
97
+ return discovered;
98
+ }
99
+ mapRouteToSource(targetUrl, routes) {
100
+ let urlPath = targetUrl;
101
+ try {
102
+ if (targetUrl.startsWith('http')) {
103
+ urlPath = new URL(targetUrl).pathname;
104
+ }
105
+ }
106
+ catch {
107
+ urlPath = targetUrl;
108
+ }
109
+ urlPath = urlPath.replace(/\/$/, '') || '/';
110
+ const match = routes.find(r => r.routePath === urlPath);
111
+ if (match) {
112
+ return {
113
+ urlPath,
114
+ matchedRoute: match,
115
+ sourceFilePath: match.sourceFilePath,
116
+ confidence: 1.0,
117
+ resolutionMethod: 'exact_match'
118
+ };
119
+ }
120
+ for (const r of routes) {
121
+ if (!r.isDynamic)
122
+ continue;
123
+ const regexStr = '^' + r.routePath.replace(/:[a-zA-Z0-9_-]+/g, '[^/]+') + '$';
124
+ try {
125
+ if (new RegExp(regexStr).test(urlPath)) {
126
+ return {
127
+ urlPath,
128
+ matchedRoute: r,
129
+ sourceFilePath: r.sourceFilePath,
130
+ confidence: 0.95,
131
+ resolutionMethod: 'pattern_match'
132
+ };
133
+ }
134
+ }
135
+ catch { }
136
+ }
137
+ return {
138
+ urlPath,
139
+ confidence: 0.0,
140
+ resolutionMethod: 'unmapped'
141
+ };
142
+ }
143
+ async findMetadataImplementation(projectDir, route) {
144
+ const fullPath = path.join(projectDir, route.sourceFilePath);
145
+ if (!fs.existsSync(fullPath))
146
+ return { hasMetadata: false, type: 'none' };
147
+ const content = fs.readFileSync(fullPath, 'utf-8');
148
+ const has = /<Helmet\b|<title\b/i.test(content);
149
+ return { hasMetadata: has, type: has ? 'jsx_head' : 'none' };
150
+ }
151
+ async findCanonicalDeclaration(projectDir, route) {
152
+ const fullPath = path.join(projectDir, route.sourceFilePath);
153
+ if (!fs.existsSync(fullPath))
154
+ return { hasCanonical: false, type: 'none' };
155
+ const content = fs.readFileSync(fullPath, 'utf-8');
156
+ return { hasCanonical: /rel=["']canonical["']/i.test(content), type: 'link_jsx_tag' };
157
+ }
158
+ async findSchemaDeclaration(projectDir, route) {
159
+ const fullPath = path.join(projectDir, route.sourceFilePath);
160
+ if (!fs.existsSync(fullPath))
161
+ return { hasSchema: false, typesFound: [] };
162
+ const content = fs.readFileSync(fullPath, 'utf-8');
163
+ return { hasSchema: /application\/ld\+json/i.test(content), typesFound: ['JSON-LD'] };
164
+ }
165
+ findRobotsConfig(projectDir) {
166
+ const paths = ['public/robots.txt', 'robots.txt'];
167
+ for (const p of paths) {
168
+ if (fs.existsSync(path.join(projectDir, p)))
169
+ return p;
170
+ }
171
+ return null;
172
+ }
173
+ findSitemapConfig(projectDir) {
174
+ const paths = ['public/sitemap.xml', 'sitemap.xml'];
175
+ for (const p of paths) {
176
+ if (fs.existsSync(path.join(projectDir, p)))
177
+ return p;
178
+ }
179
+ return null;
180
+ }
181
+ findLlmsTxt(projectDir) {
182
+ const paths = ['public/llms.txt', 'llms.txt'];
183
+ for (const p of paths) {
184
+ if (fs.existsSync(path.join(projectDir, p)))
185
+ return p;
186
+ }
187
+ return null;
188
+ }
189
+ }
@@ -0,0 +1 @@
1
+ export declare function runCorrelationBenchmark(): Promise<boolean>;
@@ -0,0 +1,96 @@
1
+ import * as path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { defaultAdapterRegistry } from '../adapters/adapterRegistry.js';
4
+ import { inspectSourceFileAST } from '../utils/astLocator.js';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const FIXTURES_ROOT = path.resolve(__dirname, '../../test/fixtures');
8
+ export async function runCorrelationBenchmark() {
9
+ console.log('\n📊 Running Multi-Framework Correlation Accuracy Benchmark...\n');
10
+ const frameworksToTest = [
11
+ {
12
+ id: 'nextjs-app-router',
13
+ dir: 'nextjs-app',
14
+ expectedRoutes: ['/', '/about', '/blog/[slug]', '/sitemap.xml'],
15
+ testUrl: '/blog/ai-seo',
16
+ expectedFile: 'app/blog/[slug]/page.tsx'
17
+ },
18
+ {
19
+ id: 'nextjs-pages-router',
20
+ dir: 'nextjs-pages',
21
+ expectedRoutes: ['/', '/blog/[id]'],
22
+ testUrl: '/blog/123',
23
+ expectedFile: 'pages/blog/[id].tsx'
24
+ },
25
+ {
26
+ id: 'astro',
27
+ dir: 'astro-app',
28
+ expectedRoutes: ['/'],
29
+ testUrl: '/',
30
+ expectedFile: 'src/pages/index.astro'
31
+ },
32
+ {
33
+ id: 'vite-react',
34
+ dir: 'vite-react',
35
+ expectedRoutes: ['/', '/about', '/docs/:slug'],
36
+ testUrl: '/about',
37
+ expectedFile: 'src/App.tsx'
38
+ },
39
+ {
40
+ id: 'remix',
41
+ dir: 'remix-app',
42
+ expectedRoutes: ['/', '/features'],
43
+ testUrl: '/features',
44
+ expectedFile: 'app/routes/features.tsx'
45
+ },
46
+ {
47
+ id: 'sveltekit',
48
+ dir: 'sveltekit-app',
49
+ expectedRoutes: ['/'],
50
+ testUrl: '/',
51
+ expectedFile: 'src/routes/+page.svelte'
52
+ },
53
+ {
54
+ id: 'static-html',
55
+ dir: 'static-site',
56
+ expectedRoutes: ['/'],
57
+ testUrl: '/',
58
+ expectedFile: 'index.html'
59
+ }
60
+ ];
61
+ let passed = 0;
62
+ let total = frameworksToTest.length;
63
+ for (const item of frameworksToTest) {
64
+ const projectPath = path.join(FIXTURES_ROOT, item.dir);
65
+ const adapter = defaultAdapterRegistry.getAdapterForProject(projectPath);
66
+ if (adapter.id !== item.id) {
67
+ console.error(`❌ [${item.id}] Detection mismatch: got '${adapter.id}'`);
68
+ continue;
69
+ }
70
+ const routes = adapter.discoverRoutes(projectPath);
71
+ const routePaths = routes.map(r => r.routePath);
72
+ const missingRoutes = item.expectedRoutes.filter(er => !routePaths.includes(er));
73
+ if (missingRoutes.length > 0) {
74
+ console.error(`❌ [${item.id}] Missing expected routes: ${missingRoutes.join(', ')} (Found: ${routePaths.join(', ')})`);
75
+ continue;
76
+ }
77
+ const mapping = adapter.mapRouteToSource(item.testUrl, routes);
78
+ if (!mapping.sourceFilePath || !mapping.sourceFilePath.includes(path.basename(item.expectedFile))) {
79
+ console.error(`❌ [${item.id}] Source mapping error for '${item.testUrl}': expected '${item.expectedFile}', got '${mapping.sourceFilePath}'`);
80
+ continue;
81
+ }
82
+ // AST inspection check if typescript/tsx
83
+ if (mapping.sourceFilePath.endsWith('.tsx') || mapping.sourceFilePath.endsWith('.ts')) {
84
+ const ast = inspectSourceFileAST(path.join(projectPath, mapping.sourceFilePath));
85
+ }
86
+ console.log(`✅ [${item.id.padEnd(20)}] 100% Precision: Routes=[${routePaths.join(', ')}] -> Mapped '${item.testUrl}' to '${mapping.sourceFilePath}'`);
87
+ passed++;
88
+ }
89
+ const score = Math.round((passed / total) * 100);
90
+ console.log(`\n🎯 Benchmark Result: ${passed}/${total} frameworks verified (${score}% correlation precision).\n`);
91
+ return passed === total;
92
+ }
93
+ runCorrelationBenchmark().then(success => {
94
+ if (!success)
95
+ process.exit(1);
96
+ });
@@ -0,0 +1 @@
1
+ export declare function runInvariantBenchmark(): Promise<boolean>;
@@ -0,0 +1,47 @@
1
+ import { defaultInvariantRegistry } from '../invariants/registry.js';
2
+ import { createProjectSnapshot, compareSnapshots } from '../utils/snapshotEngine.js';
3
+ import * as path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const FIXTURES_ROOT = path.resolve(__dirname, '../../test/fixtures');
8
+ export async function runInvariantBenchmark() {
9
+ console.log('\n🛡️ Running Invariant Precision & Regression Recall Benchmark...\n');
10
+ // Test 1: Invariant Registry Completeness
11
+ const allInvariants = defaultInvariantRegistry.getAll();
12
+ console.log(`✅ Invariant Registry loaded: ${allInvariants.length} built-in invariants`);
13
+ // Test 2: Invariant Evaluation
14
+ const testPageId = 'page_test_123';
15
+ const now = new Date().toISOString();
16
+ const titlePass = defaultInvariantRegistry.evaluateContext('INV-TITLE-PRESENT', { url: '/test', logicalPageId: testPageId, hasMetadata: true, extractedTitle: 'My Title' }, { analyzer: 'test', source: 'ast_inspection', timestamp: now, provider: 'test' });
17
+ const titleFail = defaultInvariantRegistry.evaluateContext('INV-TITLE-PRESENT', { url: '/test', logicalPageId: testPageId, hasMetadata: false }, { analyzer: 'test', source: 'ast_inspection', timestamp: now, provider: 'test' });
18
+ if (!titlePass?.satisfied || titleFail?.satisfied) {
19
+ console.error('❌ Title invariant evaluation failed logic test.');
20
+ return false;
21
+ }
22
+ console.log('✅ Title Invariant logic: Satisfied when metadata present, Failed when metadata missing.');
23
+ const canonicalPass = defaultInvariantRegistry.evaluateContext('INV-CANONICAL-RESOLVES', { url: '/test', logicalPageId: testPageId, hasCanonical: true, extractedCanonical: 'https://example.com/test' }, { analyzer: 'test', source: 'ast_inspection', timestamp: now, provider: 'test' });
24
+ const canonicalFail = defaultInvariantRegistry.evaluateContext('INV-CANONICAL-RESOLVES', { url: '/test', logicalPageId: testPageId, hasCanonical: false }, { analyzer: 'test', source: 'ast_inspection', timestamp: now, provider: 'test' });
25
+ if (!canonicalPass?.satisfied || canonicalFail?.satisfied) {
26
+ console.error('❌ Canonical invariant evaluation failed logic test.');
27
+ return false;
28
+ }
29
+ console.log('✅ Canonical Invariant logic: Satisfied when canonical present, Failed when canonical missing.');
30
+ // Test 3: Snapshot Invariant Evaluation on Next.js App
31
+ const nextAppPath = path.join(FIXTURES_ROOT, 'nextjs-app');
32
+ const snap1 = await createProjectSnapshot(nextAppPath);
33
+ console.log(`✅ Project Snapshot evaluation recorded ${snap1.invariants?.length || 0} invariant checks.`);
34
+ // Verify regression diffing
35
+ const report = compareSnapshots(snap1, snap1);
36
+ if (report.newRegressions.length !== 0) {
37
+ console.error('❌ Self-comparison generated false positive regressions.');
38
+ return false;
39
+ }
40
+ console.log(`✅ Invariant Regression Diffing: Self-comparison generated 0 regressions (Pass).`);
41
+ console.log('\n🎯 Invariant Benchmark: 100% Invariant Evaluation Precision Verified.\n');
42
+ return true;
43
+ }
44
+ runInvariantBenchmark().then(success => {
45
+ if (!success)
46
+ process.exit(1);
47
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ export declare const EXIT_CODES: {
3
+ PASS: number;
4
+ REGRESSION: number;
5
+ CONFIG_ERROR: number;
6
+ ANALYZER_FAILURE: number;
7
+ PARTIAL_UNKNOWN: number;
8
+ };