cbrowser 18.13.4 → 18.14.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 (87) hide show
  1. package/dist/agent-personas.d.ts +55 -0
  2. package/dist/agent-personas.d.ts.map +1 -0
  3. package/dist/agent-personas.js +252 -0
  4. package/dist/agent-personas.js.map +1 -0
  5. package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
  6. package/dist/analysis/agent-ready-audit.js +584 -0
  7. package/dist/analysis/agent-ready-audit.js.map +1 -1
  8. package/dist/analysis/competitive-benchmark.d.ts +14 -0
  9. package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
  10. package/dist/analysis/competitive-benchmark.js +245 -0
  11. package/dist/analysis/competitive-benchmark.js.map +1 -1
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +5 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/llms-txt/diff.d.ts +85 -0
  17. package/dist/llms-txt/diff.d.ts.map +1 -0
  18. package/dist/llms-txt/diff.js +234 -0
  19. package/dist/llms-txt/diff.js.map +1 -0
  20. package/dist/llms-txt/index.d.ts +19 -0
  21. package/dist/llms-txt/index.d.ts.map +1 -0
  22. package/dist/llms-txt/index.js +19 -0
  23. package/dist/llms-txt/index.js.map +1 -0
  24. package/dist/llms-txt/validator.d.ts +80 -0
  25. package/dist/llms-txt/validator.d.ts.map +1 -0
  26. package/dist/llms-txt/validator.js +341 -0
  27. package/dist/llms-txt/validator.js.map +1 -0
  28. package/dist/mcp-server-remote.d.ts.map +1 -1
  29. package/dist/mcp-server-remote.js +36 -13
  30. package/dist/mcp-server-remote.js.map +1 -1
  31. package/dist/mcp-server.d.ts.map +1 -1
  32. package/dist/mcp-server.js +26 -2
  33. package/dist/mcp-server.js.map +1 -1
  34. package/dist/mcp-tools/base/analysis-tools.d.ts +1 -1
  35. package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -1
  36. package/dist/mcp-tools/base/analysis-tools.js +56 -2
  37. package/dist/mcp-tools/base/analysis-tools.js.map +1 -1
  38. package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
  39. package/dist/mcp-tools/base/cognitive-tools.js +14 -1
  40. package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
  41. package/dist/mcp-tools/base/index.d.ts +7 -3
  42. package/dist/mcp-tools/base/index.d.ts.map +1 -1
  43. package/dist/mcp-tools/base/index.js +14 -4
  44. package/dist/mcp-tools/base/index.js.map +1 -1
  45. package/dist/mcp-tools/base/llms-txt-tools.d.ts +12 -0
  46. package/dist/mcp-tools/base/llms-txt-tools.d.ts.map +1 -0
  47. package/dist/mcp-tools/base/llms-txt-tools.js +119 -0
  48. package/dist/mcp-tools/base/llms-txt-tools.js.map +1 -0
  49. package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
  50. package/dist/mcp-tools/base/persona-comparison-tools.js +12 -2
  51. package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
  52. package/dist/mcp-tools/base/remediation-tools.d.ts +12 -0
  53. package/dist/mcp-tools/base/remediation-tools.d.ts.map +1 -0
  54. package/dist/mcp-tools/base/remediation-tools.js +106 -0
  55. package/dist/mcp-tools/base/remediation-tools.js.map +1 -0
  56. package/dist/mcp-tools/index.d.ts +10 -10
  57. package/dist/mcp-tools/index.d.ts.map +1 -1
  58. package/dist/mcp-tools/index.js +11 -11
  59. package/dist/mcp-tools/index.js.map +1 -1
  60. package/dist/persona-questionnaire.d.ts +1 -1
  61. package/dist/persona-questionnaire.d.ts.map +1 -1
  62. package/dist/persona-questionnaire.js +12 -2
  63. package/dist/persona-questionnaire.js.map +1 -1
  64. package/dist/personas.d.ts +4 -2
  65. package/dist/personas.d.ts.map +1 -1
  66. package/dist/personas.js +12 -1
  67. package/dist/personas.js.map +1 -1
  68. package/dist/remediation/index.d.ts +19 -0
  69. package/dist/remediation/index.d.ts.map +1 -0
  70. package/dist/remediation/index.js +19 -0
  71. package/dist/remediation/index.js.map +1 -0
  72. package/dist/remediation/llms-txt.d.ts +65 -0
  73. package/dist/remediation/llms-txt.d.ts.map +1 -0
  74. package/dist/remediation/llms-txt.js +219 -0
  75. package/dist/remediation/llms-txt.js.map +1 -0
  76. package/dist/remediation/patch-generator.d.ts +54 -0
  77. package/dist/remediation/patch-generator.d.ts.map +1 -0
  78. package/dist/remediation/patch-generator.js +274 -0
  79. package/dist/remediation/patch-generator.js.map +1 -0
  80. package/dist/remediation/structured-data.d.ts +64 -0
  81. package/dist/remediation/structured-data.d.ts.map +1 -0
  82. package/dist/remediation/structured-data.js +352 -0
  83. package/dist/remediation/structured-data.js.map +1 -0
  84. package/dist/types.d.ts +213 -0
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js.map +1 -1
  87. package/package.json +1 -1
@@ -381,6 +381,575 @@ async function detectLowFindabilityElements(ctx) {
381
381
  summary.totalElements += poorSelectors.length;
382
382
  }
383
383
  // ============================================================================
384
+ // AI-Specific Detection Functions (v17.0.0)
385
+ // ============================================================================
386
+ /**
387
+ * Detect machine-readable metadata (JSON-LD, OpenGraph, Twitter Cards, landmarks)
388
+ * @since 17.0.0
389
+ */
390
+ async function detectMachineMetadata(ctx) {
391
+ const { page, issues, summary } = ctx;
392
+ const metadata = await page.evaluate(() => {
393
+ // Check for JSON-LD
394
+ const jsonLdScripts = document.querySelectorAll('script[type="application/ld+json"]');
395
+ const hasJsonLd = jsonLdScripts.length > 0;
396
+ let jsonLdValid = true;
397
+ jsonLdScripts.forEach(script => {
398
+ try {
399
+ JSON.parse(script.textContent || '');
400
+ }
401
+ catch {
402
+ jsonLdValid = false;
403
+ }
404
+ });
405
+ // Check for OpenGraph tags
406
+ const ogTags = document.querySelectorAll('meta[property^="og:"]');
407
+ const hasOg = ogTags.length > 0;
408
+ const ogTitle = document.querySelector('meta[property="og:title"]');
409
+ const ogDescription = document.querySelector('meta[property="og:description"]');
410
+ // Check for Twitter Cards
411
+ const twitterTags = document.querySelectorAll('meta[name^="twitter:"]');
412
+ const hasTwitter = twitterTags.length > 0;
413
+ // Check for semantic landmarks
414
+ const hasMain = !!document.querySelector('main, [role="main"]');
415
+ const hasNav = !!document.querySelector('nav, [role="navigation"]');
416
+ const hasHeader = !!document.querySelector('header, [role="banner"]');
417
+ const hasFooter = !!document.querySelector('footer, [role="contentinfo"]');
418
+ return {
419
+ jsonLd: { present: hasJsonLd, valid: jsonLdValid, count: jsonLdScripts.length },
420
+ og: { present: hasOg, hasTitle: !!ogTitle, hasDescription: !!ogDescription, count: ogTags.length },
421
+ twitter: { present: hasTwitter, count: twitterTags.length },
422
+ landmarks: { main: hasMain, nav: hasNav, header: hasHeader, footer: hasFooter },
423
+ };
424
+ });
425
+ // Track metadata count
426
+ let metadataCount = 0;
427
+ if (metadata.jsonLd.present)
428
+ metadataCount++;
429
+ if (metadata.og.present)
430
+ metadataCount++;
431
+ if (metadata.twitter.present)
432
+ metadataCount++;
433
+ summary.machineMetadataCount = metadataCount;
434
+ // Report missing JSON-LD
435
+ if (!metadata.jsonLd.present) {
436
+ issues.push({
437
+ category: "semantics",
438
+ severity: "medium",
439
+ subcategory: "machine-metadata",
440
+ element: "head",
441
+ description: "No JSON-LD structured data found",
442
+ detectionMethod: "json-ld-check",
443
+ recommendation: "Add JSON-LD schema markup for better AI agent understanding",
444
+ codeExample: `<script type="application/ld+json">
445
+ {
446
+ "@context": "https://schema.org",
447
+ "@type": "WebPage",
448
+ "name": "Page Title",
449
+ "description": "Page description"
450
+ }
451
+ </script>`,
452
+ });
453
+ }
454
+ else if (!metadata.jsonLd.valid) {
455
+ issues.push({
456
+ category: "semantics",
457
+ severity: "high",
458
+ subcategory: "machine-metadata",
459
+ element: "script[type='application/ld+json']",
460
+ description: "Invalid JSON-LD (malformed JSON)",
461
+ detectionMethod: "json-ld-check",
462
+ recommendation: "Fix JSON syntax errors in structured data",
463
+ });
464
+ }
465
+ // Report missing OpenGraph
466
+ if (!metadata.og.present) {
467
+ issues.push({
468
+ category: "semantics",
469
+ severity: "low",
470
+ subcategory: "machine-metadata",
471
+ element: "head",
472
+ description: "No OpenGraph meta tags found",
473
+ detectionMethod: "og-check",
474
+ recommendation: "Add OpenGraph tags for better content previews",
475
+ codeExample: `<meta property="og:title" content="Page Title">
476
+ <meta property="og:description" content="Page description">
477
+ <meta property="og:image" content="https://example.com/image.jpg">`,
478
+ });
479
+ }
480
+ // Report missing landmarks
481
+ const missingLandmarks = [];
482
+ if (!metadata.landmarks.main)
483
+ missingLandmarks.push("main");
484
+ if (!metadata.landmarks.nav)
485
+ missingLandmarks.push("nav");
486
+ if (missingLandmarks.length > 0) {
487
+ issues.push({
488
+ category: "accessibility",
489
+ severity: "medium",
490
+ subcategory: "machine-metadata",
491
+ element: "body",
492
+ description: `Missing semantic landmarks: ${missingLandmarks.join(", ")}`,
493
+ detectionMethod: "landmark-check",
494
+ recommendation: "Add semantic landmark elements for page structure",
495
+ codeExample: `<header role="banner">...</header>
496
+ <nav role="navigation">...</nav>
497
+ <main role="main">...</main>
498
+ <footer role="contentinfo">...</footer>`,
499
+ });
500
+ }
501
+ }
502
+ /**
503
+ * Detect navigation patterns (breadcrumbs, skip links, heading hierarchy)
504
+ * @since 17.0.0
505
+ */
506
+ async function detectNavigationPatterns(ctx) {
507
+ const { page, issues, summary } = ctx;
508
+ const navPatterns = await page.evaluate(() => {
509
+ // Check for breadcrumbs
510
+ const breadcrumbNav = document.querySelector('nav[aria-label*="breadcrumb" i], nav[aria-label*="Breadcrumb" i], [role="navigation"][aria-label*="breadcrumb" i]');
511
+ const breadcrumbSchema = document.querySelector('[itemtype*="BreadcrumbList"]');
512
+ const hasBreadcrumbs = !!breadcrumbNav || !!breadcrumbSchema;
513
+ // Check for skip links
514
+ const skipLinks = document.querySelectorAll('a[href^="#"]:first-child, a[href^="#main"], a[href^="#content"], .skip-link, .skip-to-content');
515
+ const hasSkipLink = skipLinks.length > 0;
516
+ // Check heading hierarchy
517
+ const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
518
+ const headingLevels = headings.map(h => parseInt(h.tagName[1]));
519
+ let hierarchyValid = true;
520
+ let hierarchyIssue = '';
521
+ const h1Count = headingLevels.filter(l => l === 1).length;
522
+ if (h1Count === 0) {
523
+ hierarchyValid = false;
524
+ hierarchyIssue = 'No H1 found';
525
+ }
526
+ else if (h1Count > 1) {
527
+ hierarchyValid = false;
528
+ hierarchyIssue = `Multiple H1 elements (${h1Count})`;
529
+ }
530
+ // Check for skipped levels (e.g., h1 -> h3)
531
+ for (let i = 1; i < headingLevels.length; i++) {
532
+ if (headingLevels[i] > headingLevels[i - 1] + 1) {
533
+ hierarchyValid = false;
534
+ hierarchyIssue = `Skipped heading level (h${headingLevels[i - 1]} to h${headingLevels[i]})`;
535
+ break;
536
+ }
537
+ }
538
+ // Check page title
539
+ const pageTitle = document.title?.trim();
540
+ return {
541
+ breadcrumbs: hasBreadcrumbs,
542
+ skipLink: hasSkipLink,
543
+ headings: { valid: hierarchyValid, issue: hierarchyIssue, count: headings.length },
544
+ pageTitle: { present: !!pageTitle, length: pageTitle?.length || 0 },
545
+ };
546
+ });
547
+ // Track navigation aids
548
+ let navAidsCount = 0;
549
+ if (navPatterns.breadcrumbs)
550
+ navAidsCount++;
551
+ if (navPatterns.skipLink)
552
+ navAidsCount++;
553
+ summary.navigationAidsCount = navAidsCount;
554
+ // Report missing breadcrumbs (only for multi-page sites with depth)
555
+ if (!navPatterns.breadcrumbs) {
556
+ issues.push({
557
+ category: "findability",
558
+ severity: "low",
559
+ subcategory: "navigation-patterns",
560
+ element: "nav",
561
+ description: "No breadcrumb navigation found",
562
+ detectionMethod: "breadcrumb-check",
563
+ recommendation: "Add breadcrumb navigation for hierarchical sites",
564
+ codeExample: `<nav aria-label="Breadcrumb">
565
+ <ol>
566
+ <li><a href="/">Home</a></li>
567
+ <li><a href="/section">Section</a></li>
568
+ <li aria-current="page">Current Page</li>
569
+ </ol>
570
+ </nav>`,
571
+ });
572
+ }
573
+ // Report missing skip link
574
+ if (!navPatterns.skipLink) {
575
+ issues.push({
576
+ category: "accessibility",
577
+ severity: "medium",
578
+ subcategory: "navigation-patterns",
579
+ element: "body",
580
+ description: "No skip-to-content link found",
581
+ detectionMethod: "skip-link-check",
582
+ recommendation: "Add a skip link for keyboard navigation",
583
+ codeExample: `<a href="#main-content" class="skip-link">Skip to main content</a>
584
+ <!-- CSS: .skip-link { position: absolute; left: -10000px; } .skip-link:focus { left: 0; } -->`,
585
+ });
586
+ }
587
+ // Report heading hierarchy issues
588
+ if (!navPatterns.headings.valid) {
589
+ issues.push({
590
+ category: "semantics",
591
+ severity: "medium",
592
+ subcategory: "navigation-patterns",
593
+ element: "h1-h6",
594
+ description: `Heading hierarchy issue: ${navPatterns.headings.issue}`,
595
+ detectionMethod: "heading-hierarchy-check",
596
+ recommendation: "Use a single H1 and maintain proper heading order (h1 → h2 → h3)",
597
+ });
598
+ }
599
+ // Report missing or short page title
600
+ if (!navPatterns.pageTitle.present) {
601
+ issues.push({
602
+ category: "findability",
603
+ severity: "high",
604
+ subcategory: "navigation-patterns",
605
+ element: "title",
606
+ description: "Page has no title",
607
+ detectionMethod: "page-title-check",
608
+ recommendation: "Add a descriptive <title> element",
609
+ });
610
+ }
611
+ else if (navPatterns.pageTitle.length < 10) {
612
+ issues.push({
613
+ category: "findability",
614
+ severity: "low",
615
+ subcategory: "navigation-patterns",
616
+ element: "title",
617
+ description: "Page title is very short (< 10 chars)",
618
+ detectionMethod: "page-title-check",
619
+ recommendation: "Use a more descriptive page title",
620
+ });
621
+ }
622
+ }
623
+ /**
624
+ * Detect actionable elements (action verbs on buttons, aria-describedby)
625
+ * @since 17.0.0
626
+ */
627
+ async function detectActionableElements(ctx) {
628
+ const { page, issues } = ctx;
629
+ const actionAnalysis = await page.evaluate(() => {
630
+ const buttons = Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]'));
631
+ // Generic/weak button labels
632
+ const genericLabels = ['submit', 'click', 'ok', 'yes', 'no', 'go', 'next', 'back', 'continue', 'send', 'done'];
633
+ // Strong action verbs
634
+ const actionVerbs = ['add', 'create', 'save', 'delete', 'remove', 'edit', 'update', 'download', 'upload', 'share', 'copy', 'search', 'filter', 'sort', 'buy', 'subscribe', 'register', 'login', 'logout', 'sign'];
635
+ const weakButtons = [];
636
+ let elementsWithDescribedBy = 0;
637
+ buttons.forEach(btn => {
638
+ const text = (btn.textContent?.trim() || btn.getAttribute('value') || btn.getAttribute('aria-label') || '').toLowerCase();
639
+ const words = text.split(/\s+/);
640
+ // Check if button uses generic label without action verb
641
+ const isGeneric = words.some(w => genericLabels.includes(w)) && !words.some(w => actionVerbs.some(v => w.startsWith(v)));
642
+ if (isGeneric && text.length < 20) {
643
+ weakButtons.push({
644
+ selector: btn.tagName.toLowerCase() + (btn.id ? `#${btn.id}` : ''),
645
+ text: text.slice(0, 30),
646
+ });
647
+ }
648
+ // Track aria-describedby usage
649
+ if (btn.hasAttribute('aria-describedby')) {
650
+ elementsWithDescribedBy++;
651
+ }
652
+ });
653
+ return {
654
+ weakButtons: weakButtons.slice(0, 5), // Limit to avoid noise
655
+ totalButtons: buttons.length,
656
+ elementsWithDescribedBy,
657
+ };
658
+ });
659
+ // Report buttons with generic labels
660
+ for (const btn of actionAnalysis.weakButtons) {
661
+ issues.push({
662
+ category: "findability",
663
+ severity: "low",
664
+ subcategory: "actionable-elements",
665
+ element: btn.selector,
666
+ description: `Button with generic label: "${btn.text}"`,
667
+ detectionMethod: "action-verb-check",
668
+ recommendation: "Use specific action verbs (e.g., 'Save Changes' instead of 'Submit')",
669
+ codeExample: `<!-- Instead of: -->
670
+ <button>Submit</button>
671
+ <!-- Use: -->
672
+ <button>Save Changes</button>
673
+ <button>Create Account</button>
674
+ <button>Download Report</button>`,
675
+ });
676
+ }
677
+ }
678
+ /**
679
+ * Detect content-to-navigation ratio
680
+ * @since 17.0.0
681
+ */
682
+ async function detectContentChrome(ctx) {
683
+ const { page, issues } = ctx;
684
+ const ratio = await page.evaluate(() => {
685
+ const viewport = { width: window.innerWidth, height: window.innerHeight };
686
+ const viewportArea = viewport.width * viewport.height;
687
+ // Calculate nav/header/footer area
688
+ let chromeArea = 0;
689
+ const chromeElements = document.querySelectorAll('nav, header, footer, aside, [role="navigation"], [role="banner"], [role="contentinfo"], [role="complementary"]');
690
+ chromeElements.forEach(el => {
691
+ const rect = el.getBoundingClientRect();
692
+ // Only count visible elements in viewport
693
+ if (rect.width > 0 && rect.height > 0 && rect.top < viewport.height) {
694
+ const visibleHeight = Math.min(rect.bottom, viewport.height) - Math.max(rect.top, 0);
695
+ const visibleWidth = Math.min(rect.right, viewport.width) - Math.max(rect.left, 0);
696
+ if (visibleHeight > 0 && visibleWidth > 0) {
697
+ chromeArea += visibleWidth * visibleHeight;
698
+ }
699
+ }
700
+ });
701
+ // Calculate main content area
702
+ let mainArea = 0;
703
+ const mainEl = document.querySelector('main, [role="main"], article, .content, #content, #main');
704
+ if (mainEl) {
705
+ const rect = mainEl.getBoundingClientRect();
706
+ if (rect.width > 0 && rect.height > 0) {
707
+ const visibleHeight = Math.min(rect.bottom, viewport.height) - Math.max(rect.top, 0);
708
+ const visibleWidth = Math.min(rect.right, viewport.width) - Math.max(rect.left, 0);
709
+ if (visibleHeight > 0 && visibleWidth > 0) {
710
+ mainArea = visibleWidth * visibleHeight;
711
+ }
712
+ }
713
+ }
714
+ const chromeRatio = chromeArea / viewportArea;
715
+ const contentRatio = mainArea / viewportArea;
716
+ return {
717
+ chromeRatio: Math.round(chromeRatio * 100),
718
+ contentRatio: Math.round(contentRatio * 100),
719
+ viewportArea,
720
+ };
721
+ });
722
+ // Flag excessive chrome (> 50% of viewport)
723
+ if (ratio.chromeRatio > 50) {
724
+ issues.push({
725
+ category: "findability",
726
+ severity: "medium",
727
+ subcategory: "content-chrome",
728
+ element: "viewport",
729
+ description: `Navigation/chrome occupies ${ratio.chromeRatio}% of above-fold viewport`,
730
+ detectionMethod: "content-chrome-check",
731
+ recommendation: "Reduce navigation prominence or use collapsible menus",
732
+ });
733
+ }
734
+ }
735
+ /**
736
+ * Detect API exposure (links to /api/ endpoints, GraphQL hints)
737
+ * @since 17.0.0
738
+ */
739
+ async function detectApiExposure(ctx) {
740
+ const { page, issues, summary } = ctx;
741
+ const apiInfo = await page.evaluate(() => {
742
+ const pageSource = document.documentElement.outerHTML;
743
+ // Look for /api/ patterns in links and scripts
744
+ const apiPatterns = pageSource.match(/["'](\/api\/[^"']*|https?:\/\/[^"']*\/api\/[^"']*)["']/gi) || [];
745
+ const uniqueApiPaths = [...new Set(apiPatterns.map(p => p.replace(/["']/g, '').split('?')[0]))];
746
+ // Look for GraphQL hints
747
+ const hasGraphQL = /graphql|__schema|__typename/i.test(pageSource);
748
+ const graphqlEndpoint = pageSource.match(/["'](\/graphql|https?:\/\/[^"']*graphql[^"']*)["']/i);
749
+ return {
750
+ apiEndpoints: uniqueApiPaths.slice(0, 10),
751
+ hasGraphQL,
752
+ graphqlEndpoint: graphqlEndpoint ? graphqlEndpoint[1] : null,
753
+ };
754
+ });
755
+ summary.apiEndpointsCount = apiInfo.apiEndpoints.length;
756
+ // Report API exposure as informational (positive for agents)
757
+ if (apiInfo.apiEndpoints.length > 0) {
758
+ issues.push({
759
+ category: "findability",
760
+ severity: "low", // Actually positive - just noting for awareness
761
+ subcategory: "api-exposure",
762
+ element: "script/link",
763
+ description: `${apiInfo.apiEndpoints.length} API endpoint(s) detected in page source`,
764
+ detectionMethod: "api-exposure-check",
765
+ recommendation: "API endpoints are agent-friendly. Ensure they're documented.",
766
+ });
767
+ }
768
+ if (apiInfo.hasGraphQL) {
769
+ issues.push({
770
+ category: "findability",
771
+ severity: "low",
772
+ subcategory: "api-exposure",
773
+ element: apiInfo.graphqlEndpoint || "script",
774
+ description: "GraphQL API detected",
775
+ detectionMethod: "graphql-check",
776
+ recommendation: "GraphQL is agent-friendly. Consider enabling introspection for agents.",
777
+ });
778
+ }
779
+ }
780
+ /**
781
+ * Detect /llms.txt presence
782
+ * @since 17.0.0
783
+ */
784
+ async function detectLlmsTxt(ctx) {
785
+ const { page, issues, summary } = ctx;
786
+ try {
787
+ const pageUrl = new URL(page.url());
788
+ const llmsTxtUrl = `${pageUrl.origin}/llms.txt`;
789
+ // Use page context to fetch (same cookies/auth)
790
+ const response = await page.context().request.get(llmsTxtUrl, {
791
+ timeout: 5000,
792
+ failOnStatusCode: false,
793
+ });
794
+ const status = response.status();
795
+ summary.hasLlmsTxt = status === 200;
796
+ if (status === 200) {
797
+ const content = await response.text();
798
+ const lines = content.split('\n').filter(l => l.trim());
799
+ // Basic validation
800
+ const hasTitle = lines.some(l => l.startsWith('#'));
801
+ const hasLinks = lines.some(l => l.includes('[') && l.includes(']('));
802
+ if (!hasTitle || !hasLinks) {
803
+ issues.push({
804
+ category: "semantics",
805
+ severity: "low",
806
+ subcategory: "llms-txt",
807
+ element: "/llms.txt",
808
+ description: "llms.txt exists but may be incomplete (missing headers or links)",
809
+ detectionMethod: "llms-txt-check",
810
+ recommendation: "Add markdown headers (#) and links ([text](url)) to llms.txt",
811
+ });
812
+ }
813
+ // No issue if llms.txt is present and valid - it's a positive signal
814
+ }
815
+ else if (status === 404) {
816
+ issues.push({
817
+ category: "semantics",
818
+ severity: "low",
819
+ subcategory: "llms-txt",
820
+ element: "/llms.txt",
821
+ description: "No /llms.txt found (recommended for AI agent documentation)",
822
+ detectionMethod: "llms-txt-check",
823
+ recommendation: "Add /llms.txt to help AI agents understand your site",
824
+ codeExample: `# Site Name
825
+
826
+ > Brief description of the site
827
+
828
+ ## Documentation
829
+ - [Getting Started](/docs/start)
830
+ - [API Reference](/api)
831
+
832
+ ## Important Pages
833
+ - [Pricing](/pricing)
834
+ - [Contact](/contact)`,
835
+ });
836
+ }
837
+ }
838
+ catch {
839
+ // Silently handle fetch errors - don't penalize for network issues
840
+ summary.hasLlmsTxt = false;
841
+ }
842
+ }
843
+ /**
844
+ * Detect state persistence patterns (CSRF tokens, session indicators)
845
+ * @since 17.0.0
846
+ */
847
+ async function detectStatePersistence(ctx) {
848
+ const { page, issues } = ctx;
849
+ const stateInfo = await page.evaluate(() => {
850
+ // Look for CSRF tokens
851
+ const csrfInputs = document.querySelectorAll('input[name*="csrf" i], input[name*="token" i], input[name="_token"], input[name="authenticity_token"]');
852
+ const csrfMeta = document.querySelector('meta[name*="csrf" i]');
853
+ const hasCsrf = csrfInputs.length > 0 || !!csrfMeta;
854
+ // Look for session indicators in forms
855
+ const hiddenInputs = document.querySelectorAll('input[type="hidden"]');
856
+ const sessionIndicators = Array.from(hiddenInputs).filter(input => {
857
+ const name = input.getAttribute('name')?.toLowerCase() || '';
858
+ return name.includes('session') || name.includes('state') || name.includes('nonce');
859
+ });
860
+ // Look for forms that might have non-idempotent actions
861
+ const forms = document.querySelectorAll('form');
862
+ const postForms = Array.from(forms).filter(f => f.method.toLowerCase() === 'post');
863
+ return {
864
+ hasCsrf,
865
+ csrfCount: csrfInputs.length + (csrfMeta ? 1 : 0),
866
+ sessionIndicators: sessionIndicators.length,
867
+ postFormCount: postForms.length,
868
+ };
869
+ });
870
+ // Report CSRF presence as informational (awareness for agents)
871
+ if (stateInfo.hasCsrf) {
872
+ issues.push({
873
+ category: "stability",
874
+ severity: "low",
875
+ subcategory: "state-persistence",
876
+ element: "form",
877
+ description: `${stateInfo.csrfCount} CSRF token(s) detected - agents need fresh tokens per request`,
878
+ detectionMethod: "csrf-check",
879
+ recommendation: "Ensure API endpoints support token refresh or use stateless auth for agents",
880
+ });
881
+ }
882
+ if (stateInfo.sessionIndicators > 0) {
883
+ issues.push({
884
+ category: "stability",
885
+ severity: "low",
886
+ subcategory: "state-persistence",
887
+ element: "input[type='hidden']",
888
+ description: `${stateInfo.sessionIndicators} session state indicator(s) in forms`,
889
+ detectionMethod: "session-check",
890
+ recommendation: "Document required session state for programmatic form submission",
891
+ });
892
+ }
893
+ }
894
+ /**
895
+ * Detect dynamic content patterns (loading states, infinite scroll, lazy load)
896
+ * @since 17.0.0
897
+ */
898
+ async function detectDynamicContent(ctx) {
899
+ const { page, issues, summary } = ctx;
900
+ const dynamicInfo = await page.evaluate(() => {
901
+ // Look for loading indicators
902
+ const loadingIndicators = document.querySelectorAll('[class*="loading" i], [class*="spinner" i], [class*="skeleton" i], [aria-busy="true"], [data-loading]');
903
+ // Look for infinite scroll patterns
904
+ const infiniteScrollHints = document.querySelectorAll('[data-infinite], [class*="infinite" i], [class*="load-more" i]');
905
+ // Look for lazy-load images
906
+ const lazyImages = document.querySelectorAll('img[loading="lazy"], img[data-src], img[data-lazy]');
907
+ // Check for intersection observer usage (common for infinite scroll)
908
+ const hasIntersectionObserver = typeof IntersectionObserver !== 'undefined';
909
+ // Look for "Load More" buttons
910
+ const loadMoreButtons = Array.from(document.querySelectorAll('button, a')).filter(el => {
911
+ const text = el.textContent?.toLowerCase() || '';
912
+ return text.includes('load more') || text.includes('show more') || text.includes('view more');
913
+ });
914
+ return {
915
+ loadingIndicators: loadingIndicators.length,
916
+ infiniteScrollHints: infiniteScrollHints.length,
917
+ lazyImages: lazyImages.length,
918
+ loadMoreButtons: loadMoreButtons.length,
919
+ hasIntersectionObserver,
920
+ };
921
+ });
922
+ const dynamicCount = (dynamicInfo.loadingIndicators > 0 ? 1 : 0) +
923
+ (dynamicInfo.infiniteScrollHints > 0 ? 1 : 0) +
924
+ (dynamicInfo.lazyImages > 5 ? 1 : 0) +
925
+ (dynamicInfo.loadMoreButtons > 0 ? 1 : 0);
926
+ summary.dynamicContentCount = dynamicCount;
927
+ // Report infinite scroll as a potential challenge for agents
928
+ if (dynamicInfo.infiniteScrollHints > 0 || dynamicInfo.loadMoreButtons > 0) {
929
+ issues.push({
930
+ category: "stability",
931
+ severity: "medium",
932
+ subcategory: "dynamic-content",
933
+ element: "body",
934
+ description: "Infinite scroll or load-more pattern detected",
935
+ detectionMethod: "infinite-scroll-check",
936
+ recommendation: "Provide pagination alternative or API endpoint for programmatic access",
937
+ });
938
+ }
939
+ // Report loading indicators
940
+ if (dynamicInfo.loadingIndicators > 3) {
941
+ issues.push({
942
+ category: "stability",
943
+ severity: "low",
944
+ subcategory: "dynamic-content",
945
+ element: "body",
946
+ description: `${dynamicInfo.loadingIndicators} loading indicator(s) found - agents should wait for content`,
947
+ detectionMethod: "loading-state-check",
948
+ recommendation: "Use aria-busy and loading states consistently for better agent detection",
949
+ });
950
+ }
951
+ }
952
+ // ============================================================================
384
953
  // Report Generation
385
954
  // ============================================================================
386
955
  function generateRecommendations(issues) {
@@ -874,6 +1443,12 @@ export async function runAgentReadyAudit(url, _options = {}) {
874
1443
  stickyOverlays: 0,
875
1444
  customDropdowns: 0,
876
1445
  elementsWithoutText: 0,
1446
+ // AI-specific counters (v17.0.0)
1447
+ machineMetadataCount: 0,
1448
+ navigationAidsCount: 0,
1449
+ hasLlmsTxt: false,
1450
+ apiEndpointsCount: 0,
1451
+ dynamicContentCount: 0,
877
1452
  };
878
1453
  const ctx = { page, issues, summary };
879
1454
  // Run all detection functions
@@ -884,6 +1459,15 @@ export async function runAgentReadyAudit(url, _options = {}) {
884
1459
  await detectMissingAltText(ctx);
885
1460
  await detectBadLinks(ctx);
886
1461
  await detectLowFindabilityElements(ctx);
1462
+ // AI-specific detection functions (v17.0.0)
1463
+ await detectMachineMetadata(ctx);
1464
+ await detectNavigationPatterns(ctx);
1465
+ await detectActionableElements(ctx);
1466
+ await detectContentChrome(ctx);
1467
+ await detectApiExposure(ctx);
1468
+ await detectLlmsTxt(ctx);
1469
+ await detectStatePersistence(ctx);
1470
+ await detectDynamicContent(ctx);
887
1471
  // Update summary
888
1472
  summary.problematicElements = issues.length;
889
1473
  // Calculate scores