seo-gravity-mcp 1.3.1 → 1.3.3

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 (39) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/adapterRegistry.d.ts +1 -0
  3. package/dist/adapters/adapterRegistry.js +6 -2
  4. package/dist/adapters/staticAdapter.d.ts +1 -1
  5. package/dist/adapters/staticAdapter.js +3 -2
  6. package/dist/adapters/unknownAdapter.d.ts +16 -0
  7. package/dist/adapters/unknownAdapter.js +49 -0
  8. package/dist/benchmark/methodology.js +1 -1
  9. package/dist/cli.js +4 -3
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +4 -2
  12. package/dist/invariants/registry.js +5 -5
  13. package/dist/invariants/types.d.ts +6 -0
  14. package/dist/policy/loader.d.ts +126 -1
  15. package/dist/policy/loader.js +85 -22
  16. package/dist/server/server.js +3 -2
  17. package/dist/test.js +165 -36
  18. package/dist/tools/orchestration.d.ts +3 -1
  19. package/dist/tools/orchestration.js +10 -8
  20. package/dist/tools/technical.js +6 -3
  21. package/dist/types/evidence.d.ts +2 -2
  22. package/dist/types/seo.d.ts +2 -0
  23. package/dist/utils/astLocator.d.ts +2 -0
  24. package/dist/utils/astLocator.js +15 -6
  25. package/dist/utils/cacheManager.d.ts +3 -0
  26. package/dist/utils/cacheManager.js +30 -0
  27. package/dist/utils/gitDiffEngine.js +7 -3
  28. package/dist/utils/jsdomRenderer.js +2 -0
  29. package/dist/utils/projectScanner.d.ts +1 -1
  30. package/dist/utils/projectScanner.js +4 -2
  31. package/dist/utils/scraper.js +20 -5
  32. package/dist/utils/searchEngines.js +28 -32
  33. package/dist/utils/snapshotEngine.d.ts +1 -1
  34. package/dist/utils/snapshotEngine.js +100 -38
  35. package/dist/utils/urlNormalizer.d.ts +2 -0
  36. package/dist/utils/urlNormalizer.js +60 -0
  37. package/dist/version.d.ts +2 -0
  38. package/dist/version.js +2 -0
  39. package/package.json +4 -3
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # 🚀 SEO Gravity (v1.3.1)
1
+ # 🚀 SEO Gravity (v1.3.3)
2
2
 
3
3
  <div align="center">
4
4
 
@@ -1,5 +1,6 @@
1
1
  import { FrameworkAdapter } from './types.js';
2
2
  export declare class AdapterRegistry {
3
+ private unknownAdapter;
3
4
  private adapters;
4
5
  getAdapterForProject(projectDir: string): FrameworkAdapter;
5
6
  getAdapterById(id: string): FrameworkAdapter | undefined;
@@ -15,7 +15,9 @@ import { PhpClassicAdapter } from './phpClassicAdapter.js';
15
15
  import { SsgAdapter } from './ssgAdapter.js';
16
16
  import { ViteReactAdapter } from './viteReactAdapter.js';
17
17
  import { StaticAdapter } from './staticAdapter.js';
18
+ import { UnknownAdapter } from './unknownAdapter.js';
18
19
  export class AdapterRegistry {
20
+ unknownAdapter = new UnknownAdapter();
19
21
  adapters = [
20
22
  // Specialized JS/TS Frameworks
21
23
  new NextAppAdapter(),
@@ -36,7 +38,7 @@ export class AdapterRegistry {
36
38
  // Static Site Generators & SPAs
37
39
  new SsgAdapter(),
38
40
  new ViteReactAdapter(),
39
- // Universal Fallback
41
+ // Static HTML Sites
40
42
  new StaticAdapter()
41
43
  ];
42
44
  getAdapterForProject(projectDir) {
@@ -45,9 +47,11 @@ export class AdapterRegistry {
45
47
  return adapter;
46
48
  }
47
49
  }
48
- return this.adapters[this.adapters.length - 1]; // fallback static
50
+ return this.unknownAdapter;
49
51
  }
50
52
  getAdapterById(id) {
53
+ if (id === 'unknown')
54
+ return this.unknownAdapter;
51
55
  return this.adapters.find(a => a.id === id);
52
56
  }
53
57
  getAllAdapters() {
@@ -3,7 +3,7 @@ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../ty
3
3
  export declare class StaticAdapter implements FrameworkAdapter {
4
4
  id: string;
5
5
  name: string;
6
- detect(_projectDir: string): boolean;
6
+ detect(projectDir: string): boolean;
7
7
  getProjectInfo(projectDir: string): ProjectFrameworkInfo;
8
8
  discoverRoutes(projectDir: string): DiscoveredRoute[];
9
9
  mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
@@ -3,8 +3,9 @@ import * as path from 'path';
3
3
  export class StaticAdapter {
4
4
  id = 'static-html';
5
5
  name = 'Static HTML / Web Application';
6
- detect(_projectDir) {
7
- return true; // Fallback
6
+ detect(projectDir) {
7
+ return fs.existsSync(path.join(projectDir, 'index.html')) ||
8
+ fs.existsSync(path.join(projectDir, 'public/index.html'));
8
9
  }
9
10
  getProjectInfo(projectDir) {
10
11
  return {
@@ -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 UnknownAdapter 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,49 @@
1
+ import * as path from 'path';
2
+ export class UnknownAdapter {
3
+ id = 'unknown';
4
+ name = 'Unknown / Unrecognized Framework';
5
+ detect(_projectDir) {
6
+ return false;
7
+ }
8
+ getProjectInfo(projectDir) {
9
+ return {
10
+ framework: 'unknown',
11
+ name: path.basename(projectDir),
12
+ hasTypeScript: false,
13
+ hasSitemapConfig: false,
14
+ hasRobotsConfig: false,
15
+ hasLlmsTxt: false,
16
+ rootDir: path.resolve(projectDir),
17
+ routesDir: '.',
18
+ defaultDevPort: 3000
19
+ };
20
+ }
21
+ discoverRoutes(_projectDir) {
22
+ return [];
23
+ }
24
+ mapRouteToSource(targetUrl, _routes) {
25
+ return {
26
+ urlPath: targetUrl,
27
+ confidence: 0.0,
28
+ resolutionMethod: 'unmapped'
29
+ };
30
+ }
31
+ async findMetadataImplementation(_projectDir, _route) {
32
+ return { hasMetadata: false, type: 'none' };
33
+ }
34
+ async findCanonicalDeclaration(_projectDir, _route) {
35
+ return { hasCanonical: false, type: 'none' };
36
+ }
37
+ async findSchemaDeclaration(_projectDir, _route) {
38
+ return { hasSchema: false, typesFound: [] };
39
+ }
40
+ findRobotsConfig(_projectDir) {
41
+ return null;
42
+ }
43
+ findSitemapConfig(_projectDir) {
44
+ return null;
45
+ }
46
+ findLlmsTxt(_projectDir) {
47
+ return null;
48
+ }
49
+ }
@@ -179,7 +179,7 @@ export function generateBenchmarkMethodologyReport() {
179
179
  }
180
180
  return {
181
181
  title: 'SEO Gravity Multi-Framework Correlation & Invariant Benchmark Methodology',
182
- version: '1.3.1',
182
+ version: '1.3.3',
183
183
  evaluatedAt: new Date().toISOString(),
184
184
  definitionOfCorrelation: 'A correlation is defined as a verified mapping connecting an observed URL or Route Pattern to its exact physical source file, template, route configuration, and AST symbol coordinate range.',
185
185
  totalFrameworksTested: BENCHMARK_FIXTURES.length,
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { runDifferentialAudit } from './utils/gitDiffEngine.js';
6
6
  import { exportFindingsToSarif } from './utils/sarifExporter.js';
7
7
  import { PolicyLoader } from './policy/loader.js';
8
8
  import { formatPrCommentMarkdown } from './utils/prCommentFormatter.js';
9
+ import { VERSION } from './version.js';
9
10
  export const EXIT_CODES = {
10
11
  PASS: 0,
11
12
  REGRESSION: 1,
@@ -57,7 +58,7 @@ async function runCli() {
57
58
  }
58
59
  else {
59
60
  // Pretty format
60
- console.log(`\n🚀 SEO Gravity CLI (v1.3.1) — SEO Engineering Infrastructure\n`);
61
+ console.log(`\n🚀 SEO Gravity CLI (v${VERSION}) — SEO Engineering Infrastructure\n`);
61
62
  console.log(`Auditing project at: ${path.resolve(projectDir)}...`);
62
63
  console.log(`Policy Profile: ${policy.profile}`);
63
64
  console.log(`\n========================================`);
@@ -93,7 +94,7 @@ async function runCli() {
93
94
  console.log(JSON.stringify(snap.snapshot, null, 2));
94
95
  }
95
96
  else {
96
- console.log(`\n🚀 SEO Gravity CLI (v1.3.1)\n`);
97
+ console.log(`\n🚀 SEO Gravity CLI (v${VERSION})\n`);
97
98
  console.log(`✅ Snapshot created and saved to: ${snap.savedToPath}`);
98
99
  console.log(`Score: ${snap.snapshot.scores.overallHealth}/100 | Invariants: ${snap.snapshot.invariants?.length || 0}`);
99
100
  }
@@ -105,7 +106,7 @@ async function runCli() {
105
106
  console.error('❌ Configuration Error: --baseline <path_to_snapshot.json> is required for check command.');
106
107
  process.exit(EXIT_CODES.CONFIG_ERROR);
107
108
  }
108
- const checkRes = await checkRegression(projectDir, baselinePath, baseUrl);
109
+ const checkRes = await checkRegression(projectDir, baselinePath, baseUrl, policy);
109
110
  if (format === 'sarif') {
110
111
  const sarif = exportFindingsToSarif(checkRes.regressionReport.newRegressions, projectDir);
111
112
  const serialized = JSON.stringify(sarif, null, 2);
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@
2
2
  export * from './server/types.js';
3
3
  export * from './server/registry.js';
4
4
  export * from './server/server.js';
5
+ export * from './version.js';
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { startStdioServer } from './server/server.js';
3
+ import { VERSION, PACKAGE_NAME } from './version.js';
3
4
  export * from './server/types.js';
4
5
  export * from './server/registry.js';
5
6
  export * from './server/server.js';
7
+ export * from './version.js';
6
8
  // Start Stdio MCP Server
7
9
  startStdioServer({
8
- name: 'seo-gravity-mcp',
9
- version: '1.2.0'
10
+ name: PACKAGE_NAME,
11
+ version: VERSION
10
12
  }).catch((err) => {
11
13
  console.error('Fatal error starting SEO Gravity MCP Server:', err);
12
14
  process.exit(1);
@@ -49,8 +49,8 @@ export const BUILTIN_INVARIANTS = [
49
49
  type: 'ast',
50
50
  description: has ? 'Canonical declaration found in AST' : 'Canonical missing from source AST',
51
51
  sourceFile: ctx.sourceFilePath,
52
- startLine: 1,
53
- endLine: 1,
52
+ startLine: ctx.sourceRange?.startLine,
53
+ endLine: ctx.sourceRange?.endLine,
54
54
  timestamp: new Date().toISOString()
55
55
  } : undefined
56
56
  };
@@ -69,7 +69,7 @@ export const BUILTIN_INVARIANTS = [
69
69
  remediationGuide: 'Export title in page metadata, define @section/block title, or add <title> in component head.',
70
70
  verificationMethod: 'Inspect page <title> in AST, template, or rendered DOM.',
71
71
  evaluate(ctx) {
72
- const has = Boolean(ctx.hasMetadata || ctx.extractedTitle);
72
+ const has = ctx.hasTitle !== undefined ? ctx.hasTitle : Boolean(ctx.extractedTitle || ctx.hasMetadata);
73
73
  return {
74
74
  satisfied: has,
75
75
  observedCondition: has ? `Title present ("${ctx.extractedTitle || 'Declared'}")` : 'Title missing',
@@ -78,8 +78,8 @@ export const BUILTIN_INVARIANTS = [
78
78
  type: 'ast',
79
79
  description: has ? `Title metadata: "${ctx.extractedTitle || 'Declared'}"` : 'Title metadata missing',
80
80
  sourceFile: ctx.sourceFilePath,
81
- startLine: 1,
82
- endLine: 1,
81
+ startLine: ctx.sourceRange?.startLine,
82
+ endLine: ctx.sourceRange?.endLine,
83
83
  timestamp: new Date().toISOString()
84
84
  } : undefined
85
85
  };
@@ -7,7 +7,13 @@ export interface InvariantEvaluationContext {
7
7
  url: string;
8
8
  logicalPageId: string;
9
9
  sourceFilePath?: string;
10
+ sourceRange?: {
11
+ startLine: number;
12
+ endLine: number;
13
+ };
10
14
  statusCode?: number;
15
+ hasTitle?: boolean;
16
+ hasDescription?: boolean;
11
17
  hasMetadata?: boolean;
12
18
  hasCanonical?: boolean;
13
19
  isIndexable?: boolean;
@@ -1,7 +1,132 @@
1
+ import { z } from 'zod';
1
2
  import { PolicyConfig } from './types.js';
2
3
  import { InvariantDiffItem } from '../types/canonical.js';
4
+ export declare const InvariantRuleSchema: z.ZodObject<{
5
+ enabled: z.ZodOptional<z.ZodBoolean>;
6
+ requirementLevel: z.ZodOptional<z.ZodEnum<["REQUIRED", "CONDITIONAL", "RECOMMENDED", "OPTIONAL"]>>;
7
+ severity: z.ZodOptional<z.ZodEnum<["critical", "high", "medium", "low", "info"]>>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ requirementLevel?: "REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL" | undefined;
10
+ severity?: "critical" | "high" | "medium" | "low" | "info" | undefined;
11
+ enabled?: boolean | undefined;
12
+ }, {
13
+ requirementLevel?: "REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL" | undefined;
14
+ severity?: "critical" | "high" | "medium" | "low" | "info" | undefined;
15
+ enabled?: boolean | undefined;
16
+ }>;
17
+ export declare const RegressionPolicySchema: z.ZodObject<{
18
+ failOnLevels: z.ZodOptional<z.ZodArray<z.ZodEnum<["REQUIRED", "CONDITIONAL", "RECOMMENDED", "OPTIONAL"]>, "many">>;
19
+ failOnSeverities: z.ZodOptional<z.ZodArray<z.ZodEnum<["critical", "high", "medium", "low", "info"]>, "many">>;
20
+ allowExpectedChanges: z.ZodOptional<z.ZodBoolean>;
21
+ maxAllowedRegressions: z.ZodOptional<z.ZodNumber>;
22
+ }, "strip", z.ZodTypeAny, {
23
+ allowExpectedChanges?: boolean | undefined;
24
+ failOnLevels?: ("REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL")[] | undefined;
25
+ failOnSeverities?: ("critical" | "high" | "medium" | "low" | "info")[] | undefined;
26
+ maxAllowedRegressions?: number | undefined;
27
+ }, {
28
+ allowExpectedChanges?: boolean | undefined;
29
+ failOnLevels?: ("REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL")[] | undefined;
30
+ failOnSeverities?: ("critical" | "high" | "medium" | "low" | "info")[] | undefined;
31
+ maxAllowedRegressions?: number | undefined;
32
+ }>;
33
+ export declare const PolicyConfigSchema: z.ZodObject<{
34
+ version: z.ZodOptional<z.ZodUnion<[z.ZodNumber, z.ZodString]>>;
35
+ profile: z.ZodOptional<z.ZodEnum<["strict", "balanced", "startup", "ecommerce", "documentation", "custom"]>>;
36
+ regression: z.ZodOptional<z.ZodObject<{
37
+ failOnLevels: z.ZodOptional<z.ZodArray<z.ZodEnum<["REQUIRED", "CONDITIONAL", "RECOMMENDED", "OPTIONAL"]>, "many">>;
38
+ failOnSeverities: z.ZodOptional<z.ZodArray<z.ZodEnum<["critical", "high", "medium", "low", "info"]>, "many">>;
39
+ allowExpectedChanges: z.ZodOptional<z.ZodBoolean>;
40
+ maxAllowedRegressions: z.ZodOptional<z.ZodNumber>;
41
+ }, "strip", z.ZodTypeAny, {
42
+ allowExpectedChanges?: boolean | undefined;
43
+ failOnLevels?: ("REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL")[] | undefined;
44
+ failOnSeverities?: ("critical" | "high" | "medium" | "low" | "info")[] | undefined;
45
+ maxAllowedRegressions?: number | undefined;
46
+ }, {
47
+ allowExpectedChanges?: boolean | undefined;
48
+ failOnLevels?: ("REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL")[] | undefined;
49
+ failOnSeverities?: ("critical" | "high" | "medium" | "low" | "info")[] | undefined;
50
+ maxAllowedRegressions?: number | undefined;
51
+ }>>;
52
+ invariants: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
53
+ enabled: z.ZodOptional<z.ZodBoolean>;
54
+ requirementLevel: z.ZodOptional<z.ZodEnum<["REQUIRED", "CONDITIONAL", "RECOMMENDED", "OPTIONAL"]>>;
55
+ severity: z.ZodOptional<z.ZodEnum<["critical", "high", "medium", "low", "info"]>>;
56
+ }, "strip", z.ZodTypeAny, {
57
+ requirementLevel?: "REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL" | undefined;
58
+ severity?: "critical" | "high" | "medium" | "low" | "info" | undefined;
59
+ enabled?: boolean | undefined;
60
+ }, {
61
+ requirementLevel?: "REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL" | undefined;
62
+ severity?: "critical" | "high" | "medium" | "low" | "info" | undefined;
63
+ enabled?: boolean | undefined;
64
+ }>>>;
65
+ policy: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
66
+ framework: z.ZodOptional<z.ZodObject<{
67
+ adapter: z.ZodOptional<z.ZodString>;
68
+ force: z.ZodOptional<z.ZodBoolean>;
69
+ entrypoint: z.ZodOptional<z.ZodString>;
70
+ }, "strip", z.ZodTypeAny, {
71
+ adapter?: string | undefined;
72
+ force?: boolean | undefined;
73
+ entrypoint?: string | undefined;
74
+ }, {
75
+ adapter?: string | undefined;
76
+ force?: boolean | undefined;
77
+ entrypoint?: string | undefined;
78
+ }>>;
79
+ }, "strip", z.ZodTypeAny, {
80
+ framework?: {
81
+ adapter?: string | undefined;
82
+ force?: boolean | undefined;
83
+ entrypoint?: string | undefined;
84
+ } | undefined;
85
+ invariants?: Record<string, {
86
+ requirementLevel?: "REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL" | undefined;
87
+ severity?: "critical" | "high" | "medium" | "low" | "info" | undefined;
88
+ enabled?: boolean | undefined;
89
+ }> | undefined;
90
+ version?: string | number | undefined;
91
+ profile?: "strict" | "balanced" | "startup" | "ecommerce" | "documentation" | "custom" | undefined;
92
+ regression?: {
93
+ allowExpectedChanges?: boolean | undefined;
94
+ failOnLevels?: ("REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL")[] | undefined;
95
+ failOnSeverities?: ("critical" | "high" | "medium" | "low" | "info")[] | undefined;
96
+ maxAllowedRegressions?: number | undefined;
97
+ } | undefined;
98
+ policy?: Record<string, any> | undefined;
99
+ }, {
100
+ framework?: {
101
+ adapter?: string | undefined;
102
+ force?: boolean | undefined;
103
+ entrypoint?: string | undefined;
104
+ } | undefined;
105
+ invariants?: Record<string, {
106
+ requirementLevel?: "REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL" | undefined;
107
+ severity?: "critical" | "high" | "medium" | "low" | "info" | undefined;
108
+ enabled?: boolean | undefined;
109
+ }> | undefined;
110
+ version?: string | number | undefined;
111
+ profile?: "strict" | "balanced" | "startup" | "ecommerce" | "documentation" | "custom" | undefined;
112
+ regression?: {
113
+ allowExpectedChanges?: boolean | undefined;
114
+ failOnLevels?: ("REQUIRED" | "CONDITIONAL" | "RECOMMENDED" | "OPTIONAL")[] | undefined;
115
+ failOnSeverities?: ("critical" | "high" | "medium" | "low" | "info")[] | undefined;
116
+ maxAllowedRegressions?: number | undefined;
117
+ } | undefined;
118
+ policy?: Record<string, any> | undefined;
119
+ }>;
3
120
  export declare class PolicyLoader {
4
121
  static resolvePolicy(projectDir?: string, explicitPath?: string): PolicyConfig;
5
- private static parseConfigFile;
122
+ static parseConfigFile(filePath: string): any;
123
+ static parseYaml(content: string): Record<string, any>;
6
124
  static isRegressionBreachingPolicy(diff: InvariantDiffItem, policy: PolicyConfig): boolean;
125
+ static evaluatePolicyGate(diffs: InvariantDiffItem[], policy: PolicyConfig): {
126
+ pass: boolean;
127
+ breachingDiffs: InvariantDiffItem[];
128
+ totalBreaches: number;
129
+ maxAllowed: number;
130
+ verdict: string;
131
+ };
7
132
  }
@@ -1,6 +1,31 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
+ import * as YAML from 'yaml';
4
+ import { z } from 'zod';
3
5
  import { BUILTIN_PROFILES } from './profiles.js';
6
+ export const InvariantRuleSchema = z.object({
7
+ enabled: z.boolean().optional(),
8
+ requirementLevel: z.enum(['REQUIRED', 'CONDITIONAL', 'RECOMMENDED', 'OPTIONAL']).optional(),
9
+ severity: z.enum(['critical', 'high', 'medium', 'low', 'info']).optional()
10
+ });
11
+ export const RegressionPolicySchema = z.object({
12
+ failOnLevels: z.array(z.enum(['REQUIRED', 'CONDITIONAL', 'RECOMMENDED', 'OPTIONAL'])).optional(),
13
+ failOnSeverities: z.array(z.enum(['critical', 'high', 'medium', 'low', 'info'])).optional(),
14
+ allowExpectedChanges: z.boolean().optional(),
15
+ maxAllowedRegressions: z.number().int().min(0).optional()
16
+ });
17
+ export const PolicyConfigSchema = z.object({
18
+ version: z.union([z.number(), z.string()]).optional(),
19
+ profile: z.enum(['strict', 'balanced', 'startup', 'ecommerce', 'documentation', 'custom']).optional(),
20
+ regression: RegressionPolicySchema.optional(),
21
+ invariants: z.record(InvariantRuleSchema).optional(),
22
+ policy: z.record(z.any()).optional(),
23
+ framework: z.object({
24
+ adapter: z.string().optional(),
25
+ force: z.boolean().optional(),
26
+ entrypoint: z.string().optional()
27
+ }).optional()
28
+ });
4
29
  export class PolicyLoader {
5
30
  static resolvePolicy(projectDir = '.', explicitPath) {
6
31
  let rawConfig = null;
@@ -27,18 +52,39 @@ export class PolicyLoader {
27
52
  if (!rawConfig) {
28
53
  return baseProfile;
29
54
  }
30
- // Merge overrides
55
+ // Normalize snake_case regression options to camelCase
56
+ const rawReg = rawConfig.regression || {};
57
+ const normalizedRegression = {
58
+ ...baseProfile.regression,
59
+ failOnLevels: rawReg.failOnLevels || rawReg.fail_on_levels || baseProfile.regression?.failOnLevels,
60
+ failOnSeverities: rawReg.failOnSeverities || rawReg.fail_on_severities || baseProfile.regression?.failOnSeverities,
61
+ allowExpectedChanges: rawReg.allowExpectedChanges ?? rawReg.allow_expected_changes ?? baseProfile.regression?.allowExpectedChanges,
62
+ maxAllowedRegressions: rawReg.maxAllowedRegressions ?? rawReg.max_allowed_regressions ?? baseProfile.regression?.maxAllowedRegressions
63
+ };
64
+ // Normalize invariant policy overrides
65
+ const mergedInvariants = {
66
+ ...baseProfile.invariants,
67
+ ...(rawConfig.invariants || {})
68
+ };
69
+ // Support convenient high-level policy shortcuts (e.g. policy: { canonical: required, sitemap: recommended })
70
+ const rawPolicy = rawConfig.policy || {};
71
+ if (rawPolicy.canonical) {
72
+ const lvl = String(rawPolicy.canonical).toUpperCase();
73
+ mergedInvariants['INV-CANONICAL-RESOLVES'] = { requirementLevel: lvl, severity: 'high', enabled: true };
74
+ }
75
+ if (rawPolicy.sitemap) {
76
+ const lvl = String(rawPolicy.sitemap).toUpperCase();
77
+ mergedInvariants['INV-SITEMAP-PRESENT'] = { requirementLevel: lvl, severity: 'medium', enabled: true };
78
+ }
79
+ if (rawPolicy.llms_txt || rawPolicy.llmsTxt) {
80
+ const lvl = String(rawPolicy.llms_txt || rawPolicy.llmsTxt).toUpperCase();
81
+ mergedInvariants['INV-LLMS-TXT'] = { requirementLevel: lvl, severity: 'low', enabled: true };
82
+ }
31
83
  const merged = {
32
84
  version: rawConfig.version || baseProfile.version,
33
85
  profile: baseProfileName,
34
- regression: {
35
- ...baseProfile.regression,
36
- ...rawConfig.regression
37
- },
38
- invariants: {
39
- ...baseProfile.invariants,
40
- ...rawConfig.invariants
41
- },
86
+ regression: normalizedRegression,
87
+ invariants: mergedInvariants,
42
88
  framework: rawConfig.framework
43
89
  };
44
90
  return merged;
@@ -49,29 +95,30 @@ export class PolicyLoader {
49
95
  if (filePath.endsWith('.json')) {
50
96
  return JSON.parse(content);
51
97
  }
52
- // Basic YAML key-value parser for simple .seo-gravity.yml
53
- const parsed = { profile: 'balanced', invariants: {}, regression: {} };
54
- const lines = content.split('\n');
55
- for (const line of lines) {
56
- const trimmed = line.trim();
57
- if (!trimmed || trimmed.startsWith('#'))
58
- continue;
59
- if (trimmed.startsWith('profile:')) {
60
- parsed.profile = trimmed.split(':')[1].trim().replace(/['"]/g, '');
61
- }
62
- }
63
- return parsed;
98
+ return this.parseYaml(content);
64
99
  }
65
100
  catch {
66
101
  return null;
67
102
  }
68
103
  }
104
+ static parseYaml(content) {
105
+ try {
106
+ const parsed = YAML.parse(content);
107
+ return (typeof parsed === 'object' && parsed !== null) ? parsed : {};
108
+ }
109
+ catch {
110
+ return {};
111
+ }
112
+ }
69
113
  static isRegressionBreachingPolicy(diff, policy) {
114
+ if (diff.status === 'EXPECTED_CHANGE') {
115
+ return policy.regression?.allowExpectedChanges === false;
116
+ }
70
117
  if (diff.status !== 'NEW_REGRESSION')
71
118
  return false;
72
119
  const invOverride = policy.invariants?.[diff.invariantId];
73
120
  if (invOverride && invOverride.enabled === false) {
74
- return false; // Disabled by project policy
121
+ return false; // Explicitly disabled by project policy
75
122
  }
76
123
  const level = invOverride?.requirementLevel || diff.requirementLevel || 'REQUIRED';
77
124
  const severity = invOverride?.severity || diff.severity || 'high';
@@ -81,4 +128,20 @@ export class PolicyLoader {
81
128
  const severityBreached = failSeverities.includes(severity);
82
129
  return levelBreached && severityBreached;
83
130
  }
131
+ static evaluatePolicyGate(diffs, policy) {
132
+ const breachingDiffs = diffs.filter(d => this.isRegressionBreachingPolicy(d, policy));
133
+ const totalBreaches = breachingDiffs.length;
134
+ const maxAllowed = policy.regression?.maxAllowedRegressions ?? 0;
135
+ const pass = totalBreaches <= maxAllowed;
136
+ const verdict = pass
137
+ ? `✅ PASSED (Policy: ${policy.profile}): ${totalBreaches} breach(es) within allowed threshold (max: ${maxAllowed}).`
138
+ : `🚨 FAILED (Policy: ${policy.profile}): ${totalBreaches} invariant regression(s) breached policy (max allowed: ${maxAllowed}).`;
139
+ return {
140
+ pass,
141
+ breachingDiffs,
142
+ totalBreaches,
143
+ maxAllowed,
144
+ verdict
145
+ };
146
+ }
84
147
  }
@@ -2,10 +2,11 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { TOOLS, executeTool } from './registry.js';
5
+ import { VERSION, PACKAGE_NAME } from '../version.js';
5
6
  export function createMcpServer(options = {}) {
6
7
  const server = new Server({
7
- name: options.name || 'seo-gravity-mcp',
8
- version: options.version || '1.2.0'
8
+ name: options.name || PACKAGE_NAME,
9
+ version: options.version || VERSION
9
10
  }, {
10
11
  capabilities: {
11
12
  tools: {}