seo-gravity-mcp 1.2.0 → 1.3.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 (48) hide show
  1. package/README.md +91 -54
  2. package/dist/adapters/adapterRegistry.d.ts +1 -0
  3. package/dist/adapters/adapterRegistry.js +29 -2
  4. package/dist/adapters/gatsbyAdapter.d.ts +17 -0
  5. package/dist/adapters/gatsbyAdapter.js +134 -0
  6. package/dist/adapters/laravelAdapter.d.ts +17 -0
  7. package/dist/adapters/laravelAdapter.js +192 -0
  8. package/dist/adapters/nuxtAdapter.d.ts +17 -0
  9. package/dist/adapters/nuxtAdapter.js +183 -0
  10. package/dist/adapters/phpClassicAdapter.d.ts +17 -0
  11. package/dist/adapters/phpClassicAdapter.js +135 -0
  12. package/dist/adapters/qwikCityAdapter.d.ts +17 -0
  13. package/dist/adapters/qwikCityAdapter.js +156 -0
  14. package/dist/adapters/solidStartAdapter.d.ts +17 -0
  15. package/dist/adapters/solidStartAdapter.js +132 -0
  16. package/dist/adapters/ssgAdapter.d.ts +17 -0
  17. package/dist/adapters/ssgAdapter.js +150 -0
  18. package/dist/adapters/symfonyAdapter.d.ts +17 -0
  19. package/dist/adapters/symfonyAdapter.js +136 -0
  20. package/dist/adapters/tanStackRouterAdapter.d.ts +17 -0
  21. package/dist/adapters/tanStackRouterAdapter.js +179 -0
  22. package/dist/adapters/wordpressAdapter.d.ts +17 -0
  23. package/dist/adapters/wordpressAdapter.js +184 -0
  24. package/dist/benchmark/correlationBenchmark.js +27 -72
  25. package/dist/benchmark/invariantBenchmark.js +43 -19
  26. package/dist/benchmark/methodology.d.ts +26 -0
  27. package/dist/benchmark/methodology.js +194 -0
  28. package/dist/cli.js +40 -7
  29. package/dist/invariants/registry.d.ts +1 -1
  30. package/dist/invariants/registry.js +101 -39
  31. package/dist/invariants/types.d.ts +5 -1
  32. package/dist/policy/loader.d.ts +7 -0
  33. package/dist/policy/loader.js +84 -0
  34. package/dist/policy/profiles.d.ts +2 -0
  35. package/dist/policy/profiles.js +102 -0
  36. package/dist/policy/types.d.ts +23 -0
  37. package/dist/policy/types.js +1 -0
  38. package/dist/test.js +57 -104
  39. package/dist/types/canonical.d.ts +12 -2
  40. package/dist/types/evidence.d.ts +70 -0
  41. package/dist/types/evidence.js +1 -0
  42. package/dist/types/findings.d.ts +1 -1
  43. package/dist/utils/gitDiffEngine.d.ts +14 -0
  44. package/dist/utils/gitDiffEngine.js +36 -8
  45. package/dist/utils/prCommentFormatter.d.ts +3 -0
  46. package/dist/utils/prCommentFormatter.js +63 -0
  47. package/dist/utils/snapshotEngine.js +3 -0
  48. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # 🚀 SEO Gravity (v1.2.0)
1
+ # 🚀 SEO Gravity (v1.3.0)
2
2
 
3
3
  <div align="center">
4
4
 
@@ -6,13 +6,14 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/seo-gravity-mcp?style=for-the-badge&logo=npm&color=CB3837)](https://www.npmjs.com/package/seo-gravity-mcp)
7
7
  ![MCP Protocol](https://img.shields.io/badge/MCP-Protocol-blue?style=for-the-badge&logo=anthropic)
8
8
  ![SARIF Ready](https://img.shields.io/badge/SARIF-GitHub_Code_Scanning-success?style=for-the-badge)
9
- ![CLI Ready](https://img.shields.io/badge/CLI-CI%2FCD_Ready-success?style=for-the-badge)
9
+ ![PR Comment Ready](https://img.shields.io/badge/PR_Comment-GitHub_Ready-blueviolet?style=for-the-badge)
10
+ ![17 Frameworks](https://img.shields.io/badge/Frameworks-17_Supported-blueviolet?style=for-the-badge)
10
11
  ![TypeScript](https://img.shields.io/badge/TypeScript-5.7+-3178C6?style=for-the-badge&logo=typescript&logoColor=white)
11
12
  ![Zero API Cost](https://img.shields.io/badge/Zero_API_Cost-100%25_Free-orange?style=for-the-badge)
12
13
 
13
14
  **The SEO Engineering Infrastructure Layer for AI Coding Agents & CI/CD Pipelines.**
14
15
 
15
- [⚡ Instant Start](#-instant-start) • [💻 CLI & CI/CD](#-command-line--cicd-runner) • [🛡️ Invariant Truth Layer](#-formal-seo-invariant-registry) • [🏛 5-Layer Architecture](#-5-layer-system-architecture) • [🧩 Framework Adapters](#-modular-framework-adapters) • [🛠 Tool Catalog (35 Tools)](#-complete-tool-catalog) • [📄 License](#-license)
16
+ [⚡ Instant Start](#-instant-start) • [💻 CLI & CI/CD](#-command-line--cicd-runner) • [🛡️ Invariant Truth & Policy](#-formal-seo-invariant-registry--policy-engine) • [🏛 5-Layer Architecture](#-5-layer-system-architecture) • [🧩 17 Framework Adapters](#-17-modular-framework-adapters--benchmark) • [🛠 Tool Catalog (35 Tools)](#-complete-tool-catalog) • [📄 License](#-license)
16
17
 
17
18
  </div>
18
19
 
@@ -22,12 +23,12 @@
22
23
 
23
24
  **SEO Gravity** is an **SEO engineering infrastructure layer** designed specifically for AI coding agents (Antigravity, Cursor, Claude Code, Windsurf, Cline, Roo Code) and automated CI/CD pipelines.
24
25
 
25
- Instead of operating as a passive reporting tool, SEO Gravity connects **rendered web observations** directly to **source code AST nodes and line numbers** across modern web frameworks (Next.js App/Pages Router, Astro, Vite/React, Remix, SvelteKit), generates actionable code fix blueprints, and enforces invariant-based regression checks.
26
+ Instead of operating as a passive reporting tool, SEO Gravity connects **rendered web observations** directly to **source code AST nodes, templates, and line numbers** across **17 modern web frameworks, full-stack backend architectures, and CMS ecosystems** (Next.js, WordPress, Laravel, Symfony, Astro, Vite, Remix, SvelteKit, Nuxt 3, TanStack Start, SolidStart, Qwik City, Gatsby, and Markdown SSGs), evaluates semantic SEO invariants against customizable project policies, generates actionable code fix blueprints, and enforces automated regression gates.
26
27
 
27
28
  ```text
28
29
  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
29
30
  │ 1. DISCOVER │ ──▶ │ 2. OBSERVE │ ──▶ │ 3. DIAGNOSE │ ──▶ │ 4. FIX │ ──▶ │ 5. VERIFY │
30
- │ Framework │ │ Live/Dev SSR │ │ AST Line Nos │ │ AI Modifies │ │ Invariant │
31
+ 17 Framework │ │ Live/Dev SSR │ │ AST Line Nos │ │ AI Modifies │ │ Invariant │
31
32
  │ Adapters │ │ Crawl Graph │ │ Prioritized │ │ Components │ │ Regression │
32
33
  │ Source Map │ │ Provenance │ │ Action Plan │ │ Snippets │ │ Git Stamping │
33
34
  └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
@@ -38,75 +39,106 @@ Instead of operating as a passive reporting tool, SEO Gravity connects **rendere
38
39
  ## 🏛 5-Layer System Architecture
39
40
 
40
41
  ```text
41
- ┌─────────────────────────────────────────────────────────────┐
42
- │ 1. AGENT & CLI INTERFACE
43
- MCP Server (35 tools) + Standalone CLI (`seo-gravity`)
44
- ├─────────────────────────────────────────────────────────────┤
45
- 2. INTELLIGENCE & REASONING LAYER
46
- │ Root-cause diagnosis, deterministic priority, fix plans │
47
- ├─────────────────────────────────────────────────────────────┤
48
- 3. ANALYSIS & INVARIANT LAYER
49
- SEO Invariants (HTTP 200, canonical, schema, noindex)
50
- ├─────────────────────────────────────────────────────────────┤
51
- 4. OBSERVATION LAYER (Factual Raw State)
52
- Observations Findings; AST inspection, headers, DOM
53
- ├─────────────────────────────────────────────────────────────┤
54
- 5. PROVIDER & ADAPTER LAYER
55
- │ Framework Adapters (Next, Astro, Vite), Providers, Cache │
56
- └─────────────────────────────────────────────────────────────┘
42
+ ┌─────────────────────────────────────────────────────────────────────────────┐
43
+ LAYER 1: AGENT & CLI INTERFACE
44
+ MCP Server (35 tools) + Standalone CLI (`seo-gravity`)
45
+ │ • GitHub PR Comment Formatter (`--format pr-comment`) │
46
+ • OASIS SARIF v2.1.0 Exporter (`--format sarif`)
47
+ ├─────────────────────────────────────────────────────────────────────────────┤
48
+ │ LAYER 2: INTELLIGENCE & REASONING LAYER │
49
+ Root-cause diagnosis, deterministic priority, fix plans
50
+ Strategic Opportunity Engine & SEO Experimentation Engine
51
+ ├─────────────────────────────────────────────────────────────────────────────┤
52
+ LAYER 3: ANALYSIS, POLICY & INVARIANT LAYER
53
+ Formal Invariant Registry (Requirement Levels vs Severity)
54
+ │ • Project Policy Engine (`.seo-gravity.yml` & 5 preset profiles) │
55
+ Semantic AST Git Differential Engine (Risk scoring)
56
+ ├─────────────────────────────────────────────────────────────────────────────┤
57
+ │ LAYER 4: OBSERVATION LAYER (Factual Raw State) │
58
+ │ • Observations ≠ Findings; Page Identity Normalization │
59
+ │ • Polymorphic Evidence: AST, Template, RouteConfig, DOM, Header, Runtime │
60
+ ├─────────────────────────────────────────────────────────────────────────────┤
61
+ │ LAYER 5: PROVIDER & ADAPTER LAYER │
62
+ │ • 17 Framework Adapters (JS/TS/PHP/SSG), Providers, Cache Provenance │
63
+ └─────────────────────────────────────────────────────────────────────────────┘
57
64
  ```
58
65
 
59
66
  ---
60
67
 
61
- ## 🛡️ Formal SEO Invariant Registry
68
+ ## 🛡️ Formal SEO Invariant Registry & Policy Engine
62
69
 
63
- SEO Gravity enforces strict **semantic invariants** rather than heuristic score drops. A regression occurs only when a previously satisfied invariant is violated:
70
+ SEO Gravity evaluates facts against formal invariant contracts and allows teams to configure organizational standards via `.seo-gravity.yml`:
64
71
 
65
- | Invariant ID | Name | Target Scope | Description |
66
- | :--- | :--- | :--- | :--- |
67
- | `INV-HTTP-200` | HTTP Success Status | Route | Published routes must return HTTP 200 OK without 4xx/5xx errors. |
68
- | `INV-CANONICAL-RESOLVES` | Canonical URL Declaration | Page | Indexable pages must declare a canonical URL to prevent duplicate indexing. |
69
- | `INV-TITLE-PRESENT` | Title Tag Metadata | Page | Pages must declare unique, descriptive title metadata in AST components. |
70
- | `INV-LINK-ACCESSIBLE` | Link Reachability | Crawl Graph | Public pages must be reachable via internal links (0 orphan pages). |
71
- | `INV-ROBOTS-ALLOWED` | Robots Configuration | Site | Project must provide `robots.txt` or `robots.ts` specifying bot directives. |
72
- | `INV-SITEMAP-PRESENT` | XML Sitemap Configuration | Site | Project must publish `sitemap.xml` or dynamic `sitemap.ts`. |
73
- | `INV-LLMS-TXT` | AI Context Documentation | Site | Project should provide `/llms.txt` for AI agent discovery. |
72
+ | Invariant ID | Name | Scope | Requirement Level | Severity | Semantic Failure Condition |
73
+ | :--- | :--- | :---: | :---: | :---: | :--- |
74
+ | `INV-HTTP-200` | HTTP Success Status | Route | **`REQUIRED`** | Critical | Published routes return 4xx or 5xx HTTP status. |
75
+ | `INV-CANONICAL-RESOLVES` | Canonical URL Declaration | Page | **`CONDITIONAL`** | High | Indexable page lacks `<link rel="canonical">` or canonical metadata. |
76
+ | `INV-TITLE-PRESENT` | Title Tag Metadata | Page | **`REQUIRED`** | High | Component metadata or `<title>` tag is missing. |
77
+ | `INV-LINK-ACCESSIBLE` | Link Reachability | Crawl Graph | **`CONDITIONAL`** | Medium | Public page has 0 internal inlinks (Orphan Page). |
78
+ | `INV-ROBOTS-ALLOWED` | Robots Policy Determinable | Site | **`CONDITIONAL`** | Medium | Bot directives are indeterminable or block critical assets. |
79
+ | `INV-SITEMAP-PRESENT` | XML Sitemap Configuration | Site | **`RECOMMENDED`** | Medium | Missing `sitemap.xml` or dynamic sitemap route. |
80
+ | `INV-LLMS-TXT` | AI Context Documentation | Site | **`RECOMMENDED`** | Low | Missing `/llms.txt` context file for AI crawlers (GEO). |
81
+
82
+ ### Project Policy Profiles (`.seo-gravity.yml`)
83
+
84
+ Configure project policies with built-in presets (`strict`, `balanced`, `startup`, `ecommerce`, `documentation`):
85
+
86
+ ```yaml
87
+ version: 1
88
+ profile: balanced # strict | balanced | startup | ecommerce | documentation
89
+
90
+ policy:
91
+ canonical: required
92
+ sitemap: recommended
93
+ llms_txt: recommended
94
+
95
+ regression:
96
+ fail_on_levels: [REQUIRED, CONDITIONAL]
97
+ fail_on_severities: [critical, high]
98
+ ```
74
99
 
75
100
  ---
76
101
 
77
- ## 🧩 Modular Framework Adapters & Benchmark
102
+ ## 🧩 17 Modular Framework Adapters & Benchmark
78
103
 
79
- Tested with **100% correlation accuracy** across 7 web application frameworks:
104
+ Tested with **100% correlation accuracy** across 17 web application frameworks and CMS ecosystems:
80
105
 
81
- | Framework Adapter | Detection | Route Discovery | AST Inspection | Benchmark |
106
+ | Category | Framework Adapter | Detection Signature | Routing & Template Model | Benchmark Result |
82
107
  | :--- | :--- | :--- | :--- | :---: |
83
- | **`NextAppAdapter`** | `next` + `app/` | App Router hierarchy, route groups `(group)`, dynamic `[slug]` | Pinpoints `metadata` export & `generateMetadata()` lines | **100%** |
84
- | **`NextPagesAdapter`** | `next` + `pages/` | Pages router hierarchy, dynamic `[id]` | Scans `<Head>` & `<NextSeo>` JSX components | **100%** |
85
- | **`AstroAdapter`** | `astro` + `src/pages` | `.astro`, `.md`, `.mdx` pages | Scans Astro frontmatter & `<title>` tags | **100%** |
86
- | **`ViteReactAdapter`** | `vite` / `react` | `index.html` + React Router routes | Scans `<Helmet>` & route definition trees | **100%** |
87
- | **`RemixAdapter`** | `@remix-run/react` | `app/routes/` | Inspects `meta` and `links` export functions | **100%** |
88
- | **`SvelteKitAdapter`** | `@sveltejs/kit` | `src/routes/` (`+page.svelte`) | Scans `<svelte:head>` components | **100%** |
89
- | **`StaticAdapter`** | Static HTML files | Recursive HTML directory traversal | Direct DOM / meta element inspection | **100%** |
108
+ | **JS / TS SSR** | **`NextAppAdapter`** | `next` + `app/` | App Router hierarchy, route groups `(group)`, dynamic `[slug]` | **100%** |
109
+ | **JS / TS SSR** | **`NextPagesAdapter`** | `next` + `pages/` | Pages router hierarchy, dynamic `[id]` | **100%** |
110
+ | **JS / TS SSR** | **`AstroAdapter`** | `astro` + `src/pages` | `.astro`, `.md`, `.mdx` pages | **100%** |
111
+ | **JS / TS SSR** | **`RemixAdapter`** | `@remix-run/react` | Flat routes `app/routes/`, index `_index.tsx`, dynamic `$slug` | **100%** |
112
+ | **JS / TS SSR** | **`SvelteKitAdapter`** | `@sveltejs/kit` | `src/routes/` (`+page.svelte`), dynamic `[slug]` | **100%** |
113
+ | **JS / TS SSR** | **`NuxtAdapter`** | `nuxt` in `package.json` | `pages/**/*.vue` with `useHead()`, `useSeoMeta()` | **100%** |
114
+ | **JS / TS SSR** | **`TanStackRouterAdapter`** | `@tanstack/react-router` | `src/routes/` with `createFileRoute` & `head()` | **100%** |
115
+ | **JS / TS SSR** | **`SolidStartAdapter`** | `@solidjs/start` | `src/routes/` with `<Title>`, `<Meta>`, `<Link>` | **100%** |
116
+ | **JS / TS SSR** | **`QwikCityAdapter`** | `@builder.io/qwik-city` | `src/routes/**/index.tsx` with `DocumentHead` | **100%** |
117
+ | **JS / TS SSG** | **`GatsbyAdapter`** | `gatsby` in `package.json` | `src/pages/` with `export const Head` | **100%** |
118
+ | **PHP / CMS** | **`WordPressAdapter`** | `style.css` / `wp-content` | Classic templates (`front-page.php`, `single.php`) & FSE block templates | **100%** |
119
+ | **PHP Fullstack** | **`LaravelAdapter`** | `artisan` / `laravel/framework` | `routes/web.php` + Blade templates (`@section('title')`) | **100%** |
120
+ | **PHP Fullstack** | **`SymfonyAdapter`** | `symfony.lock` / `composer.json` | `config/routes.yaml` / `#[Route]` + Twig templates (`{% block title %}`) | **100%** |
121
+ | **PHP Classic** | **`PhpClassicAdapter`** | `*.php` files | File-based routing (`index.php`, `about.php`) | **100%** |
122
+ | **Markdown SSG** | **`SsgAdapter`** | `hugo.toml`, `_config.yml`, `11ty` | `content/**/*.md`, `_posts/**/*.md` YAML/TOML frontmatter | **100%** |
123
+ | **React SPA** | **`ViteReactAdapter`** | `vite` + `react` | `index.html` + React Router route trees | **100%** |
124
+ | **Static HTML** | **`StaticAdapter`** | Static `.html` files | Recursive HTML directory traversal | **100%** |
90
125
 
91
126
  ---
92
127
 
93
- ## 💻 Command-Line & CI/CD Runner (SARIF Ready)
128
+ ## 💻 Command-Line & CI/CD Runner
94
129
 
95
- SEO Gravity includes a standalone CLI binary (`seo-gravity`) with native **SARIF v2.1.0** export for GitHub Code Scanning:
130
+ SEO Gravity includes a standalone CLI binary (`seo-gravity`) with native **SARIF v2.1.0** and **GitHub PR Comment** export:
96
131
 
97
132
  ```bash
98
- # 1. Audit project (Terminal / JSON / SARIF)
133
+ # 1. Audit project (Terminal / JSON / SARIF / PR-Comment)
99
134
  npx seo-gravity-mcp audit --project ./my-app
100
135
  npx seo-gravity-mcp audit --project ./my-app --format sarif --output seo-findings.sarif
101
136
 
102
- # 2. Create baseline snapshot
103
- npx seo-gravity-mcp snapshot --project ./my-app --output baseline.json
104
-
105
- # 3. Check for SEO regressions in CI/CD PR gate (Exits with 0 or 1)
106
- npx seo-gravity-mcp check --project ./my-app --baseline baseline.json
137
+ # 2. Check regressions in CI/CD against project policy (Exits 0 or 1)
138
+ npx seo-gravity-mcp check --project ./my-app --baseline baseline.json --policy .seo-gravity.yml --format pr-comment
107
139
 
108
- # 4. Targeted Semantic Git differential audit on modified routes only
109
- npx seo-gravity-mcp diff --project ./my-app --base-ref origin/main
140
+ # 3. Targeted Semantic Git differential audit & SEO Code Review on modified routes
141
+ npx seo-gravity-mcp review --project ./my-app --base-ref origin/main --format pr-comment --output pr-comment.md
110
142
  ```
111
143
 
112
144
  ### GitHub Actions CI/CD Integration
@@ -126,7 +158,12 @@ jobs:
126
158
  node-version: 20
127
159
 
128
160
  - name: Run SEO Invariant Regression Check
129
- run: npx seo-gravity-mcp check --project . --baseline .seo-baseline.json --format sarif --output results.sarif
161
+ run: |
162
+ npx seo-gravity-mcp check \
163
+ --project . \
164
+ --baseline .seo-baseline.json \
165
+ --format sarif \
166
+ --output results.sarif
130
167
 
131
168
  - name: Upload SARIF to GitHub Security Tab
132
169
  uses: github/codeql-action/upload-sarif@v3
@@ -171,7 +208,7 @@ npx -y seo-gravity-mcp
171
208
 
172
209
  ---
173
210
 
174
- ## 🛠 Complete Tool Catalog
211
+ ## 🛠 Complete Tool Catalog (35 Tools)
175
212
 
176
213
  ### Layer 0: Agent Orchestration & Remediation (Flagship)
177
214
  | Tool Name | Operational Class | Description |
@@ -3,5 +3,6 @@ export declare class AdapterRegistry {
3
3
  private adapters;
4
4
  getAdapterForProject(projectDir: string): FrameworkAdapter;
5
5
  getAdapterById(id: string): FrameworkAdapter | undefined;
6
+ getAllAdapters(): FrameworkAdapter[];
6
7
  }
7
8
  export declare const defaultAdapterRegistry: AdapterRegistry;
@@ -1,19 +1,43 @@
1
1
  import { NextAppAdapter } from './nextAppAdapter.js';
2
2
  import { NextPagesAdapter } from './nextPagesAdapter.js';
3
3
  import { AstroAdapter } from './astroAdapter.js';
4
- import { ViteReactAdapter } from './viteReactAdapter.js';
5
4
  import { RemixAdapter } from './remixAdapter.js';
6
5
  import { SvelteKitAdapter } from './svelteKitAdapter.js';
6
+ import { NuxtAdapter } from './nuxtAdapter.js';
7
+ import { TanStackRouterAdapter } from './tanStackRouterAdapter.js';
8
+ import { SolidStartAdapter } from './solidStartAdapter.js';
9
+ import { QwikCityAdapter } from './qwikCityAdapter.js';
10
+ import { GatsbyAdapter } from './gatsbyAdapter.js';
11
+ import { WordPressAdapter } from './wordpressAdapter.js';
12
+ import { LaravelAdapter } from './laravelAdapter.js';
13
+ import { SymfonyAdapter } from './symfonyAdapter.js';
14
+ import { PhpClassicAdapter } from './phpClassicAdapter.js';
15
+ import { SsgAdapter } from './ssgAdapter.js';
16
+ import { ViteReactAdapter } from './viteReactAdapter.js';
7
17
  import { StaticAdapter } from './staticAdapter.js';
8
18
  export class AdapterRegistry {
9
19
  adapters = [
20
+ // Specialized JS/TS Frameworks
10
21
  new NextAppAdapter(),
11
22
  new NextPagesAdapter(),
12
23
  new AstroAdapter(),
13
24
  new RemixAdapter(),
14
25
  new SvelteKitAdapter(),
26
+ new NuxtAdapter(),
27
+ new TanStackRouterAdapter(),
28
+ new SolidStartAdapter(),
29
+ new QwikCityAdapter(),
30
+ new GatsbyAdapter(),
31
+ // PHP & CMS Frameworks
32
+ new WordPressAdapter(),
33
+ new LaravelAdapter(),
34
+ new SymfonyAdapter(),
35
+ new PhpClassicAdapter(),
36
+ // Static Site Generators & SPAs
37
+ new SsgAdapter(),
15
38
  new ViteReactAdapter(),
16
- new StaticAdapter() // fallback
39
+ // Universal Fallback
40
+ new StaticAdapter()
17
41
  ];
18
42
  getAdapterForProject(projectDir) {
19
43
  for (const adapter of this.adapters) {
@@ -26,5 +50,8 @@ export class AdapterRegistry {
26
50
  getAdapterById(id) {
27
51
  return this.adapters.find(a => a.id === id);
28
52
  }
53
+ getAllAdapters() {
54
+ return [...this.adapters];
55
+ }
29
56
  }
30
57
  export const defaultAdapterRegistry = new AdapterRegistry();
@@ -0,0 +1,17 @@
1
+ import { FrameworkAdapter, FrameworkDetectionResult, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
2
+ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
3
+ export declare class GatsbyAdapter implements FrameworkAdapter {
4
+ id: string;
5
+ name: string;
6
+ detect(projectDir: string): boolean;
7
+ detectDetailed(projectDir: string): FrameworkDetectionResult;
8
+ getProjectInfo(projectDir: string): ProjectFrameworkInfo;
9
+ discoverRoutes(projectDir: string): DiscoveredRoute[];
10
+ mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
11
+ findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
12
+ findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
13
+ findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
14
+ findRobotsConfig(projectDir: string): string | null;
15
+ findSitemapConfig(projectDir: string): string | null;
16
+ findLlmsTxt(projectDir: string): string | null;
17
+ }
@@ -0,0 +1,134 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ export class GatsbyAdapter {
4
+ id = 'gatsby';
5
+ name = 'Gatsby Framework';
6
+ detect(projectDir) {
7
+ return this.detectDetailed(projectDir).matched;
8
+ }
9
+ detectDetailed(projectDir) {
10
+ const evidence = [];
11
+ if (fs.existsSync(path.join(projectDir, 'gatsby-config.js')) || fs.existsSync(path.join(projectDir, 'gatsby-config.ts'))) {
12
+ evidence.push('gatsby-config detected');
13
+ }
14
+ const pkgPath = path.join(projectDir, 'package.json');
15
+ if (fs.existsSync(pkgPath)) {
16
+ try {
17
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
18
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
19
+ if (deps['gatsby'])
20
+ evidence.push('gatsby in package dependencies');
21
+ }
22
+ catch { }
23
+ }
24
+ return {
25
+ matched: evidence.length > 0,
26
+ confidence: evidence.length > 0 ? 0.95 : 0.0,
27
+ evidence
28
+ };
29
+ }
30
+ getProjectInfo(projectDir) {
31
+ return {
32
+ framework: 'gatsby',
33
+ name: path.basename(projectDir),
34
+ hasTypeScript: fs.existsSync(path.join(projectDir, 'tsconfig.json')),
35
+ hasSitemapConfig: Boolean(this.findSitemapConfig(projectDir)),
36
+ hasRobotsConfig: Boolean(this.findRobotsConfig(projectDir)),
37
+ hasLlmsTxt: Boolean(this.findLlmsTxt(projectDir)),
38
+ rootDir: path.resolve(projectDir),
39
+ routesDir: 'src/pages',
40
+ devCommand: 'npm run develop',
41
+ defaultDevPort: 8000
42
+ };
43
+ }
44
+ discoverRoutes(projectDir) {
45
+ const routesDir = path.join(projectDir, 'src/pages');
46
+ const discovered = [];
47
+ if (!fs.existsSync(routesDir))
48
+ return discovered;
49
+ const scan = (dir, prefix) => {
50
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
51
+ for (const e of entries) {
52
+ const full = path.join(dir, e.name);
53
+ if (e.isDirectory()) {
54
+ scan(full, prefix === '/' ? `/${e.name}` : `${prefix}/${e.name}`);
55
+ }
56
+ else if (e.isFile() && /\.(tsx|jsx|js|ts)$/.test(e.name)) {
57
+ const base = e.name.replace(/\.(tsx|jsx|js|ts)$/, '');
58
+ const routePath = base === 'index' ? prefix : (prefix === '/' ? `/${base}` : `${prefix}/${base}`);
59
+ const rel = path.relative(projectDir, full).replace(/\\/g, '/');
60
+ const content = fs.readFileSync(full, 'utf-8');
61
+ discovered.push({
62
+ routePath: routePath === '' ? '/' : routePath,
63
+ sourceFilePath: rel,
64
+ isDynamic: false,
65
+ dynamicParams: [],
66
+ routeType: 'page',
67
+ hasMetadataExport: /export\s+const\s+Head\b|<title\b/i.test(content),
68
+ hasGenerateMetadata: false,
69
+ hasSchemaMarkup: /application\/ld\+json/.test(content),
70
+ hasHeadComponent: /export\s+const\s+Head\b/i.test(content),
71
+ isClientComponent: false
72
+ });
73
+ }
74
+ }
75
+ };
76
+ scan(routesDir, '/');
77
+ return discovered;
78
+ }
79
+ mapRouteToSource(targetUrl, routes) {
80
+ let urlPath = targetUrl.startsWith('http') ? new URL(targetUrl).pathname : targetUrl;
81
+ urlPath = urlPath.replace(/\/$/, '') || '/';
82
+ const match = routes.find(r => r.routePath === urlPath);
83
+ return {
84
+ urlPath,
85
+ matchedRoute: match,
86
+ sourceFilePath: match?.sourceFilePath,
87
+ confidence: match ? 1.0 : 0.0,
88
+ resolutionMethod: match ? 'exact_match' : 'unmapped'
89
+ };
90
+ }
91
+ async findMetadataImplementation(projectDir, route) {
92
+ const full = path.join(projectDir, route.sourceFilePath);
93
+ if (!fs.existsSync(full))
94
+ return { hasMetadata: false, type: 'none' };
95
+ const content = fs.readFileSync(full, 'utf-8');
96
+ const has = /export\s+const\s+Head\b|<title\b/i.test(content);
97
+ return { hasMetadata: has, type: has ? 'static_export' : 'none' };
98
+ }
99
+ async findCanonicalDeclaration(projectDir, route) {
100
+ const full = path.join(projectDir, route.sourceFilePath);
101
+ if (!fs.existsSync(full))
102
+ return { hasCanonical: false, type: 'none' };
103
+ const content = fs.readFileSync(full, 'utf-8');
104
+ return { hasCanonical: /rel=["']canonical["']/i.test(content), type: 'link_jsx_tag' };
105
+ }
106
+ async findSchemaDeclaration(projectDir, route) {
107
+ const full = path.join(projectDir, route.sourceFilePath);
108
+ if (!fs.existsSync(full))
109
+ return { hasSchema: false, typesFound: [] };
110
+ const content = fs.readFileSync(full, 'utf-8');
111
+ return { hasSchema: /application\/ld\+json/i.test(content), typesFound: ['JSON-LD'] };
112
+ }
113
+ findRobotsConfig(projectDir) {
114
+ const paths = ['static/robots.txt', 'robots.txt'];
115
+ for (const p of paths)
116
+ if (fs.existsSync(path.join(projectDir, p)))
117
+ return p;
118
+ return null;
119
+ }
120
+ findSitemapConfig(projectDir) {
121
+ const paths = ['public/sitemap.xml', 'sitemap.xml'];
122
+ for (const p of paths)
123
+ if (fs.existsSync(path.join(projectDir, p)))
124
+ return p;
125
+ return null;
126
+ }
127
+ findLlmsTxt(projectDir) {
128
+ const paths = ['static/llms.txt', 'llms.txt'];
129
+ for (const p of paths)
130
+ if (fs.existsSync(path.join(projectDir, p)))
131
+ return p;
132
+ return null;
133
+ }
134
+ }
@@ -0,0 +1,17 @@
1
+ import { FrameworkAdapter, FrameworkDetectionResult, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
2
+ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
3
+ export declare class LaravelAdapter implements FrameworkAdapter {
4
+ id: string;
5
+ name: string;
6
+ detect(projectDir: string): boolean;
7
+ detectDetailed(projectDir: string): FrameworkDetectionResult;
8
+ getProjectInfo(projectDir: string): ProjectFrameworkInfo;
9
+ discoverRoutes(projectDir: string): DiscoveredRoute[];
10
+ mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
11
+ findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
12
+ findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
13
+ findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
14
+ findRobotsConfig(projectDir: string): string | null;
15
+ findSitemapConfig(projectDir: string): string | null;
16
+ findLlmsTxt(projectDir: string): string | null;
17
+ }
@@ -0,0 +1,192 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ export class LaravelAdapter {
4
+ id = 'laravel';
5
+ name = 'Laravel (Blade & Artisan)';
6
+ detect(projectDir) {
7
+ return this.detectDetailed(projectDir).matched;
8
+ }
9
+ detectDetailed(projectDir) {
10
+ const evidence = [];
11
+ if (fs.existsSync(path.join(projectDir, 'artisan'))) {
12
+ evidence.push('artisan CLI entry file detected');
13
+ }
14
+ const composerPath = path.join(projectDir, 'composer.json');
15
+ if (fs.existsSync(composerPath)) {
16
+ try {
17
+ const composer = JSON.parse(fs.readFileSync(composerPath, 'utf-8'));
18
+ const deps = { ...composer.require, ...composer['require-dev'] };
19
+ if (deps['laravel/framework']) {
20
+ evidence.push('laravel/framework in composer.json require');
21
+ }
22
+ }
23
+ catch { }
24
+ }
25
+ return {
26
+ matched: evidence.length > 0,
27
+ confidence: evidence.length >= 2 ? 0.99 : 0.92,
28
+ evidence
29
+ };
30
+ }
31
+ getProjectInfo(projectDir) {
32
+ return {
33
+ framework: 'laravel',
34
+ name: path.basename(projectDir),
35
+ hasTypeScript: false,
36
+ hasSitemapConfig: Boolean(this.findSitemapConfig(projectDir)),
37
+ hasRobotsConfig: Boolean(this.findRobotsConfig(projectDir)),
38
+ hasLlmsTxt: Boolean(this.findLlmsTxt(projectDir)),
39
+ rootDir: path.resolve(projectDir),
40
+ routesDir: 'routes',
41
+ devCommand: 'php artisan serve',
42
+ defaultDevPort: 8000
43
+ };
44
+ }
45
+ discoverRoutes(projectDir) {
46
+ const discovered = [];
47
+ const webRoutesPath = path.join(projectDir, 'routes/web.php');
48
+ if (fs.existsSync(webRoutesPath)) {
49
+ const content = fs.readFileSync(webRoutesPath, 'utf-8');
50
+ const routeMatches = content.matchAll(/Route::(get|view|match)\(\s*['"]([^'"]+)['"]/g);
51
+ for (const m of routeMatches) {
52
+ const rawPath = m[2];
53
+ const routePath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
54
+ if (!discovered.some(d => d.routePath === routePath)) {
55
+ discovered.push({
56
+ routePath,
57
+ sourceFilePath: 'routes/web.php',
58
+ isDynamic: routePath.includes('{'),
59
+ dynamicParams: (routePath.match(/\{([a-zA-Z0-9_]+)\}/g) || []).map(p => p.slice(1, -1)),
60
+ routeType: 'page',
61
+ hasMetadataExport: true,
62
+ hasGenerateMetadata: false,
63
+ hasSchemaMarkup: false,
64
+ hasHeadComponent: true,
65
+ isClientComponent: false
66
+ });
67
+ }
68
+ }
69
+ }
70
+ // Also scan Blade views in resources/views
71
+ const viewsDir = path.join(projectDir, 'resources/views');
72
+ if (fs.existsSync(viewsDir)) {
73
+ const scanViews = (dir, prefix) => {
74
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
75
+ for (const e of entries) {
76
+ const full = path.join(dir, e.name);
77
+ if (e.isDirectory()) {
78
+ scanViews(full, prefix === '/' ? `/${e.name}` : `${prefix}/${e.name}`);
79
+ }
80
+ else if (e.isFile() && e.name.endsWith('.blade.php')) {
81
+ const base = e.name.replace(/\.blade\.php$/, '');
82
+ const routePath = base === 'welcome' || base === 'index' ? '/' : (prefix === '/' ? `/${base}` : `${prefix}/${base}`);
83
+ const rel = path.relative(projectDir, full).replace(/\\/g, '/');
84
+ const viewContent = fs.readFileSync(full, 'utf-8');
85
+ const existing = discovered.find(d => d.routePath === routePath);
86
+ if (existing) {
87
+ existing.sourceFilePath = rel;
88
+ existing.hasMetadataExport = /@section\(['"]title['"]|<title\b|<x-slot:title/i.test(viewContent);
89
+ existing.hasSchemaMarkup = /application\/ld\+json/.test(viewContent);
90
+ }
91
+ else {
92
+ discovered.push({
93
+ routePath,
94
+ sourceFilePath: rel,
95
+ isDynamic: false,
96
+ dynamicParams: [],
97
+ routeType: 'page',
98
+ hasMetadataExport: /@section\(['"]title['"]|<title\b|<x-slot:title/i.test(viewContent),
99
+ hasGenerateMetadata: false,
100
+ hasSchemaMarkup: /application\/ld\+json/.test(viewContent),
101
+ hasHeadComponent: true,
102
+ isClientComponent: false
103
+ });
104
+ }
105
+ }
106
+ }
107
+ };
108
+ scanViews(viewsDir, '/');
109
+ }
110
+ return discovered;
111
+ }
112
+ mapRouteToSource(targetUrl, routes) {
113
+ let urlPath = targetUrl.startsWith('http') ? new URL(targetUrl).pathname : targetUrl;
114
+ urlPath = urlPath.replace(/\/$/, '') || '/';
115
+ const match = routes.find(r => r.routePath === urlPath);
116
+ if (match) {
117
+ return {
118
+ urlPath,
119
+ matchedRoute: match,
120
+ sourceFilePath: match.sourceFilePath,
121
+ confidence: 1.0,
122
+ resolutionMethod: 'exact_match'
123
+ };
124
+ }
125
+ // Dynamic patterns {param}
126
+ for (const r of routes) {
127
+ if (!r.isDynamic)
128
+ continue;
129
+ const regexStr = '^' + r.routePath.replace(/\{[a-zA-Z0-9_]+\}/g, '[^/]+') + '$';
130
+ try {
131
+ if (new RegExp(regexStr).test(urlPath)) {
132
+ return {
133
+ urlPath,
134
+ matchedRoute: r,
135
+ sourceFilePath: r.sourceFilePath,
136
+ confidence: 0.95,
137
+ resolutionMethod: 'pattern_match'
138
+ };
139
+ }
140
+ }
141
+ catch { }
142
+ }
143
+ return {
144
+ urlPath,
145
+ confidence: 0.0,
146
+ resolutionMethod: 'unmapped'
147
+ };
148
+ }
149
+ async findMetadataImplementation(projectDir, route) {
150
+ const full = path.join(projectDir, route.sourceFilePath);
151
+ if (!fs.existsSync(full))
152
+ return { hasMetadata: false, type: 'none' };
153
+ const content = fs.readFileSync(full, 'utf-8');
154
+ const has = /@section\(['"]title['"]|<title\b|<x-slot:title/i.test(content);
155
+ return { hasMetadata: has, type: has ? 'jsx_head' : 'none' };
156
+ }
157
+ async findCanonicalDeclaration(projectDir, route) {
158
+ const full = path.join(projectDir, route.sourceFilePath);
159
+ if (!fs.existsSync(full))
160
+ return { hasCanonical: false, type: 'none' };
161
+ const content = fs.readFileSync(full, 'utf-8');
162
+ return { hasCanonical: /rel=["']canonical["']/i.test(content), type: 'link_jsx_tag' };
163
+ }
164
+ async findSchemaDeclaration(projectDir, route) {
165
+ const full = path.join(projectDir, route.sourceFilePath);
166
+ if (!fs.existsSync(full))
167
+ return { hasSchema: false, typesFound: [] };
168
+ const content = fs.readFileSync(full, 'utf-8');
169
+ return { hasSchema: /application\/ld\+json/i.test(content), typesFound: ['JSON-LD'] };
170
+ }
171
+ findRobotsConfig(projectDir) {
172
+ const paths = ['public/robots.txt', 'robots.txt'];
173
+ for (const p of paths)
174
+ if (fs.existsSync(path.join(projectDir, p)))
175
+ return p;
176
+ return null;
177
+ }
178
+ findSitemapConfig(projectDir) {
179
+ const paths = ['public/sitemap.xml', 'sitemap.xml'];
180
+ for (const p of paths)
181
+ if (fs.existsSync(path.join(projectDir, p)))
182
+ return p;
183
+ return null;
184
+ }
185
+ findLlmsTxt(projectDir) {
186
+ const paths = ['public/llms.txt', 'llms.txt'];
187
+ for (const p of paths)
188
+ if (fs.existsSync(path.join(projectDir, p)))
189
+ return p;
190
+ return null;
191
+ }
192
+ }