@rankcli/agent-runtime 0.0.29 → 0.0.31

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/dist/index.js CHANGED
@@ -3352,7 +3352,9 @@ __export(index_exports, {
3352
3352
  analyzeUrlSafety: () => analyzeUrlSafety,
3353
3353
  analyzers: () => analyzers_exports,
3354
3354
  applyFixes: () => applyFixes,
3355
+ assessReplacement: () => assessReplacement,
3355
3356
  buildDeterministicFixes: () => buildDeterministicFixes,
3357
+ buildFrameworkGuidance: () => buildFrameworkGuidance,
3356
3358
  buildGSCApiRequest: () => buildGSCApiRequest,
3357
3359
  buildGSCRequest: () => buildGSCRequest,
3358
3360
  calculateAIVisibilityScore: () => calculateAIVisibilityScore,
@@ -3393,6 +3395,7 @@ __export(index_exports, {
3393
3395
  createPullRequest: () => createPullRequest,
3394
3396
  createSEOCommit: () => createSEOCommit,
3395
3397
  createSEOCommits: () => createSEOCommits,
3398
+ describeConcerns: () => describeConcerns,
3396
3399
  detectDuplicates: () => detectDuplicates,
3397
3400
  detectFramework: () => detectFramework4,
3398
3401
  detectGitHubPages: () => detectGitHubPages,
@@ -3619,6 +3622,7 @@ var RECIPES = {
3619
3622
  jekyll: null,
3620
3623
  eleventy: null,
3621
3624
  pelican: null,
3625
+ sphinx: null,
3622
3626
  // Unknown
3623
3627
  unknown: null
3624
3628
  };
@@ -7738,234 +7742,156 @@ var ISSUE_DEFINITIONS = {
7738
7742
  // src/audit/engine.ts
7739
7743
  init_http();
7740
7744
 
7741
- // src/frameworks/detector.ts
7742
- var HTML_SIGNATURES = [
7743
- // ============ JavaScript/TypeScript Frameworks ============
7744
- // Next.js
7745
- {
7746
- framework: "nextjs",
7747
- language: "typescript",
7748
- patterns: [/__next/, /_next\//, /data-nscript/, /__NEXT_DATA__/, /next\/script/],
7749
- confidence: "high"
7750
- },
7751
- // Nuxt
7752
- {
7753
- framework: "nuxt",
7754
- language: "typescript",
7755
- patterns: [/__nuxt/, /_nuxt\//, /data-n-head/, /nuxt-link/, /__NUXT__/],
7756
- confidence: "high"
7757
- },
7758
- // Gatsby
7759
- {
7760
- framework: "gatsby",
7761
- language: "javascript",
7762
- patterns: [/___gatsby/, /gatsby-/, /gatsby-image/, /data-gatsby/],
7763
- confidence: "high"
7764
- },
7765
- // Astro
7766
- {
7767
- framework: "astro",
7768
- language: "typescript",
7769
- patterns: [/astro-/, /data-astro/, /_astro\//],
7770
- confidence: "high"
7771
- },
7772
- // SvelteKit
7773
- {
7774
- framework: "sveltekit",
7775
- language: "typescript",
7776
- patterns: [/__sveltekit/, /_app\/immutable/, /svelte-kit/],
7777
- confidence: "high"
7778
- },
7779
- // Svelte (standalone)
7780
- {
7781
- framework: "svelte",
7782
- language: "javascript",
7783
- patterns: [/svelte-\w+/, /__svelte/],
7784
- confidence: "medium"
7785
- },
7786
- // Remix
7787
- {
7788
- framework: "remix",
7789
- language: "typescript",
7790
- patterns: [/__remix/, /data-remix/],
7791
- confidence: "high"
7792
- },
7793
- // Solid.js
7794
- {
7795
- framework: "solidjs",
7796
- language: "typescript",
7797
- patterns: [/data-solid/, /_solid/],
7798
- confidence: "high"
7799
- },
7800
- // Qwik
7801
- {
7802
- framework: "qwik",
7803
- language: "typescript",
7804
- patterns: [/q:container/, /q:base/, /q:version/, /q:id=/, /qwik/],
7805
- confidence: "high"
7806
- },
7807
- // Fresh (Deno)
7808
- {
7809
- framework: "fresh",
7810
- language: "typescript",
7811
- patterns: [/_frsh\//, /data-fresh/],
7812
- confidence: "high"
7813
- },
7814
- // React (generic - lower priority)
7745
+ // src/frameworks/signals.ts
7746
+ var WEIGHT = { DECISIVE: 100, STRONG: 45, WEAK: 15 };
7747
+ var SCORE_FLOOR = 45;
7748
+ var SIGNALS = [
7749
+ // ---- Meta-frameworks: JavaScript ------------------------------------------------
7750
+ { framework: "nextjs", language: "typescript", weight: WEIGHT.DECISIVE, name: "next-build-dir", html: /\/_next\/(static|image|data)\// },
7751
+ { framework: "nextjs", language: "typescript", weight: WEIGHT.DECISIVE, name: "next-data", html: /__NEXT_DATA__|self\.__next_f/ },
7752
+ { framework: "nextjs", language: "typescript", weight: WEIGHT.DECISIVE, name: "next-mount", html: /<div id=["']__next["']/ },
7753
+ { framework: "nextjs", language: "typescript", weight: WEIGHT.DECISIVE, name: "next-powered-by", header: /^x-powered-by: next\.js/ },
7754
+ { framework: "nuxt", language: "typescript", weight: WEIGHT.DECISIVE, name: "nuxt-build-dir", html: /\/_nuxt\// },
7755
+ { framework: "nuxt", language: "typescript", weight: WEIGHT.DECISIVE, name: "nuxt-payload", html: /__NUXT__|__NUXT_DATA__|<div id="__nuxt"/ },
7756
+ { framework: "nuxt", language: "typescript", weight: WEIGHT.DECISIVE, name: "nuxt-powered-by", header: /^x-powered-by: nuxt/ },
7757
+ { framework: "astro", language: "typescript", weight: WEIGHT.DECISIVE, name: "astro-build-dir", html: /\/_astro\// },
7758
+ { framework: "astro", language: "typescript", weight: WEIGHT.DECISIVE, name: "astro-island", html: /<astro-island|astro-slot|data-astro-(cid|source-file|transition)/ },
7759
+ { framework: "astro", language: "typescript", weight: WEIGHT.STRONG, name: "astro-generator", html: /name=["']?generator["']?[^>]*content=["']?Astro/i },
7760
+ { framework: "sveltekit", language: "typescript", weight: WEIGHT.DECISIVE, name: "sveltekit-immutable", html: /\/_app\/immutable\// },
7761
+ { framework: "sveltekit", language: "typescript", weight: WEIGHT.DECISIVE, name: "sveltekit-runtime", html: /__sveltekit|kit\.start\(/ },
7762
+ { framework: "remix", language: "typescript", weight: WEIGHT.DECISIVE, name: "remix-context", html: /__remixContext|__remixManifest|__remixRouteModules/ },
7763
+ { framework: "remix", language: "typescript", weight: WEIGHT.STRONG, name: "remix-build-dir", html: /\/build\/_shared\/|\/build\/entry\.client-/ },
7764
+ { framework: "gatsby", language: "javascript", weight: WEIGHT.DECISIVE, name: "gatsby-runtime", html: /___gatsby|window\.___chunkMapping/ },
7765
+ { framework: "gatsby", language: "javascript", weight: WEIGHT.STRONG, name: "gatsby-page-data", html: /\/page-data\/(app-data\.json|sq\/)/ },
7766
+ { framework: "qwik", language: "typescript", weight: WEIGHT.DECISIVE, name: "qwik-container", html: /q:container|q:base|q:version|qwikloader/ },
7767
+ { framework: "fresh", language: "typescript", weight: WEIGHT.DECISIVE, name: "fresh-build-dir", html: /\/_frsh\// },
7768
+ { framework: "solidjs", language: "typescript", weight: WEIGHT.STRONG, name: "solid-hydration", html: /_\$HY\b|data-hk=["']/ },
7769
+ // ---- Base UI libraries: weaker by nature, and that is the point -----------------
7770
+ { framework: "angular", language: "typescript", weight: WEIGHT.DECISIVE, name: "angular-version", html: /ng-version=["']/ },
7771
+ { framework: "angular", language: "typescript", weight: WEIGHT.DECISIVE, name: "angular-content-attr", html: /_ngcontent-|_nghost-/ },
7772
+ { framework: "react", language: "javascript", weight: WEIGHT.STRONG, name: "react-root-marker", html: /data-reactroot|__reactContainer\$|__REACT_DEVTOOLS/ },
7773
+ // A Vite-built SPA: an empty mount point plus Vite's hashed entry bundle. Two weak facts
7774
+ // that together mean something; either one alone does not.
7775
+ { framework: "react", language: "javascript", weight: WEIGHT.WEAK, name: "react-empty-root", html: /<div id="root"><\/div>/ },
7776
+ { framework: "react", language: "javascript", weight: WEIGHT.WEAK, name: "react-refresh", html: /RefreshRuntime|react-refresh/ },
7815
7777
  {
7816
7778
  framework: "react",
7817
7779
  language: "javascript",
7818
- // data-reactroot/data-react-helmet are React <=15 hydration markers -
7819
- // React 18's createRoot() never adds them, so every modern CRA/Vite
7820
- // React app (an empty <div id="root">) went undetected from HTML alone.
7821
- patterns: [/data-reactroot/, /data-react-helmet/, /_react/, /react-app/, /<div id="root"><\/div>/],
7822
- confidence: "medium"
7823
- },
7824
- // Vue (generic - lower priority)
7825
- {
7826
- framework: "vue",
7827
- language: "javascript",
7828
- patterns: [/data-v-[a-f0-9]+/, /v-cloak/, /__vue/],
7829
- confidence: "medium"
7830
- },
7831
- // Angular
7832
- {
7833
- framework: "angular",
7834
- language: "typescript",
7835
- patterns: [/ng-version/, /_ngcontent/, /ng-\w+=/, /\[ng/],
7836
- confidence: "high"
7837
- },
7838
- // ============ Hypermedia Frameworks ============
7839
- // HTMX
7840
- {
7841
- framework: "htmx",
7842
- language: "unknown",
7843
- patterns: [/hx-get/, /hx-post/, /hx-trigger/, /hx-swap/, /htmx\.org/],
7844
- confidence: "high"
7845
- },
7846
- // Hotwire/Turbo
7847
- {
7848
- framework: "hotwire",
7849
- language: "ruby",
7850
- patterns: [/turbo-frame/, /turbo-stream/, /data-turbo/],
7851
- confidence: "high"
7852
- },
7853
- // ============ PHP Frameworks ============
7854
- // WordPress
7855
- {
7856
- framework: "wordpress",
7857
- language: "php",
7858
- // Technical signatures only. These patterns run over the whole page, so the old
7859
- // /wordpress/i matched the word anywhere in the copy: rankcli.dev's own "detects
7860
- // Rails, Django, Laravel, and WordPress" line was enough to detect its React SPA as
7861
- // WordPress and then tell it to install an SEO plugin. A generator meta tag is the
7862
- // one place the page says it about itself.
7863
- patterns: [/wp-content/, /wp-includes/, /wp-json/, /name=["']generator["'][^>]*content=["']WordPress/i],
7864
- confidence: "high"
7865
- },
7866
- // Laravel
7867
- {
7868
- framework: "laravel",
7869
- language: "php",
7870
- // /laravel/i had the same problem as WordPress above - it matched the word in prose.
7871
- patterns: [/laravel_session/, /XSRF-TOKEN/, /@csrf/],
7872
- confidence: "medium"
7873
- },
7874
- // Symfony
7875
- {
7876
- framework: "symfony",
7877
- language: "php",
7878
- // Likewise /symfony/, which matched any lowercase mention.
7879
- patterns: [/sf2/, /_sf_/, /profiler\.css/],
7880
- confidence: "medium"
7780
+ weight: WEIGHT.STRONG,
7781
+ name: "react-vite-spa",
7782
+ // Vite's entry chunk plus React's conventional mount point. Medium confidence by
7783
+ // construction: #app instead of #root stays unknown, because Vue, Solid and others use it.
7784
+ // Not `<div id="root"></div>`: a prerendered SPA has content inside its mount point, which
7785
+ // is what RankCLI's own site does and what the first version of this signal missed.
7786
+ allHtml: [/<script[^>]+src=["'][^"']+\.js["']/, /<div id="root"[\s>]/]
7881
7787
  },
7882
- // ============ Ruby Frameworks ============
7883
- // Rails
7884
- {
7885
- framework: "rails",
7886
- language: "ruby",
7887
- patterns: [/csrf-token/, /turbolinks/, /data-turbo/, /rails-ujs/, /action-cable/],
7888
- confidence: "medium"
7889
- },
7890
- // ============ Python Frameworks ============
7891
- // Django
7892
- {
7893
- framework: "django",
7894
- language: "python",
7895
- patterns: [/csrfmiddlewaretoken/, /django/, /__debug__\//],
7896
- confidence: "medium"
7897
- },
7898
- // Flask
7899
- {
7900
- framework: "flask",
7901
- language: "python",
7902
- patterns: [/flask/, /werkzeug/i],
7903
- confidence: "low"
7904
- },
7905
- // FastAPI
7906
- {
7907
- framework: "fastapi",
7908
- language: "python",
7909
- patterns: [/fastapi/, /openapi\.json/],
7910
- confidence: "low"
7911
- },
7912
- // ============ Java Frameworks ============
7913
- // Spring Boot
7914
- {
7915
- framework: "spring",
7916
- language: "java",
7917
- // /th:/ matched "width:" and "height:" in inline CSS, which made almost every styled
7918
- // page on the web a Spring Boot app - rankcli.dev included, once WordPress stopped
7919
- // matching first. Thymeleaf attributes are what th: actually means.
7920
- patterns: [/\sth:[a-z-]+=/, /thymeleaf/, /_csrf/],
7921
- confidence: "medium"
7922
- },
7923
- // ============ C# Frameworks ============
7924
- // ASP.NET Core
7925
- {
7926
- framework: "aspnet",
7927
- language: "csharp",
7928
- patterns: [/aspnetcore/, /__RequestVerificationToken/, /_framework\/blazor/],
7929
- confidence: "medium"
7930
- },
7931
- // ============ Elixir Frameworks ============
7932
- // Phoenix
7933
- {
7934
- framework: "phoenix",
7935
- language: "elixir",
7936
- patterns: [/phx-/, /live-socket/, /data-phx/],
7937
- confidence: "high"
7938
- },
7939
- // ============ Static Site Generators ============
7940
- // Hugo
7941
- {
7942
- framework: "hugo",
7943
- language: "go",
7944
- patterns: [/name=["']generator["'][^>]*content=["']Hugo/i],
7945
- confidence: "medium"
7946
- },
7947
- // Jekyll
7948
- {
7949
- framework: "jekyll",
7950
- language: "ruby",
7951
- patterns: [/name=["']generator["'][^>]*content=["']Jekyll/i],
7952
- confidence: "medium"
7953
- },
7954
- // Eleventy
7955
- {
7956
- framework: "eleventy",
7957
- language: "javascript",
7958
- patterns: [/11ty/, /eleventy/, /generator.*eleventy/i],
7959
- confidence: "medium"
7960
- },
7961
- // Pelican
7962
- {
7963
- framework: "pelican",
7964
- language: "python",
7965
- patterns: [/pelican/, /generator.*pelican/i],
7966
- confidence: "medium"
7967
- }
7788
+ { framework: "vue", language: "javascript", weight: WEIGHT.DECISIVE, name: "vitepress-runtime", html: /__VP_HASH_MAP__|__VP_SITE_DATA__/ },
7789
+ { framework: "vue", language: "javascript", weight: WEIGHT.DECISIVE, name: "vue-scoped-style", html: /data-v-[0-9a-f]{6,10}\b/ },
7790
+ { framework: "vue", language: "javascript", weight: WEIGHT.WEAK, name: "vue-cloak", html: /v-cloak|<div id="app"><\/div>/ },
7791
+ { framework: "svelte", language: "javascript", weight: WEIGHT.WEAK, name: "svelte-class", html: /class=["'][^"']*svelte-[0-9a-z]{6}/ },
7792
+ // ---- Ruby -----------------------------------------------------------------------
7793
+ // csrf-token alone is emitted by Rails, Laravel, Phoenix and Django. csrf-param is Rails.
7794
+ { framework: "rails", language: "ruby", weight: WEIGHT.DECISIVE, name: "rails-csrf-param", html: /name=["']csrf-param["']/ },
7795
+ { framework: "rails", language: "ruby", weight: WEIGHT.STRONG, name: "rails-ujs", html: /rails-ujs|turbolinks|data-remote=["']true["']|action_cable|actioncable/ },
7796
+ { framework: "rails", language: "ruby", weight: WEIGHT.STRONG, name: "rails-session-cookie", header: /^set-cookie: [^;]*_session=/ },
7797
+ { framework: "rails", language: "ruby", weight: WEIGHT.STRONG, name: "rails-app-server", header: /^(server|x-powered-by): (puma|passenger|unicorn|phusion)/ },
7798
+ { framework: "hotwire", language: "ruby", weight: WEIGHT.STRONG, name: "turbo-elements", html: /<turbo-frame|<turbo-stream|data-turbo-track/ },
7799
+ // ---- Python ---------------------------------------------------------------------
7800
+ { framework: "django", language: "python", weight: WEIGHT.DECISIVE, name: "django-csrf-input", html: /name=["']csrfmiddlewaretoken["']/ },
7801
+ { framework: "django", language: "python", weight: WEIGHT.DECISIVE, name: "django-csrf-cookie", header: /^set-cookie: csrftoken=/ },
7802
+ { framework: "django", language: "python", weight: WEIGHT.STRONG, name: "django-admin-static", html: /\/static\/admin\/(css|js)\// },
7803
+ { framework: "django", language: "python", weight: WEIGHT.WEAK, name: "django-wsgi-server", header: /^(server|x-powered-by): (gunicorn|uvicorn|daphne)/ },
7804
+ { framework: "flask", language: "python", weight: WEIGHT.STRONG, name: "flask-server", header: /^server: werkzeug/ },
7805
+ { framework: "fastapi", language: "python", weight: WEIGHT.STRONG, name: "fastapi-docs", html: /swagger-ui-bundle\.js[\s\S]{0,200}openapi\.json/ },
7806
+ // ---- PHP ------------------------------------------------------------------------
7807
+ { framework: "wordpress", language: "php", weight: WEIGHT.DECISIVE, name: "wp-paths", html: /\/wp-content\/|\/wp-includes\/|\/wp-json\// },
7808
+ { framework: "wordpress", language: "php", weight: WEIGHT.DECISIVE, name: "wp-generator", html: /name=["']?generator["']?[^>]*content=["']?WordPress/i },
7809
+ { framework: "wordpress", language: "php", weight: WEIGHT.STRONG, name: "wp-powered-by", header: /^x-powered-by: wordpress/ },
7810
+ { framework: "laravel", language: "php", weight: WEIGHT.DECISIVE, name: "laravel-session-cookie", header: /^set-cookie: laravel_session=/ },
7811
+ { framework: "laravel", language: "php", weight: WEIGHT.STRONG, name: "laravel-xsrf-cookie", header: /^set-cookie: XSRF-TOKEN=/i },
7812
+ { framework: "laravel", language: "php", weight: WEIGHT.STRONG, name: "laravel-livewire", html: /livewire|wire:(click|model|submit)/i },
7813
+ { framework: "laravel", language: "php", weight: WEIGHT.WEAK, name: "laravel-vite-build", html: /\/build\/assets\/app-[A-Za-z0-9_-]{8}\.js/ },
7814
+ { framework: "symfony", language: "php", weight: WEIGHT.DECISIVE, name: "symfony-profiler", html: /\/_profiler\/|sf-toolbar|sf-dump/ },
7815
+ { framework: "symfony", language: "php", weight: WEIGHT.STRONG, name: "symfony-cookie", header: /^set-cookie: (sfsession|symfony)/i },
7816
+ // ---- Elixir ---------------------------------------------------------------------
7817
+ { framework: "phoenix", language: "elixir", weight: WEIGHT.DECISIVE, name: "phoenix-liveview", html: /phx-(click|submit|change|value|hook|socket|main|track-static)/ },
7818
+ { framework: "phoenix", language: "elixir", weight: WEIGHT.STRONG, name: "phoenix-cowboy", header: /^server: cowboy/ },
7819
+ // ---- Java / C# / Go -------------------------------------------------------------
7820
+ { framework: "spring", language: "java", weight: WEIGHT.STRONG, name: "thymeleaf-attr", html: /\sth:[a-z-]+=/ },
7821
+ { framework: "spring", language: "java", weight: WEIGHT.STRONG, name: "spring-server", header: /^server: (apache-coyote|tomcat|jetty)/ },
7822
+ { framework: "aspnet", language: "csharp", weight: WEIGHT.DECISIVE, name: "aspnet-verification-token", html: /__RequestVerificationToken|__VIEWSTATE/ },
7823
+ { framework: "aspnet", language: "csharp", weight: WEIGHT.STRONG, name: "aspnet-server", header: /^(server: kestrel|x-powered-by: asp\.net|x-aspnet-version:)/ },
7824
+ // "gin" used to be tested as a substring of the Server header, which matches nginx - the
7825
+ // single most common web server there is. Anchored to the whole value now.
7826
+ { framework: "go", language: "go", weight: WEIGHT.STRONG, name: "go-server", header: /^server: (gin|echo|fiber|gorilla)(\/|$| )/ },
7827
+ // ---- Hypermedia -----------------------------------------------------------------
7828
+ { framework: "htmx", language: "unknown", weight: WEIGHT.STRONG, name: "htmx-attrs", html: /\shx-(get|post|put|delete|trigger|swap|target)=/ },
7829
+ { framework: "htmx", language: "unknown", weight: WEIGHT.DECISIVE, name: "htmx-script", html: /htmx(\.min)?\.js|unpkg\.com\/htmx/ },
7830
+ // ---- Static site generators: they announce themselves -----------------------------
7831
+ { framework: "hugo", language: "go", weight: WEIGHT.DECISIVE, name: "hugo-generator", html: /name=["']?generator["']?[^>]*content=["']?Hugo/i },
7832
+ { framework: "hugo", language: "go", weight: WEIGHT.STRONG, name: "hugo-generator-header", header: /^x-generator: hugo/ },
7833
+ { framework: "jekyll", language: "ruby", weight: WEIGHT.DECISIVE, name: "jekyll-generator", html: /name=["']?generator["']?[^>]*content=["']?Jekyll/i },
7834
+ { framework: "eleventy", language: "javascript", weight: WEIGHT.DECISIVE, name: "eleventy-generator", html: /name=["']?generator["']?[^>]*content=["']?Eleventy/i },
7835
+ { framework: "pelican", language: "python", weight: WEIGHT.DECISIVE, name: "pelican-generator", html: /name=["']?generator["']?[^>]*content=["']?Pelican/i },
7836
+ // Sphinx: what nearly every Python project's documentation is built with, and the shape
7837
+ // flask.palletsprojects.com turned out to be. Structural markers only - not the word
7838
+ // "sphinx", which appears in plenty of prose.
7839
+ { framework: "sphinx", language: "python", weight: WEIGHT.DECISIVE, name: "sphinx-doc-options", html: /_static\/documentation_options\.js/ },
7840
+ { framework: "sphinx", language: "python", weight: WEIGHT.STRONG, name: "sphinx-theme-markup", html: /class=["'][^"']*sphinxsidebar|sphinx-highlight|_static\/searchtools\.js/ },
7841
+ { framework: "sphinx", language: "python", weight: WEIGHT.STRONG, name: "docutils-generator", html: /name=["']?generator["']?[^>]*content=["']?Docutils/i }
7968
7842
  ];
7843
+ function headerLines(headers) {
7844
+ if (!headers) return "";
7845
+ return Object.entries(headers).map(([k, v]) => `${k.toLowerCase()}: ${String(v).toLowerCase()}`).join("\n");
7846
+ }
7847
+ function detectFromEvidence(input) {
7848
+ const html = input.html ?? "";
7849
+ const headers = headerLines(input.headers);
7850
+ const byFramework = /* @__PURE__ */ new Map();
7851
+ for (const signal of SIGNALS) {
7852
+ const headerHit = signal.header && headers && new RegExp(
7853
+ signal.header.source,
7854
+ signal.header.flags.includes("m") ? signal.header.flags : `${signal.header.flags}m`
7855
+ ).test(headers);
7856
+ const allHit = signal.allHtml && html && signal.allHtml.every((re) => re.test(html));
7857
+ const hit = signal.html && html && signal.html.test(html) || headerHit || allHit;
7858
+ if (!hit) continue;
7859
+ const existing = byFramework.get(signal.framework);
7860
+ if (existing) {
7861
+ existing.score += signal.weight;
7862
+ existing.matched.push(signal.name);
7863
+ } else {
7864
+ byFramework.set(signal.framework, {
7865
+ framework: signal.framework,
7866
+ language: signal.language,
7867
+ score: signal.weight,
7868
+ matched: [signal.name]
7869
+ });
7870
+ }
7871
+ }
7872
+ const ranked = [...byFramework.values()].sort((a, b) => b.score - a.score || a.framework.localeCompare(b.framework));
7873
+ const best = ranked[0];
7874
+ if (!best || best.score < SCORE_FLOOR) {
7875
+ return {
7876
+ framework: "unknown",
7877
+ language: "unknown",
7878
+ confidence: "low",
7879
+ score: best?.score ?? 0,
7880
+ matched: best?.matched ?? [],
7881
+ alternatives: ranked
7882
+ };
7883
+ }
7884
+ return {
7885
+ framework: best.framework,
7886
+ language: best.language,
7887
+ confidence: best.score >= WEIGHT.DECISIVE ? "high" : "medium",
7888
+ score: best.score,
7889
+ matched: best.matched,
7890
+ alternatives: ranked.slice(1)
7891
+ };
7892
+ }
7893
+
7894
+ // src/frameworks/detector.ts
7969
7895
  var PACKAGE_DEPENDENCIES = [
7970
7896
  // Meta-frameworks (check first - more specific)
7971
7897
  { framework: "nextjs", language: "typescript", packages: ["next"], meta_framework: "react" },
@@ -8015,19 +7941,14 @@ var ELIXIR_DEPENDENCIES = [
8015
7941
  { framework: "phoenix", language: "elixir", packages: [":phoenix", "phoenix"] }
8016
7942
  ];
8017
7943
  function detectFromHtml(html) {
8018
- for (const signature of HTML_SIGNATURES) {
8019
- for (const pattern of signature.patterns) {
8020
- if (pattern.test(html)) {
8021
- return {
8022
- framework: signature.framework,
8023
- language: signature.language,
8024
- confidence: signature.confidence,
8025
- detected_from: "html"
8026
- };
8027
- }
8028
- }
8029
- }
8030
- return null;
7944
+ const result = detectFromEvidence({ html });
7945
+ if (result.framework === "unknown") return null;
7946
+ return {
7947
+ framework: result.framework,
7948
+ language: result.language,
7949
+ confidence: result.confidence,
7950
+ detected_from: "html"
7951
+ };
8031
7952
  }
8032
7953
  function detectFromPackageJson(packageJson) {
8033
7954
  const allDeps = {
@@ -8169,52 +8090,14 @@ function detectFromMixExs(mixContent) {
8169
8090
  return null;
8170
8091
  }
8171
8092
  function detectFromHeaders(headers) {
8172
- const poweredBy = headers["x-powered-by"]?.toLowerCase() || "";
8173
- const server = headers["server"]?.toLowerCase() || "";
8174
- const generator = headers["x-generator"]?.toLowerCase() || "";
8175
- if (poweredBy.includes("next.js") || poweredBy.includes("next")) {
8176
- return { framework: "nextjs", language: "typescript", confidence: "high", detected_from: "headers" };
8177
- }
8178
- if (poweredBy.includes("nuxt")) {
8179
- return { framework: "nuxt", language: "typescript", confidence: "high", detected_from: "headers" };
8180
- }
8181
- if (poweredBy.includes("remix")) {
8182
- return { framework: "remix", language: "typescript", confidence: "high", detected_from: "headers" };
8183
- }
8184
- if (poweredBy.includes("phusion") || poweredBy.includes("passenger") || poweredBy.includes("puma") || poweredBy.includes("unicorn")) {
8185
- return { framework: "rails", language: "ruby", confidence: "medium", detected_from: "headers" };
8186
- }
8187
- if (poweredBy.includes("gunicorn") || poweredBy.includes("uvicorn") || poweredBy.includes("daphne")) {
8188
- return null;
8189
- }
8190
- if (poweredBy.includes("php")) {
8191
- return null;
8192
- }
8193
- if (server.includes("tomcat") || server.includes("jetty") || poweredBy.includes("spring")) {
8194
- return { framework: "spring", language: "java", confidence: "medium", detected_from: "headers" };
8195
- }
8196
- if (poweredBy.includes("asp.net") || server.includes("kestrel")) {
8197
- return { framework: "aspnet", language: "csharp", confidence: "high", detected_from: "headers" };
8198
- }
8199
- if (poweredBy.includes("phoenix") || poweredBy.includes("cowboy")) {
8200
- return { framework: "phoenix", language: "elixir", confidence: "medium", detected_from: "headers" };
8201
- }
8202
- if (server.includes("gin") || server.includes("echo") || server.includes("fiber")) {
8203
- return { framework: "go", language: "go", confidence: "medium", detected_from: "headers" };
8204
- }
8205
- if (generator.includes("hugo")) {
8206
- return { framework: "hugo", language: "go", confidence: "high", detected_from: "headers" };
8207
- }
8208
- if (generator.includes("jekyll")) {
8209
- return { framework: "jekyll", language: "ruby", confidence: "high", detected_from: "headers" };
8210
- }
8211
- if (generator.includes("eleventy") || generator.includes("11ty")) {
8212
- return { framework: "eleventy", language: "javascript", confidence: "high", detected_from: "headers" };
8213
- }
8214
- if (generator.includes("pelican")) {
8215
- return { framework: "pelican", language: "python", confidence: "high", detected_from: "headers" };
8216
- }
8217
- return null;
8093
+ const result = detectFromEvidence({ headers });
8094
+ if (result.framework === "unknown") return null;
8095
+ return {
8096
+ framework: result.framework,
8097
+ language: result.language,
8098
+ confidence: result.confidence,
8099
+ detected_from: "headers"
8100
+ };
8218
8101
  }
8219
8102
  function detectFramework(options) {
8220
8103
  if (options.packageJson) {
@@ -8249,13 +8132,16 @@ function detectFramework(options) {
8249
8132
  const result = detectFromMixExs(options.mixExsContent);
8250
8133
  if (result) return result;
8251
8134
  }
8252
- if (options.headers) {
8253
- const result = detectFromHeaders(options.headers);
8254
- if (result) return result;
8255
- }
8256
- if (options.html) {
8257
- const result = detectFromHtml(options.html);
8258
- if (result) return result;
8135
+ if (options.html || options.headers) {
8136
+ const result = detectFromEvidence({ html: options.html, headers: options.headers });
8137
+ if (result.framework !== "unknown") {
8138
+ return {
8139
+ framework: result.framework,
8140
+ language: result.language,
8141
+ confidence: result.confidence,
8142
+ detected_from: options.html ? "html" : "headers"
8143
+ };
8144
+ }
8259
8145
  }
8260
8146
  if (options.files) {
8261
8147
  if (options.files.some((f) => f.includes("next.config"))) {
@@ -8362,6 +8248,7 @@ function getFrameworkDisplayName(framework) {
8362
8248
  jekyll: "Jekyll",
8363
8249
  eleventy: "Eleventy (11ty)",
8364
8250
  pelican: "Pelican",
8251
+ sphinx: "Sphinx",
8365
8252
  unknown: "Unknown"
8366
8253
  };
8367
8254
  return names[framework];
@@ -12740,20 +12627,20 @@ function findMissingPreconnects(thirdPartyDomains, existingHints) {
12740
12627
  }
12741
12628
  })
12742
12629
  );
12743
- const missing = [];
12630
+ const missing2 = [];
12744
12631
  for (const domain of thirdPartyDomains) {
12745
12632
  try {
12746
12633
  const hostname = new URL(domain).hostname;
12747
12634
  const recommended = CRITICAL_THIRD_PARTIES[hostname] || [domain];
12748
12635
  for (const rec of recommended) {
12749
12636
  if (!preconnected.has(rec)) {
12750
- missing.push(rec);
12637
+ missing2.push(rec);
12751
12638
  }
12752
12639
  }
12753
12640
  } catch {
12754
12641
  }
12755
12642
  }
12756
- return [...new Set(missing)];
12643
+ return [...new Set(missing2)];
12757
12644
  }
12758
12645
  function analyzeResourceHints(html, url) {
12759
12646
  const issues = [];
@@ -33049,6 +32936,167 @@ function findAppRoot(files) {
33049
32936
  return `${nonRoot[0]}/`;
33050
32937
  }
33051
32938
 
32939
+ // src/fixer/safety.ts
32940
+ var HTML_EXT = /\.html?$/i;
32941
+ var SOURCE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|vue|svelte|astro)$/i;
32942
+ function attributeValues(html, pattern) {
32943
+ const found = /* @__PURE__ */ new Set();
32944
+ for (const match of html.matchAll(pattern)) {
32945
+ const value = match[1]?.trim();
32946
+ if (value) found.add(value);
32947
+ }
32948
+ return found;
32949
+ }
32950
+ function scriptSources(html) {
32951
+ return attributeValues(html, /<script[^>]+src=["']([^"']+)["']/gi);
32952
+ }
32953
+ function stylesheetHrefs(html) {
32954
+ return attributeValues(html, /<link[^>]+href=["']([^"']+)["'][^>]*>/gi);
32955
+ }
32956
+ function verificationTokens(html) {
32957
+ const found = /* @__PURE__ */ new Set();
32958
+ for (const match of html.matchAll(/<meta[^>]+name=["']([^"']*(?:site-verification|verify|verification)[^"']*)["'][^>]*>/gi)) {
32959
+ found.add(match[1].toLowerCase());
32960
+ }
32961
+ return found;
32962
+ }
32963
+ function importSpecifiers(source) {
32964
+ const found = /* @__PURE__ */ new Set();
32965
+ for (const match of source.matchAll(/\bfrom\s+["']([^"']+)["']/g)) found.add(match[1]);
32966
+ for (const match of source.matchAll(/\brequire\(\s*["']([^"']+)["']\s*\)/g)) found.add(match[1]);
32967
+ return found;
32968
+ }
32969
+ function missing(before, after) {
32970
+ return [...before].filter((value) => !after.has(value));
32971
+ }
32972
+ function xmlConcern(xml) {
32973
+ const stack = [];
32974
+ for (const match of xml.matchAll(/<\/?([A-Za-z_][\w:.-]*)[^>]*?(\/?)>/g)) {
32975
+ const [tag, name, selfClosing] = match;
32976
+ if (tag.startsWith("<?") || tag.startsWith("<!")) continue;
32977
+ if (tag.startsWith("</")) {
32978
+ if (stack.pop() !== name) return `unbalanced </${name}>`;
32979
+ } else if (!selfClosing) {
32980
+ stack.push(name);
32981
+ }
32982
+ }
32983
+ return stack.length ? `unclosed <${stack[stack.length - 1]}>` : null;
32984
+ }
32985
+ function sitemapUrlCount(xml) {
32986
+ return [...xml.matchAll(/<loc>/gi)].length;
32987
+ }
32988
+ function newestLastmod(xml) {
32989
+ const dates = [...xml.matchAll(/<lastmod>([^<]+)<\/lastmod>/gi)].map((m) => Date.parse(m[1].trim())).filter((t) => !Number.isNaN(t));
32990
+ return dates.length ? Math.max(...dates) : null;
32991
+ }
32992
+ function jsonLdConcerns(html) {
32993
+ const bad = [];
32994
+ for (const match of html.matchAll(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)) {
32995
+ try {
32996
+ JSON.parse(match[1]);
32997
+ } catch (error) {
32998
+ bad.push(error.message);
32999
+ }
33000
+ }
33001
+ return bad;
33002
+ }
33003
+ function assessReplacement(path3, original, replacement) {
33004
+ const concerns = [];
33005
+ const add = (code, detail) => concerns.push({ code, detail });
33006
+ if (/\.json$/i.test(path3) || /\.webmanifest$/i.test(path3)) {
33007
+ try {
33008
+ JSON.parse(replacement);
33009
+ } catch (error) {
33010
+ add("invalid_json", error.message);
33011
+ }
33012
+ }
33013
+ if (/\.xml$/i.test(path3)) {
33014
+ const problem = xmlConcern(replacement);
33015
+ if (problem) add("invalid_xml", problem);
33016
+ }
33017
+ if (HTML_EXT.test(path3)) {
33018
+ for (const problem of jsonLdConcerns(replacement)) add("invalid_json_ld", problem);
33019
+ }
33020
+ if (original === null) return concerns;
33021
+ if (replacement.length < original.length * 0.6) {
33022
+ add("shrunk", `${Math.round(replacement.length / original.length * 100)}% of the original length`);
33023
+ }
33024
+ if (HTML_EXT.test(path3)) {
33025
+ for (const tag of ["</head>", "</body>", "</html>"]) {
33026
+ if (original.includes(tag) && !replacement.includes(tag)) add("lost_closing_tag", tag);
33027
+ }
33028
+ const lostScripts = missing(scriptSources(original), scriptSources(replacement));
33029
+ if (lostScripts.length) add("lost_script", lostScripts.join(", "));
33030
+ const lostStyles = missing(stylesheetHrefs(original), stylesheetHrefs(replacement));
33031
+ if (lostStyles.length) add("lost_stylesheet", lostStyles.join(", "));
33032
+ const lostVerification = missing(verificationTokens(original), verificationTokens(replacement));
33033
+ if (lostVerification.length) add("lost_verification", lostVerification.join(", "));
33034
+ }
33035
+ if (SOURCE_EXT.test(path3)) {
33036
+ const lostImports = missing(importSpecifiers(original), importSpecifiers(replacement));
33037
+ if (lostImports.length) add("lost_import", lostImports.join(", "));
33038
+ }
33039
+ if (/sitemap[^/]*\.xml$/i.test(path3)) {
33040
+ const before = sitemapUrlCount(original);
33041
+ const after = sitemapUrlCount(replacement);
33042
+ if (after < before) add("fewer_sitemap_urls", `${before} -> ${after}`);
33043
+ const newestBefore = newestLastmod(original);
33044
+ const newestAfter = newestLastmod(replacement);
33045
+ if (newestBefore !== null && newestAfter !== null && newestAfter < newestBefore) {
33046
+ add(
33047
+ "lastmod_moved_backwards",
33048
+ `${new Date(newestBefore).toISOString().slice(0, 10)} -> ${new Date(newestAfter).toISOString().slice(0, 10)}`
33049
+ );
33050
+ }
33051
+ }
33052
+ return concerns;
33053
+ }
33054
+ function describeConcerns(concerns) {
33055
+ return concerns.map((c) => `${c.code} (${c.detail})`).join("; ");
33056
+ }
33057
+
33058
+ // src/fixer/framework-guidance.ts
33059
+ var DEFAULT_MAX_CHARS = 4e3;
33060
+ function checkBlock(check) {
33061
+ const fix = check.howToFix?.trim();
33062
+ return [
33063
+ `### ${check.title}${check.code ? ` (${check.code})` : ""}`,
33064
+ check.description?.trim(),
33065
+ fix ? `How this framework wants it fixed:
33066
+ ${fix}` : null
33067
+ ].filter(Boolean).join("\n");
33068
+ }
33069
+ function buildFrameworkGuidance(recipe, options = {}) {
33070
+ if (!recipe) return "";
33071
+ const budget = options.maxChars ?? DEFAULT_MAX_CHARS;
33072
+ const reported = new Set(options.issueCodes ?? []);
33073
+ const header = [
33074
+ `## ${recipe.display_name} conventions`,
33075
+ `Renders on the server by default: ${recipe.ssr_default ? "yes" : "no"}.`,
33076
+ recipe.seo_challenges?.length ? `Known traps in this framework:
33077
+ ${recipe.seo_challenges.map((c) => `- ${c}`).join("\n")}` : null
33078
+ ].filter(Boolean).join("\n");
33079
+ const checks = recipe.checks ?? [];
33080
+ const relevant = checks.filter((c) => c.code && reported.has(c.code));
33081
+ const rest = checks.filter((c) => !relevant.includes(c));
33082
+ let out = header;
33083
+ let included = 0;
33084
+ for (const check of [...relevant, ...rest]) {
33085
+ const block = checkBlock(check);
33086
+ if (out.length + block.length + 2 > budget) break;
33087
+ out += `
33088
+
33089
+ ${block}`;
33090
+ included++;
33091
+ }
33092
+ if (included < checks.length) {
33093
+ out += `
33094
+
33095
+ _(${checks.length - included} more ${recipe.display_name} checks omitted.)_`;
33096
+ }
33097
+ return out;
33098
+ }
33099
+
33052
33100
  // src/git/commit-helper.ts
33053
33101
  var import_child_process2 = require("child_process");
33054
33102
  var import_util2 = require("util");
@@ -35576,8 +35624,12 @@ async function analyzeComprehensive(html, url, options = {}) {
35576
35624
  // src/frameworks/index.ts
35577
35625
  var frameworks_exports = {};
35578
35626
  __export(frameworks_exports, {
35627
+ SCORE_FLOOR: () => SCORE_FLOOR,
35628
+ SIGNALS: () => SIGNALS,
35579
35629
  SUPPORTED_FRAMEWORKS: () => SUPPORTED_FRAMEWORKS,
35630
+ WEIGHT: () => WEIGHT,
35580
35631
  detectFramework: () => detectFramework,
35632
+ detectFromEvidence: () => detectFromEvidence,
35581
35633
  detectFromHeaders: () => detectFromHeaders,
35582
35634
  detectFromHtml: () => detectFromHtml,
35583
35635
  detectFromPackageJson: () => detectFromPackageJson,
@@ -35594,19 +35646,16 @@ __export(frameworks_exports, {
35594
35646
  loadRecipe: () => loadRecipe
35595
35647
  });
35596
35648
  var SUPPORTED_FRAMEWORKS = [
35597
- "react",
35598
- "nextjs",
35599
- "vue",
35600
- "nuxt",
35601
35649
  "angular",
35602
- "svelte",
35603
- "sveltekit",
35604
35650
  "astro",
35605
- "remix",
35606
- "gatsby",
35607
- "rails",
35608
- "laravel",
35609
35651
  "django",
35652
+ "laravel",
35653
+ "nextjs",
35654
+ "nuxt",
35655
+ "rails",
35656
+ "react",
35657
+ "sveltekit",
35658
+ "vue",
35610
35659
  "wordpress"
35611
35660
  ];
35612
35661
  // Annotate the CommonJS export names for ESM import in node:
@@ -35656,7 +35705,9 @@ var SUPPORTED_FRAMEWORKS = [
35656
35705
  analyzeUrlSafety,
35657
35706
  analyzers,
35658
35707
  applyFixes,
35708
+ assessReplacement,
35659
35709
  buildDeterministicFixes,
35710
+ buildFrameworkGuidance,
35660
35711
  buildGSCApiRequest,
35661
35712
  buildGSCRequest,
35662
35713
  calculateAIVisibilityScore,
@@ -35697,6 +35748,7 @@ var SUPPORTED_FRAMEWORKS = [
35697
35748
  createPullRequest,
35698
35749
  createSEOCommit,
35699
35750
  createSEOCommits,
35751
+ describeConcerns,
35700
35752
  detectDuplicates,
35701
35753
  detectFramework,
35702
35754
  detectGitHubPages,