seo-gravity-mcp 1.3.2 → 1.3.4
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 +1 -1
- package/dist/adapters/adapterRegistry.d.ts +1 -0
- package/dist/adapters/adapterRegistry.js +6 -2
- package/dist/adapters/staticAdapter.d.ts +1 -1
- package/dist/adapters/staticAdapter.js +3 -2
- package/dist/adapters/unknownAdapter.d.ts +16 -0
- package/dist/adapters/unknownAdapter.js +49 -0
- package/dist/benchmark/methodology.js +1 -1
- package/dist/invariants/registry.js +26 -17
- package/dist/invariants/types.d.ts +9 -1
- package/dist/policy/loader.d.ts +126 -3
- package/dist/policy/loader.js +96 -109
- package/dist/policy/profiles.d.ts +2 -2
- package/dist/policy/types.d.ts +2 -1
- package/dist/test.js +90 -20
- package/dist/tools/orchestration.js +3 -7
- package/dist/types/evidence.d.ts +4 -4
- package/dist/types/seo.d.ts +2 -0
- package/dist/utils/astLocator.d.ts +2 -0
- package/dist/utils/astLocator.js +10 -1
- package/dist/utils/crawlGraph.js +41 -51
- package/dist/utils/gitDiffEngine.js +58 -24
- package/dist/utils/jsdomRenderer.d.ts +2 -1
- package/dist/utils/jsdomRenderer.js +15 -17
- package/dist/utils/projectScanner.d.ts +1 -1
- package/dist/utils/projectScanner.js +4 -2
- package/dist/utils/scraper.d.ts +6 -7
- package/dist/utils/scraper.js +13 -25
- package/dist/utils/snapshotEngine.d.ts +1 -1
- package/dist/utils/snapshotEngine.js +60 -36
- package/dist/utils/urlNormalizer.d.ts +2 -0
- package/dist/utils/urlNormalizer.js +60 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
|
@@ -5,20 +5,9 @@ import { inspectSourceFileAST } from './astLocator.js';
|
|
|
5
5
|
import { defaultInvariantRegistry } from '../invariants/registry.js';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
|
-
import
|
|
9
|
-
export function computeLogicalPageId(urlOrPath) {
|
|
10
|
-
|
|
11
|
-
let origin = 'default';
|
|
12
|
-
try {
|
|
13
|
-
if (clean.startsWith('http')) {
|
|
14
|
-
const u = new URL(clean);
|
|
15
|
-
origin = u.origin.toLowerCase();
|
|
16
|
-
clean = u.pathname;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
catch { }
|
|
20
|
-
clean = clean.replace(/\/$/, '') || '/';
|
|
21
|
-
return 'page_' + crypto.createHash('sha256').update(`${origin}:${clean}`).digest('hex').substring(0, 12);
|
|
8
|
+
import { computeNormalizedLogicalPageId } from './urlNormalizer.js';
|
|
9
|
+
export function computeLogicalPageId(urlOrPath, baseUrl) {
|
|
10
|
+
return computeNormalizedLogicalPageId(urlOrPath, baseUrl);
|
|
22
11
|
}
|
|
23
12
|
export function extractGitMetadata(projectDir) {
|
|
24
13
|
try {
|
|
@@ -170,13 +159,18 @@ export async function createProjectSnapshot(projectPath, options = {}) {
|
|
|
170
159
|
const pageId = computeLogicalPageId(route.routePath);
|
|
171
160
|
const fullSrcPath = path.join(resolvedPath, route.sourceFilePath);
|
|
172
161
|
const ast = inspectSourceFileAST(fullSrcPath);
|
|
173
|
-
// Metadata invariant
|
|
162
|
+
// Metadata & Title invariant (disaggregated)
|
|
174
163
|
const hasMeta = ast.hasMetadataExport || ast.hasGenerateMetadata || route.hasHeadComponent;
|
|
164
|
+
const hasTitle = Boolean(ast.extractedTitle || route.hasHeadComponent);
|
|
175
165
|
const titleInv = defaultInvariantRegistry.evaluateContext('INV-TITLE-PRESENT', {
|
|
176
166
|
url: route.routePath,
|
|
177
167
|
logicalPageId: pageId,
|
|
168
|
+
sourceFilePath: route.sourceFilePath,
|
|
169
|
+
sourceRange: ast.metadataRange,
|
|
170
|
+
hasTitle,
|
|
178
171
|
hasMetadata: hasMeta,
|
|
179
|
-
extractedTitle: ast.extractedTitle
|
|
172
|
+
extractedTitle: ast.extractedTitle,
|
|
173
|
+
hasDescription: Boolean(ast.extractedDescription)
|
|
180
174
|
}, {
|
|
181
175
|
analyzer: 'astLocator',
|
|
182
176
|
source: 'ast_inspection',
|
|
@@ -190,6 +184,8 @@ export async function createProjectSnapshot(projectPath, options = {}) {
|
|
|
190
184
|
const canonicalInv = defaultInvariantRegistry.evaluateContext('INV-CANONICAL-RESOLVES', {
|
|
191
185
|
url: route.routePath,
|
|
192
186
|
logicalPageId: pageId,
|
|
187
|
+
sourceFilePath: route.sourceFilePath,
|
|
188
|
+
sourceRange: ast.canonicalRange,
|
|
193
189
|
hasCanonical,
|
|
194
190
|
extractedCanonical: ast.extractedCanonical
|
|
195
191
|
}, {
|
|
@@ -211,11 +207,9 @@ export async function createProjectSnapshot(projectPath, options = {}) {
|
|
|
211
207
|
evidenceType: 'observed',
|
|
212
208
|
evidence: `File '${route.sourceFilePath}' has no metadata declaration.`,
|
|
213
209
|
affectedUrl: route.routePath,
|
|
214
|
-
sourceLocation:
|
|
215
|
-
filePath: route.sourceFilePath,
|
|
216
|
-
|
|
217
|
-
endLine: 1
|
|
218
|
-
},
|
|
210
|
+
sourceLocation: ast.metadataRange
|
|
211
|
+
? { filePath: route.sourceFilePath, startLine: ast.metadataRange.startLine, endLine: ast.metadataRange.endLine }
|
|
212
|
+
: { filePath: route.sourceFilePath },
|
|
219
213
|
sourceRange: ast.metadataRange,
|
|
220
214
|
likelyRootCause: 'Page component does not declare SEO metadata export.',
|
|
221
215
|
recommendation: `Add metadata export or title tag in ${route.sourceFilePath}.`,
|
|
@@ -238,7 +232,9 @@ export async function createProjectSnapshot(projectPath, options = {}) {
|
|
|
238
232
|
evidenceType: 'observed',
|
|
239
233
|
evidence: `Route '${route.routePath}' (${route.sourceFilePath}) does not declare a canonical URL.`,
|
|
240
234
|
affectedUrl: route.routePath,
|
|
241
|
-
sourceLocation:
|
|
235
|
+
sourceLocation: ast.canonicalRange
|
|
236
|
+
? { filePath: route.sourceFilePath, startLine: ast.canonicalRange.startLine, endLine: ast.canonicalRange.endLine }
|
|
237
|
+
: { filePath: route.sourceFilePath },
|
|
242
238
|
sourceRange: ast.canonicalRange,
|
|
243
239
|
likelyRootCause: 'Page does not declare its canonical master URL, risking duplicate indexing.',
|
|
244
240
|
recommendation: `Add canonical URL declaration to ${route.sourceFilePath}.`,
|
|
@@ -283,7 +279,15 @@ export async function createProjectSnapshot(projectPath, options = {}) {
|
|
|
283
279
|
}
|
|
284
280
|
}
|
|
285
281
|
}
|
|
286
|
-
catch {
|
|
282
|
+
catch (err) {
|
|
283
|
+
crawlGraphSummary = {
|
|
284
|
+
state: 'FAILED',
|
|
285
|
+
error: err.message || 'Crawl error',
|
|
286
|
+
baseUrl,
|
|
287
|
+
pagesCrawled: 0,
|
|
288
|
+
orphanPages: []
|
|
289
|
+
};
|
|
290
|
+
}
|
|
287
291
|
}
|
|
288
292
|
// 4. Compute Multidimensional Scores
|
|
289
293
|
const scores = calculateMultiDimensionalScores(findings, {
|
|
@@ -390,19 +394,39 @@ export function compareSnapshots(baseline, current) {
|
|
|
390
394
|
}
|
|
391
395
|
else if (bInv && !cInv) {
|
|
392
396
|
// Invariant disappeared in current snapshot!
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
397
|
+
const currentRouteStillExists = (current.discoveredRoutes || []).some(r => r.routePath === bInv.url);
|
|
398
|
+
if (currentRouteStillExists) {
|
|
399
|
+
// Active route lost its invariant evidence -> Genuine regression
|
|
400
|
+
status = bInv.satisfied ? 'NEW_REGRESSION' : 'RESOLVED';
|
|
401
|
+
invariantDiffs.push({
|
|
402
|
+
invariantId: bInv.id,
|
|
403
|
+
logicalPageId: bInv.logicalPageId,
|
|
404
|
+
url: bInv.url,
|
|
405
|
+
status,
|
|
406
|
+
baselineSatisfied: bInv.satisfied,
|
|
407
|
+
currentSatisfied: false,
|
|
408
|
+
requirementLevel: bInv.requirementLevel || 'REQUIRED',
|
|
409
|
+
severity: bInv.severity || 'high',
|
|
410
|
+
evidence: bInv.evidence,
|
|
411
|
+
message: `${bInv.description}: Missing invariant evidence for active route (${status})`
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
// Route was intentionally deleted or removed -> Expected change
|
|
416
|
+
status = 'EXPECTED_CHANGE';
|
|
417
|
+
invariantDiffs.push({
|
|
418
|
+
invariantId: bInv.id,
|
|
419
|
+
logicalPageId: bInv.logicalPageId,
|
|
420
|
+
url: bInv.url,
|
|
421
|
+
status,
|
|
422
|
+
baselineSatisfied: bInv.satisfied,
|
|
423
|
+
currentSatisfied: false,
|
|
424
|
+
requirementLevel: bInv.requirementLevel || 'REQUIRED',
|
|
425
|
+
severity: 'low',
|
|
426
|
+
evidence: bInv.evidence,
|
|
427
|
+
message: `${bInv.description}: Route was removed (EXPECTED_CHANGE)`
|
|
428
|
+
});
|
|
429
|
+
}
|
|
406
430
|
}
|
|
407
431
|
}
|
|
408
432
|
// Score deltas
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
export function normalizeUrl(rawUrl, baseUrl) {
|
|
3
|
+
if (!rawUrl)
|
|
4
|
+
return '/';
|
|
5
|
+
try {
|
|
6
|
+
let resolved;
|
|
7
|
+
if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://')) {
|
|
8
|
+
resolved = new URL(rawUrl);
|
|
9
|
+
}
|
|
10
|
+
else if (baseUrl) {
|
|
11
|
+
resolved = new URL(rawUrl, baseUrl);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
resolved = new URL(rawUrl, 'http://localhost');
|
|
15
|
+
}
|
|
16
|
+
// Lowercase hostname
|
|
17
|
+
resolved.hostname = resolved.hostname.toLowerCase();
|
|
18
|
+
// Strip default ports
|
|
19
|
+
if ((resolved.protocol === 'http:' && resolved.port === '80') ||
|
|
20
|
+
(resolved.protocol === 'https:' && resolved.port === '443')) {
|
|
21
|
+
resolved.port = '';
|
|
22
|
+
}
|
|
23
|
+
// Normalize path (remove duplicate slashes, keep root or trim trailing slash)
|
|
24
|
+
let pathname = resolved.pathname.replace(/\/+/g, '/');
|
|
25
|
+
if (pathname.length > 1 && pathname.endsWith('/')) {
|
|
26
|
+
pathname = pathname.slice(0, -1);
|
|
27
|
+
}
|
|
28
|
+
resolved.pathname = pathname;
|
|
29
|
+
// Sort query parameters deterministically
|
|
30
|
+
const params = Array.from(resolved.searchParams.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
31
|
+
resolved.search = '';
|
|
32
|
+
for (const [k, v] of params) {
|
|
33
|
+
resolved.searchParams.append(k, v);
|
|
34
|
+
}
|
|
35
|
+
// Remove hash/fragment for canonical logical page identity
|
|
36
|
+
resolved.hash = '';
|
|
37
|
+
if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://') || baseUrl) {
|
|
38
|
+
return resolved.toString();
|
|
39
|
+
}
|
|
40
|
+
return resolved.pathname + resolved.search;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return rawUrl.trim().replace(/\/+/g, '/').replace(/\/$/, '') || '/';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function computeNormalizedLogicalPageId(urlOrPath, baseUrl) {
|
|
47
|
+
const norm = normalizeUrl(urlOrPath, baseUrl);
|
|
48
|
+
let origin = 'default';
|
|
49
|
+
let pathOnly = norm;
|
|
50
|
+
try {
|
|
51
|
+
if (norm.startsWith('http')) {
|
|
52
|
+
const u = new URL(norm);
|
|
53
|
+
origin = u.origin;
|
|
54
|
+
pathOnly = u.pathname + u.search;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch { }
|
|
58
|
+
const payload = `${origin}:${pathOnly}`;
|
|
59
|
+
return 'page_' + crypto.createHash('sha256').update(payload).digest('hex').substring(0, 12);
|
|
60
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "1.3.
|
|
1
|
+
export declare const VERSION = "1.3.4";
|
|
2
2
|
export declare const PACKAGE_NAME = "seo-gravity-mcp";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '1.3.
|
|
1
|
+
export const VERSION = '1.3.4';
|
|
2
2
|
export const PACKAGE_NAME = 'seo-gravity-mcp';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seo-gravity-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "SEO Engineering Infrastructure Layer for AI Coding Agents and CI/CD Pipelines",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"cheerio": "^1.0.0",
|
|
38
38
|
"fast-xml-parser": "^5.0.8",
|
|
39
39
|
"jsdom": "^26.0.0",
|
|
40
|
+
"yaml": "^2.9.0",
|
|
40
41
|
"zod": "^3.24.2"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|