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.
- package/README.md +118 -105
- package/dist/adapters/adapterRegistry.d.ts +7 -0
- package/dist/adapters/adapterRegistry.js +30 -0
- package/dist/adapters/astroAdapter.d.ts +17 -0
- package/dist/adapters/astroAdapter.js +191 -0
- package/dist/adapters/nextAppAdapter.d.ts +17 -0
- package/dist/adapters/nextAppAdapter.js +265 -0
- package/dist/adapters/nextPagesAdapter.d.ts +17 -0
- package/dist/adapters/nextPagesAdapter.js +209 -0
- package/dist/adapters/remixAdapter.d.ts +16 -0
- package/dist/adapters/remixAdapter.js +118 -0
- package/dist/adapters/staticAdapter.d.ts +16 -0
- package/dist/adapters/staticAdapter.js +117 -0
- package/dist/adapters/svelteKitAdapter.d.ts +16 -0
- package/dist/adapters/svelteKitAdapter.js +122 -0
- package/dist/adapters/types.d.ts +39 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/viteReactAdapter.d.ts +16 -0
- package/dist/adapters/viteReactAdapter.js +189 -0
- package/dist/benchmark/correlationBenchmark.d.ts +1 -0
- package/dist/benchmark/correlationBenchmark.js +96 -0
- package/dist/benchmark/invariantBenchmark.d.ts +1 -0
- package/dist/benchmark/invariantBenchmark.js +47 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +162 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8 -646
- package/dist/invariants/registry.d.ts +12 -0
- package/dist/invariants/registry.js +178 -0
- package/dist/invariants/types.d.ts +38 -0
- package/dist/invariants/types.js +1 -0
- package/dist/providers/providerRegistry.d.ts +65 -0
- package/dist/providers/providerRegistry.js +64 -0
- package/dist/providers/types.d.ts +22 -0
- package/dist/providers/types.js +1 -0
- package/dist/server/registry.d.ts +3 -0
- package/dist/server/registry.js +559 -0
- package/dist/server/server.d.ts +70 -0
- package/dist/server/server.js +53 -0
- package/dist/server/types.d.ts +7 -0
- package/dist/server/types.js +1 -0
- package/dist/test.js +108 -138
- package/dist/tools/orchestration.js +23 -21
- package/dist/types/canonical.d.ts +64 -0
- package/dist/types/canonical.js +1 -0
- package/dist/types/findings.d.ts +22 -0
- package/dist/types/findings.js +1 -1
- package/dist/types/seo.d.ts +24 -0
- package/dist/utils/astLocator.d.ts +14 -0
- package/dist/utils/astLocator.js +137 -0
- package/dist/utils/cacheManager.d.ts +29 -0
- package/dist/utils/cacheManager.js +60 -0
- package/dist/utils/experimentEngine.d.ts +26 -0
- package/dist/utils/experimentEngine.js +41 -0
- package/dist/utils/findingEngine.d.ts +2 -1
- package/dist/utils/findingEngine.js +1 -0
- package/dist/utils/gitDiffEngine.d.ts +28 -0
- package/dist/utils/gitDiffEngine.js +109 -0
- package/dist/utils/opportunityEngine.d.ts +19 -0
- package/dist/utils/opportunityEngine.js +61 -0
- package/dist/utils/projectScanner.d.ts +4 -2
- package/dist/utils/projectScanner.js +14 -346
- package/dist/utils/sarifExporter.d.ts +64 -0
- package/dist/utils/sarifExporter.js +81 -0
- package/dist/utils/snapshotEngine.d.ts +3 -1
- package/dist/utils/snapshotEngine.js +232 -94
- package/package.json +8 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/test.js
CHANGED
|
@@ -3,163 +3,133 @@ import { fileURLToPath } from 'url';
|
|
|
3
3
|
// Core Tool Imports
|
|
4
4
|
import { auditOnPage } from './tools/onpage.js';
|
|
5
5
|
import { auditGeoAiReadiness } from './tools/geo.js';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
import { auditProject,
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
6
|
+
// Modular Server & Agent Orchestration
|
|
7
|
+
import { createMcpServer } from './server/server.js';
|
|
8
|
+
import { TOOLS } from './server/registry.js';
|
|
9
|
+
import { auditProject, prioritizeFindings, createSnapshotTool, checkRegression } from './tools/orchestration.js';
|
|
10
|
+
import { defaultAdapterRegistry } from './adapters/adapterRegistry.js';
|
|
11
|
+
import { inspectSourceFileAST } from './utils/astLocator.js';
|
|
12
|
+
import { defaultCacheManager } from './utils/cacheManager.js';
|
|
13
|
+
import { defaultProviderRegistry } from './providers/providerRegistry.js';
|
|
14
|
+
import { detectOpportunities } from './utils/opportunityEngine.js';
|
|
15
|
+
import { exportFindingsToSarif } from './utils/sarifExporter.js';
|
|
16
|
+
import { defaultInvariantRegistry } from './invariants/registry.js';
|
|
13
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
18
|
const __dirname = path.dirname(__filename);
|
|
15
19
|
const FIXTURES_DIR = path.resolve(__dirname, '../test/fixtures');
|
|
16
20
|
async function runTests() {
|
|
17
|
-
console.log('🧪 Starting SEO Gravity MCP v1.0
|
|
21
|
+
console.log('🧪 Starting SEO Gravity MCP v1.2.0 (Engineering Infrastructure) Test Suite...\n');
|
|
18
22
|
// -------------------------------------------------------------
|
|
19
|
-
// Test 1:
|
|
23
|
+
// Test 1: Modular Server & 35 Tool Catalog
|
|
20
24
|
// -------------------------------------------------------------
|
|
21
|
-
console.log('1️⃣ Testing
|
|
25
|
+
console.log('1️⃣ Testing Modular Server Architecture...');
|
|
26
|
+
const server = createMcpServer({ name: 'seo-gravity-mcp', version: '1.2.0' });
|
|
27
|
+
console.log(` ✅ Server Initialized: ${TOOLS.length} tools registered across all 8 architectural layers.`);
|
|
28
|
+
// -------------------------------------------------------------
|
|
29
|
+
// Test 2: On-Page & GEO Evidence Analysis
|
|
30
|
+
// -------------------------------------------------------------
|
|
31
|
+
console.log('\n2️⃣ Testing On-Page & GEO Evidence Analysis...');
|
|
22
32
|
const sampleHtml = `
|
|
23
33
|
<!DOCTYPE html>
|
|
24
34
|
<html>
|
|
25
35
|
<head>
|
|
26
|
-
<title>Best Project Management Software in 2026
|
|
27
|
-
<meta name="description" content="
|
|
28
|
-
<link rel="canonical" href="https://example.com/best-
|
|
36
|
+
<title>Best Project Management Software in 2026</title>
|
|
37
|
+
<meta name="description" content="Compare the best agile tools for engineering teams.">
|
|
38
|
+
<link rel="canonical" href="https://example.com/best-tools">
|
|
29
39
|
</head>
|
|
30
40
|
<body>
|
|
31
|
-
<h1>Best Project Management Software
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
<h2>2. Top Features Comparison</h2>
|
|
35
|
-
<p>Key features include Gantt charts, real-time collaboration, and API integrations.</p>
|
|
36
|
-
<img src="hero.png" alt="Project management dashboard preview">
|
|
37
|
-
<a href="/pricing">View Pricing</a>
|
|
38
|
-
<a href="https://wikipedia.org/wiki/Project_management">Learn more on Wikipedia</a>
|
|
41
|
+
<h1>Best Project Management Software</h1>
|
|
42
|
+
<p>Project management software refers to digital platforms designed to plan and organize workflows.</p>
|
|
43
|
+
<a href="/pricing">Pricing</a>
|
|
39
44
|
</body>
|
|
40
45
|
</html>
|
|
41
46
|
`;
|
|
42
47
|
const onpage = await auditOnPage(sampleHtml, 'project management software');
|
|
43
|
-
|
|
48
|
+
const geo = await auditGeoAiReadiness(sampleHtml, 'best project management software');
|
|
49
|
+
console.log(` ✅ On-Page Score: ${onpage.overallScore}/100 | Title: ${onpage.titleAudit.status}`);
|
|
50
|
+
console.log(` ✅ GEO AI Retrieval Signals: Direct Answer=${geo.aiRetrievalSignals?.directEntityDefinition}`);
|
|
44
51
|
// -------------------------------------------------------------
|
|
45
|
-
// Test
|
|
52
|
+
// Test 3: Framework Adapters & Multi-Framework Route Discovery
|
|
46
53
|
// -------------------------------------------------------------
|
|
47
|
-
console.log('\
|
|
48
|
-
const geo = await auditGeoAiReadiness(sampleHtml, 'best project management software');
|
|
49
|
-
console.log(` ✅ GEO Score: ${geo.overallGeoScore}/100 | Citation: ${geo.citationLikelihood} | Confidence: ${geo.confidence}`);
|
|
50
|
-
console.log(` ✅ AI Retrieval Signals: Direct Answer=${geo.aiRetrievalSignals?.directEntityDefinition}, Stats=${geo.aiRetrievalSignals?.factualDataAndStats}`);
|
|
51
|
-
// -------------------------------------------------------------
|
|
52
|
-
// Test 3: Schema Generation & Validation
|
|
53
|
-
// -------------------------------------------------------------
|
|
54
|
-
console.log('\n3️⃣ Testing Schema.org JSON-LD Generation & Validation...');
|
|
55
|
-
const schema = generateSchemaMarkup('FAQPage', {
|
|
56
|
-
items: [
|
|
57
|
-
{ question: 'What is project management software?', answer: 'It is a platform to coordinate team tasks and roadmaps.' },
|
|
58
|
-
{ question: 'Is it free to start?', answer: 'Yes, free tiers are available for small teams.' }
|
|
59
|
-
]
|
|
60
|
-
});
|
|
61
|
-
const val = await validateSchema(schema.jsonLdScript.replace(/<script[^>]*>/, '').replace(/<\/script>/, ''));
|
|
62
|
-
console.log(` ✅ Schema Validated (${val.schemasDetectedCount} detected, Valid: ${val.schemas[0]?.isValid})`);
|
|
63
|
-
// -------------------------------------------------------------
|
|
64
|
-
// Test 4: Keyword Clustering & Intent Classification
|
|
65
|
-
// -------------------------------------------------------------
|
|
66
|
-
console.log('\n4️⃣ Testing Keyword Clustering & Search Intent Classification...');
|
|
67
|
-
const keywords = [
|
|
68
|
-
'best running shoes',
|
|
69
|
-
'buy running shoes online',
|
|
70
|
-
'how to choose running shoes',
|
|
71
|
-
'running shoes for flat feet',
|
|
72
|
-
'running shoes review',
|
|
73
|
-
'running shoes sale discount'
|
|
74
|
-
];
|
|
75
|
-
const clusters = clusterKeywords(keywords);
|
|
76
|
-
const intents = classifySearchIntent(keywords);
|
|
77
|
-
console.log(` ✅ Clustered ${keywords.length} keywords into ${clusters.clusterCount} cluster(s)`);
|
|
78
|
-
console.log(` ✅ Intent: "${keywords[1]}" -> ${intents[1].intent} (${(intents[1].confidenceScore * 100).toFixed(0)}%)`);
|
|
79
|
-
// -------------------------------------------------------------
|
|
80
|
-
// Test 5: Priority Scoring Algorithm
|
|
81
|
-
// -------------------------------------------------------------
|
|
82
|
-
console.log('\n5️⃣ Testing Finding Priority Scoring Algorithm...');
|
|
83
|
-
const p1 = calculatePriorityScore('critical', 1.0, 'trivial', 'site_wide');
|
|
84
|
-
const p2 = calculatePriorityScore('low', 0.8, 'high', 'isolated');
|
|
85
|
-
console.log(` ✅ Critical Site-wide Issue Priority: ${p1.priorityScore} (Tier: ${p1.priorityTier})`);
|
|
86
|
-
console.log(` ✅ Isolated Low-Severity Issue Priority: ${p2.priorityScore} (Tier: ${p2.priorityTier})`);
|
|
87
|
-
// -------------------------------------------------------------
|
|
88
|
-
// Test 6: Project Framework Detection & Route Discovery
|
|
89
|
-
// -------------------------------------------------------------
|
|
90
|
-
console.log('\n6️⃣ Testing Framework Scanner & Route Discovery on Fixtures...');
|
|
54
|
+
console.log('\n3️⃣ Testing Framework Adapters (Next.js, Astro, Remix, Vite, SvelteKit)...');
|
|
91
55
|
const nextAppPath = path.join(FIXTURES_DIR, 'nextjs-app');
|
|
92
|
-
const
|
|
93
|
-
const nextRoutes = discoverRoutes(nextAppPath
|
|
94
|
-
console.log(` ✅ Next.js
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
console.log(` ✅
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.log(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
console.log(
|
|
125
|
-
|
|
126
|
-
console.log(`
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
console.log(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
console.log(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log(`
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// -------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
console.log(` ✅
|
|
154
|
-
// -------------------------------------------------------------
|
|
155
|
-
// Test
|
|
156
|
-
// -------------------------------------------------------------
|
|
157
|
-
console.log('\
|
|
158
|
-
const
|
|
159
|
-
console.log(` ✅
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
56
|
+
const nextAdapter = defaultAdapterRegistry.getAdapterForProject(nextAppPath);
|
|
57
|
+
const nextRoutes = nextAdapter.discoverRoutes(nextAppPath);
|
|
58
|
+
console.log(` ✅ Next.js Adapter: ${nextAdapter.name} -> Discovered ${nextRoutes.length} route(s)`);
|
|
59
|
+
const pagesAppPath = path.join(FIXTURES_DIR, 'nextjs-pages');
|
|
60
|
+
const pagesAdapter = defaultAdapterRegistry.getAdapterForProject(pagesAppPath);
|
|
61
|
+
const pagesRoutes = pagesAdapter.discoverRoutes(pagesAppPath);
|
|
62
|
+
console.log(` ✅ Next.js Pages Adapter: ${pagesAdapter.name} -> Discovered ${pagesRoutes.length} route(s)`);
|
|
63
|
+
const viteAppPath = path.join(FIXTURES_DIR, 'vite-react');
|
|
64
|
+
const viteAdapter = defaultAdapterRegistry.getAdapterForProject(viteAppPath);
|
|
65
|
+
const viteRoutes = viteAdapter.discoverRoutes(viteAppPath);
|
|
66
|
+
console.log(` ✅ Vite React Adapter: ${viteAdapter.name} -> Discovered ${viteRoutes.length} route(s)`);
|
|
67
|
+
const remixAppPath = path.join(FIXTURES_DIR, 'remix-app');
|
|
68
|
+
const remixAdapter = defaultAdapterRegistry.getAdapterForProject(remixAppPath);
|
|
69
|
+
const remixRoutes = remixAdapter.discoverRoutes(remixAppPath);
|
|
70
|
+
console.log(` ✅ Remix Adapter: ${remixAdapter.name} -> Discovered ${remixRoutes.length} route(s)`);
|
|
71
|
+
const svelteAppPath = path.join(FIXTURES_DIR, 'sveltekit-app');
|
|
72
|
+
const svelteAdapter = defaultAdapterRegistry.getAdapterForProject(svelteAppPath);
|
|
73
|
+
const svelteRoutes = svelteAdapter.discoverRoutes(svelteAppPath);
|
|
74
|
+
console.log(` ✅ SvelteKit Adapter: ${svelteAdapter.name} -> Discovered ${svelteRoutes.length} route(s)`);
|
|
75
|
+
// -------------------------------------------------------------
|
|
76
|
+
// Test 4: Deep AST & Line-Range Correlation Engine
|
|
77
|
+
// -------------------------------------------------------------
|
|
78
|
+
console.log('\n4️⃣ Testing AST Inspection & Line-Range Locator...');
|
|
79
|
+
const homePageFile = path.join(nextAppPath, 'app/page.tsx');
|
|
80
|
+
const astResult = inspectSourceFileAST(homePageFile);
|
|
81
|
+
console.log(` ✅ AST Analysis for app/page.tsx:`);
|
|
82
|
+
console.log(` - Has Metadata Export: ${astResult.hasMetadataExport} (Lines ${astResult.metadataRange?.startLine}-${astResult.metadataRange?.endLine})`);
|
|
83
|
+
console.log(` - Title Extracted: "${astResult.extractedTitle}"`);
|
|
84
|
+
console.log(` - Canonical Declared: ${astResult.hasCanonicalDeclaration} ("${astResult.extractedCanonical}")`);
|
|
85
|
+
// -------------------------------------------------------------
|
|
86
|
+
// Test 5: Formal Invariant Registry & Evaluation
|
|
87
|
+
// -------------------------------------------------------------
|
|
88
|
+
console.log('\n5️⃣ Testing Formal Invariant Registry...');
|
|
89
|
+
const allInvariants = defaultInvariantRegistry.getAll();
|
|
90
|
+
console.log(` ✅ Loaded Invariants: ${allInvariants.length} formal rules (${allInvariants.map(i => i.id).join(', ')})`);
|
|
91
|
+
const snapResult = await createSnapshotTool(nextAppPath);
|
|
92
|
+
console.log(` ✅ Snapshot ID: ${snapResult.snapshot.snapshotId}`);
|
|
93
|
+
console.log(` ✅ Formal Invariants Evaluated: ${snapResult.snapshot.invariants?.length || 0} invariant check(s) recorded`);
|
|
94
|
+
const regCheck = await checkRegression(nextAppPath, snapResult.snapshot);
|
|
95
|
+
console.log(` ✅ Invariant Regression Check: ${regCheck.verdict}`);
|
|
96
|
+
// -------------------------------------------------------------
|
|
97
|
+
// Test 6: SARIF v2.1.0 Exporter
|
|
98
|
+
// -------------------------------------------------------------
|
|
99
|
+
console.log('\n6️⃣ Testing SARIF v2.1.0 Exporter for CI/CD...');
|
|
100
|
+
const sarif = exportFindingsToSarif(snapResult.snapshot.findings, nextAppPath);
|
|
101
|
+
console.log(` ✅ SARIF Report Generated: Version ${sarif.version}, Tool: ${sarif.runs[0].tool.driver.name}, Rules: ${sarif.runs[0].tool.driver.rules.length}, Results: ${sarif.runs[0].results.length}`);
|
|
102
|
+
// -------------------------------------------------------------
|
|
103
|
+
// Test 7: Cache Manager with Provenance
|
|
104
|
+
// -------------------------------------------------------------
|
|
105
|
+
console.log('\n7️⃣ Testing Content-Hash Cache Manager with Provenance...');
|
|
106
|
+
const cacheKey = defaultCacheManager.computeKey('test_ast', { file: 'page.tsx', size: 500 });
|
|
107
|
+
defaultCacheManager.set(cacheKey, { parsed: true }, 10000, 'typescript_ast');
|
|
108
|
+
const cachedVal = defaultCacheManager.getWithMetadata(cacheKey);
|
|
109
|
+
console.log(` ✅ Cache Provenance: Provider=${cachedVal?.metadata.provider}, Age=${cachedVal?.metadata.ageMs}ms, Key=${cachedVal?.metadata.key}`);
|
|
110
|
+
// -------------------------------------------------------------
|
|
111
|
+
// Test 8: Pluggable Provider Abstraction
|
|
112
|
+
// -------------------------------------------------------------
|
|
113
|
+
console.log('\n8️⃣ Testing Pluggable Provider Abstraction...');
|
|
114
|
+
const serpProv = defaultProviderRegistry.getSerpProvider();
|
|
115
|
+
const speedProv = defaultProviderRegistry.getPageSpeedProvider();
|
|
116
|
+
console.log(` ✅ Default SERP Provider: ${serpProv.name} (Free: ${serpProv.isFree})`);
|
|
117
|
+
console.log(` ✅ Default PageSpeed Provider: ${speedProv.name}`);
|
|
118
|
+
// -------------------------------------------------------------
|
|
119
|
+
// Test 9: Opportunity Engine
|
|
120
|
+
// -------------------------------------------------------------
|
|
121
|
+
console.log('\n9️⃣ Testing Strategic Opportunity Engine...');
|
|
122
|
+
const opps = await detectOpportunities(nextAppPath, ['saas pricing calculator', 'agile workflows']);
|
|
123
|
+
console.log(` ✅ Opportunities Discovered: ${opps.totalOpportunitiesFound} strategic opportunity item(s)`);
|
|
124
|
+
// -------------------------------------------------------------
|
|
125
|
+
// Test 10: Flagship Project Audit & Sprint Prioritization
|
|
126
|
+
// -------------------------------------------------------------
|
|
127
|
+
console.log('\n🔟 Testing Flagship Project Audit & Sprint Prioritization...');
|
|
128
|
+
const audit = await auditProject(nextAppPath);
|
|
129
|
+
const sprints = await prioritizeFindings(nextAppPath);
|
|
130
|
+
console.log(` ✅ Audit Complete: Overall Score=${audit.scores.overallHealth}/100 | Dynamic Routes=${audit.routesSummary.dynamicRoutesCount}`);
|
|
131
|
+
console.log(` ✅ Priority Sprints: Quick Wins=${sprints.sprints.quickWins.length}, Critical=${sprints.sprints.criticalBlockers.length}`);
|
|
132
|
+
console.log('\n🎉 ALL v1.2.0 ARCHITECTURAL & CI TEST SUITES PASSED CLEANLY!\n');
|
|
163
133
|
}
|
|
164
134
|
runTests().catch(err => {
|
|
165
135
|
console.error('❌ Test failed:', err);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { getProjectAdapter } from '../utils/projectScanner.js';
|
|
4
4
|
import { createProjectSnapshot, compareSnapshots } from '../utils/snapshotEngine.js';
|
|
5
5
|
import { calculateMultiDimensionalScores } from '../utils/findingEngine.js';
|
|
6
|
+
import { inspectSourceFileAST } from '../utils/astLocator.js';
|
|
6
7
|
export async function auditProject(projectPath, baseUrl, crawlDepth = 2) {
|
|
7
8
|
const snapshot = await createProjectSnapshot(projectPath, {
|
|
8
9
|
baseUrl,
|
|
@@ -21,7 +22,6 @@ export async function auditProject(projectPath, baseUrl, crawlDepth = 2) {
|
|
|
21
22
|
for (const f of snapshot.findings) {
|
|
22
23
|
findingsByCategory[f.category] = (findingsByCategory[f.category] || 0) + 1;
|
|
23
24
|
}
|
|
24
|
-
// Sort by priorityScore descending
|
|
25
25
|
const sortedFindings = [...snapshot.findings].sort((a, b) => b.priorityScore - a.priorityScore);
|
|
26
26
|
const missingMetaCount = snapshot.discoveredRoutes.filter(r => r.routeType === 'page' && !r.hasMetadataExport && !r.hasGenerateMetadata).length;
|
|
27
27
|
const missingSchemaCount = snapshot.discoveredRoutes.filter(r => r.routeType === 'page' && !r.hasSchemaMarkup).length;
|
|
@@ -34,6 +34,7 @@ export async function auditProject(projectPath, baseUrl, crawlDepth = 2) {
|
|
|
34
34
|
schemaVersion: 'seo.gravity/v1',
|
|
35
35
|
projectPath: snapshot.projectPath,
|
|
36
36
|
scannedAt: snapshot.createdAt,
|
|
37
|
+
gitMetadata: snapshot.gitMetadata,
|
|
37
38
|
framework: snapshot.frameworkInfo,
|
|
38
39
|
routesSummary: {
|
|
39
40
|
totalDiscovered: snapshot.discoveredRoutes.length,
|
|
@@ -60,9 +61,10 @@ export async function auditProject(projectPath, baseUrl, crawlDepth = 2) {
|
|
|
60
61
|
}
|
|
61
62
|
export async function diagnoseSeo(projectPath, targetUrlOrFile, focusIssueId) {
|
|
62
63
|
const resolvedPath = path.resolve(projectPath);
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
64
|
+
const adapter = getProjectAdapter(resolvedPath);
|
|
65
|
+
const frameworkInfo = adapter.getProjectInfo(resolvedPath);
|
|
66
|
+
const discoveredRoutes = adapter.discoverRoutes(resolvedPath);
|
|
67
|
+
const mapping = adapter.mapRouteToSource(targetUrlOrFile, discoveredRoutes);
|
|
66
68
|
let sourceFile = mapping.sourceFilePath;
|
|
67
69
|
if (!sourceFile && fs.existsSync(path.join(resolvedPath, targetUrlOrFile))) {
|
|
68
70
|
sourceFile = targetUrlOrFile;
|
|
@@ -70,22 +72,18 @@ export async function diagnoseSeo(projectPath, targetUrlOrFile, focusIssueId) {
|
|
|
70
72
|
const detectedIssues = [];
|
|
71
73
|
const rootCauses = [];
|
|
72
74
|
const fixBlueprints = [];
|
|
75
|
+
let ast = null;
|
|
73
76
|
let fileContent = '';
|
|
74
77
|
if (sourceFile) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// File read failed
|
|
78
|
+
const absPath = path.join(resolvedPath, sourceFile);
|
|
79
|
+
if (fs.existsSync(absPath)) {
|
|
80
|
+
fileContent = fs.readFileSync(absPath, 'utf-8');
|
|
81
|
+
ast = inspectSourceFileAST(absPath);
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
// Next.js App Router diagnostics
|
|
83
|
-
if (frameworkInfo.framework === 'nextjs-app-router' && sourceFile) {
|
|
84
|
-
|
|
85
|
-
const hasGenMeta = /export\s+(async\s+)?function\s+generateMetadata\b/.test(fileContent);
|
|
86
|
-
const hasCanonical = /canonical/.test(fileContent);
|
|
87
|
-
const hasSchema = /application\/ld\+json/.test(fileContent);
|
|
88
|
-
if (!hasMeta && !hasGenMeta) {
|
|
85
|
+
if (frameworkInfo.framework === 'nextjs-app-router' && sourceFile && ast) {
|
|
86
|
+
if (!ast.hasMetadataExport && !ast.hasGenerateMetadata) {
|
|
89
87
|
const issueId = 'SEO-METATAG-001';
|
|
90
88
|
detectedIssues.push({
|
|
91
89
|
id: issueId,
|
|
@@ -96,7 +94,8 @@ export async function diagnoseSeo(projectPath, targetUrlOrFile, focusIssueId) {
|
|
|
96
94
|
evidenceType: 'observed',
|
|
97
95
|
evidence: `File '${sourceFile}' contains no 'metadata' or 'generateMetadata' export.`,
|
|
98
96
|
affectedUrl: targetUrlOrFile,
|
|
99
|
-
sourceLocation: { filePath: sourceFile, line: 1 },
|
|
97
|
+
sourceLocation: { filePath: sourceFile, line: 1, startLine: 1, endLine: 1 },
|
|
98
|
+
sourceRange: { filePath: sourceFile, startLine: 1, endLine: 1, astNodeType: 'SourceFile' },
|
|
100
99
|
likelyRootCause: 'Next.js App Router relies on explicit metadata exports to generate <title> and <meta> tags.',
|
|
101
100
|
recommendation: 'Export a Metadata object or dynamic generateMetadata function.',
|
|
102
101
|
expectedImpact: 'Generates proper search snippet title and description.',
|
|
@@ -113,18 +112,20 @@ export async function diagnoseSeo(projectPath, targetUrlOrFile, focusIssueId) {
|
|
|
113
112
|
issueId,
|
|
114
113
|
description: 'Missing Next.js metadata export in page component.',
|
|
115
114
|
sourceCodeSnippet: fileContent.slice(0, 150),
|
|
115
|
+
sourceRange: { filePath: sourceFile, startLine: 1, endLine: 1 },
|
|
116
116
|
whyItOccurs: 'Without metadata export, Next.js falls back to empty root title, causing generic SERP snippets.'
|
|
117
117
|
});
|
|
118
118
|
fixBlueprints.push({
|
|
119
119
|
issueId,
|
|
120
120
|
title: 'Add static metadata export',
|
|
121
121
|
targetFile: sourceFile,
|
|
122
|
+
sourceRange: { filePath: sourceFile, startLine: 1, endLine: 1 },
|
|
122
123
|
actionType: 'modify_file',
|
|
123
124
|
codeToInsertOrReplace: `import type { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n title: 'Descriptive Page Title | Brand',\n description: 'High-converting search meta description under 155 characters.',\n};\n`,
|
|
124
125
|
verificationInstructions: 'Run `npm run build` to ensure type-check passes, then curl page and inspect <head>.'
|
|
125
126
|
});
|
|
126
127
|
}
|
|
127
|
-
if (!
|
|
128
|
+
if (!ast.hasCanonicalDeclaration) {
|
|
128
129
|
const issueId = 'SEO-CANONICAL-001';
|
|
129
130
|
detectedIssues.push({
|
|
130
131
|
id: issueId,
|
|
@@ -135,7 +136,8 @@ export async function diagnoseSeo(projectPath, targetUrlOrFile, focusIssueId) {
|
|
|
135
136
|
evidenceType: 'observed',
|
|
136
137
|
evidence: `File '${sourceFile}' does not declare alternates.canonical.`,
|
|
137
138
|
affectedUrl: targetUrlOrFile,
|
|
138
|
-
sourceLocation: { filePath: sourceFile },
|
|
139
|
+
sourceLocation: { filePath: sourceFile, line: 1 },
|
|
140
|
+
sourceRange: ast.canonicalRange || { filePath: sourceFile, startLine: 1, endLine: 1 },
|
|
139
141
|
likelyRootCause: 'Page does not declare its canonical master URL, risking duplicate content indexation.',
|
|
140
142
|
recommendation: 'Add alternates.canonical to page metadata.',
|
|
141
143
|
expectedImpact: 'Consolidates ranking signals onto the primary URL.',
|
|
@@ -152,6 +154,7 @@ export async function diagnoseSeo(projectPath, targetUrlOrFile, focusIssueId) {
|
|
|
152
154
|
issueId,
|
|
153
155
|
title: 'Add canonical tag to metadata',
|
|
154
156
|
targetFile: sourceFile,
|
|
157
|
+
sourceRange: ast.metadataRange,
|
|
155
158
|
actionType: 'modify_file',
|
|
156
159
|
codeToInsertOrReplace: `alternates: {\n canonical: 'https://yourdomain.com${mapping.matchedRoute?.routePath || ''}',\n},`,
|
|
157
160
|
verificationInstructions: 'Verify <link rel="canonical"> in server response.'
|
|
@@ -163,6 +166,7 @@ export async function diagnoseSeo(projectPath, targetUrlOrFile, focusIssueId) {
|
|
|
163
166
|
targetUrlOrFile,
|
|
164
167
|
matchedRoute: mapping.matchedRoute,
|
|
165
168
|
sourceLocation: sourceFile ? { filePath: sourceFile, line: 1 } : undefined,
|
|
169
|
+
sourceRange: ast?.metadataRange || (sourceFile ? { filePath: sourceFile, startLine: 1, endLine: 1 } : undefined),
|
|
166
170
|
detectedIssues,
|
|
167
171
|
likelyRootCauses: rootCauses,
|
|
168
172
|
suggestedFixBlueprints: fixBlueprints
|
|
@@ -195,14 +199,12 @@ export async function prioritizeFindings(findingsOrProjectPath, maxCount = 20) {
|
|
|
195
199
|
minorPolish.push(f);
|
|
196
200
|
}
|
|
197
201
|
}
|
|
198
|
-
// Sort each sprint by priorityScore descending
|
|
199
202
|
quickWins.sort((a, b) => b.priorityScore - a.priorityScore);
|
|
200
203
|
criticalBlockers.sort((a, b) => b.priorityScore - a.priorityScore);
|
|
201
204
|
architecturalImprovements.sort((a, b) => b.priorityScore - a.priorityScore);
|
|
202
205
|
minorPolish.sort((a, b) => b.priorityScore - a.priorityScore);
|
|
203
206
|
const initialScores = calculateMultiDimensionalScores(findingsList);
|
|
204
207
|
const currentScore = initialScores.overallHealth;
|
|
205
|
-
// Estimate post quick-wins score
|
|
206
208
|
const remainingAfterQuickWins = findingsList.filter(f => !quickWins.includes(f));
|
|
207
209
|
const scoreAfterQuickWins = calculateMultiDimensionalScores(remainingAfterQuickWins).overallHealth;
|
|
208
210
|
const totalEffortHours = quickWins.length * 0.25 +
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type ProvenanceSource = 'rendered_dom' | 'raw_html' | 'ast_inspection' | 'http_headers' | 'robots_txt' | 'sitemap_xml' | 'static_analysis' | 'provider_api';
|
|
2
|
+
export interface Provenance {
|
|
3
|
+
analyzer: string;
|
|
4
|
+
source: ProvenanceSource;
|
|
5
|
+
timestamp: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PageIdentity {
|
|
9
|
+
logicalPageId: string;
|
|
10
|
+
canonicalUrl?: string;
|
|
11
|
+
observedUrl: string;
|
|
12
|
+
routePattern?: string;
|
|
13
|
+
normalizedPath: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Observation {
|
|
16
|
+
id: string;
|
|
17
|
+
logicalPageId: string;
|
|
18
|
+
observedUrl: string;
|
|
19
|
+
key: string;
|
|
20
|
+
rawValue: any;
|
|
21
|
+
normalizedValue?: string | number | boolean;
|
|
22
|
+
provenance: Provenance;
|
|
23
|
+
}
|
|
24
|
+
export interface ASTSourceRange {
|
|
25
|
+
filePath: string;
|
|
26
|
+
startLine: number;
|
|
27
|
+
endLine: number;
|
|
28
|
+
startColumn?: number;
|
|
29
|
+
endColumn?: number;
|
|
30
|
+
astNodeType?: string;
|
|
31
|
+
exportName?: string;
|
|
32
|
+
snippet?: string;
|
|
33
|
+
}
|
|
34
|
+
export type InvariantType = 'INVARIANT_HTTP_200' | 'INVARIANT_CANONICAL_PRESENT' | 'INVARIANT_INDEXABLE' | 'INVARIANT_SCHEMA_VALID' | 'INVARIANT_TITLE_PRESENT' | 'INVARIANT_DESCRIPTION_PRESENT' | 'INVARIANT_LINK_ACCESSIBLE' | 'INVARIANT_ROBOTS_ALLOWED';
|
|
35
|
+
export interface SEOInvariant {
|
|
36
|
+
id: InvariantType;
|
|
37
|
+
logicalPageId: string;
|
|
38
|
+
url: string;
|
|
39
|
+
description: string;
|
|
40
|
+
expectedCondition: string;
|
|
41
|
+
observedCondition: string;
|
|
42
|
+
satisfied: boolean;
|
|
43
|
+
provenance: Provenance;
|
|
44
|
+
}
|
|
45
|
+
export type InvariantDiffStatus = 'NEW_REGRESSION' | 'RESOLVED' | 'UNCHANGED' | 'EXPECTED_CHANGE' | 'UNKNOWN';
|
|
46
|
+
export interface InvariantDiffItem {
|
|
47
|
+
invariantId: InvariantType;
|
|
48
|
+
logicalPageId: string;
|
|
49
|
+
url: string;
|
|
50
|
+
status: InvariantDiffStatus;
|
|
51
|
+
baselineSatisfied: boolean;
|
|
52
|
+
currentSatisfied: boolean;
|
|
53
|
+
message: string;
|
|
54
|
+
sourceRange?: ASTSourceRange;
|
|
55
|
+
}
|
|
56
|
+
export interface GitMetadata {
|
|
57
|
+
commitHash?: string;
|
|
58
|
+
shortHash?: string;
|
|
59
|
+
branch?: string;
|
|
60
|
+
isDirty?: boolean;
|
|
61
|
+
author?: string;
|
|
62
|
+
commitDate?: string;
|
|
63
|
+
changedFilesSinceBase?: string[];
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/findings.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Provenance, Observation, ASTSourceRange, SEOInvariant, InvariantDiffItem, GitMetadata } from './canonical.js';
|
|
2
|
+
export * from './canonical.js';
|
|
1
3
|
export type FindingCategory = 'technical' | 'content' | 'discoverability' | 'authority' | 'entity' | 'performance' | 'ai_readiness';
|
|
2
4
|
export type SeverityLevel = 'critical' | 'high' | 'medium' | 'low' | 'info';
|
|
3
5
|
export type EvidenceType = 'observed' | 'derived' | 'heuristic' | 'predictive';
|
|
@@ -8,6 +10,8 @@ export interface SourceLocation {
|
|
|
8
10
|
filePath: string;
|
|
9
11
|
line?: number;
|
|
10
12
|
column?: number;
|
|
13
|
+
startLine?: number;
|
|
14
|
+
endLine?: number;
|
|
11
15
|
exportName?: string;
|
|
12
16
|
componentName?: string;
|
|
13
17
|
snippet?: string;
|
|
@@ -18,17 +22,24 @@ export interface SuggestedFix {
|
|
|
18
22
|
explanation: string;
|
|
19
23
|
snippet?: string;
|
|
20
24
|
diffPreview?: string;
|
|
25
|
+
startLine?: number;
|
|
26
|
+
endLine?: number;
|
|
21
27
|
}
|
|
22
28
|
export interface Finding {
|
|
23
29
|
id: string;
|
|
30
|
+
ruleId?: string;
|
|
24
31
|
category: FindingCategory;
|
|
25
32
|
title: string;
|
|
26
33
|
severity: SeverityLevel;
|
|
27
34
|
confidence: number;
|
|
28
35
|
evidenceType: EvidenceType;
|
|
29
36
|
evidence: string;
|
|
37
|
+
observations?: Observation[];
|
|
38
|
+
provenance?: Provenance;
|
|
30
39
|
affectedUrl: string;
|
|
40
|
+
logicalPageId?: string;
|
|
31
41
|
sourceLocation?: SourceLocation;
|
|
42
|
+
sourceRange?: ASTSourceRange;
|
|
32
43
|
likelyRootCause?: string;
|
|
33
44
|
recommendation: string;
|
|
34
45
|
suggestedFix?: SuggestedFix;
|
|
@@ -88,16 +99,19 @@ export interface DiscoveredRoute {
|
|
|
88
99
|
hasSchemaMarkup: boolean;
|
|
89
100
|
hasHeadComponent: boolean;
|
|
90
101
|
isClientComponent: boolean;
|
|
102
|
+
astSourceRange?: ASTSourceRange;
|
|
91
103
|
}
|
|
92
104
|
export interface RouteSourceMapping {
|
|
93
105
|
urlPath: string;
|
|
94
106
|
matchedRoute?: DiscoveredRoute;
|
|
95
107
|
sourceFilePath?: string;
|
|
108
|
+
sourceRange?: ASTSourceRange;
|
|
96
109
|
confidence: number;
|
|
97
110
|
resolutionMethod: 'exact_match' | 'pattern_match' | 'fallback_heuristic' | 'unmapped';
|
|
98
111
|
}
|
|
99
112
|
export interface CrawlGraphNode {
|
|
100
113
|
url: string;
|
|
114
|
+
logicalPageId?: string;
|
|
101
115
|
routePath?: string;
|
|
102
116
|
title: string;
|
|
103
117
|
statusCode: number;
|
|
@@ -134,10 +148,13 @@ export interface ProjectSnapshot {
|
|
|
134
148
|
snapshotId: string;
|
|
135
149
|
createdAt: string;
|
|
136
150
|
projectPath: string;
|
|
151
|
+
gitMetadata?: GitMetadata;
|
|
137
152
|
frameworkInfo: ProjectFrameworkInfo;
|
|
138
153
|
discoveredRoutes: DiscoveredRoute[];
|
|
139
154
|
scores: MultiDimensionalScores;
|
|
140
155
|
findings: Finding[];
|
|
156
|
+
observations?: Observation[];
|
|
157
|
+
invariants?: SEOInvariant[];
|
|
141
158
|
crawlGraph?: CrawlGraphSummary;
|
|
142
159
|
routeMappings: RouteSourceMapping[];
|
|
143
160
|
}
|
|
@@ -157,6 +174,7 @@ export interface RegressionReport {
|
|
|
157
174
|
resolvedFindings: Finding[];
|
|
158
175
|
newRegressions: Finding[];
|
|
159
176
|
unresolvedFindings: Finding[];
|
|
177
|
+
invariantDiffs?: InvariantDiffItem[];
|
|
160
178
|
totalResolvedCount: number;
|
|
161
179
|
totalNewRegressionsCount: number;
|
|
162
180
|
alerts: string[];
|
|
@@ -165,6 +183,7 @@ export interface ProjectAuditResult {
|
|
|
165
183
|
schemaVersion: 'seo.gravity/v1';
|
|
166
184
|
projectPath: string;
|
|
167
185
|
scannedAt: string;
|
|
186
|
+
gitMetadata?: GitMetadata;
|
|
168
187
|
framework: ProjectFrameworkInfo;
|
|
169
188
|
routesSummary: {
|
|
170
189
|
totalDiscovered: number;
|
|
@@ -189,17 +208,20 @@ export interface DiagnosticResult {
|
|
|
189
208
|
targetUrlOrFile: string;
|
|
190
209
|
matchedRoute?: DiscoveredRoute;
|
|
191
210
|
sourceLocation?: SourceLocation;
|
|
211
|
+
sourceRange?: ASTSourceRange;
|
|
192
212
|
detectedIssues: Finding[];
|
|
193
213
|
likelyRootCauses: Array<{
|
|
194
214
|
issueId: string;
|
|
195
215
|
description: string;
|
|
196
216
|
sourceCodeSnippet?: string;
|
|
217
|
+
sourceRange?: ASTSourceRange;
|
|
197
218
|
whyItOccurs: string;
|
|
198
219
|
}>;
|
|
199
220
|
suggestedFixBlueprints: Array<{
|
|
200
221
|
issueId: string;
|
|
201
222
|
title: string;
|
|
202
223
|
targetFile: string;
|
|
224
|
+
sourceRange?: ASTSourceRange;
|
|
203
225
|
actionType: 'modify_file' | 'create_file' | 'config_update';
|
|
204
226
|
codeToInsertOrReplace: string;
|
|
205
227
|
verificationInstructions: string;
|
package/dist/types/findings.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from './canonical.js';
|