@forkpoint/agent-lighthouse-core 0.4.0 → 1.0.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.
- package/dist/index.d.mts +67 -1
- package/dist/index.d.ts +67 -1
- package/dist/index.js +226 -1587
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +224 -1587
- package/dist/index.mjs.map +1 -1
- package/migration-map.json +110 -0
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -40,6 +40,7 @@ __export(index_exports, {
|
|
|
40
40
|
CheckResultSchema: () => CheckResultSchema,
|
|
41
41
|
CheckStatusSchema: () => CheckStatusSchema,
|
|
42
42
|
DEFAULT_SCAN_LIMIT: () => DEFAULT_SCAN_LIMIT,
|
|
43
|
+
DeprecationNoticeSchema: () => DeprecationNoticeSchema,
|
|
43
44
|
FixEffortSchema: () => FixEffortSchema,
|
|
44
45
|
MAX_CONCURRENT_REQUESTS: () => MAX_CONCURRENT_REQUESTS,
|
|
45
46
|
MAX_PAGES_PER_SCAN: () => MAX_PAGES_PER_SCAN,
|
|
@@ -84,6 +85,7 @@ __export(index_exports, {
|
|
|
84
85
|
getTierColor: () => getTierColor,
|
|
85
86
|
getTierLabel: () => getTierLabel,
|
|
86
87
|
getWordCount: () => getWordCount,
|
|
88
|
+
isInformative: () => isInformative,
|
|
87
89
|
isPrivateIp: () => isPrivateIp,
|
|
88
90
|
isSafeUrl: () => isSafeUrl,
|
|
89
91
|
joinUrl: () => joinUrl,
|
|
@@ -801,6 +803,10 @@ var AuditGuidanceSchema = import_zod.z.object({
|
|
|
801
803
|
tags: import_zod.z.array(import_zod.z.string().max(50)).max(20).optional()
|
|
802
804
|
});
|
|
803
805
|
var ScoreDisplayModeSchema = import_zod.z.enum(["binary", "ternary", "informative"]);
|
|
806
|
+
var DeprecationNoticeSchema = import_zod.z.object({
|
|
807
|
+
notice: import_zod.z.string().min(1).max(500),
|
|
808
|
+
link: import_zod.z.string().url()
|
|
809
|
+
});
|
|
804
810
|
var AuditMetaSchema = import_zod.z.object({
|
|
805
811
|
id: import_zod.z.string(),
|
|
806
812
|
category: import_zod.z.string(),
|
|
@@ -808,10 +814,12 @@ var AuditMetaSchema = import_zod.z.object({
|
|
|
808
814
|
failureTitle: import_zod.z.string(),
|
|
809
815
|
description: import_zod.z.string(),
|
|
810
816
|
scoreDisplayMode: ScoreDisplayModeSchema,
|
|
811
|
-
weight
|
|
817
|
+
// Deprecated audits carry weight 0 (excluded from scoring).
|
|
818
|
+
weight: import_zod.z.number().nonnegative(),
|
|
812
819
|
applicablePageTypes: import_zod.z.array(import_zod.z.string()).optional(),
|
|
813
820
|
defaultPriority: CheckPrioritySchema,
|
|
814
|
-
guidance: AuditGuidanceSchema.optional()
|
|
821
|
+
guidance: AuditGuidanceSchema.optional(),
|
|
822
|
+
deprecated: DeprecationNoticeSchema.optional()
|
|
815
823
|
});
|
|
816
824
|
var CheckResultSchema = import_zod.z.object({
|
|
817
825
|
id: import_zod.z.string().max(20),
|
|
@@ -833,7 +841,8 @@ var CheckResultSchema = import_zod.z.object({
|
|
|
833
841
|
code: import_zod.z.string().max(1e4).optional(),
|
|
834
842
|
docsUrl: import_zod.z.string().max(2048).url().optional().or(import_zod.z.string().length(0))
|
|
835
843
|
}).optional(),
|
|
836
|
-
tags: import_zod.z.array(import_zod.z.string().max(50)).max(20).optional()
|
|
844
|
+
tags: import_zod.z.array(import_zod.z.string().max(50)).max(20).optional(),
|
|
845
|
+
deprecated: DeprecationNoticeSchema.optional()
|
|
837
846
|
});
|
|
838
847
|
|
|
839
848
|
// src/audit.ts
|
|
@@ -926,7 +935,8 @@ var Audit = class _Audit {
|
|
|
926
935
|
docsUrl: meta.guidance?.docsUrl,
|
|
927
936
|
effort: meta.guidance?.effort
|
|
928
937
|
},
|
|
929
|
-
tags: meta.guidance?.tags
|
|
938
|
+
tags: meta.guidance?.tags,
|
|
939
|
+
deprecated: meta.deprecated
|
|
930
940
|
};
|
|
931
941
|
}
|
|
932
942
|
};
|
|
@@ -2629,90 +2639,16 @@ var NoBrokenLinksAudit = class _NoBrokenLinksAudit extends Audit {
|
|
|
2629
2639
|
}
|
|
2630
2640
|
};
|
|
2631
2641
|
|
|
2632
|
-
// src/audits/content-discoverability/navigation-json.ts
|
|
2633
|
-
function isOk14(result) {
|
|
2634
|
-
return result.status === 200;
|
|
2635
|
-
}
|
|
2636
|
-
var NavigationJsonAudit = class extends Audit {
|
|
2637
|
-
static meta = {
|
|
2638
|
-
id: "1.21",
|
|
2639
|
-
category: "content-discoverability",
|
|
2640
|
-
title: "navigation.json present",
|
|
2641
|
-
failureTitle: "navigation.json present",
|
|
2642
|
-
description: "A navigation.json file gives AI agents a machine-readable map of your site hierarchy, helping them navigate your site like a human would.",
|
|
2643
|
-
scoreDisplayMode: "binary",
|
|
2644
|
-
weight: 1,
|
|
2645
|
-
defaultPriority: "medium",
|
|
2646
|
-
guidance: {
|
|
2647
|
-
impact: "Without a machine-readable navigation structure, AI agents must infer your site hierarchy from HTML parsing, which is error-prone and incomplete. A navigation.json gives agents a clear map of your site, enabling accurate multi-step browsing.",
|
|
2648
|
-
fix: "Create a /navigation.json file at your site root with a JSON structure representing your site menu hierarchy. Include labels, URLs, and nested children for submenus.",
|
|
2649
|
-
code: '{\n "name": "Your Site",\n "items": [\n { "label": "Home", "url": "/" },\n { "label": "Products", "url": "/products", "children": [\n { "label": "Product A", "url": "/products/a" },\n { "label": "Product B", "url": "/products/b" }\n ]},\n { "label": "About", "url": "/about" }\n ]\n}',
|
|
2650
|
-
effort: "easy",
|
|
2651
|
-
tags: ["navigation", "structured-data", "discoverability"]
|
|
2652
|
-
}
|
|
2653
|
-
};
|
|
2654
|
-
audit(ctx) {
|
|
2655
|
-
const result = ctx.rootFiles["/navigation.json"];
|
|
2656
|
-
if (!result || !isOk14(result)) {
|
|
2657
|
-
return this.fail(
|
|
2658
|
-
"No navigation.json found at the site root.",
|
|
2659
|
-
"GET /navigation.json returns 200 with valid JSON",
|
|
2660
|
-
result ? `HTTP ${result.status}` : "No response",
|
|
2661
|
-
{
|
|
2662
|
-
priority: "medium",
|
|
2663
|
-
description: "A navigation.json file gives AI agents a machine-readable map of your site hierarchy. This helps agents navigate your site like a human would, following logical paths through menus and sections.",
|
|
2664
|
-
code: `{
|
|
2665
|
-
"name": "Your Site",
|
|
2666
|
-
"items": [
|
|
2667
|
-
{ "label": "Home", "url": "/" },
|
|
2668
|
-
{ "label": "Products", "url": "/products", "children": [
|
|
2669
|
-
{ "label": "Product A", "url": "/products/a" },
|
|
2670
|
-
{ "label": "Product B", "url": "/products/b" }
|
|
2671
|
-
]},
|
|
2672
|
-
{ "label": "About", "url": "/about" }
|
|
2673
|
-
]
|
|
2674
|
-
}`
|
|
2675
|
-
}
|
|
2676
|
-
);
|
|
2677
|
-
}
|
|
2678
|
-
try {
|
|
2679
|
-
JSON.parse(result.body);
|
|
2680
|
-
} catch {
|
|
2681
|
-
return this.fail(
|
|
2682
|
-
"navigation.json exists but contains invalid JSON.",
|
|
2683
|
-
"Valid JSON content",
|
|
2684
|
-
"Invalid JSON",
|
|
2685
|
-
{
|
|
2686
|
-
priority: "medium",
|
|
2687
|
-
description: "Your navigation.json has invalid JSON syntax, so AI agents cannot parse it. Validate the JSON and fix any syntax errors (missing commas, unquoted keys, etc.).",
|
|
2688
|
-
code: `{
|
|
2689
|
-
"name": "Your Site",
|
|
2690
|
-
"items": [
|
|
2691
|
-
{ "label": "Home", "url": "/" },
|
|
2692
|
-
{ "label": "About", "url": "/about" }
|
|
2693
|
-
]
|
|
2694
|
-
}`
|
|
2695
|
-
}
|
|
2696
|
-
);
|
|
2697
|
-
}
|
|
2698
|
-
return this.pass(
|
|
2699
|
-
"navigation.json exists with valid JSON.",
|
|
2700
|
-
"HTTP 200 with valid JSON",
|
|
2701
|
-
"Valid JSON"
|
|
2702
|
-
);
|
|
2703
|
-
}
|
|
2704
|
-
};
|
|
2705
|
-
|
|
2706
2642
|
// src/audits/content-discoverability/no-orphan-pages.ts
|
|
2707
2643
|
var cheerio7 = __toESM(require("cheerio"));
|
|
2708
|
-
function
|
|
2644
|
+
function isOk14(result) {
|
|
2709
2645
|
return result.status === 200;
|
|
2710
2646
|
}
|
|
2711
2647
|
function getSitemapResult5(ctx) {
|
|
2712
2648
|
const sitemap = ctx.rootFiles["/sitemap.xml"];
|
|
2713
|
-
if (sitemap &&
|
|
2649
|
+
if (sitemap && isOk14(sitemap)) return sitemap;
|
|
2714
2650
|
const index = ctx.rootFiles["/sitemap-index.xml"];
|
|
2715
|
-
if (index &&
|
|
2651
|
+
if (index && isOk14(index)) return index;
|
|
2716
2652
|
return null;
|
|
2717
2653
|
}
|
|
2718
2654
|
var NoOrphanPagesAudit = class extends Audit {
|
|
@@ -2754,7 +2690,7 @@ var NoOrphanPagesAudit = class extends Audit {
|
|
|
2754
2690
|
}
|
|
2755
2691
|
const llmsUrls = /* @__PURE__ */ new Set();
|
|
2756
2692
|
const llmsResult = ctx.rootFiles["/llms.txt"];
|
|
2757
|
-
if (llmsResult &&
|
|
2693
|
+
if (llmsResult && isOk14(llmsResult)) {
|
|
2758
2694
|
const links = extractMarkdownLinks(llmsResult.body);
|
|
2759
2695
|
for (const link of links) {
|
|
2760
2696
|
try {
|
|
@@ -5065,82 +5001,6 @@ var SpeakableSchemaAudit = class extends Audit {
|
|
|
5065
5001
|
}
|
|
5066
5002
|
};
|
|
5067
5003
|
|
|
5068
|
-
// src/audits/structured-data/potential-action.ts
|
|
5069
|
-
function matchesAnyType3(schema, types) {
|
|
5070
|
-
return types.some((t) => {
|
|
5071
|
-
const st = schema["@type"];
|
|
5072
|
-
if (typeof st === "string") return st === t;
|
|
5073
|
-
if (Array.isArray(st)) return st.includes(t);
|
|
5074
|
-
return false;
|
|
5075
|
-
});
|
|
5076
|
-
}
|
|
5077
|
-
function allSchemas5(ctx) {
|
|
5078
|
-
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5079
|
-
}
|
|
5080
|
-
var PotentialActionAudit = class extends Audit {
|
|
5081
|
-
static meta = {
|
|
5082
|
-
id: "3.10",
|
|
5083
|
-
category: "structured-data",
|
|
5084
|
-
title: "potentialAction on service pages",
|
|
5085
|
-
failureTitle: "potentialAction on service pages",
|
|
5086
|
-
description: "AI agents use potentialAction to understand what actions users can take on your site (order, book, contact). This enables agentic workflows where ChatGPT or Claude can guide users directly to the right action URL instead of just describing your service.",
|
|
5087
|
-
scoreDisplayMode: "binary",
|
|
5088
|
-
weight: 1,
|
|
5089
|
-
applicablePageTypes: ["homepage", "product"],
|
|
5090
|
-
defaultPriority: "medium",
|
|
5091
|
-
guidance: {
|
|
5092
|
-
impact: "Without potentialAction schema, AI agents cannot determine what actions users can take on your site (order, book, contact). This prevents agentic workflows where ChatGPT or Claude could guide users directly to the right action URL, reducing conversion from AI-driven traffic.",
|
|
5093
|
-
fix: "Add a potentialAction property to your Organization or Service schema with a ContactAction, OrderAction, or BookAction type and a target URL.",
|
|
5094
|
-
code: `{
|
|
5095
|
-
"@context": "https://schema.org",
|
|
5096
|
-
"@type": "Organization",
|
|
5097
|
-
"name": "Your Company",
|
|
5098
|
-
"potentialAction": {
|
|
5099
|
-
"@type": "OrderAction",
|
|
5100
|
-
"target": "https://yoursite.com/order"
|
|
5101
|
-
}
|
|
5102
|
-
}`,
|
|
5103
|
-
effort: "easy",
|
|
5104
|
-
docsUrl: "https://schema.org/potentialAction",
|
|
5105
|
-
tags: ["json-ld", "schema", "actions", "agentic-commerce"]
|
|
5106
|
-
}
|
|
5107
|
-
};
|
|
5108
|
-
audit(ctx) {
|
|
5109
|
-
const actionTypes = ["ContactAction", "OrderAction", "BookAction"];
|
|
5110
|
-
const schemas = allSchemas5(ctx);
|
|
5111
|
-
const withAction = schemas.filter((s) => {
|
|
5112
|
-
const obj = s;
|
|
5113
|
-
const action = obj["potentialAction"];
|
|
5114
|
-
if (!action) return false;
|
|
5115
|
-
const actions = Array.isArray(action) ? action : [action];
|
|
5116
|
-
return actions.some(
|
|
5117
|
-
(a) => a && typeof a === "object" && matchesAnyType3(a, actionTypes)
|
|
5118
|
-
);
|
|
5119
|
-
});
|
|
5120
|
-
const found = withAction.length > 0;
|
|
5121
|
-
if (found) {
|
|
5122
|
-
return this.pass(
|
|
5123
|
-
`potentialAction (ContactAction/OrderAction/BookAction) found on ${withAction.length} schema(s).`,
|
|
5124
|
-
"At least one page with potentialAction (ContactAction, OrderAction, or BookAction).",
|
|
5125
|
-
`${withAction.length} schema(s) with qualifying potentialAction`
|
|
5126
|
-
);
|
|
5127
|
-
}
|
|
5128
|
-
return this.fail(
|
|
5129
|
-
"No potentialAction with ContactAction, OrderAction, or BookAction found.",
|
|
5130
|
-
"At least one page with potentialAction (ContactAction, OrderAction, or BookAction).",
|
|
5131
|
-
"None",
|
|
5132
|
-
{
|
|
5133
|
-
priority: "medium",
|
|
5134
|
-
description: "AI agents use potentialAction to understand what actions users can take on your site (order, book, contact). This enables agentic workflows where ChatGPT or Claude can guide users directly to the right action URL instead of just describing your service.",
|
|
5135
|
-
code: `"potentialAction": {
|
|
5136
|
-
"@type": "OrderAction",
|
|
5137
|
-
"target": "https://yoursite.com/order"
|
|
5138
|
-
}`
|
|
5139
|
-
}
|
|
5140
|
-
);
|
|
5141
|
-
}
|
|
5142
|
-
};
|
|
5143
|
-
|
|
5144
5004
|
// src/audits/structured-data/howto-schema.ts
|
|
5145
5005
|
function matchesType4(schema, type) {
|
|
5146
5006
|
const t = schema["@type"];
|
|
@@ -5277,7 +5137,7 @@ var HowToSchemaAudit = class extends Audit {
|
|
|
5277
5137
|
};
|
|
5278
5138
|
|
|
5279
5139
|
// src/audits/structured-data/local-business-schema.ts
|
|
5280
|
-
function
|
|
5140
|
+
function matchesAnyType3(schema, types) {
|
|
5281
5141
|
return types.some((t) => {
|
|
5282
5142
|
const st = schema["@type"];
|
|
5283
5143
|
if (typeof st === "string") return st === t;
|
|
@@ -5285,12 +5145,12 @@ function matchesAnyType4(schema, types) {
|
|
|
5285
5145
|
return false;
|
|
5286
5146
|
});
|
|
5287
5147
|
}
|
|
5288
|
-
function
|
|
5148
|
+
function allSchemas5(ctx) {
|
|
5289
5149
|
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5290
5150
|
}
|
|
5291
5151
|
function hasPostalAddressBlock(page) {
|
|
5292
5152
|
const schemas = flattenJsonLd(page.structuredData ?? page.jsonLd);
|
|
5293
|
-
if (schemas.some((s) =>
|
|
5153
|
+
if (schemas.some((s) => matchesAnyType3(s, ["PostalAddress"]))) {
|
|
5294
5154
|
return true;
|
|
5295
5155
|
}
|
|
5296
5156
|
return page.$('[itemtype*="PostalAddress"]').length > 0;
|
|
@@ -5356,9 +5216,9 @@ var LocalBusinessSchemaAudit = class extends Audit {
|
|
|
5356
5216
|
"No physical location indicators found."
|
|
5357
5217
|
);
|
|
5358
5218
|
}
|
|
5359
|
-
const schemas =
|
|
5219
|
+
const schemas = allSchemas5(ctx);
|
|
5360
5220
|
const localSchemas = schemas.filter(
|
|
5361
|
-
(s) =>
|
|
5221
|
+
(s) => matchesAnyType3(s, ["LocalBusiness", "ProfessionalService"])
|
|
5362
5222
|
);
|
|
5363
5223
|
const found = localSchemas.length > 0;
|
|
5364
5224
|
if (found) {
|
|
@@ -5388,7 +5248,7 @@ var LocalBusinessSchemaAudit = class extends Audit {
|
|
|
5388
5248
|
};
|
|
5389
5249
|
|
|
5390
5250
|
// src/audits/structured-data/review-schema.ts
|
|
5391
|
-
function
|
|
5251
|
+
function matchesAnyType4(schema, types) {
|
|
5392
5252
|
return types.some((t) => {
|
|
5393
5253
|
const st = schema["@type"];
|
|
5394
5254
|
if (typeof st === "string") return st === t;
|
|
@@ -5396,7 +5256,7 @@ function matchesAnyType5(schema, types) {
|
|
|
5396
5256
|
return false;
|
|
5397
5257
|
});
|
|
5398
5258
|
}
|
|
5399
|
-
function
|
|
5259
|
+
function allSchemas6(ctx) {
|
|
5400
5260
|
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5401
5261
|
}
|
|
5402
5262
|
function hasTestimonialContent(page) {
|
|
@@ -5465,9 +5325,9 @@ var ReviewSchemaAudit = class extends Audit {
|
|
|
5465
5325
|
}
|
|
5466
5326
|
);
|
|
5467
5327
|
}
|
|
5468
|
-
const schemas =
|
|
5328
|
+
const schemas = allSchemas6(ctx);
|
|
5469
5329
|
const reviewSchemas = schemas.filter(
|
|
5470
|
-
(s) =>
|
|
5330
|
+
(s) => matchesAnyType4(s, ["Review", "AggregateRating"])
|
|
5471
5331
|
);
|
|
5472
5332
|
const schemasWithReviewProp = schemas.filter((s) => {
|
|
5473
5333
|
const obj = s;
|
|
@@ -5499,7 +5359,7 @@ var ReviewSchemaAudit = class extends Audit {
|
|
|
5499
5359
|
};
|
|
5500
5360
|
|
|
5501
5361
|
// src/audits/structured-data/offer-schema.ts
|
|
5502
|
-
function
|
|
5362
|
+
function matchesAnyType5(schema, types) {
|
|
5503
5363
|
return types.some((t) => {
|
|
5504
5364
|
const st = schema["@type"];
|
|
5505
5365
|
if (typeof st === "string") return st === t;
|
|
@@ -5547,7 +5407,7 @@ var OfferSchemaAudit = class extends Audit {
|
|
|
5547
5407
|
const pagesWithOffer = productPages.filter((p) => {
|
|
5548
5408
|
const schemas = flattenJsonLd(p.structuredData ?? p.jsonLd);
|
|
5549
5409
|
const hasOfferSchema = schemas.some(
|
|
5550
|
-
(s) =>
|
|
5410
|
+
(s) => matchesAnyType5(s, ["Offer", "AggregateOffer"])
|
|
5551
5411
|
);
|
|
5552
5412
|
const hasOfferProp = schemas.some((s) => {
|
|
5553
5413
|
const obj = s;
|
|
@@ -5561,7 +5421,7 @@ var OfferSchemaAudit = class extends Audit {
|
|
|
5561
5421
|
});
|
|
5562
5422
|
if (hasOfferSchema) {
|
|
5563
5423
|
const first2 = schemas.find(
|
|
5564
|
-
(s) =>
|
|
5424
|
+
(s) => matchesAnyType5(s, ["Offer", "AggregateOffer"])
|
|
5565
5425
|
);
|
|
5566
5426
|
return first2 && first2["price"] !== void 0 && !!first2["priceCurrency"];
|
|
5567
5427
|
}
|
|
@@ -5620,10 +5480,10 @@ function matchesType5(schema, type) {
|
|
|
5620
5480
|
if (Array.isArray(t)) return t.includes(type);
|
|
5621
5481
|
return false;
|
|
5622
5482
|
}
|
|
5623
|
-
function
|
|
5483
|
+
function matchesAnyType6(schema, types) {
|
|
5624
5484
|
return types.some((t) => matchesType5(schema, t));
|
|
5625
5485
|
}
|
|
5626
|
-
function
|
|
5486
|
+
function allSchemas7(ctx) {
|
|
5627
5487
|
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5628
5488
|
}
|
|
5629
5489
|
function hasProps3(obj, keys) {
|
|
@@ -5663,13 +5523,13 @@ var AuthorSchemaAudit = class extends Audit {
|
|
|
5663
5523
|
}
|
|
5664
5524
|
};
|
|
5665
5525
|
audit(ctx) {
|
|
5666
|
-
const schemas =
|
|
5526
|
+
const schemas = allSchemas7(ctx);
|
|
5667
5527
|
const personSchemas = schemas.filter(
|
|
5668
5528
|
(s) => matchesType5(s, "Person")
|
|
5669
5529
|
);
|
|
5670
5530
|
const authorFromArticles = [];
|
|
5671
5531
|
for (const s of schemas) {
|
|
5672
|
-
if (
|
|
5532
|
+
if (matchesAnyType6(s, ["Article", "NewsArticle", "BlogPosting"])) {
|
|
5673
5533
|
const author = s["author"];
|
|
5674
5534
|
if (Array.isArray(author)) {
|
|
5675
5535
|
for (const a of author) {
|
|
@@ -5739,120 +5599,8 @@ var AuthorSchemaAudit = class extends Audit {
|
|
|
5739
5599
|
}
|
|
5740
5600
|
};
|
|
5741
5601
|
|
|
5742
|
-
// src/audits/structured-data/action-schema.ts
|
|
5743
|
-
function matchesAnyType8(schema, types) {
|
|
5744
|
-
return types.some((t) => {
|
|
5745
|
-
const st = schema["@type"];
|
|
5746
|
-
if (typeof st === "string") return st === t;
|
|
5747
|
-
if (Array.isArray(st)) return st.includes(t);
|
|
5748
|
-
return false;
|
|
5749
|
-
});
|
|
5750
|
-
}
|
|
5751
|
-
function isConfirmationUrl(url) {
|
|
5752
|
-
return /\/(thank-?you|confirmation|success|order-complete)\b/i.test(url);
|
|
5753
|
-
}
|
|
5754
|
-
var ActionSchemaAudit = class extends Audit {
|
|
5755
|
-
static meta = {
|
|
5756
|
-
id: "3.16",
|
|
5757
|
-
category: "structured-data",
|
|
5758
|
-
title: "ConfirmAction/ReserveAction schema",
|
|
5759
|
-
failureTitle: "ConfirmAction/ReserveAction schema",
|
|
5760
|
-
description: "AI agents use ConfirmAction/ReserveAction schema to complete transactions on behalf of users in agentic workflows. Without this schema on your confirmation pages, agents cannot programmatically verify that a booking or purchase was successful.",
|
|
5761
|
-
scoreDisplayMode: "ternary",
|
|
5762
|
-
weight: 1,
|
|
5763
|
-
defaultPriority: "low",
|
|
5764
|
-
guidance: {
|
|
5765
|
-
impact: "Without ConfirmAction or ReserveAction schema on confirmation pages, AI agents cannot programmatically verify that a booking or purchase completed successfully. This breaks end-to-end agentic commerce workflows, forcing users back into manual checkout confirmation.",
|
|
5766
|
-
fix: "Add a ConfirmAction or ReserveAction as a potentialAction on your thank-you/confirmation page JSON-LD. Include the target URL pointing to the confirmation endpoint.",
|
|
5767
|
-
code: `{
|
|
5768
|
-
"@context": "https://schema.org",
|
|
5769
|
-
"@type": "WebPage",
|
|
5770
|
-
"potentialAction": {
|
|
5771
|
-
"@type": "ConfirmAction",
|
|
5772
|
-
"target": "https://yoursite.com/confirm"
|
|
5773
|
-
}
|
|
5774
|
-
}`,
|
|
5775
|
-
effort: "moderate",
|
|
5776
|
-
docsUrl: "https://schema.org/ConfirmAction",
|
|
5777
|
-
tags: ["json-ld", "schema", "agentic-commerce", "actions"]
|
|
5778
|
-
}
|
|
5779
|
-
};
|
|
5780
|
-
audit(ctx) {
|
|
5781
|
-
const confirmationPages = ctx.pages.filter((p) => isConfirmationUrl(p.url));
|
|
5782
|
-
if (confirmationPages.length === 0) {
|
|
5783
|
-
return this.warn(
|
|
5784
|
-
"No thank-you or confirmation pages detected to evaluate.",
|
|
5785
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5786
|
-
"No confirmation pages detected (URLs containing /thank-you/, /confirmation/, /success/).",
|
|
5787
|
-
{
|
|
5788
|
-
priority: "low",
|
|
5789
|
-
description: "If your site has booking or purchase flows, AI agents use ConfirmAction/ReserveAction schema to complete transactions on behalf of users. This enables end-to-end agentic workflows where the agent can confirm a booking without the user manually navigating forms.",
|
|
5790
|
-
code: `"potentialAction": {
|
|
5791
|
-
"@type": "ConfirmAction",
|
|
5792
|
-
"target": "https://yoursite.com/confirm"
|
|
5793
|
-
}`
|
|
5794
|
-
}
|
|
5795
|
-
);
|
|
5796
|
-
}
|
|
5797
|
-
const actionTypes = ["ConfirmAction", "ReserveAction"];
|
|
5798
|
-
const pagesWithAction = confirmationPages.filter((p) => {
|
|
5799
|
-
const schemas = flattenJsonLd(p.structuredData ?? p.jsonLd);
|
|
5800
|
-
const hasActionSchema = schemas.some(
|
|
5801
|
-
(s) => matchesAnyType8(s, actionTypes)
|
|
5802
|
-
);
|
|
5803
|
-
const hasActionProp = schemas.some((s) => {
|
|
5804
|
-
const obj = s;
|
|
5805
|
-
const action = obj["potentialAction"];
|
|
5806
|
-
if (!action) return false;
|
|
5807
|
-
const actions = Array.isArray(action) ? action : [action];
|
|
5808
|
-
return actions.some(
|
|
5809
|
-
(a) => a && typeof a === "object" && matchesAnyType8(a, actionTypes)
|
|
5810
|
-
);
|
|
5811
|
-
});
|
|
5812
|
-
return hasActionSchema || hasActionProp;
|
|
5813
|
-
});
|
|
5814
|
-
const allHave = pagesWithAction.length === confirmationPages.length;
|
|
5815
|
-
const someHave = pagesWithAction.length > 0;
|
|
5816
|
-
if (allHave) {
|
|
5817
|
-
return this.pass(
|
|
5818
|
-
`ConfirmAction/ReserveAction found on all ${confirmationPages.length} confirmation page(s).`,
|
|
5819
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5820
|
-
`${pagesWithAction.length}/${confirmationPages.length} confirmation pages with action schema`
|
|
5821
|
-
);
|
|
5822
|
-
}
|
|
5823
|
-
if (someHave) {
|
|
5824
|
-
return this.warn(
|
|
5825
|
-
`ConfirmAction/ReserveAction found on ${pagesWithAction.length} of ${confirmationPages.length} confirmation page(s).`,
|
|
5826
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5827
|
-
`${pagesWithAction.length}/${confirmationPages.length} confirmation pages with action schema`,
|
|
5828
|
-
{
|
|
5829
|
-
priority: "low",
|
|
5830
|
-
description: "AI agents use ConfirmAction/ReserveAction schema to complete transactions on behalf of users in agentic workflows. Without this schema on your confirmation pages, agents cannot programmatically verify that a booking or purchase was successful.",
|
|
5831
|
-
code: `"potentialAction": {
|
|
5832
|
-
"@type": "ConfirmAction",
|
|
5833
|
-
"target": "https://yoursite.com/confirm"
|
|
5834
|
-
}`
|
|
5835
|
-
}
|
|
5836
|
-
);
|
|
5837
|
-
}
|
|
5838
|
-
return this.fail(
|
|
5839
|
-
`No ConfirmAction/ReserveAction found on ${confirmationPages.length} confirmation page(s).`,
|
|
5840
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5841
|
-
`${pagesWithAction.length}/${confirmationPages.length} confirmation pages with action schema`,
|
|
5842
|
-
{
|
|
5843
|
-
priority: "low",
|
|
5844
|
-
description: "AI agents use ConfirmAction/ReserveAction schema to complete transactions on behalf of users in agentic workflows. Without this schema on your confirmation pages, agents cannot programmatically verify that a booking or purchase was successful.",
|
|
5845
|
-
code: `"potentialAction": {
|
|
5846
|
-
"@type": "ConfirmAction",
|
|
5847
|
-
"target": "https://yoursite.com/confirm"
|
|
5848
|
-
}`
|
|
5849
|
-
}
|
|
5850
|
-
);
|
|
5851
|
-
}
|
|
5852
|
-
};
|
|
5853
|
-
|
|
5854
5602
|
// src/audits/structured-data/product-identifiers.ts
|
|
5855
|
-
function
|
|
5603
|
+
function matchesAnyType7(schema, types) {
|
|
5856
5604
|
return types.some((t) => {
|
|
5857
5605
|
const st = schema["@type"];
|
|
5858
5606
|
if (typeof st === "string") return st === t;
|
|
@@ -5890,7 +5638,7 @@ var ProductIdentifiersAudit = class extends Audit {
|
|
|
5890
5638
|
audit(ctx) {
|
|
5891
5639
|
const schemas = ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5892
5640
|
const products = schemas.filter(
|
|
5893
|
-
(s) =>
|
|
5641
|
+
(s) => matchesAnyType7(s, [
|
|
5894
5642
|
"Product",
|
|
5895
5643
|
"IndividualProduct",
|
|
5896
5644
|
"ProductModel"
|
|
@@ -5955,7 +5703,7 @@ var ProductIdentifiersAudit = class extends Audit {
|
|
|
5955
5703
|
};
|
|
5956
5704
|
|
|
5957
5705
|
// src/audits/structured-data/product-details.ts
|
|
5958
|
-
function
|
|
5706
|
+
function matchesAnyType8(schema, types) {
|
|
5959
5707
|
return types.some((t) => {
|
|
5960
5708
|
const st = schema["@type"];
|
|
5961
5709
|
if (typeof st === "string") return st === t;
|
|
@@ -5999,7 +5747,7 @@ var ProductDetailsAudit = class extends Audit {
|
|
|
5999
5747
|
audit(ctx) {
|
|
6000
5748
|
const schemas = ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
6001
5749
|
const products = schemas.filter(
|
|
6002
|
-
(s) =>
|
|
5750
|
+
(s) => matchesAnyType8(s, [
|
|
6003
5751
|
"Product",
|
|
6004
5752
|
"IndividualProduct",
|
|
6005
5753
|
"ProductModel"
|
|
@@ -6118,7 +5866,7 @@ var ProductReviewsAudit = class extends Audit {
|
|
|
6118
5866
|
};
|
|
6119
5867
|
|
|
6120
5868
|
// src/audits/structured-data/product-transaction-certainty.ts
|
|
6121
|
-
function
|
|
5869
|
+
function matchesAnyType9(schema, types) {
|
|
6122
5870
|
return types.some((t) => {
|
|
6123
5871
|
const st = schema["@type"];
|
|
6124
5872
|
if (typeof st === "string") return st === t;
|
|
@@ -6177,7 +5925,7 @@ var ProductTransactionCertaintyAudit = class extends Audit {
|
|
|
6177
5925
|
audit(ctx) {
|
|
6178
5926
|
const schemas = ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
6179
5927
|
const products = schemas.filter(
|
|
6180
|
-
(s) =>
|
|
5928
|
+
(s) => matchesAnyType9(s, [
|
|
6181
5929
|
"Product",
|
|
6182
5930
|
"IndividualProduct",
|
|
6183
5931
|
"ProductModel"
|
|
@@ -6882,53 +6630,6 @@ var LlmsTxtLinkAudit = class extends Audit {
|
|
|
6882
6630
|
}
|
|
6883
6631
|
};
|
|
6884
6632
|
|
|
6885
|
-
// src/audits/meta-tags/llms-full-txt-link.ts
|
|
6886
|
-
var LlmsFullTxtLinkAudit = class extends Audit {
|
|
6887
|
-
static meta = {
|
|
6888
|
-
id: "4.12",
|
|
6889
|
-
category: "meta-tags",
|
|
6890
|
-
title: "llms-full.txt link in head",
|
|
6891
|
-
failureTitle: "llms-full.txt link in head",
|
|
6892
|
-
description: "The llms-full.txt file provides AI agents with a comprehensive, unabridged version of your content optimized for ingestion into context windows. Adding this link in <head> lets agents choose between the summary (llms.txt) and full content versions based on their context budget.",
|
|
6893
|
-
scoreDisplayMode: "binary",
|
|
6894
|
-
weight: 1,
|
|
6895
|
-
defaultPriority: "medium",
|
|
6896
|
-
guidance: {
|
|
6897
|
-
impact: "The llms-full.txt file provides AI agents with a comprehensive, unabridged version of your content optimized for large context windows. Without it, agents are limited to the summary version, potentially missing important details about your offerings.",
|
|
6898
|
-
fix: 'Create a llms-full.txt file with comprehensive content and add a <link rel="alternate"> tag in <head> pointing to it.',
|
|
6899
|
-
code: '<link rel="alternate" type="text/plain" href="/llms-full.txt" title="LLMs-full.txt">',
|
|
6900
|
-
effort: "moderate",
|
|
6901
|
-
docsUrl: "https://llmstxt.org/",
|
|
6902
|
-
tags: ["meta-tags", "llms-txt", "ai-discovery"]
|
|
6903
|
-
}
|
|
6904
|
-
};
|
|
6905
|
-
audit(ctx) {
|
|
6906
|
-
const page = ctx.pages[0];
|
|
6907
|
-
const link = page?.headLinks?.find(
|
|
6908
|
-
(l) => l.rel === "alternate" && l.type === "text/plain" && (l.title ?? "").toLowerCase().includes("llms-full")
|
|
6909
|
-
);
|
|
6910
|
-
if (link) {
|
|
6911
|
-
return this.pass(
|
|
6912
|
-
`llms-full.txt link found: "${link.href}".`,
|
|
6913
|
-
'<link rel="alternate" type="text/plain" title="...LLMs-full...">',
|
|
6914
|
-
`href="${link.href}" title="${link.title}"`,
|
|
6915
|
-
page.url
|
|
6916
|
-
);
|
|
6917
|
-
}
|
|
6918
|
-
return this.fail(
|
|
6919
|
-
"No llms-full.txt link found in <head>.",
|
|
6920
|
-
'<link rel="alternate" type="text/plain" title="...LLMs-full...">',
|
|
6921
|
-
"Not found",
|
|
6922
|
-
{
|
|
6923
|
-
priority: "medium",
|
|
6924
|
-
description: "The llms-full.txt file provides AI agents with a comprehensive, unabridged version of your content optimized for ingestion into context windows. Adding this link in <head> lets agents choose between the summary (llms.txt) and full content versions based on their context budget.",
|
|
6925
|
-
code: '<link rel="alternate" type="text/plain" href="/llms-full.txt" title="LLMs-full.txt">'
|
|
6926
|
-
},
|
|
6927
|
-
page?.url
|
|
6928
|
-
);
|
|
6929
|
-
}
|
|
6930
|
-
};
|
|
6931
|
-
|
|
6932
6633
|
// src/audits/meta-tags/ai-content-declaration.ts
|
|
6933
6634
|
var AiContentDeclarationAudit = class extends Audit {
|
|
6934
6635
|
static meta = {
|
|
@@ -6986,50 +6687,6 @@ var AiContentDeclarationAudit = class extends Audit {
|
|
|
6986
6687
|
}
|
|
6987
6688
|
};
|
|
6988
6689
|
|
|
6989
|
-
// src/audits/meta-tags/ai-instructions.ts
|
|
6990
|
-
var AiInstructionsAudit = class extends Audit {
|
|
6991
|
-
static meta = {
|
|
6992
|
-
id: "4.14",
|
|
6993
|
-
category: "meta-tags",
|
|
6994
|
-
title: "ai-instructions meta",
|
|
6995
|
-
failureTitle: "ai-instructions meta",
|
|
6996
|
-
description: "The ai-instructions meta tag gives AI agents a plain-English brief on how to interact with your site and represent your content. It acts like a system prompt for any AI agent visiting your page, telling it your preferred summarization style, content focus, and usage guidelines.",
|
|
6997
|
-
scoreDisplayMode: "binary",
|
|
6998
|
-
weight: 1,
|
|
6999
|
-
defaultPriority: "medium",
|
|
7000
|
-
guidance: {
|
|
7001
|
-
impact: "Without ai-instructions, AI agents have no guidance on how to interact with your site or represent your content. They may summarize your pages inaccurately, speculate about features, or miss your preferred content focus.",
|
|
7002
|
-
fix: 'Add a <meta name="ai-instructions"> tag with plain-English instructions telling AI agents how to summarize and represent your content.',
|
|
7003
|
-
code: '<meta name="ai-instructions" content="Summarise this page as a product overview. Focus on features and pricing. Do not speculate about unreleased features.">',
|
|
7004
|
-
effort: "trivial",
|
|
7005
|
-
tags: ["meta-tags", "ai-policy", "ai-discovery"]
|
|
7006
|
-
}
|
|
7007
|
-
};
|
|
7008
|
-
audit(ctx) {
|
|
7009
|
-
const page = ctx.pages[0];
|
|
7010
|
-
const value = (page?.meta?.["ai-instructions"] ?? "").trim();
|
|
7011
|
-
if (value) {
|
|
7012
|
-
return this.pass(
|
|
7013
|
-
`ai-instructions meta tag is present.`,
|
|
7014
|
-
"meta[ai-instructions] with non-empty content",
|
|
7015
|
-
value.length > 80 ? value.slice(0, 80) + "..." : value,
|
|
7016
|
-
page.url
|
|
7017
|
-
);
|
|
7018
|
-
}
|
|
7019
|
-
return this.fail(
|
|
7020
|
-
"No ai-instructions meta tag found.",
|
|
7021
|
-
"meta[ai-instructions] with non-empty content",
|
|
7022
|
-
"Not found",
|
|
7023
|
-
{
|
|
7024
|
-
priority: "medium",
|
|
7025
|
-
description: "The ai-instructions meta tag gives AI agents a plain-English brief on how to interact with your site and represent your content. It acts like a system prompt for any AI agent visiting your page, telling it your preferred summarization style, content focus, and usage guidelines.",
|
|
7026
|
-
code: '<meta name="ai-instructions" content="Summarise this page as a product overview. Focus on features and pricing. Do not speculate about unreleased features.">'
|
|
7027
|
-
},
|
|
7028
|
-
page?.url
|
|
7029
|
-
);
|
|
7030
|
-
}
|
|
7031
|
-
};
|
|
7032
|
-
|
|
7033
6690
|
// src/audits/meta-tags/markdown-alternate.ts
|
|
7034
6691
|
var MarkdownAlternateAudit = class extends Audit {
|
|
7035
6692
|
static meta = {
|
|
@@ -7120,94 +6777,47 @@ var RssFeedLinkAudit = class extends Audit {
|
|
|
7120
6777
|
}
|
|
7121
6778
|
};
|
|
7122
6779
|
|
|
7123
|
-
// src/audits/meta-tags/
|
|
7124
|
-
var
|
|
6780
|
+
// src/audits/meta-tags/openapi-link.ts
|
|
6781
|
+
var OpenApiLinkAudit = class extends Audit {
|
|
7125
6782
|
static meta = {
|
|
7126
|
-
id: "4.
|
|
6783
|
+
id: "4.18",
|
|
7127
6784
|
category: "meta-tags",
|
|
7128
|
-
title: "
|
|
7129
|
-
failureTitle: "
|
|
7130
|
-
description: "
|
|
6785
|
+
title: "OpenAPI spec link in head",
|
|
6786
|
+
failureTitle: "OpenAPI spec link in head",
|
|
6787
|
+
description: "AI agents use OpenAPI specifications to understand your API endpoints, parameters, and response formats. An OpenAPI link in <head> enables agents to programmatically interact with your API without manual documentation parsing, powering agentic workflows that call your services.",
|
|
7131
6788
|
scoreDisplayMode: "binary",
|
|
7132
6789
|
weight: 1,
|
|
7133
6790
|
defaultPriority: "low",
|
|
7134
6791
|
guidance: {
|
|
7135
|
-
impact: "Without an
|
|
7136
|
-
fix: 'If your site
|
|
7137
|
-
code: '<link rel="alternate" type="application/json" href="/
|
|
6792
|
+
impact: "Without an OpenAPI spec link, AI agents cannot programmatically understand your API endpoints, parameters, and response formats. This blocks agentic workflows that could call your API services on behalf of users.",
|
|
6793
|
+
fix: 'If your site has an API, create an OpenAPI specification and add a <link rel="alternate"> tag in <head> pointing to it.',
|
|
6794
|
+
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">',
|
|
7138
6795
|
effort: "complex",
|
|
7139
|
-
docsUrl: "https://
|
|
7140
|
-
tags: ["meta-tags", "
|
|
6796
|
+
docsUrl: "https://swagger.io/specification/",
|
|
6797
|
+
tags: ["meta-tags", "openapi", "api", "ai-discovery"]
|
|
7141
6798
|
}
|
|
7142
6799
|
};
|
|
7143
6800
|
audit(ctx) {
|
|
7144
6801
|
const page = ctx.pages[0];
|
|
7145
6802
|
const link = page?.headLinks?.find(
|
|
7146
|
-
(l) => l.rel === "alternate" && l.type === "application/json" && (l.title ?? "").toLowerCase().includes("
|
|
6803
|
+
(l) => l.rel === "alternate" && l.type === "application/json" && (l.title ?? "").toLowerCase().includes("openapi")
|
|
7147
6804
|
);
|
|
7148
6805
|
if (link) {
|
|
7149
6806
|
return this.pass(
|
|
7150
|
-
`
|
|
7151
|
-
'<link rel="alternate" type="application/json" title="...
|
|
6807
|
+
`OpenAPI spec link found: "${link.href}".`,
|
|
6808
|
+
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7152
6809
|
`href="${link.href}" title="${link.title}"`,
|
|
7153
6810
|
page.url
|
|
7154
6811
|
);
|
|
7155
6812
|
}
|
|
7156
6813
|
return this.fail(
|
|
7157
|
-
"No
|
|
7158
|
-
'<link rel="alternate" type="application/json" title="...
|
|
6814
|
+
"No OpenAPI spec link found in <head>.",
|
|
6815
|
+
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7159
6816
|
"Not found",
|
|
7160
6817
|
{
|
|
7161
6818
|
priority: "low",
|
|
7162
|
-
description: "
|
|
7163
|
-
code: '<link rel="alternate" type="application/json" href="/
|
|
7164
|
-
},
|
|
7165
|
-
page?.url
|
|
7166
|
-
);
|
|
7167
|
-
}
|
|
7168
|
-
};
|
|
7169
|
-
|
|
7170
|
-
// src/audits/meta-tags/openapi-link.ts
|
|
7171
|
-
var OpenApiLinkAudit = class extends Audit {
|
|
7172
|
-
static meta = {
|
|
7173
|
-
id: "4.18",
|
|
7174
|
-
category: "meta-tags",
|
|
7175
|
-
title: "OpenAPI spec link in head",
|
|
7176
|
-
failureTitle: "OpenAPI spec link in head",
|
|
7177
|
-
description: "AI agents use OpenAPI specifications to understand your API endpoints, parameters, and response formats. An OpenAPI link in <head> enables agents to programmatically interact with your API without manual documentation parsing, powering agentic workflows that call your services.",
|
|
7178
|
-
scoreDisplayMode: "binary",
|
|
7179
|
-
weight: 1,
|
|
7180
|
-
defaultPriority: "low",
|
|
7181
|
-
guidance: {
|
|
7182
|
-
impact: "Without an OpenAPI spec link, AI agents cannot programmatically understand your API endpoints, parameters, and response formats. This blocks agentic workflows that could call your API services on behalf of users.",
|
|
7183
|
-
fix: 'If your site has an API, create an OpenAPI specification and add a <link rel="alternate"> tag in <head> pointing to it.',
|
|
7184
|
-
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">',
|
|
7185
|
-
effort: "complex",
|
|
7186
|
-
docsUrl: "https://swagger.io/specification/",
|
|
7187
|
-
tags: ["meta-tags", "openapi", "api", "ai-discovery"]
|
|
7188
|
-
}
|
|
7189
|
-
};
|
|
7190
|
-
audit(ctx) {
|
|
7191
|
-
const page = ctx.pages[0];
|
|
7192
|
-
const link = page?.headLinks?.find(
|
|
7193
|
-
(l) => l.rel === "alternate" && l.type === "application/json" && (l.title ?? "").toLowerCase().includes("openapi")
|
|
7194
|
-
);
|
|
7195
|
-
if (link) {
|
|
7196
|
-
return this.pass(
|
|
7197
|
-
`OpenAPI spec link found: "${link.href}".`,
|
|
7198
|
-
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7199
|
-
`href="${link.href}" title="${link.title}"`,
|
|
7200
|
-
page.url
|
|
7201
|
-
);
|
|
7202
|
-
}
|
|
7203
|
-
return this.fail(
|
|
7204
|
-
"No OpenAPI spec link found in <head>.",
|
|
7205
|
-
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7206
|
-
"Not found",
|
|
7207
|
-
{
|
|
7208
|
-
priority: "low",
|
|
7209
|
-
description: "AI agents use OpenAPI specifications to understand your API endpoints, parameters, and response formats. An OpenAPI link in <head> enables agents to programmatically interact with your API without manual documentation parsing, powering agentic workflows that call your services.",
|
|
7210
|
-
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">'
|
|
6819
|
+
description: "AI agents use OpenAPI specifications to understand your API endpoints, parameters, and response formats. An OpenAPI link in <head> enables agents to programmatically interact with your API without manual documentation parsing, powering agentic workflows that call your services.",
|
|
6820
|
+
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">'
|
|
7211
6821
|
},
|
|
7212
6822
|
page?.url
|
|
7213
6823
|
);
|
|
@@ -7732,7 +7342,7 @@ var OpenApiOperationIdsAudit = class _OpenApiOperationIdsAudit extends Audit {
|
|
|
7732
7342
|
}
|
|
7733
7343
|
};
|
|
7734
7344
|
|
|
7735
|
-
// src/audits/agent-tools/openapi-
|
|
7345
|
+
// src/audits/agent-tools/openapi-servers.ts
|
|
7736
7346
|
function tryParseJson4(body) {
|
|
7737
7347
|
try {
|
|
7738
7348
|
return JSON.parse(body);
|
|
@@ -7751,90 +7361,6 @@ function getOpenApiSpec3(ctx) {
|
|
|
7751
7361
|
}
|
|
7752
7362
|
return void 0;
|
|
7753
7363
|
}
|
|
7754
|
-
var OpenApiAiInstructionsAudit = class _OpenApiAiInstructionsAudit extends Audit {
|
|
7755
|
-
static meta = {
|
|
7756
|
-
id: "5.4",
|
|
7757
|
-
category: "agent-tools",
|
|
7758
|
-
title: "x-ai-instructions in OpenAPI",
|
|
7759
|
-
failureTitle: "x-ai-instructions in OpenAPI",
|
|
7760
|
-
description: "The x-ai-instructions field lets you give natural-language guidance to AI agents about how to use your API. This is your chance to explain business logic, rate limits, authentication flow, and common use cases in plain English.",
|
|
7761
|
-
scoreDisplayMode: "binary",
|
|
7762
|
-
weight: 1,
|
|
7763
|
-
defaultPriority: "medium",
|
|
7764
|
-
guidance: {
|
|
7765
|
-
impact: "Without x-ai-instructions, AI agents must infer your API's business logic, usage patterns, and constraints from endpoint names alone. This leads to incorrect API usage, violated rate limits, and poor user experiences.",
|
|
7766
|
-
fix: "Add an x-ai-instructions string to the info object of your OpenAPI spec. Describe common workflows, rate limits, authentication requirements, and any sequencing constraints in plain English.",
|
|
7767
|
-
code: `"info": {
|
|
7768
|
-
"title": "Your Site API",
|
|
7769
|
-
"version": "1.0.0",
|
|
7770
|
-
"x-ai-instructions": "This API lets you search content, submit contact forms, and retrieve product details. Always call searchContent before submitContact. Rate limit: 10 requests/minute. No authentication required for read endpoints."
|
|
7771
|
-
}`,
|
|
7772
|
-
effort: "trivial",
|
|
7773
|
-
tags: ["openapi", "ai-instructions", "api"]
|
|
7774
|
-
}
|
|
7775
|
-
};
|
|
7776
|
-
audit(ctx) {
|
|
7777
|
-
const spec = getOpenApiSpec3(ctx);
|
|
7778
|
-
if (!spec) {
|
|
7779
|
-
return this.fail(
|
|
7780
|
-
"No parseable OpenAPI JSON spec found.",
|
|
7781
|
-
"info object has x-ai-instructions field",
|
|
7782
|
-
"No spec",
|
|
7783
|
-
{
|
|
7784
|
-
priority: "medium",
|
|
7785
|
-
description: _OpenApiAiInstructionsAudit.meta.description,
|
|
7786
|
-
code: `"info": {
|
|
7787
|
-
"title": "Your Site API",
|
|
7788
|
-
"version": "1.0.0",
|
|
7789
|
-
"x-ai-instructions": "This API lets you search content, submit contact forms, and retrieve product details. Always call searchContent before submitContact. Rate limit: 10 requests/minute. No authentication required for read endpoints."
|
|
7790
|
-
}`
|
|
7791
|
-
}
|
|
7792
|
-
);
|
|
7793
|
-
}
|
|
7794
|
-
const info = spec["info"];
|
|
7795
|
-
if (isObject4(info) && typeof info["x-ai-instructions"] === "string" && info["x-ai-instructions"]) {
|
|
7796
|
-
return this.pass(
|
|
7797
|
-
"OpenAPI info object contains x-ai-instructions.",
|
|
7798
|
-
"info object has x-ai-instructions field",
|
|
7799
|
-
"x-ai-instructions present"
|
|
7800
|
-
);
|
|
7801
|
-
}
|
|
7802
|
-
return this.fail(
|
|
7803
|
-
"OpenAPI info object does not contain x-ai-instructions.",
|
|
7804
|
-
"info object has x-ai-instructions field",
|
|
7805
|
-
"x-ai-instructions missing",
|
|
7806
|
-
{
|
|
7807
|
-
priority: "medium",
|
|
7808
|
-
description: _OpenApiAiInstructionsAudit.meta.description,
|
|
7809
|
-
code: `"info": {
|
|
7810
|
-
"title": "Your Site API",
|
|
7811
|
-
"version": "1.0.0",
|
|
7812
|
-
"x-ai-instructions": "This API lets you search content, submit contact forms, and retrieve product details. Always call searchContent before submitContact. Rate limit: 10 requests/minute. No authentication required for read endpoints."
|
|
7813
|
-
}`
|
|
7814
|
-
}
|
|
7815
|
-
);
|
|
7816
|
-
}
|
|
7817
|
-
};
|
|
7818
|
-
|
|
7819
|
-
// src/audits/agent-tools/openapi-servers.ts
|
|
7820
|
-
function tryParseJson5(body) {
|
|
7821
|
-
try {
|
|
7822
|
-
return JSON.parse(body);
|
|
7823
|
-
} catch {
|
|
7824
|
-
return void 0;
|
|
7825
|
-
}
|
|
7826
|
-
}
|
|
7827
|
-
function isObject5(val) {
|
|
7828
|
-
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
7829
|
-
}
|
|
7830
|
-
function getOpenApiSpec4(ctx) {
|
|
7831
|
-
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
7832
|
-
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
7833
|
-
const parsed = tryParseJson5(jsonResult.body);
|
|
7834
|
-
if (isObject5(parsed)) return parsed;
|
|
7835
|
-
}
|
|
7836
|
-
return void 0;
|
|
7837
|
-
}
|
|
7838
7364
|
var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
7839
7365
|
static meta = {
|
|
7840
7366
|
id: "5.5",
|
|
@@ -7860,7 +7386,7 @@ var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
|
7860
7386
|
}
|
|
7861
7387
|
};
|
|
7862
7388
|
async audit(ctx) {
|
|
7863
|
-
const spec =
|
|
7389
|
+
const spec = getOpenApiSpec3(ctx);
|
|
7864
7390
|
if (!spec) {
|
|
7865
7391
|
return this.fail(
|
|
7866
7392
|
"No parseable OpenAPI JSON spec found.",
|
|
@@ -7897,7 +7423,7 @@ var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
|
7897
7423
|
);
|
|
7898
7424
|
}
|
|
7899
7425
|
const firstWithUrl = servers.find(
|
|
7900
|
-
(s) =>
|
|
7426
|
+
(s) => isObject4(s) && typeof s["url"] === "string" && s["url"]
|
|
7901
7427
|
);
|
|
7902
7428
|
if (!firstWithUrl) {
|
|
7903
7429
|
return this.fail(
|
|
@@ -7962,34 +7488,34 @@ var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
|
7962
7488
|
};
|
|
7963
7489
|
|
|
7964
7490
|
// src/audits/agent-tools/openapi-schemas.ts
|
|
7965
|
-
function
|
|
7491
|
+
function tryParseJson5(body) {
|
|
7966
7492
|
try {
|
|
7967
7493
|
return JSON.parse(body);
|
|
7968
7494
|
} catch {
|
|
7969
7495
|
return void 0;
|
|
7970
7496
|
}
|
|
7971
7497
|
}
|
|
7972
|
-
function
|
|
7498
|
+
function isObject5(val) {
|
|
7973
7499
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
7974
7500
|
}
|
|
7975
7501
|
var HTTP_METHODS3 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
7976
|
-
function
|
|
7502
|
+
function getOpenApiSpec4(ctx) {
|
|
7977
7503
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
7978
7504
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
7979
|
-
const parsed =
|
|
7980
|
-
if (
|
|
7505
|
+
const parsed = tryParseJson5(jsonResult.body);
|
|
7506
|
+
if (isObject5(parsed)) return parsed;
|
|
7981
7507
|
}
|
|
7982
7508
|
return void 0;
|
|
7983
7509
|
}
|
|
7984
7510
|
function getOperations3(spec) {
|
|
7985
7511
|
const paths = spec["paths"];
|
|
7986
|
-
if (!
|
|
7512
|
+
if (!isObject5(paths)) return [];
|
|
7987
7513
|
const ops = [];
|
|
7988
7514
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
7989
|
-
if (!
|
|
7515
|
+
if (!isObject5(pathItem)) continue;
|
|
7990
7516
|
for (const method of HTTP_METHODS3) {
|
|
7991
7517
|
const op = pathItem[method];
|
|
7992
|
-
if (
|
|
7518
|
+
if (isObject5(op)) {
|
|
7993
7519
|
ops.push({ path, method, op });
|
|
7994
7520
|
}
|
|
7995
7521
|
}
|
|
@@ -8050,7 +7576,7 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
8050
7576
|
}
|
|
8051
7577
|
};
|
|
8052
7578
|
audit(ctx) {
|
|
8053
|
-
const spec =
|
|
7579
|
+
const spec = getOpenApiSpec4(ctx);
|
|
8054
7580
|
if (!spec) {
|
|
8055
7581
|
return this.fail(
|
|
8056
7582
|
"No parseable OpenAPI JSON spec found.",
|
|
@@ -8151,11 +7677,11 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
8151
7677
|
if (["post", "put", "patch"].includes(method)) {
|
|
8152
7678
|
writeMethods++;
|
|
8153
7679
|
const rb = op["requestBody"];
|
|
8154
|
-
if (
|
|
7680
|
+
if (isObject5(rb)) {
|
|
8155
7681
|
const content = rb["content"];
|
|
8156
|
-
if (
|
|
7682
|
+
if (isObject5(content)) {
|
|
8157
7683
|
for (const mediaType of Object.values(content)) {
|
|
8158
|
-
if (
|
|
7684
|
+
if (isObject5(mediaType) && mediaType["schema"]) {
|
|
8159
7685
|
withRequestSchema++;
|
|
8160
7686
|
break;
|
|
8161
7687
|
}
|
|
@@ -8164,13 +7690,13 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
8164
7690
|
}
|
|
8165
7691
|
}
|
|
8166
7692
|
const responses = op["responses"];
|
|
8167
|
-
if (
|
|
7693
|
+
if (isObject5(responses)) {
|
|
8168
7694
|
for (const resp of Object.values(responses)) {
|
|
8169
|
-
if (
|
|
7695
|
+
if (isObject5(resp)) {
|
|
8170
7696
|
const content = resp["content"];
|
|
8171
|
-
if (
|
|
7697
|
+
if (isObject5(content)) {
|
|
8172
7698
|
for (const mediaType of Object.values(content)) {
|
|
8173
|
-
if (
|
|
7699
|
+
if (isObject5(mediaType) && mediaType["schema"]) {
|
|
8174
7700
|
withResponseSchema++;
|
|
8175
7701
|
break;
|
|
8176
7702
|
}
|
|
@@ -8247,14 +7773,14 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
8247
7773
|
};
|
|
8248
7774
|
|
|
8249
7775
|
// src/audits/agent-tools/ai-catalog-exists.ts
|
|
8250
|
-
function
|
|
7776
|
+
function tryParseJson6(body) {
|
|
8251
7777
|
try {
|
|
8252
7778
|
return JSON.parse(body);
|
|
8253
7779
|
} catch {
|
|
8254
7780
|
return void 0;
|
|
8255
7781
|
}
|
|
8256
7782
|
}
|
|
8257
|
-
function
|
|
7783
|
+
function isObject6(val) {
|
|
8258
7784
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8259
7785
|
}
|
|
8260
7786
|
var AiCatalogExistsAudit = class _AiCatalogExistsAudit extends Audit {
|
|
@@ -8326,8 +7852,8 @@ var AiCatalogExistsAudit = class _AiCatalogExistsAudit extends Audit {
|
|
|
8326
7852
|
}
|
|
8327
7853
|
);
|
|
8328
7854
|
}
|
|
8329
|
-
const parsed =
|
|
8330
|
-
if (!
|
|
7855
|
+
const parsed = tryParseJson6(result.body);
|
|
7856
|
+
if (!isObject6(parsed)) {
|
|
8331
7857
|
return this.fail(
|
|
8332
7858
|
"ai-catalog.json is not valid JSON.",
|
|
8333
7859
|
"/.well-known/ai-catalog.json returns 200 with valid JSON containing services array",
|
|
@@ -8406,14 +7932,14 @@ var AiCatalogExistsAudit = class _AiCatalogExistsAudit extends Audit {
|
|
|
8406
7932
|
};
|
|
8407
7933
|
|
|
8408
7934
|
// src/audits/agent-tools/ai-catalog-metadata.ts
|
|
8409
|
-
function
|
|
7935
|
+
function tryParseJson7(body) {
|
|
8410
7936
|
try {
|
|
8411
7937
|
return JSON.parse(body);
|
|
8412
7938
|
} catch {
|
|
8413
7939
|
return void 0;
|
|
8414
7940
|
}
|
|
8415
7941
|
}
|
|
8416
|
-
function
|
|
7942
|
+
function isObject7(val) {
|
|
8417
7943
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8418
7944
|
}
|
|
8419
7945
|
var AiCatalogMetadataAudit = class _AiCatalogMetadataAudit extends Audit {
|
|
@@ -8466,8 +7992,8 @@ var AiCatalogMetadataAudit = class _AiCatalogMetadataAudit extends Audit {
|
|
|
8466
7992
|
}
|
|
8467
7993
|
);
|
|
8468
7994
|
}
|
|
8469
|
-
const parsed =
|
|
8470
|
-
if (!
|
|
7995
|
+
const parsed = tryParseJson7(result.body);
|
|
7996
|
+
if (!isObject7(parsed)) {
|
|
8471
7997
|
return this.fail(
|
|
8472
7998
|
"ai-catalog.json is not valid JSON.",
|
|
8473
7999
|
"Has version, name, description, capabilities, owner, contact, lastUpdated",
|
|
@@ -8540,14 +8066,14 @@ var AiCatalogMetadataAudit = class _AiCatalogMetadataAudit extends Audit {
|
|
|
8540
8066
|
};
|
|
8541
8067
|
|
|
8542
8068
|
// src/audits/agent-tools/ai-catalog-urls.ts
|
|
8543
|
-
function
|
|
8069
|
+
function tryParseJson8(body) {
|
|
8544
8070
|
try {
|
|
8545
8071
|
return JSON.parse(body);
|
|
8546
8072
|
} catch {
|
|
8547
8073
|
return void 0;
|
|
8548
8074
|
}
|
|
8549
8075
|
}
|
|
8550
|
-
function
|
|
8076
|
+
function isObject8(val) {
|
|
8551
8077
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8552
8078
|
}
|
|
8553
8079
|
var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
@@ -8596,8 +8122,8 @@ var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
|
8596
8122
|
}
|
|
8597
8123
|
);
|
|
8598
8124
|
}
|
|
8599
|
-
const parsed =
|
|
8600
|
-
if (!
|
|
8125
|
+
const parsed = tryParseJson8(result.body);
|
|
8126
|
+
if (!isObject8(parsed) || !Array.isArray(parsed["services"])) {
|
|
8601
8127
|
return this.fail(
|
|
8602
8128
|
"ai-catalog.json has no services array.",
|
|
8603
8129
|
"Each service URL returns HTTP 200",
|
|
@@ -8619,7 +8145,7 @@ var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
|
8619
8145
|
const services = parsed["services"];
|
|
8620
8146
|
const urls = [];
|
|
8621
8147
|
for (const svc of services) {
|
|
8622
|
-
if (
|
|
8148
|
+
if (isObject8(svc) && typeof svc["url"] === "string" && svc["url"]) {
|
|
8623
8149
|
urls.push(svc["url"]);
|
|
8624
8150
|
}
|
|
8625
8151
|
}
|
|
@@ -8691,14 +8217,14 @@ var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
|
8691
8217
|
};
|
|
8692
8218
|
|
|
8693
8219
|
// src/audits/agent-tools/agents-json.ts
|
|
8694
|
-
function
|
|
8220
|
+
function tryParseJson9(body) {
|
|
8695
8221
|
try {
|
|
8696
8222
|
return JSON.parse(body);
|
|
8697
8223
|
} catch {
|
|
8698
8224
|
return void 0;
|
|
8699
8225
|
}
|
|
8700
8226
|
}
|
|
8701
|
-
function
|
|
8227
|
+
function isObject9(val) {
|
|
8702
8228
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8703
8229
|
}
|
|
8704
8230
|
var AgentsJsonAudit = class _AgentsJsonAudit extends Audit {
|
|
@@ -8766,8 +8292,8 @@ var AgentsJsonAudit = class _AgentsJsonAudit extends Audit {
|
|
|
8766
8292
|
}
|
|
8767
8293
|
);
|
|
8768
8294
|
}
|
|
8769
|
-
const parsed =
|
|
8770
|
-
if (!
|
|
8295
|
+
const parsed = tryParseJson9(result.body);
|
|
8296
|
+
if (!isObject9(parsed) && !Array.isArray(parsed)) {
|
|
8771
8297
|
return this.fail(
|
|
8772
8298
|
"agents.json is not valid JSON.",
|
|
8773
8299
|
"/.well-known/agents.json returns 200 with valid JSON",
|
|
@@ -8805,160 +8331,15 @@ var AgentsJsonAudit = class _AgentsJsonAudit extends Audit {
|
|
|
8805
8331
|
}
|
|
8806
8332
|
};
|
|
8807
8333
|
|
|
8808
|
-
// src/audits/agent-tools/ai-plugin-json.ts
|
|
8809
|
-
function tryParseJson11(body) {
|
|
8810
|
-
try {
|
|
8811
|
-
return JSON.parse(body);
|
|
8812
|
-
} catch {
|
|
8813
|
-
return void 0;
|
|
8814
|
-
}
|
|
8815
|
-
}
|
|
8816
|
-
function isObject11(val) {
|
|
8817
|
-
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8818
|
-
}
|
|
8819
|
-
var AiPluginJsonAudit = class _AiPluginJsonAudit extends Audit {
|
|
8820
|
-
static meta = {
|
|
8821
|
-
id: "5.11",
|
|
8822
|
-
category: "agent-tools",
|
|
8823
|
-
title: "ai-plugin.json exists",
|
|
8824
|
-
failureTitle: "ai-plugin.json exists",
|
|
8825
|
-
description: "ai-plugin.json is the ChatGPT plugin manifest format. Even if you do not build a ChatGPT plugin, having this file helps AI agents understand your site as a tool with human-readable and model-readable names, logos, and API references.",
|
|
8826
|
-
scoreDisplayMode: "ternary",
|
|
8827
|
-
weight: 1,
|
|
8828
|
-
defaultPriority: "medium",
|
|
8829
|
-
guidance: {
|
|
8830
|
-
impact: "ai-plugin.json is the standard manifest used by ChatGPT and other AI platforms to register your site as a tool. Without it, your site cannot be installed as a plugin, and agents lose the human-readable and model-readable names needed for reliable interactions.",
|
|
8831
|
-
fix: "Create a /.well-known/ai-plugin.json with at minimum schema_version, name_for_human, name_for_model, description_for_human, description_for_model, auth, and an api reference pointing to your OpenAPI spec.",
|
|
8832
|
-
code: `// /.well-known/ai-plugin.json
|
|
8833
|
-
{
|
|
8834
|
-
"schema_version": "v1",
|
|
8835
|
-
"name_for_human": "Your Site Name",
|
|
8836
|
-
"name_for_model": "your_site",
|
|
8837
|
-
"description_for_human": "What your site does for users.",
|
|
8838
|
-
"description_for_model": "Use this plugin to search content and submit inquiries.",
|
|
8839
|
-
"auth": { "type": "none" },
|
|
8840
|
-
"api": {
|
|
8841
|
-
"type": "openapi",
|
|
8842
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8843
|
-
},
|
|
8844
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8845
|
-
"contact_email": "hello@yoursite.com"
|
|
8846
|
-
}`,
|
|
8847
|
-
effort: "easy",
|
|
8848
|
-
docsUrl: "https://platform.openai.com/docs/plugins/getting-started/plugin-manifest",
|
|
8849
|
-
tags: ["ai-plugin", "chatgpt", "discovery", "agent-protocol"]
|
|
8850
|
-
}
|
|
8851
|
-
};
|
|
8852
|
-
audit(ctx) {
|
|
8853
|
-
const result = ctx.rootFiles["/.well-known/ai-plugin.json"];
|
|
8854
|
-
if (!result || result.status !== 200 || !result.body) {
|
|
8855
|
-
return this.fail(
|
|
8856
|
-
"/.well-known/ai-plugin.json not found or not accessible.",
|
|
8857
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8858
|
-
result ? `HTTP ${result.status}` : "Not fetched",
|
|
8859
|
-
{
|
|
8860
|
-
priority: "medium",
|
|
8861
|
-
description: _AiPluginJsonAudit.meta.description,
|
|
8862
|
-
code: `// /.well-known/ai-plugin.json
|
|
8863
|
-
{
|
|
8864
|
-
"schema_version": "v1",
|
|
8865
|
-
"name_for_human": "Your Site Name",
|
|
8866
|
-
"name_for_model": "your_site",
|
|
8867
|
-
"description_for_human": "What your site does for users.",
|
|
8868
|
-
"description_for_model": "Use this plugin to search content, submit inquiries, and get product details from Your Site.",
|
|
8869
|
-
"auth": { "type": "none" },
|
|
8870
|
-
"api": {
|
|
8871
|
-
"type": "openapi",
|
|
8872
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8873
|
-
},
|
|
8874
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8875
|
-
"contact_email": "hello@yoursite.com"
|
|
8876
|
-
}`
|
|
8877
|
-
}
|
|
8878
|
-
);
|
|
8879
|
-
}
|
|
8880
|
-
const parsed = tryParseJson11(result.body);
|
|
8881
|
-
if (!isObject11(parsed)) {
|
|
8882
|
-
return this.fail(
|
|
8883
|
-
"ai-plugin.json is not valid JSON.",
|
|
8884
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8885
|
-
"Invalid JSON",
|
|
8886
|
-
{
|
|
8887
|
-
priority: "medium",
|
|
8888
|
-
description: _AiPluginJsonAudit.meta.description,
|
|
8889
|
-
code: `// /.well-known/ai-plugin.json
|
|
8890
|
-
{
|
|
8891
|
-
"schema_version": "v1",
|
|
8892
|
-
"name_for_human": "Your Site Name",
|
|
8893
|
-
"name_for_model": "your_site",
|
|
8894
|
-
"description_for_human": "What your site does for users.",
|
|
8895
|
-
"description_for_model": "Use this plugin to search content, submit inquiries, and get product details from Your Site.",
|
|
8896
|
-
"auth": { "type": "none" },
|
|
8897
|
-
"api": {
|
|
8898
|
-
"type": "openapi",
|
|
8899
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8900
|
-
},
|
|
8901
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8902
|
-
"contact_email": "hello@yoursite.com"
|
|
8903
|
-
}`
|
|
8904
|
-
}
|
|
8905
|
-
);
|
|
8906
|
-
}
|
|
8907
|
-
const requiredFields = ["schema_version", "name_for_human", "name_for_model"];
|
|
8908
|
-
const missing = requiredFields.filter((f) => typeof parsed[f] !== "string" || !parsed[f]);
|
|
8909
|
-
if (missing.length === 0) {
|
|
8910
|
-
return this.pass(
|
|
8911
|
-
"ai-plugin.json found with all required fields.",
|
|
8912
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8913
|
-
// requiredFields guarantees these three are non-empty strings here.
|
|
8914
|
-
`schema_version=${parsed["schema_version"]}, name_for_human=${parsed["name_for_human"]}, name_for_model=${parsed["name_for_model"]}`
|
|
8915
|
-
);
|
|
8916
|
-
}
|
|
8917
|
-
const recommendation = {
|
|
8918
|
-
priority: "medium",
|
|
8919
|
-
description: _AiPluginJsonAudit.meta.description,
|
|
8920
|
-
code: `// /.well-known/ai-plugin.json
|
|
8921
|
-
{
|
|
8922
|
-
"schema_version": "v1",
|
|
8923
|
-
"name_for_human": "Your Site Name",
|
|
8924
|
-
"name_for_model": "your_site",
|
|
8925
|
-
"description_for_human": "What your site does for users.",
|
|
8926
|
-
"description_for_model": "Use this plugin to search content, submit inquiries, and get product details from Your Site.",
|
|
8927
|
-
"auth": { "type": "none" },
|
|
8928
|
-
"api": {
|
|
8929
|
-
"type": "openapi",
|
|
8930
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8931
|
-
},
|
|
8932
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8933
|
-
"contact_email": "hello@yoursite.com"
|
|
8934
|
-
}`
|
|
8935
|
-
};
|
|
8936
|
-
if (missing.length < requiredFields.length) {
|
|
8937
|
-
return this.warn(
|
|
8938
|
-
`ai-plugin.json is missing fields: ${missing.join(", ")}.`,
|
|
8939
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8940
|
-
`Missing: ${missing.join(", ")}`,
|
|
8941
|
-
recommendation
|
|
8942
|
-
);
|
|
8943
|
-
}
|
|
8944
|
-
return this.fail(
|
|
8945
|
-
`ai-plugin.json is missing all required fields: ${missing.join(", ")}.`,
|
|
8946
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8947
|
-
`Missing: ${missing.join(", ")}`,
|
|
8948
|
-
recommendation
|
|
8949
|
-
);
|
|
8950
|
-
}
|
|
8951
|
-
};
|
|
8952
|
-
|
|
8953
8334
|
// src/audits/agent-tools/mcp-discovery.ts
|
|
8954
|
-
function
|
|
8335
|
+
function tryParseJson10(body) {
|
|
8955
8336
|
try {
|
|
8956
8337
|
return JSON.parse(body);
|
|
8957
8338
|
} catch {
|
|
8958
8339
|
return void 0;
|
|
8959
8340
|
}
|
|
8960
8341
|
}
|
|
8961
|
-
function
|
|
8342
|
+
function isObject10(val) {
|
|
8962
8343
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8963
8344
|
}
|
|
8964
8345
|
var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
@@ -8997,8 +8378,8 @@ var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
|
8997
8378
|
audit(ctx) {
|
|
8998
8379
|
const result = ctx.rootFiles["/.well-known/mcp/servers.json"];
|
|
8999
8380
|
if (result && result.status === 200 && result.body) {
|
|
9000
|
-
const parsed =
|
|
9001
|
-
if (!
|
|
8381
|
+
const parsed = tryParseJson10(result.body);
|
|
8382
|
+
if (!isObject10(parsed)) {
|
|
9002
8383
|
return this.fail(
|
|
9003
8384
|
"mcp/servers.json is not valid JSON.",
|
|
9004
8385
|
"/.well-known/mcp/servers.json returns 200 with valid JSON containing servers array",
|
|
@@ -9031,8 +8412,8 @@ var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
|
9031
8412
|
}
|
|
9032
8413
|
const ucpResult = ctx.rootFiles["/.well-known/ucp"];
|
|
9033
8414
|
if (ucpResult && ucpResult.status === 200 && ucpResult.body) {
|
|
9034
|
-
const ucpParsed =
|
|
9035
|
-
if (
|
|
8415
|
+
const ucpParsed = tryParseJson10(ucpResult.body);
|
|
8416
|
+
if (isObject10(ucpParsed)) {
|
|
9036
8417
|
const ucpObj = ucpParsed["ucp"] ?? ucpParsed;
|
|
9037
8418
|
const services = ucpParsed["services"] || ucpObj["services"];
|
|
9038
8419
|
const capabilities = ucpParsed["capabilities"] || ucpObj["capabilities"];
|
|
@@ -9059,14 +8440,14 @@ var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
|
9059
8440
|
};
|
|
9060
8441
|
|
|
9061
8442
|
// src/audits/agent-tools/mcp-endpoint.ts
|
|
9062
|
-
function
|
|
8443
|
+
function tryParseJson11(body) {
|
|
9063
8444
|
try {
|
|
9064
8445
|
return JSON.parse(body);
|
|
9065
8446
|
} catch {
|
|
9066
8447
|
return void 0;
|
|
9067
8448
|
}
|
|
9068
8449
|
}
|
|
9069
|
-
function
|
|
8450
|
+
function isObject11(val) {
|
|
9070
8451
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9071
8452
|
}
|
|
9072
8453
|
var McpEndpointAudit = class _McpEndpointAudit extends Audit {
|
|
@@ -9116,8 +8497,8 @@ Content-Type: application/json
|
|
|
9116
8497
|
let targetEndpointUrl;
|
|
9117
8498
|
const result = ctx.rootFiles["/.well-known/mcp/servers.json"];
|
|
9118
8499
|
if (result && result.status === 200 && result.body) {
|
|
9119
|
-
const parsed =
|
|
9120
|
-
if (!
|
|
8500
|
+
const parsed = tryParseJson11(result.body);
|
|
8501
|
+
if (!isObject11(parsed) || !Array.isArray(parsed["servers"])) {
|
|
9121
8502
|
return this.fail(
|
|
9122
8503
|
"servers.json has no servers array.",
|
|
9123
8504
|
"MCP server URL responds to JSON-RPC initialize request",
|
|
@@ -9130,8 +8511,8 @@ Content-Type: application/json
|
|
|
9130
8511
|
);
|
|
9131
8512
|
}
|
|
9132
8513
|
const servers = parsed["servers"];
|
|
9133
|
-
const serverUrl = servers.find((s) =>
|
|
9134
|
-
if (!serverUrl || !
|
|
8514
|
+
const serverUrl = servers.find((s) => isObject11(s) && typeof s["url"] === "string" && s["url"]);
|
|
8515
|
+
if (!serverUrl || !isObject11(serverUrl)) {
|
|
9135
8516
|
return this.fail(
|
|
9136
8517
|
"No server URL found in servers.json.",
|
|
9137
8518
|
"MCP server URL responds to JSON-RPC initialize request",
|
|
@@ -9148,8 +8529,8 @@ Content-Type: application/json
|
|
|
9148
8529
|
if (!targetEndpointUrl) {
|
|
9149
8530
|
const ucpResult = ctx.rootFiles["/.well-known/ucp"];
|
|
9150
8531
|
if (ucpResult && ucpResult.status === 200 && ucpResult.body) {
|
|
9151
|
-
const ucpParsed =
|
|
9152
|
-
if (
|
|
8532
|
+
const ucpParsed = tryParseJson11(ucpResult.body);
|
|
8533
|
+
if (isObject11(ucpParsed)) {
|
|
9153
8534
|
const ucpObj = ucpParsed["ucp"] ?? ucpParsed;
|
|
9154
8535
|
const services = ucpParsed["services"] || ucpObj["services"];
|
|
9155
8536
|
if (services) {
|
|
@@ -9157,7 +8538,7 @@ Content-Type: application/json
|
|
|
9157
8538
|
const svcList = services[key];
|
|
9158
8539
|
if (Array.isArray(svcList)) {
|
|
9159
8540
|
for (const svc of svcList) {
|
|
9160
|
-
if (
|
|
8541
|
+
if (isObject11(svc) && svc["transport"] === "mcp" && typeof svc["endpoint"] === "string") {
|
|
9161
8542
|
targetEndpointUrl = svc["endpoint"];
|
|
9162
8543
|
break;
|
|
9163
8544
|
}
|
|
@@ -9200,8 +8581,8 @@ Content-Type: application/json
|
|
|
9200
8581
|
contentType: "application/json"
|
|
9201
8582
|
});
|
|
9202
8583
|
if (response.status === 200) {
|
|
9203
|
-
const respBody =
|
|
9204
|
-
if (
|
|
8584
|
+
const respBody = tryParseJson11(response.body);
|
|
8585
|
+
if (isObject11(respBody) && respBody["jsonrpc"] === "2.0" && !("error" in respBody) && isObject11(respBody["result"]) && typeof respBody["result"]["protocolVersion"] === "string") {
|
|
9205
8586
|
return this.pass(
|
|
9206
8587
|
`MCP endpoint at ${url} responded with valid JSON-RPC initialize result.`,
|
|
9207
8588
|
"MCP server URL responds to JSON-RPC initialize request",
|
|
@@ -9317,14 +8698,14 @@ Content-Type: application/json
|
|
|
9317
8698
|
};
|
|
9318
8699
|
|
|
9319
8700
|
// src/audits/agent-tools/mcp-capabilities.ts
|
|
9320
|
-
function
|
|
8701
|
+
function tryParseJson12(body) {
|
|
9321
8702
|
try {
|
|
9322
8703
|
return JSON.parse(body);
|
|
9323
8704
|
} catch {
|
|
9324
8705
|
return void 0;
|
|
9325
8706
|
}
|
|
9326
8707
|
}
|
|
9327
|
-
function
|
|
8708
|
+
function isObject12(val) {
|
|
9328
8709
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9329
8710
|
}
|
|
9330
8711
|
var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
@@ -9362,8 +8743,8 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9362
8743
|
const result = ctx.rootFiles["/.well-known/mcp/servers.json"];
|
|
9363
8744
|
const ucpResult = ctx.rootFiles["/.well-known/ucp"];
|
|
9364
8745
|
if (result && result.status === 200 && result.body) {
|
|
9365
|
-
const parsed =
|
|
9366
|
-
if (!
|
|
8746
|
+
const parsed = tryParseJson12(result.body);
|
|
8747
|
+
if (!isObject12(parsed) || !Array.isArray(parsed["servers"])) {
|
|
9367
8748
|
return this.fail(
|
|
9368
8749
|
"servers.json has no servers array.",
|
|
9369
8750
|
"servers.json or MCP response declares tools, resources, or prompts",
|
|
@@ -9379,14 +8760,14 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9379
8760
|
const capabilityKeys = ["tools", "resources", "prompts"];
|
|
9380
8761
|
const foundCapabilities = [];
|
|
9381
8762
|
for (const server of servers) {
|
|
9382
|
-
if (!
|
|
8763
|
+
if (!isObject12(server)) continue;
|
|
9383
8764
|
for (const key of capabilityKeys) {
|
|
9384
8765
|
if (server[key] !== void 0 && server[key] !== false) {
|
|
9385
8766
|
foundCapabilities.push(key);
|
|
9386
8767
|
}
|
|
9387
8768
|
}
|
|
9388
8769
|
const caps = server["capabilities"];
|
|
9389
|
-
if (
|
|
8770
|
+
if (isObject12(caps)) {
|
|
9390
8771
|
for (const key of capabilityKeys) {
|
|
9391
8772
|
if (caps[key] !== void 0 && caps[key] !== false && !foundCapabilities.includes(key)) {
|
|
9392
8773
|
foundCapabilities.push(key);
|
|
@@ -9414,11 +8795,11 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9414
8795
|
);
|
|
9415
8796
|
}
|
|
9416
8797
|
if (ucpResult && ucpResult.status === 200 && ucpResult.body) {
|
|
9417
|
-
const ucpParsed =
|
|
9418
|
-
if (
|
|
8798
|
+
const ucpParsed = tryParseJson12(ucpResult.body);
|
|
8799
|
+
if (isObject12(ucpParsed)) {
|
|
9419
8800
|
const ucpObj = ucpParsed["ucp"] ?? ucpParsed;
|
|
9420
8801
|
const capabilities = ucpParsed["capabilities"] || ucpObj["capabilities"];
|
|
9421
|
-
if (capabilities &&
|
|
8802
|
+
if (capabilities && isObject12(capabilities)) {
|
|
9422
8803
|
const capNames = Object.keys(capabilities).map((cap) => cap.split(".").pop() || cap);
|
|
9423
8804
|
if (capNames.length > 0) {
|
|
9424
8805
|
const unique = [...new Set(capNames)];
|
|
@@ -9445,34 +8826,34 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9445
8826
|
};
|
|
9446
8827
|
|
|
9447
8828
|
// src/audits/agent-tools/contact-form.ts
|
|
9448
|
-
function
|
|
8829
|
+
function tryParseJson13(body) {
|
|
9449
8830
|
try {
|
|
9450
8831
|
return JSON.parse(body);
|
|
9451
8832
|
} catch {
|
|
9452
8833
|
return void 0;
|
|
9453
8834
|
}
|
|
9454
8835
|
}
|
|
9455
|
-
function
|
|
8836
|
+
function isObject13(val) {
|
|
9456
8837
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9457
8838
|
}
|
|
9458
8839
|
var HTTP_METHODS4 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
9459
|
-
function
|
|
8840
|
+
function getOpenApiSpec5(ctx) {
|
|
9460
8841
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
9461
8842
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
9462
|
-
const parsed =
|
|
9463
|
-
if (
|
|
8843
|
+
const parsed = tryParseJson13(jsonResult.body);
|
|
8844
|
+
if (isObject13(parsed)) return parsed;
|
|
9464
8845
|
}
|
|
9465
8846
|
return void 0;
|
|
9466
8847
|
}
|
|
9467
8848
|
function getOperations4(spec) {
|
|
9468
8849
|
const paths = spec["paths"];
|
|
9469
|
-
if (!
|
|
8850
|
+
if (!isObject13(paths)) return [];
|
|
9470
8851
|
const ops = [];
|
|
9471
8852
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
9472
|
-
if (!
|
|
8853
|
+
if (!isObject13(pathItem)) continue;
|
|
9473
8854
|
for (const method of HTTP_METHODS4) {
|
|
9474
8855
|
const op = pathItem[method];
|
|
9475
|
-
if (
|
|
8856
|
+
if (isObject13(op)) {
|
|
9476
8857
|
ops.push({ path, method, op });
|
|
9477
8858
|
}
|
|
9478
8859
|
}
|
|
@@ -9541,7 +8922,7 @@ var ContactFormAudit = class _ContactFormAudit extends Audit {
|
|
|
9541
8922
|
}
|
|
9542
8923
|
}
|
|
9543
8924
|
}
|
|
9544
|
-
const spec =
|
|
8925
|
+
const spec = getOpenApiSpec5(ctx);
|
|
9545
8926
|
if (spec) {
|
|
9546
8927
|
const ops = getOperations4(spec);
|
|
9547
8928
|
for (const { path, method } of ops) {
|
|
@@ -9588,34 +8969,34 @@ var ContactFormAudit = class _ContactFormAudit extends Audit {
|
|
|
9588
8969
|
};
|
|
9589
8970
|
|
|
9590
8971
|
// src/audits/agent-tools/search-endpoint.ts
|
|
9591
|
-
function
|
|
8972
|
+
function tryParseJson14(body) {
|
|
9592
8973
|
try {
|
|
9593
8974
|
return JSON.parse(body);
|
|
9594
8975
|
} catch {
|
|
9595
8976
|
return void 0;
|
|
9596
8977
|
}
|
|
9597
8978
|
}
|
|
9598
|
-
function
|
|
8979
|
+
function isObject14(val) {
|
|
9599
8980
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9600
8981
|
}
|
|
9601
8982
|
var HTTP_METHODS5 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
9602
|
-
function
|
|
8983
|
+
function getOpenApiSpec6(ctx) {
|
|
9603
8984
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
9604
8985
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
9605
|
-
const parsed =
|
|
9606
|
-
if (
|
|
8986
|
+
const parsed = tryParseJson14(jsonResult.body);
|
|
8987
|
+
if (isObject14(parsed)) return parsed;
|
|
9607
8988
|
}
|
|
9608
8989
|
return void 0;
|
|
9609
8990
|
}
|
|
9610
8991
|
function getOperations5(spec) {
|
|
9611
8992
|
const paths = spec["paths"];
|
|
9612
|
-
if (!
|
|
8993
|
+
if (!isObject14(paths)) return [];
|
|
9613
8994
|
const ops = [];
|
|
9614
8995
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
9615
|
-
if (!
|
|
8996
|
+
if (!isObject14(pathItem)) continue;
|
|
9616
8997
|
for (const method of HTTP_METHODS5) {
|
|
9617
8998
|
const op = pathItem[method];
|
|
9618
|
-
if (
|
|
8999
|
+
if (isObject14(op)) {
|
|
9619
9000
|
ops.push({ path, method, op });
|
|
9620
9001
|
}
|
|
9621
9002
|
}
|
|
@@ -9623,10 +9004,10 @@ function getOperations5(spec) {
|
|
|
9623
9004
|
return ops;
|
|
9624
9005
|
}
|
|
9625
9006
|
function findSearchActionUrl(obj) {
|
|
9626
|
-
if (!
|
|
9627
|
-
if ((obj["@type"] === "SearchAction" || obj["@type"] === "WebSite") &&
|
|
9007
|
+
if (!isObject14(obj)) return void 0;
|
|
9008
|
+
if ((obj["@type"] === "SearchAction" || obj["@type"] === "WebSite") && isObject14(obj["potentialAction"])) {
|
|
9628
9009
|
const action = obj["potentialAction"];
|
|
9629
|
-
if (action["@type"] === "SearchAction" &&
|
|
9010
|
+
if (action["@type"] === "SearchAction" && isObject14(action["target"])) {
|
|
9630
9011
|
const target = action["target"];
|
|
9631
9012
|
if (typeof target["urlTemplate"] === "string") return target["urlTemplate"];
|
|
9632
9013
|
}
|
|
@@ -9635,7 +9016,7 @@ function findSearchActionUrl(obj) {
|
|
|
9635
9016
|
}
|
|
9636
9017
|
}
|
|
9637
9018
|
if (obj["@type"] === "SearchAction") {
|
|
9638
|
-
if (
|
|
9019
|
+
if (isObject14(obj["target"])) {
|
|
9639
9020
|
const target = obj["target"];
|
|
9640
9021
|
if (typeof target["urlTemplate"] === "string") return target["urlTemplate"];
|
|
9641
9022
|
}
|
|
@@ -9756,7 +9137,7 @@ var SearchEndpointAudit = class _SearchEndpointAudit extends Audit {
|
|
|
9756
9137
|
}
|
|
9757
9138
|
}
|
|
9758
9139
|
}
|
|
9759
|
-
const spec =
|
|
9140
|
+
const spec = getOpenApiSpec6(ctx);
|
|
9760
9141
|
if (spec) {
|
|
9761
9142
|
const ops = getOperations5(spec);
|
|
9762
9143
|
for (const { path, method } of ops) {
|
|
@@ -9797,96 +9178,6 @@ var SearchEndpointAudit = class _SearchEndpointAudit extends Audit {
|
|
|
9797
9178
|
}
|
|
9798
9179
|
};
|
|
9799
9180
|
|
|
9800
|
-
// src/audits/agent-tools/data-action-ctas.ts
|
|
9801
|
-
var DataActionCtasAudit = class _DataActionCtasAudit extends Audit {
|
|
9802
|
-
static meta = {
|
|
9803
|
-
id: "5.17",
|
|
9804
|
-
category: "agent-tools",
|
|
9805
|
-
title: "data-action attributes on CTAs",
|
|
9806
|
-
failureTitle: "data-action attributes on CTAs",
|
|
9807
|
-
description: "data-action attributes help AI browser agents (like ChatGPT Browse and Google Mariner) identify clickable CTAs and understand what each button does. Without these hints, agents must guess which elements are interactive based on text alone.",
|
|
9808
|
-
scoreDisplayMode: "ternary",
|
|
9809
|
-
weight: 1,
|
|
9810
|
-
defaultPriority: "low",
|
|
9811
|
-
guidance: {
|
|
9812
|
-
impact: 'Without data-action attributes, AI browser agents like ChatGPT Browse and Google Mariner must guess which elements are clickable and what they do. This leads to missed conversions when agents cannot reliably identify your "Book a Demo" or "Add to Cart" buttons.',
|
|
9813
|
-
fix: 'Add data-action, data-action-type, and data-action-label attributes to your key CTA buttons and links. Use descriptive action names and categorize them (e.g., "conversion", "navigation").',
|
|
9814
|
-
code: `<button data-action="book-demo" data-action-type="conversion"
|
|
9815
|
-
data-action-label="Book a Demo">
|
|
9816
|
-
Book a Demo
|
|
9817
|
-
</button>
|
|
9818
|
-
|
|
9819
|
-
<a href="/pricing" data-action="view-pricing" data-action-type="navigation"
|
|
9820
|
-
data-action-label="See Pricing">
|
|
9821
|
-
See Pricing
|
|
9822
|
-
</a>`,
|
|
9823
|
-
effort: "easy",
|
|
9824
|
-
tags: ["html", "cta", "browser-agent", "accessibility"]
|
|
9825
|
-
}
|
|
9826
|
-
};
|
|
9827
|
-
audit(ctx) {
|
|
9828
|
-
let totalDataAction = 0;
|
|
9829
|
-
let totalDataActionType = 0;
|
|
9830
|
-
let foundPage = "";
|
|
9831
|
-
for (const page of ctx.pages) {
|
|
9832
|
-
const withDataAction = page.$("[data-action]");
|
|
9833
|
-
const withDataActionType = page.$("[data-action-type]");
|
|
9834
|
-
if (withDataAction.length > 0 || withDataActionType.length > 0) {
|
|
9835
|
-
totalDataAction += withDataAction.length;
|
|
9836
|
-
totalDataActionType += withDataActionType.length;
|
|
9837
|
-
if (!foundPage) foundPage = page.url;
|
|
9838
|
-
}
|
|
9839
|
-
}
|
|
9840
|
-
if (totalDataAction > 0 && totalDataActionType > 0) {
|
|
9841
|
-
return this.pass(
|
|
9842
|
-
`Found ${totalDataAction} element(s) with data-action and ${totalDataActionType} with data-action-type.`,
|
|
9843
|
-
"Elements with data-action and data-action-type attributes",
|
|
9844
|
-
`${totalDataAction} data-action, ${totalDataActionType} data-action-type`,
|
|
9845
|
-
foundPage
|
|
9846
|
-
);
|
|
9847
|
-
}
|
|
9848
|
-
if (totalDataAction > 0 || totalDataActionType > 0) {
|
|
9849
|
-
return this.warn(
|
|
9850
|
-
`Partial data-action markup: ${totalDataAction} data-action, ${totalDataActionType} data-action-type.`,
|
|
9851
|
-
"Elements with data-action and data-action-type attributes",
|
|
9852
|
-
`${totalDataAction} data-action, ${totalDataActionType} data-action-type`,
|
|
9853
|
-
{
|
|
9854
|
-
priority: "low",
|
|
9855
|
-
description: _DataActionCtasAudit.meta.description,
|
|
9856
|
-
code: `<button data-action="book-demo" data-action-type="conversion"
|
|
9857
|
-
data-action-label="Book a Demo">
|
|
9858
|
-
Book a Demo
|
|
9859
|
-
</button>
|
|
9860
|
-
|
|
9861
|
-
<a href="/pricing" data-action="view-pricing" data-action-type="navigation"
|
|
9862
|
-
data-action-label="See Pricing">
|
|
9863
|
-
See Pricing
|
|
9864
|
-
</a>`
|
|
9865
|
-
},
|
|
9866
|
-
foundPage
|
|
9867
|
-
);
|
|
9868
|
-
}
|
|
9869
|
-
return this.fail(
|
|
9870
|
-
"No elements with data-action or data-action-type attributes found.",
|
|
9871
|
-
"Elements with data-action and data-action-type attributes",
|
|
9872
|
-
"None",
|
|
9873
|
-
{
|
|
9874
|
-
priority: "low",
|
|
9875
|
-
description: _DataActionCtasAudit.meta.description,
|
|
9876
|
-
code: `<button data-action="book-demo" data-action-type="conversion"
|
|
9877
|
-
data-action-label="Book a Demo">
|
|
9878
|
-
Book a Demo
|
|
9879
|
-
</button>
|
|
9880
|
-
|
|
9881
|
-
<a href="/pricing" data-action="view-pricing" data-action-type="navigation"
|
|
9882
|
-
data-action-label="See Pricing">
|
|
9883
|
-
See Pricing
|
|
9884
|
-
</a>`
|
|
9885
|
-
}
|
|
9886
|
-
);
|
|
9887
|
-
}
|
|
9888
|
-
};
|
|
9889
|
-
|
|
9890
9181
|
// src/audits/agent-tools/no-blocking-captcha.ts
|
|
9891
9182
|
var CAPTCHA_PATTERNS = [
|
|
9892
9183
|
"recaptcha",
|
|
@@ -10057,14 +9348,14 @@ var FormsNoJsAudit = class _FormsNoJsAudit extends Audit {
|
|
|
10057
9348
|
};
|
|
10058
9349
|
|
|
10059
9350
|
// src/audits/agent-tools/webmcp-manifest.ts
|
|
10060
|
-
function
|
|
9351
|
+
function tryParseJson15(body) {
|
|
10061
9352
|
try {
|
|
10062
9353
|
return JSON.parse(body);
|
|
10063
9354
|
} catch {
|
|
10064
9355
|
return void 0;
|
|
10065
9356
|
}
|
|
10066
9357
|
}
|
|
10067
|
-
function
|
|
9358
|
+
function isObject15(val) {
|
|
10068
9359
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10069
9360
|
}
|
|
10070
9361
|
var WebmcpManifestAudit = class extends Audit {
|
|
@@ -10114,8 +9405,8 @@ var WebmcpManifestAudit = class extends Audit {
|
|
|
10114
9405
|
"high"
|
|
10115
9406
|
);
|
|
10116
9407
|
}
|
|
10117
|
-
const parsed =
|
|
10118
|
-
if (!
|
|
9408
|
+
const parsed = tryParseJson15(result.body);
|
|
9409
|
+
if (!isObject15(parsed)) {
|
|
10119
9410
|
return this.fail(
|
|
10120
9411
|
"/.well-known/webmcp is not valid JSON.",
|
|
10121
9412
|
"Valid JSON object with tools array",
|
|
@@ -10133,7 +9424,7 @@ var WebmcpManifestAudit = class extends Audit {
|
|
|
10133
9424
|
}
|
|
10134
9425
|
const rawTools = parsed["tools"];
|
|
10135
9426
|
const tools = rawTools.filter(
|
|
10136
|
-
(t) =>
|
|
9427
|
+
(t) => isObject15(t) && typeof t["name"] === "string"
|
|
10137
9428
|
);
|
|
10138
9429
|
if (tools.length === 0) {
|
|
10139
9430
|
return this.fail(
|
|
@@ -10350,13 +9641,13 @@ var WebmcpInputQualityAudit = class extends Audit {
|
|
|
10350
9641
|
};
|
|
10351
9642
|
|
|
10352
9643
|
// src/audits/agent-tools/webmcp-tool-naming.ts
|
|
10353
|
-
function
|
|
9644
|
+
function isObject16(val) {
|
|
10354
9645
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10355
9646
|
}
|
|
10356
9647
|
function asString(val) {
|
|
10357
9648
|
return typeof val === "string" ? val : "";
|
|
10358
9649
|
}
|
|
10359
|
-
function
|
|
9650
|
+
function tryParseJson16(body) {
|
|
10360
9651
|
try {
|
|
10361
9652
|
return JSON.parse(body);
|
|
10362
9653
|
} catch {
|
|
@@ -10399,10 +9690,10 @@ var WebmcpToolNamingAudit = class extends Audit {
|
|
|
10399
9690
|
const tools = [];
|
|
10400
9691
|
const manifestResult = ctx.rootFiles["/.well-known/webmcp"];
|
|
10401
9692
|
if (manifestResult?.status === 200 && manifestResult.body) {
|
|
10402
|
-
const parsed =
|
|
10403
|
-
if (
|
|
9693
|
+
const parsed = tryParseJson16(manifestResult.body);
|
|
9694
|
+
if (isObject16(parsed) && Array.isArray(parsed["tools"])) {
|
|
10404
9695
|
for (const tool of parsed["tools"]) {
|
|
10405
|
-
if (
|
|
9696
|
+
if (isObject16(tool)) {
|
|
10406
9697
|
const name = asString(tool["name"]);
|
|
10407
9698
|
tools.push({
|
|
10408
9699
|
name,
|
|
@@ -10479,14 +9770,14 @@ var WebmcpToolNamingAudit = class extends Audit {
|
|
|
10479
9770
|
};
|
|
10480
9771
|
|
|
10481
9772
|
// src/audits/agent-tools/webmcp-tool-annotations.ts
|
|
10482
|
-
function
|
|
9773
|
+
function tryParseJson17(body) {
|
|
10483
9774
|
try {
|
|
10484
9775
|
return JSON.parse(body);
|
|
10485
9776
|
} catch {
|
|
10486
9777
|
return void 0;
|
|
10487
9778
|
}
|
|
10488
9779
|
}
|
|
10489
|
-
function
|
|
9780
|
+
function isObject17(val) {
|
|
10490
9781
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10491
9782
|
}
|
|
10492
9783
|
function asString2(val) {
|
|
@@ -10549,15 +9840,15 @@ var WebmcpToolAnnotationsAudit = class extends Audit {
|
|
|
10549
9840
|
const seen = /* @__PURE__ */ new Set();
|
|
10550
9841
|
const manifestResult = ctx.rootFiles["/.well-known/webmcp"];
|
|
10551
9842
|
if (manifestResult?.status === 200 && manifestResult.body) {
|
|
10552
|
-
const parsed =
|
|
10553
|
-
if (
|
|
9843
|
+
const parsed = tryParseJson17(manifestResult.body);
|
|
9844
|
+
if (isObject17(parsed) && Array.isArray(parsed["tools"])) {
|
|
10554
9845
|
for (const tool of parsed["tools"]) {
|
|
10555
|
-
if (!
|
|
9846
|
+
if (!isObject17(tool)) continue;
|
|
10556
9847
|
const name = asString2(tool["name"]);
|
|
10557
9848
|
totalTools++;
|
|
10558
9849
|
if (name) seen.add(name);
|
|
10559
9850
|
const annotations = tool["annotations"];
|
|
10560
|
-
if (
|
|
9851
|
+
if (isObject17(annotations)) {
|
|
10561
9852
|
const found = SAFETY_ANNOTATIONS.filter((a) => a in annotations);
|
|
10562
9853
|
if (found.length > 0) {
|
|
10563
9854
|
toolsWithAnnotations++;
|
|
@@ -10619,203 +9910,24 @@ var WebmcpToolAnnotationsAudit = class extends Audit {
|
|
|
10619
9910
|
}
|
|
10620
9911
|
};
|
|
10621
9912
|
|
|
10622
|
-
// src/audits/agent-tools/webmcp-action-coverage.ts
|
|
10623
|
-
function tryParseJson20(body) {
|
|
10624
|
-
try {
|
|
10625
|
-
return JSON.parse(body);
|
|
10626
|
-
} catch {
|
|
10627
|
-
return void 0;
|
|
10628
|
-
}
|
|
10629
|
-
}
|
|
10630
|
-
function isObject20(val) {
|
|
10631
|
-
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10632
|
-
}
|
|
10633
|
-
function asString3(val) {
|
|
10634
|
-
return typeof val === "string" ? val : "";
|
|
10635
|
-
}
|
|
10636
|
-
var COMMERCE_ACTIONS = [
|
|
10637
|
-
{
|
|
10638
|
-
label: "Product Search",
|
|
10639
|
-
keywords: ["search", "find", "query", "browse", "filter", "lookup", "catalog"]
|
|
10640
|
-
},
|
|
10641
|
-
{
|
|
10642
|
-
label: "Product Detail",
|
|
10643
|
-
keywords: ["product", "detail", "productdetail", "getproduct", "viewproduct", "viewitem"]
|
|
10644
|
-
},
|
|
10645
|
-
{ label: "Add to Cart", keywords: ["cart", "addtocart", "basket", "additem"] },
|
|
10646
|
-
{
|
|
10647
|
-
label: "Checkout",
|
|
10648
|
-
keywords: ["checkout", "purchase", "placeorder", "completepurchase", "buyproduct"]
|
|
10649
|
-
},
|
|
10650
|
-
{
|
|
10651
|
-
label: "Account/Auth",
|
|
10652
|
-
keywords: ["login", "register", "signup", "signin", "authenticate", "createaccount"]
|
|
10653
|
-
},
|
|
10654
|
-
{
|
|
10655
|
-
label: "Contact/Support",
|
|
10656
|
-
keywords: ["contact", "support", "inquiry", "submitinquiry", "sendmessage", "helpdesk"]
|
|
10657
|
-
}
|
|
10658
|
-
];
|
|
10659
|
-
var MIN_COVERAGE = 2;
|
|
10660
|
-
var WebmcpActionCoverageAudit = class extends Audit {
|
|
10661
|
-
static meta = {
|
|
10662
|
-
id: "5.25",
|
|
10663
|
-
category: "agent-tools",
|
|
10664
|
-
title: "WebMCP commerce action coverage",
|
|
10665
|
-
failureTitle: "WebMCP commerce action coverage",
|
|
10666
|
-
description: "For e-commerce sites, WebMCP tools should cover key commerce actions: product search, product detail, add to cart, checkout, and contact/support. Broader action coverage means AI agents can complete more user tasks without falling back to manual browsing.",
|
|
10667
|
-
scoreDisplayMode: "ternary",
|
|
10668
|
-
weight: 1,
|
|
10669
|
-
defaultPriority: "medium",
|
|
10670
|
-
guidance: {
|
|
10671
|
-
impact: "AI shopping agents need to complete full purchase journeys \u2014 search, view, add to cart, checkout. If your WebMCP tools only cover search but not checkout, agents abandon the flow and users turn to competitors with full coverage.",
|
|
10672
|
-
fix: "Expose WebMCP tools for the key commerce actions: product search, product detail/view, add to cart, checkout/purchase, and contact/support.",
|
|
10673
|
-
code: `// /.well-known/webmcp \u2014 full commerce coverage
|
|
10674
|
-
{
|
|
10675
|
-
"tools": [
|
|
10676
|
-
{
|
|
10677
|
-
"name": "searchProducts",
|
|
10678
|
-
"description": "Search the product catalog by keyword, category, or filters",
|
|
10679
|
-
"annotations": { "readOnlyHint": true },
|
|
10680
|
-
"inputSchema": {
|
|
10681
|
-
"type": "object",
|
|
10682
|
-
"properties": {
|
|
10683
|
-
"query": { "type": "string" },
|
|
10684
|
-
"category": { "type": "string" },
|
|
10685
|
-
"maxPrice": { "type": "number" }
|
|
10686
|
-
},
|
|
10687
|
-
"required": ["query"]
|
|
10688
|
-
}
|
|
10689
|
-
},
|
|
10690
|
-
{
|
|
10691
|
-
"name": "getProductDetails",
|
|
10692
|
-
"description": "Get full details for a specific product by ID or URL",
|
|
10693
|
-
"annotations": { "readOnlyHint": true },
|
|
10694
|
-
"inputSchema": {
|
|
10695
|
-
"type": "object",
|
|
10696
|
-
"properties": { "productId": { "type": "string" } },
|
|
10697
|
-
"required": ["productId"]
|
|
10698
|
-
}
|
|
10699
|
-
},
|
|
10700
|
-
{
|
|
10701
|
-
"name": "addToCart",
|
|
10702
|
-
"description": "Add a product to the shopping cart with quantity",
|
|
10703
|
-
"annotations": { "readOnlyHint": false, "idempotentHint": false },
|
|
10704
|
-
"inputSchema": {
|
|
10705
|
-
"type": "object",
|
|
10706
|
-
"properties": {
|
|
10707
|
-
"productId": { "type": "string" },
|
|
10708
|
-
"quantity": { "type": "integer", "minimum": 1 }
|
|
10709
|
-
},
|
|
10710
|
-
"required": ["productId"]
|
|
10711
|
-
}
|
|
10712
|
-
},
|
|
10713
|
-
{
|
|
10714
|
-
"name": "checkout",
|
|
10715
|
-
"description": "Initiate checkout for the current cart",
|
|
10716
|
-
"annotations": { "readOnlyHint": false, "confirmationRequired": true },
|
|
10717
|
-
"inputSchema": {
|
|
10718
|
-
"type": "object",
|
|
10719
|
-
"properties": { "shippingMethod": { "type": "string" } }
|
|
10720
|
-
}
|
|
10721
|
-
}
|
|
10722
|
-
]
|
|
10723
|
-
}`,
|
|
10724
|
-
effort: "moderate",
|
|
10725
|
-
docsUrl: "https://webmcp.link/",
|
|
10726
|
-
tags: ["webmcp", "commerce", "coverage", "chrome-146"]
|
|
10727
|
-
}
|
|
10728
|
-
};
|
|
10729
|
-
audit(ctx) {
|
|
10730
|
-
const toolSignatures = [];
|
|
10731
|
-
const seen = /* @__PURE__ */ new Set();
|
|
10732
|
-
const manifestResult = ctx.rootFiles["/.well-known/webmcp"];
|
|
10733
|
-
if (manifestResult?.status === 200 && manifestResult.body) {
|
|
10734
|
-
const parsed = tryParseJson20(manifestResult.body);
|
|
10735
|
-
if (isObject20(parsed) && Array.isArray(parsed["tools"])) {
|
|
10736
|
-
for (const tool of parsed["tools"]) {
|
|
10737
|
-
if (isObject20(tool)) {
|
|
10738
|
-
const name = asString3(tool["name"]);
|
|
10739
|
-
const sig = `${name} ${asString3(tool["description"])}`.toLowerCase();
|
|
10740
|
-
toolSignatures.push(sig);
|
|
10741
|
-
if (name) seen.add(name);
|
|
10742
|
-
}
|
|
10743
|
-
}
|
|
10744
|
-
}
|
|
10745
|
-
}
|
|
10746
|
-
for (const page of ctx.pages) {
|
|
10747
|
-
page.$("form[toolname]").each((_, el) => {
|
|
10748
|
-
const name = page.$(el).attr("toolname") || "";
|
|
10749
|
-
if (name && seen.has(name)) return;
|
|
10750
|
-
const desc = page.$(el).attr("tooldescription") || "";
|
|
10751
|
-
const action = page.$(el).attr("action") || "";
|
|
10752
|
-
toolSignatures.push(`${name} ${desc} ${action}`.toLowerCase());
|
|
10753
|
-
if (name) seen.add(name);
|
|
10754
|
-
});
|
|
10755
|
-
}
|
|
10756
|
-
if (toolSignatures.length === 0) {
|
|
10757
|
-
return this.notApplicable(
|
|
10758
|
-
"No WebMCP tools found \u2014 commerce action coverage cannot be assessed.",
|
|
10759
|
-
`At least ${MIN_COVERAGE} commerce actions covered (search, product, cart, checkout, contact)`,
|
|
10760
|
-
"No WebMCP tools"
|
|
10761
|
-
);
|
|
10762
|
-
}
|
|
10763
|
-
const coveredActions = [];
|
|
10764
|
-
const missingActions = [];
|
|
10765
|
-
for (const action of COMMERCE_ACTIONS) {
|
|
10766
|
-
const matched = toolSignatures.some(
|
|
10767
|
-
(sig) => action.keywords.some((kw) => new RegExp(`\\b${kw}\\b`).test(sig))
|
|
10768
|
-
);
|
|
10769
|
-
if (matched) {
|
|
10770
|
-
coveredActions.push(action.label);
|
|
10771
|
-
} else {
|
|
10772
|
-
missingActions.push(action.label);
|
|
10773
|
-
}
|
|
10774
|
-
}
|
|
10775
|
-
const coverage = coveredActions.length;
|
|
10776
|
-
const total = COMMERCE_ACTIONS.length;
|
|
10777
|
-
if (coverage >= 4) {
|
|
10778
|
-
return this.pass(
|
|
10779
|
-
`${coverage}/${total} commerce actions covered: ${coveredActions.join(", ")}.`,
|
|
10780
|
-
`At least ${MIN_COVERAGE} commerce actions covered`,
|
|
10781
|
-
`${coverage}/${total} covered`
|
|
10782
|
-
);
|
|
10783
|
-
}
|
|
10784
|
-
if (coverage >= MIN_COVERAGE) {
|
|
10785
|
-
return this.warn(
|
|
10786
|
-
`${coverage}/${total} commerce actions covered: ${coveredActions.join(", ")}. Missing: ${missingActions.join(", ")}.`,
|
|
10787
|
-
`At least 4 commerce actions covered for strong agent support`,
|
|
10788
|
-
`${coverage}/${total} covered`,
|
|
10789
|
-
"medium"
|
|
10790
|
-
);
|
|
10791
|
-
}
|
|
10792
|
-
return this.fail(
|
|
10793
|
-
`Only ${coverage}/${total} commerce actions covered: ${coveredActions.length > 0 ? coveredActions.join(", ") : "none"}. Missing: ${missingActions.join(", ")}.`,
|
|
10794
|
-
`At least ${MIN_COVERAGE} commerce actions covered`,
|
|
10795
|
-
`${coverage}/${total} covered`,
|
|
10796
|
-
"medium"
|
|
10797
|
-
);
|
|
10798
|
-
}
|
|
10799
|
-
};
|
|
10800
|
-
|
|
10801
9913
|
// src/audits/agent-tools/openapi-description-quality.ts
|
|
10802
|
-
function
|
|
9914
|
+
function tryParseJson18(body) {
|
|
10803
9915
|
try {
|
|
10804
9916
|
return JSON.parse(body);
|
|
10805
9917
|
} catch {
|
|
10806
9918
|
return void 0;
|
|
10807
9919
|
}
|
|
10808
9920
|
}
|
|
10809
|
-
function
|
|
9921
|
+
function isObject18(val) {
|
|
10810
9922
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10811
9923
|
}
|
|
10812
9924
|
var HTTP_METHODS6 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
10813
9925
|
var MIN_DESCRIPTION_LENGTH2 = 15;
|
|
10814
|
-
function
|
|
9926
|
+
function getOpenApiSpec7(ctx) {
|
|
10815
9927
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
10816
9928
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
10817
|
-
const parsed =
|
|
10818
|
-
if (
|
|
9929
|
+
const parsed = tryParseJson18(jsonResult.body);
|
|
9930
|
+
if (isObject18(parsed)) return parsed;
|
|
10819
9931
|
}
|
|
10820
9932
|
return void 0;
|
|
10821
9933
|
}
|
|
@@ -10824,13 +9936,13 @@ function hasGoodDescription(val) {
|
|
|
10824
9936
|
}
|
|
10825
9937
|
function getCheckableItems(spec) {
|
|
10826
9938
|
const paths = spec["paths"];
|
|
10827
|
-
if (!
|
|
9939
|
+
if (!isObject18(paths)) return [];
|
|
10828
9940
|
const items = [];
|
|
10829
9941
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
10830
|
-
if (!
|
|
9942
|
+
if (!isObject18(pathItem)) continue;
|
|
10831
9943
|
for (const method of HTTP_METHODS6) {
|
|
10832
9944
|
const op = pathItem[method];
|
|
10833
|
-
if (!
|
|
9945
|
+
if (!isObject18(op)) continue;
|
|
10834
9946
|
const operation = op;
|
|
10835
9947
|
const opLabel = `${method.toUpperCase()} ${path}`;
|
|
10836
9948
|
items.push({
|
|
@@ -10840,7 +9952,7 @@ function getCheckableItems(spec) {
|
|
|
10840
9952
|
const parameters = operation["parameters"];
|
|
10841
9953
|
if (Array.isArray(parameters)) {
|
|
10842
9954
|
for (const param of parameters) {
|
|
10843
|
-
if (!
|
|
9955
|
+
if (!isObject18(param)) continue;
|
|
10844
9956
|
const name = typeof param["name"] === "string" ? param["name"] : "(unnamed)";
|
|
10845
9957
|
items.push({
|
|
10846
9958
|
label: `${opLabel} param '${name}'`,
|
|
@@ -10884,7 +9996,7 @@ var OpenApiDescriptionQualityAudit = class _OpenApiDescriptionQualityAudit exten
|
|
|
10884
9996
|
}
|
|
10885
9997
|
};
|
|
10886
9998
|
audit(ctx) {
|
|
10887
|
-
const spec =
|
|
9999
|
+
const spec = getOpenApiSpec7(ctx);
|
|
10888
10000
|
if (!spec) {
|
|
10889
10001
|
return this.notApplicable(
|
|
10890
10002
|
"No parseable OpenAPI JSON spec found at /openapi.json.",
|
|
@@ -11861,53 +10973,6 @@ var TimeElementAudit = class extends Audit {
|
|
|
11861
10973
|
}
|
|
11862
10974
|
};
|
|
11863
10975
|
|
|
11864
|
-
// src/audits/semantic-html/address-element.ts
|
|
11865
|
-
var AddressElementAudit = class extends Audit {
|
|
11866
|
-
static meta = {
|
|
11867
|
-
id: "6.12",
|
|
11868
|
-
category: "semantic-html",
|
|
11869
|
-
title: "<address> for contact info",
|
|
11870
|
-
failureTitle: "<address> for contact info",
|
|
11871
|
-
description: 'AI agents use <address> elements to extract contact information (email, phone, physical address) for structured answers to "how to contact" queries. Without semantic <address> markup, agents must guess which text on your page is contact info.',
|
|
11872
|
-
scoreDisplayMode: "binary",
|
|
11873
|
-
weight: 1,
|
|
11874
|
-
applicablePageTypes: ["homepage"],
|
|
11875
|
-
defaultPriority: "low",
|
|
11876
|
-
guidance: {
|
|
11877
|
-
impact: 'AI agents cannot reliably extract contact information (email, phone, physical address) when it is not wrapped in an <address> element. This means your business contact details may be omitted from AI-generated answers to "how do I contact" queries.',
|
|
11878
|
-
fix: "Wrap all contact information blocks (email addresses, phone numbers, physical addresses) in an <address> element. Place it in the <footer> or near the relevant content section.",
|
|
11879
|
-
code: '<address>\n <a href="mailto:info@yoursite.com">info@yoursite.com</a><br>\n <a href="tel:+1234567890">+1 (234) 567-890</a><br>\n 123 Main St, City, ST 12345\n</address>',
|
|
11880
|
-
effort: "trivial",
|
|
11881
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address",
|
|
11882
|
-
tags: ["contact", "semantic", "html"]
|
|
11883
|
-
}
|
|
11884
|
-
};
|
|
11885
|
-
audit(ctx) {
|
|
11886
|
-
let pagesWithAddress = 0;
|
|
11887
|
-
for (const page of ctx.pages) {
|
|
11888
|
-
if (page.$("address").length > 0) pagesWithAddress++;
|
|
11889
|
-
}
|
|
11890
|
-
const hasAddress = pagesWithAddress > 0;
|
|
11891
|
-
if (hasAddress) {
|
|
11892
|
-
return this.pass(
|
|
11893
|
-
`${pagesWithAddress}/${ctx.pages.length} page(s) use <address> for contact information.`,
|
|
11894
|
-
"<address> element used for contact information",
|
|
11895
|
-
`${pagesWithAddress} page(s) with <address>`
|
|
11896
|
-
);
|
|
11897
|
-
}
|
|
11898
|
-
return this.warn(
|
|
11899
|
-
"No <address> elements found. If contact information exists, consider using <address>.",
|
|
11900
|
-
"<address> element used for contact information",
|
|
11901
|
-
"No <address> elements found",
|
|
11902
|
-
{
|
|
11903
|
-
priority: "low",
|
|
11904
|
-
description: 'AI agents use <address> elements to extract contact information (email, phone, physical address) for structured answers to "how to contact" queries. Without semantic <address> markup, agents must guess which text on your page is contact info.',
|
|
11905
|
-
code: '<address>\n <a href="mailto:info@yoursite.com">info@yoursite.com</a><br>\n 123 Main St, City, ST 12345\n</address>'
|
|
11906
|
-
}
|
|
11907
|
-
);
|
|
11908
|
-
}
|
|
11909
|
-
};
|
|
11910
|
-
|
|
11911
10976
|
// src/audits/semantic-html/definition-elements.ts
|
|
11912
10977
|
var DefinitionElementsAudit = class extends Audit {
|
|
11913
10978
|
static meta = {
|
|
@@ -12068,106 +11133,31 @@ var ImageAltTextAudit = class extends Audit {
|
|
|
12068
11133
|
const mostCovered = coverage >= 0.8;
|
|
12069
11134
|
if (allCovered) {
|
|
12070
11135
|
return this.pass(
|
|
12071
|
-
`All ${totalImages} non-decorative image(s) have descriptive alt text.`,
|
|
12072
|
-
"100% of non-decorative images have non-empty descriptive alt text",
|
|
12073
|
-
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`
|
|
12074
|
-
);
|
|
12075
|
-
}
|
|
12076
|
-
if (mostCovered) {
|
|
12077
|
-
return this.warn(
|
|
12078
|
-
`${imagesWithAlt}/${totalImages} non-decorative image(s) have alt text (${Math.round(coverage * 100)}%).`,
|
|
12079
|
-
"100% of non-decorative images have non-empty descriptive alt text",
|
|
12080
|
-
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`,
|
|
12081
|
-
{
|
|
12082
|
-
priority: "high",
|
|
12083
|
-
description: "Most AI agents are text-only and rely entirely on alt text to understand images. Missing alt text makes your visual content invisible to AI systems, meaning product images, diagrams, and infographics contribute nothing to AI-generated answers about your page.",
|
|
12084
|
-
code: '<img src="product.jpg" alt="Product name shown from the front, featuring key design element">'
|
|
12085
|
-
}
|
|
12086
|
-
);
|
|
12087
|
-
}
|
|
12088
|
-
return this.fail(
|
|
12089
|
-
`${imagesWithAlt}/${totalImages} non-decorative image(s) have alt text (${Math.round(coverage * 100)}%).`,
|
|
12090
|
-
"100% of non-decorative images have non-empty descriptive alt text",
|
|
12091
|
-
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`,
|
|
12092
|
-
{
|
|
12093
|
-
priority: "high",
|
|
12094
|
-
description: "Most AI agents are text-only and rely entirely on alt text to understand images. Missing alt text makes your visual content invisible to AI systems, meaning product images, diagrams, and infographics contribute nothing to AI-generated answers about your page.",
|
|
12095
|
-
code: '<img src="product.jpg" alt="Product name shown from the front, featuring key design element">'
|
|
12096
|
-
}
|
|
12097
|
-
);
|
|
12098
|
-
}
|
|
12099
|
-
};
|
|
12100
|
-
|
|
12101
|
-
// src/audits/semantic-html/decorative-images.ts
|
|
12102
|
-
var DecorativeImagesAudit = class extends Audit {
|
|
12103
|
-
static meta = {
|
|
12104
|
-
id: "6.16",
|
|
12105
|
-
category: "semantic-html",
|
|
12106
|
-
title: "Decorative images marked correctly",
|
|
12107
|
-
failureTitle: "Decorative images marked correctly",
|
|
12108
|
-
description: 'AI agents processing the accessibility tree treat images with empty alt but no role="presentation" as potentially missing alt text rather than intentionally decorative. Adding role="presentation" explicitly tells agents to skip these images, preventing them from flagging false content gaps.',
|
|
12109
|
-
scoreDisplayMode: "ternary",
|
|
12110
|
-
weight: 1,
|
|
12111
|
-
defaultPriority: "medium",
|
|
12112
|
-
guidance: {
|
|
12113
|
-
impact: 'AI agents processing the accessibility tree treat images with empty alt but no role="presentation" as potentially missing alt text rather than intentionally decorative. This creates false-positive content gaps and wastes agent processing on irrelevant images.',
|
|
12114
|
-
fix: 'Add role="presentation" (or role="none") to all decorative images that already have an empty alt attribute. This explicitly tells AI agents and assistive technologies to skip these images entirely.',
|
|
12115
|
-
code: '<img src="decorative-border.png" alt="" role="presentation">',
|
|
12116
|
-
effort: "trivial",
|
|
12117
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/presentation_role",
|
|
12118
|
-
tags: ["images", "decorative", "accessibility", "semantic"]
|
|
12119
|
-
}
|
|
12120
|
-
};
|
|
12121
|
-
audit(ctx) {
|
|
12122
|
-
let decorativeCount = 0;
|
|
12123
|
-
let correctlyMarked = 0;
|
|
12124
|
-
for (const page of ctx.pages) {
|
|
12125
|
-
const images = extractImages(page.$);
|
|
12126
|
-
for (const img of images) {
|
|
12127
|
-
if (img.alt === "") {
|
|
12128
|
-
decorativeCount++;
|
|
12129
|
-
if (img.role === "presentation" || img.role === "none" || img.ariaHidden === "true") {
|
|
12130
|
-
correctlyMarked++;
|
|
12131
|
-
}
|
|
12132
|
-
}
|
|
12133
|
-
}
|
|
12134
|
-
}
|
|
12135
|
-
if (decorativeCount === 0) {
|
|
12136
|
-
return this.pass(
|
|
12137
|
-
"No decorative images (empty alt) found \u2014 check not applicable.",
|
|
12138
|
-
'Images with empty alt have role="presentation"',
|
|
12139
|
-
"No images with empty alt"
|
|
12140
|
-
);
|
|
12141
|
-
}
|
|
12142
|
-
const allCorrect = correctlyMarked === decorativeCount;
|
|
12143
|
-
const majorityCorrect = correctlyMarked > decorativeCount / 2;
|
|
12144
|
-
if (allCorrect) {
|
|
12145
|
-
return this.pass(
|
|
12146
|
-
`All ${decorativeCount} decorative image(s) have role="presentation".`,
|
|
12147
|
-
'Images with empty alt have role="presentation"',
|
|
12148
|
-
`${correctlyMarked}/${decorativeCount} correctly marked`
|
|
11136
|
+
`All ${totalImages} non-decorative image(s) have descriptive alt text.`,
|
|
11137
|
+
"100% of non-decorative images have non-empty descriptive alt text",
|
|
11138
|
+
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`
|
|
12149
11139
|
);
|
|
12150
11140
|
}
|
|
12151
|
-
if (
|
|
11141
|
+
if (mostCovered) {
|
|
12152
11142
|
return this.warn(
|
|
12153
|
-
`${
|
|
12154
|
-
|
|
12155
|
-
`${
|
|
11143
|
+
`${imagesWithAlt}/${totalImages} non-decorative image(s) have alt text (${Math.round(coverage * 100)}%).`,
|
|
11144
|
+
"100% of non-decorative images have non-empty descriptive alt text",
|
|
11145
|
+
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`,
|
|
12156
11146
|
{
|
|
12157
|
-
priority: "
|
|
12158
|
-
description:
|
|
12159
|
-
code: '<img src="
|
|
11147
|
+
priority: "high",
|
|
11148
|
+
description: "Most AI agents are text-only and rely entirely on alt text to understand images. Missing alt text makes your visual content invisible to AI systems, meaning product images, diagrams, and infographics contribute nothing to AI-generated answers about your page.",
|
|
11149
|
+
code: '<img src="product.jpg" alt="Product name shown from the front, featuring key design element">'
|
|
12160
11150
|
}
|
|
12161
11151
|
);
|
|
12162
11152
|
}
|
|
12163
11153
|
return this.fail(
|
|
12164
|
-
`${
|
|
12165
|
-
|
|
12166
|
-
`${
|
|
11154
|
+
`${imagesWithAlt}/${totalImages} non-decorative image(s) have alt text (${Math.round(coverage * 100)}%).`,
|
|
11155
|
+
"100% of non-decorative images have non-empty descriptive alt text",
|
|
11156
|
+
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`,
|
|
12167
11157
|
{
|
|
12168
|
-
priority: "
|
|
12169
|
-
description:
|
|
12170
|
-
code: '<img src="
|
|
11158
|
+
priority: "high",
|
|
11159
|
+
description: "Most AI agents are text-only and rely entirely on alt text to understand images. Missing alt text makes your visual content invisible to AI systems, meaning product images, diagrams, and infographics contribute nothing to AI-generated answers about your page.",
|
|
11160
|
+
code: '<img src="product.jpg" alt="Product name shown from the front, featuring key design element">'
|
|
12171
11161
|
}
|
|
12172
11162
|
);
|
|
12173
11163
|
}
|
|
@@ -12549,67 +11539,6 @@ var FakeHeadingsAudit = class extends Audit {
|
|
|
12549
11539
|
}
|
|
12550
11540
|
};
|
|
12551
11541
|
|
|
12552
|
-
// src/audits/accessibility/skip-nav.ts
|
|
12553
|
-
var SkipNavAudit = class extends Audit {
|
|
12554
|
-
static meta = {
|
|
12555
|
-
id: "7.1",
|
|
12556
|
-
category: "accessibility",
|
|
12557
|
-
title: "Skip navigation link",
|
|
12558
|
-
failureTitle: "Skip navigation link",
|
|
12559
|
-
description: "Headless browser agents (Claude computer use, GPTBot with browser) parse the accessibility tree to navigate pages efficiently. A skip navigation link lets these agents jump directly to primary content without processing every nav element, reducing latency and improving content extraction accuracy.",
|
|
12560
|
-
scoreDisplayMode: "binary",
|
|
12561
|
-
weight: 1,
|
|
12562
|
-
defaultPriority: "medium",
|
|
12563
|
-
guidance: {
|
|
12564
|
-
impact: "Headless browser agents (Claude computer use, GPTBot) parse the accessibility tree to navigate pages. A skip navigation link lets agents jump directly to primary content without processing every nav element, reducing latency and improving content extraction accuracy.",
|
|
12565
|
-
fix: 'Add a "Skip to main content" link as the first focusable element in <body>, pointing to an anchor on your <main> element.',
|
|
12566
|
-
code: '<a href="#main-content" class="skip-link">Skip to main content</a>\n<!-- ... navigation ... -->\n<main id="main-content">...</main>',
|
|
12567
|
-
effort: "trivial",
|
|
12568
|
-
docsUrl: "https://www.w3.org/WAI/WCAG21/Techniques/general/G1",
|
|
12569
|
-
tags: ["a11y", "navigation", "accessibility"]
|
|
12570
|
-
}
|
|
12571
|
-
};
|
|
12572
|
-
audit(ctx) {
|
|
12573
|
-
if (!ctx.pages || ctx.pages.length === 0) {
|
|
12574
|
-
return this.warn(
|
|
12575
|
-
"No pages scanned to check for skip navigation link.",
|
|
12576
|
-
"A skip-to-content link among the first links in <body>",
|
|
12577
|
-
"No pages scanned"
|
|
12578
|
-
);
|
|
12579
|
-
}
|
|
12580
|
-
for (const page of ctx.pages) {
|
|
12581
|
-
const $ = page.$;
|
|
12582
|
-
const bodyLinks = $("body a").slice(0, 5);
|
|
12583
|
-
let found = false;
|
|
12584
|
-
bodyLinks.each((_, el) => {
|
|
12585
|
-
const text = $(el).text().toLowerCase().trim();
|
|
12586
|
-
const href = ($(el).attr("href") ?? "").toLowerCase();
|
|
12587
|
-
if ((text.includes("skip") || text.includes("jump to") || text.includes("go to main")) && (href.includes("#main") || href.includes("#content") || href.includes("#skip"))) {
|
|
12588
|
-
found = true;
|
|
12589
|
-
}
|
|
12590
|
-
});
|
|
12591
|
-
if (found) {
|
|
12592
|
-
return this.pass(
|
|
12593
|
-
"Skip navigation link found among the first links in <body>.",
|
|
12594
|
-
"A skip-to-content link among the first links in <body>",
|
|
12595
|
-
"Skip navigation link detected",
|
|
12596
|
-
page.url
|
|
12597
|
-
);
|
|
12598
|
-
}
|
|
12599
|
-
}
|
|
12600
|
-
return this.fail(
|
|
12601
|
-
"No skip navigation link found. Screen reader and keyboard users rely on skip links to bypass repeated navigation.",
|
|
12602
|
-
"A skip-to-content link among the first links in <body>",
|
|
12603
|
-
"No skip navigation link detected in the first few <body> links",
|
|
12604
|
-
{
|
|
12605
|
-
priority: "medium",
|
|
12606
|
-
description: "Headless browser agents (Claude computer use, GPTBot with browser) parse the accessibility tree to navigate pages efficiently. A skip navigation link lets these agents jump directly to primary content without processing every nav element, reducing latency and improving content extraction accuracy.",
|
|
12607
|
-
code: '<a href="#main-content" class="skip-link">Skip to main content</a>\n<!-- Then on your main content: -->\n<main id="main-content">...</main>'
|
|
12608
|
-
}
|
|
12609
|
-
);
|
|
12610
|
-
}
|
|
12611
|
-
};
|
|
12612
|
-
|
|
12613
11542
|
// src/audits/accessibility/aria-landmarks.ts
|
|
12614
11543
|
var REQUIRED_LANDMARKS = [
|
|
12615
11544
|
{
|
|
@@ -13476,98 +12405,6 @@ var ContentTypeOptionsAudit = class extends Audit {
|
|
|
13476
12405
|
}
|
|
13477
12406
|
};
|
|
13478
12407
|
|
|
13479
|
-
// src/audits/technical-readiness/referrer-policy.ts
|
|
13480
|
-
var ReferrerPolicyAudit = class extends Audit {
|
|
13481
|
-
static meta = {
|
|
13482
|
-
id: "8.5",
|
|
13483
|
-
category: "technical-readiness",
|
|
13484
|
-
title: "Referrer-Policy header",
|
|
13485
|
-
failureTitle: "Referrer-Policy header",
|
|
13486
|
-
description: "AI trust-scoring systems check for Referrer-Policy as a privacy maturity signal. Without it, your site leaks full URL paths in referrer headers to third parties, which AI security audits flag as a privacy concern that can reduce trust scores.",
|
|
13487
|
-
scoreDisplayMode: "binary",
|
|
13488
|
-
weight: 1,
|
|
13489
|
-
defaultPriority: "medium",
|
|
13490
|
-
guidance: {
|
|
13491
|
-
impact: "Without a Referrer-Policy header, your site leaks full URL paths (including query parameters) in HTTP Referer headers when users navigate to external links. AI security audits flag this as a privacy vulnerability, and trust-scoring systems lower your site's rating. Sensitive URL parameters like session tokens or search queries may be exposed to third parties.",
|
|
13492
|
-
fix: 'Add a Referrer-Policy header to your server responses. The recommended value is "strict-origin-when-cross-origin", which sends the full URL for same-origin requests but only the origin for cross-origin requests.',
|
|
13493
|
-
code: "Referrer-Policy: strict-origin-when-cross-origin",
|
|
13494
|
-
effort: "trivial",
|
|
13495
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy",
|
|
13496
|
-
tags: ["security", "headers", "privacy"]
|
|
13497
|
-
}
|
|
13498
|
-
};
|
|
13499
|
-
audit(ctx) {
|
|
13500
|
-
const page = ctx.pages?.[0];
|
|
13501
|
-
const headers = page?.fetchResult.headers ?? {};
|
|
13502
|
-
const value = headers["referrer-policy"];
|
|
13503
|
-
if (value) {
|
|
13504
|
-
return this.pass(
|
|
13505
|
-
`Referrer-Policy header is present: ${value}`,
|
|
13506
|
-
"Referrer-Policy header present on homepage response",
|
|
13507
|
-
`referrer-policy: ${value}`,
|
|
13508
|
-
page?.url
|
|
13509
|
-
);
|
|
13510
|
-
}
|
|
13511
|
-
return this.fail(
|
|
13512
|
-
"Referrer-Policy header is missing from the homepage response.",
|
|
13513
|
-
"Referrer-Policy header present on homepage response",
|
|
13514
|
-
"Header not found",
|
|
13515
|
-
{
|
|
13516
|
-
priority: "medium",
|
|
13517
|
-
description: "AI trust-scoring systems check for Referrer-Policy as a privacy maturity signal. Without it, your site leaks full URL paths in referrer headers to third parties, which AI security audits flag as a privacy concern that can reduce trust scores.",
|
|
13518
|
-
code: "Referrer-Policy: strict-origin-when-cross-origin"
|
|
13519
|
-
},
|
|
13520
|
-
page?.url
|
|
13521
|
-
);
|
|
13522
|
-
}
|
|
13523
|
-
};
|
|
13524
|
-
|
|
13525
|
-
// src/audits/technical-readiness/permissions-policy.ts
|
|
13526
|
-
var PermissionsPolicyAudit = class extends Audit {
|
|
13527
|
-
static meta = {
|
|
13528
|
-
id: "8.6",
|
|
13529
|
-
category: "technical-readiness",
|
|
13530
|
-
title: "Permissions-Policy header",
|
|
13531
|
-
failureTitle: "Permissions-Policy header",
|
|
13532
|
-
description: "AI browser agents that visit your site may trigger permission prompts for camera, microphone, or geolocation if Permissions-Policy is not set. These prompts block agent workflows and are flagged as security concerns by AI trust-scoring systems.",
|
|
13533
|
-
scoreDisplayMode: "binary",
|
|
13534
|
-
weight: 1,
|
|
13535
|
-
defaultPriority: "medium",
|
|
13536
|
-
guidance: {
|
|
13537
|
-
impact: "Without a Permissions-Policy header, AI browser agents visiting your site may trigger unexpected permission prompts for camera, microphone, or geolocation. These prompts block automated agent workflows entirely and are flagged by AI trust-scoring systems as a security concern, reducing your site's trust score.",
|
|
13538
|
-
fix: "Add a Permissions-Policy header that disables sensitive browser features your site does not use. Deny camera, microphone, and geolocation unless your site explicitly requires them.",
|
|
13539
|
-
code: "Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()",
|
|
13540
|
-
effort: "trivial",
|
|
13541
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy",
|
|
13542
|
-
tags: ["security", "headers", "privacy"]
|
|
13543
|
-
}
|
|
13544
|
-
};
|
|
13545
|
-
audit(ctx) {
|
|
13546
|
-
const page = ctx.pages?.[0];
|
|
13547
|
-
const headers = page?.fetchResult.headers ?? {};
|
|
13548
|
-
const value = headers["permissions-policy"];
|
|
13549
|
-
if (value) {
|
|
13550
|
-
return this.pass(
|
|
13551
|
-
`Permissions-Policy header is present: ${value.length > 120 ? value.slice(0, 120) + "..." : value}`,
|
|
13552
|
-
"Permissions-Policy header present on homepage response",
|
|
13553
|
-
`permissions-policy: ${value.length > 120 ? value.slice(0, 120) + "..." : value}`,
|
|
13554
|
-
page?.url
|
|
13555
|
-
);
|
|
13556
|
-
}
|
|
13557
|
-
return this.fail(
|
|
13558
|
-
"Permissions-Policy header is missing from the homepage response.",
|
|
13559
|
-
"Permissions-Policy header present on homepage response",
|
|
13560
|
-
"Header not found",
|
|
13561
|
-
{
|
|
13562
|
-
priority: "medium",
|
|
13563
|
-
description: "AI browser agents that visit your site may trigger permission prompts for camera, microphone, or geolocation if Permissions-Policy is not set. These prompts block agent workflows and are flagged as security concerns by AI trust-scoring systems.",
|
|
13564
|
-
code: "Permissions-Policy: camera=(), microphone=(), geolocation=()"
|
|
13565
|
-
},
|
|
13566
|
-
page?.url
|
|
13567
|
-
);
|
|
13568
|
-
}
|
|
13569
|
-
};
|
|
13570
|
-
|
|
13571
12408
|
// src/audits/technical-readiness/security-txt.ts
|
|
13572
12409
|
var SecurityTxtAudit = class extends Audit {
|
|
13573
12410
|
static meta = {
|
|
@@ -14307,66 +13144,6 @@ var LcpNotLazyAudit = class extends Audit {
|
|
|
14307
13144
|
}
|
|
14308
13145
|
};
|
|
14309
13146
|
|
|
14310
|
-
// src/audits/technical-readiness/preconnect-hints.ts
|
|
14311
|
-
var PreconnectHintsAudit = class extends Audit {
|
|
14312
|
-
static meta = {
|
|
14313
|
-
id: "8.17",
|
|
14314
|
-
category: "technical-readiness",
|
|
14315
|
-
title: "Preconnect hints",
|
|
14316
|
-
failureTitle: "Preconnect hints",
|
|
14317
|
-
description: "Preconnect hints reduce the time AI crawlers spend establishing connections to third-party resources. Faster page loads mean AI agents can crawl more of your pages within their time budget, improving overall content coverage in AI knowledge bases.",
|
|
14318
|
-
scoreDisplayMode: "binary",
|
|
14319
|
-
weight: 1,
|
|
14320
|
-
defaultPriority: "low",
|
|
14321
|
-
guidance: {
|
|
14322
|
-
impact: "Without preconnect hints, each third-party resource requires a full DNS lookup, TCP handshake, and TLS negotiation before loading can begin. This adds hundreds of milliseconds per origin, slowing overall page load and reducing the number of pages AI crawlers can process within their time budget.",
|
|
14323
|
-
fix: 'Add <link rel="preconnect"> tags in <head> for your most critical third-party origins (CDNs, font providers, analytics). Limit to 2-4 origins to avoid diminishing returns.',
|
|
14324
|
-
code: '<link rel="preconnect" href="https://fonts.googleapis.com">\n<link rel="preconnect" href="https://cdn.yoursite.com" crossorigin>',
|
|
14325
|
-
effort: "trivial",
|
|
14326
|
-
docsUrl: "https://web.dev/articles/uses-rel-preconnect",
|
|
14327
|
-
tags: ["performance", "speed", "resource-hints"]
|
|
14328
|
-
}
|
|
14329
|
-
};
|
|
14330
|
-
audit(ctx) {
|
|
14331
|
-
const page = ctx.pages?.[0];
|
|
14332
|
-
if (!page) {
|
|
14333
|
-
return this.warn(
|
|
14334
|
-
"No homepage data available to check preconnect hints.",
|
|
14335
|
-
'At least one <link rel="preconnect"> tag present',
|
|
14336
|
-
"No homepage fetched",
|
|
14337
|
-
void 0,
|
|
14338
|
-
void 0
|
|
14339
|
-
);
|
|
14340
|
-
}
|
|
14341
|
-
const $ = page.$;
|
|
14342
|
-
const preconnects = $('link[rel="preconnect"]');
|
|
14343
|
-
if (preconnects.length > 0) {
|
|
14344
|
-
const hrefs = [];
|
|
14345
|
-
preconnects.each((_, el) => {
|
|
14346
|
-
const href = $(el).attr("href");
|
|
14347
|
-
if (href) hrefs.push(href);
|
|
14348
|
-
});
|
|
14349
|
-
return this.pass(
|
|
14350
|
-
`Found ${preconnects.length} preconnect hint(s): ${hrefs.slice(0, 5).join(", ")}`,
|
|
14351
|
-
'At least one <link rel="preconnect"> tag present',
|
|
14352
|
-
`${preconnects.length} preconnect hint(s)`,
|
|
14353
|
-
page.url
|
|
14354
|
-
);
|
|
14355
|
-
}
|
|
14356
|
-
return this.fail(
|
|
14357
|
-
'No <link rel="preconnect"> hints found. Preconnect hints speed up connections to critical third-party origins.',
|
|
14358
|
-
'At least one <link rel="preconnect"> tag present',
|
|
14359
|
-
"No preconnect hints found",
|
|
14360
|
-
{
|
|
14361
|
-
priority: "low",
|
|
14362
|
-
description: "Preconnect hints reduce the time AI crawlers spend establishing connections to third-party resources. Faster page loads mean AI agents can crawl more of your pages within their time budget, improving overall content coverage in AI knowledge bases.",
|
|
14363
|
-
code: '<link rel="preconnect" href="https://fonts.googleapis.com">\n<link rel="preconnect" href="https://cdn.yoursite.com">'
|
|
14364
|
-
},
|
|
14365
|
-
page.url
|
|
14366
|
-
);
|
|
14367
|
-
}
|
|
14368
|
-
};
|
|
14369
|
-
|
|
14370
13147
|
// src/audits/technical-readiness/no-broken-ai-endpoints.ts
|
|
14371
13148
|
var NoBrokenAiEndpointsAudit = class extends Audit {
|
|
14372
13149
|
static meta = {
|
|
@@ -14680,71 +13457,6 @@ var TermsOfServiceAudit = class extends Audit {
|
|
|
14680
13457
|
}
|
|
14681
13458
|
};
|
|
14682
13459
|
|
|
14683
|
-
// src/audits/technical-readiness/framework-detection.ts
|
|
14684
|
-
var FrameworkDetectionAudit = class extends Audit {
|
|
14685
|
-
static meta = {
|
|
14686
|
-
id: "8.21",
|
|
14687
|
-
category: "technical-readiness",
|
|
14688
|
-
title: "Frontend framework detection",
|
|
14689
|
-
failureTitle: "Frontend framework detection",
|
|
14690
|
-
description: "AI agents can optimize their interaction strategy if they know the underlying technology stack (e.g., React, Next.js, Vue). This also helps identify potential client-side rendering issues.",
|
|
14691
|
-
scoreDisplayMode: "informative",
|
|
14692
|
-
weight: 1,
|
|
14693
|
-
defaultPriority: "low",
|
|
14694
|
-
guidance: {
|
|
14695
|
-
impact: "This is an informational audit. Knowing your frontend framework helps identify potential rendering issues \u2014 for example, pure client-side React apps are invisible to AI crawlers that do not execute JavaScript. Framework detection also helps prioritize other audit recommendations.",
|
|
14696
|
-
fix: "No action required. This audit detects your framework automatically. If your framework relies on client-side rendering (e.g., Create React App, Vue SPA), consider switching to a server-rendering mode (Next.js, Nuxt, or SvelteKit) so AI crawlers can read your content.",
|
|
14697
|
-
effort: "trivial",
|
|
14698
|
-
tags: ["informational", "framework"]
|
|
14699
|
-
}
|
|
14700
|
-
};
|
|
14701
|
-
audit(ctx) {
|
|
14702
|
-
const page = ctx.pages[0];
|
|
14703
|
-
if (!page) {
|
|
14704
|
-
return this.fail("No pages available for analysis.", "", "None");
|
|
14705
|
-
}
|
|
14706
|
-
const { meta, $ } = page;
|
|
14707
|
-
const frameworks = [];
|
|
14708
|
-
if (meta["generator"]) {
|
|
14709
|
-
frameworks.push(`Generator: ${meta["generator"]}`);
|
|
14710
|
-
}
|
|
14711
|
-
if (meta["next-head-count"] || $('script[id="__NEXT_DATA__"]').length > 0) {
|
|
14712
|
-
frameworks.push("Next.js");
|
|
14713
|
-
}
|
|
14714
|
-
if ($('script[src*="nuxt"]').length > 0 || globalThis.window?.__NUXT__) {
|
|
14715
|
-
frameworks.push("Nuxt.js");
|
|
14716
|
-
}
|
|
14717
|
-
if ($("[data-reactroot], [data-reactid]").length > 0 || $('script[src*="react"]').length > 0) {
|
|
14718
|
-
frameworks.push("React");
|
|
14719
|
-
}
|
|
14720
|
-
if ($("[data-v-field], [data-v-]").length > 0 || $('script[src*="vue"]').length > 0) {
|
|
14721
|
-
frameworks.push("Vue.js");
|
|
14722
|
-
}
|
|
14723
|
-
if ($("app-root, [ng-version]").length > 0) {
|
|
14724
|
-
frameworks.push("Angular");
|
|
14725
|
-
}
|
|
14726
|
-
if ($('script[src*="astro"]').length > 0 || $("style[data-astro-cid]").length > 0) {
|
|
14727
|
-
frameworks.push("Astro");
|
|
14728
|
-
}
|
|
14729
|
-
if ($('script[src*="svelte"]').length > 0) {
|
|
14730
|
-
frameworks.push("Svelte");
|
|
14731
|
-
}
|
|
14732
|
-
if (frameworks.length > 0) {
|
|
14733
|
-
const unique = Array.from(new Set(frameworks));
|
|
14734
|
-
return this.pass(
|
|
14735
|
-
`Detected frameworks: ${unique.join(", ")}.`,
|
|
14736
|
-
"Identify the frontend framework used by the site.",
|
|
14737
|
-
unique.join(", ")
|
|
14738
|
-
);
|
|
14739
|
-
}
|
|
14740
|
-
return this.pass(
|
|
14741
|
-
"No specific frontend framework clearly detected.",
|
|
14742
|
-
"Identify the frontend framework used by the site.",
|
|
14743
|
-
"Generic/Unknown"
|
|
14744
|
-
);
|
|
14745
|
-
}
|
|
14746
|
-
};
|
|
14747
|
-
|
|
14748
13460
|
// src/audits/answer-engine/faq-sections.ts
|
|
14749
13461
|
var FAQ_TEXT = /frequently\s+asked\s+questions|\bFAQ['']?s?\b|common\s+questions|questions?\s*(?:&|and)\s*answers|\bQ\s*&\s*A\b/i;
|
|
14750
13462
|
function hasFaqJsonLd(p) {
|
|
@@ -15709,7 +14421,7 @@ var MetaDescriptionAeoAudit = class _MetaDescriptionAeoAudit extends Audit {
|
|
|
15709
14421
|
};
|
|
15710
14422
|
|
|
15711
14423
|
// src/audits/generative-engine/named-author.ts
|
|
15712
|
-
function
|
|
14424
|
+
function asString3(val) {
|
|
15713
14425
|
return typeof val === "string" ? val : "";
|
|
15714
14426
|
}
|
|
15715
14427
|
function findJsonLdByType(jsonLd, types) {
|
|
@@ -15796,7 +14508,7 @@ var NamedAuthorAudit = class extends Audit {
|
|
|
15796
14508
|
if (!author) continue;
|
|
15797
14509
|
const authors = Array.isArray(author) ? author : [author];
|
|
15798
14510
|
for (const a of authors) {
|
|
15799
|
-
const name = typeof a === "string" ? a : typeof a === "object" && a !== null ?
|
|
14511
|
+
const name = typeof a === "string" ? a : typeof a === "object" && a !== null ? asString3(a["name"]) : "";
|
|
15800
14512
|
const lower = name.trim().toLowerCase();
|
|
15801
14513
|
if (lower && !GENERIC_AUTHOR_NAMES.has(lower)) {
|
|
15802
14514
|
return this.pass(
|
|
@@ -16735,7 +15447,7 @@ var PublicationDateAudit = class extends Audit {
|
|
|
16735
15447
|
};
|
|
16736
15448
|
|
|
16737
15449
|
// src/audits/generative-engine/last-modified-schema.ts
|
|
16738
|
-
function
|
|
15450
|
+
function asString4(val) {
|
|
16739
15451
|
return typeof val === "string" ? val : "";
|
|
16740
15452
|
}
|
|
16741
15453
|
function findJsonLdByType5(jsonLd, types) {
|
|
@@ -16821,7 +15533,7 @@ var LastModifiedSchemaAudit = class extends Audit {
|
|
|
16821
15533
|
return this.warn(
|
|
16822
15534
|
datePublished ? `dateModified equals datePublished ("${dateModified}"). Update dateModified when content changes.` : `dateModified is set ("${dateModified}") but no datePublished for comparison.`,
|
|
16823
15535
|
"JSON-LD dateModified present and different from datePublished",
|
|
16824
|
-
`dateModified: ${dateModified}${datePublished ? `, datePublished: ${
|
|
15536
|
+
`dateModified: ${dateModified}${datePublished ? `, datePublished: ${asString4(datePublished)}` : ""}`,
|
|
16825
15537
|
{
|
|
16826
15538
|
priority: "low",
|
|
16827
15539
|
description: "AI engines compare dateModified to datePublished to detect actively maintained content. When they match, agents treat the content as never-updated since publication. Update dateModified each time you revise content to signal freshness."
|
|
@@ -16931,69 +15643,6 @@ var InternalCrossLinkingAudit = class extends Audit {
|
|
|
16931
15643
|
}
|
|
16932
15644
|
};
|
|
16933
15645
|
|
|
16934
|
-
// src/audits/generative-engine/pagination-links.ts
|
|
16935
|
-
var PaginationLinksAudit = class extends Audit {
|
|
16936
|
-
static meta = {
|
|
16937
|
-
id: "10.12",
|
|
16938
|
-
category: "generative-engine",
|
|
16939
|
-
title: "Pagination links",
|
|
16940
|
-
failureTitle: "Pagination links",
|
|
16941
|
-
description: 'AI crawlers use rel="prev" and rel="next" to navigate paginated content series without missing pages.',
|
|
16942
|
-
scoreDisplayMode: "ternary",
|
|
16943
|
-
weight: 1,
|
|
16944
|
-
applicablePageTypes: ["category"],
|
|
16945
|
-
defaultPriority: "low",
|
|
16946
|
-
guidance: {
|
|
16947
|
-
impact: 'AI crawlers use rel="prev" and rel="next" to navigate paginated content series sequentially. Without these links, agents may miss pages in a series or index paginated listings out of order, leading to incomplete content coverage in AI knowledge bases.',
|
|
16948
|
-
fix: 'Add <link rel="prev"> and <link rel="next"> tags in the <head> of paginated pages pointing to the previous and next pages in the series.',
|
|
16949
|
-
code: '<link rel="prev" href="/blog/page/1">\n<link rel="next" href="/blog/page/3">',
|
|
16950
|
-
effort: "easy",
|
|
16951
|
-
tags: ["pagination", "html", "generative-engine"]
|
|
16952
|
-
}
|
|
16953
|
-
};
|
|
16954
|
-
audit(ctx) {
|
|
16955
|
-
const page = ctx.pages[0];
|
|
16956
|
-
if (!page) {
|
|
16957
|
-
return this.fail(
|
|
16958
|
-
"No pages scanned.",
|
|
16959
|
-
'<link rel="prev"> and <link rel="next"> in head',
|
|
16960
|
-
"No pages scanned",
|
|
16961
|
-
{
|
|
16962
|
-
priority: "low",
|
|
16963
|
-
description: 'AI crawlers use rel="prev" and rel="next" to navigate paginated content series without missing pages.',
|
|
16964
|
-
code: '<link rel="prev" href="/blog/page/1">\n<link rel="next" href="/blog/page/3">'
|
|
16965
|
-
}
|
|
16966
|
-
);
|
|
16967
|
-
}
|
|
16968
|
-
for (const p of ctx.pages) {
|
|
16969
|
-
const hasPrev = p.headLinks.some((l) => l.rel === "prev");
|
|
16970
|
-
const hasNext = p.headLinks.some((l) => l.rel === "next");
|
|
16971
|
-
if (hasPrev || hasNext) {
|
|
16972
|
-
const found = [];
|
|
16973
|
-
if (hasPrev) found.push('rel="prev"');
|
|
16974
|
-
if (hasNext) found.push('rel="next"');
|
|
16975
|
-
return this.pass(
|
|
16976
|
-
`Pagination links found: ${found.join(" and ")}.`,
|
|
16977
|
-
'<link rel="prev"> and <link rel="next"> in head',
|
|
16978
|
-
found.join(", "),
|
|
16979
|
-
p.url
|
|
16980
|
-
);
|
|
16981
|
-
}
|
|
16982
|
-
}
|
|
16983
|
-
return this.warn(
|
|
16984
|
-
'No <link rel="prev"> or <link rel="next"> found on any page.',
|
|
16985
|
-
'<link rel="prev"> and <link rel="next"> in head',
|
|
16986
|
-
"Not found",
|
|
16987
|
-
{
|
|
16988
|
-
priority: "low",
|
|
16989
|
-
description: 'AI crawlers use rel="prev" and rel="next" to navigate paginated content series sequentially. Without these links, agents may miss pages in a series or index paginated listings out of order, leading to incomplete content coverage in AI knowledge bases.',
|
|
16990
|
-
code: '<link rel="prev" href="/blog/page/1">\n<link rel="next" href="/blog/page/3">'
|
|
16991
|
-
},
|
|
16992
|
-
page.url
|
|
16993
|
-
);
|
|
16994
|
-
}
|
|
16995
|
-
};
|
|
16996
|
-
|
|
16997
15646
|
// src/audits/generative-engine/unique-data.ts
|
|
16998
15647
|
var STAT_PATTERN = /\d+(?:\.\d+)?%|\$[\d,]+(?:\.\d{2})?|\b\d{1,3}(?:,\d{3})+\b|\b\d+(?:\.\d+)?x\b/;
|
|
16999
15648
|
var UniqueDataAudit = class extends Audit {
|
|
@@ -17269,7 +15918,6 @@ var defaultConfig = {
|
|
|
17269
15918
|
reg(MobileFriendlyAudit),
|
|
17270
15919
|
reg(FastPageLoadAudit),
|
|
17271
15920
|
reg(NoBrokenLinksAudit),
|
|
17272
|
-
reg(NavigationJsonAudit),
|
|
17273
15921
|
reg(NoOrphanPagesAudit),
|
|
17274
15922
|
reg(CommerceLinksAudit)
|
|
17275
15923
|
],
|
|
@@ -17313,13 +15961,11 @@ var defaultConfig = {
|
|
|
17313
15961
|
reg(FaqPageSchemaAudit),
|
|
17314
15962
|
reg(ServiceProductSchemaAudit),
|
|
17315
15963
|
reg(SpeakableSchemaAudit),
|
|
17316
|
-
reg(PotentialActionAudit),
|
|
17317
15964
|
reg(HowToSchemaAudit),
|
|
17318
15965
|
reg(LocalBusinessSchemaAudit),
|
|
17319
15966
|
reg(ReviewSchemaAudit),
|
|
17320
15967
|
reg(OfferSchemaAudit),
|
|
17321
15968
|
reg(AuthorSchemaAudit),
|
|
17322
|
-
reg(ActionSchemaAudit),
|
|
17323
15969
|
reg(ProductIdentifiersAudit),
|
|
17324
15970
|
reg(ProductDetailsAudit),
|
|
17325
15971
|
reg(ProductReviewsAudit),
|
|
@@ -17337,12 +15983,9 @@ var defaultConfig = {
|
|
|
17337
15983
|
reg(OgImageAltAudit),
|
|
17338
15984
|
reg(TwitterCardAudit),
|
|
17339
15985
|
reg(LlmsTxtLinkAudit),
|
|
17340
|
-
reg(LlmsFullTxtLinkAudit),
|
|
17341
15986
|
reg(AiContentDeclarationAudit),
|
|
17342
|
-
reg(AiInstructionsAudit),
|
|
17343
15987
|
reg(MarkdownAlternateAudit),
|
|
17344
15988
|
reg(RssFeedLinkAudit),
|
|
17345
|
-
reg(McpDiscoveryLinkAudit),
|
|
17346
15989
|
reg(OpenApiLinkAudit),
|
|
17347
15990
|
reg(AiCatalogLinkAudit),
|
|
17348
15991
|
reg(MetaRobotsAudit)
|
|
@@ -17351,20 +15994,17 @@ var defaultConfig = {
|
|
|
17351
15994
|
reg(OpenApiExistsAudit),
|
|
17352
15995
|
reg(OpenApiEndpointsAudit),
|
|
17353
15996
|
reg(OpenApiOperationIdsAudit),
|
|
17354
|
-
reg(OpenApiAiInstructionsAudit),
|
|
17355
15997
|
reg(OpenApiServersAudit),
|
|
17356
15998
|
reg(OpenApiSchemasAudit),
|
|
17357
15999
|
reg(AiCatalogExistsAudit),
|
|
17358
16000
|
reg(AiCatalogMetadataAudit),
|
|
17359
16001
|
reg(AiCatalogUrlsAudit),
|
|
17360
16002
|
reg(AgentsJsonAudit),
|
|
17361
|
-
reg(AiPluginJsonAudit),
|
|
17362
16003
|
reg(McpDiscoveryAudit),
|
|
17363
16004
|
reg(McpEndpointAudit),
|
|
17364
16005
|
reg(McpCapabilitiesAudit),
|
|
17365
16006
|
reg(ContactFormAudit),
|
|
17366
16007
|
reg(SearchEndpointAudit),
|
|
17367
|
-
reg(DataActionCtasAudit),
|
|
17368
16008
|
reg(NoBlockingCaptchaAudit),
|
|
17369
16009
|
reg(FormsNoJsAudit),
|
|
17370
16010
|
reg(WebmcpManifestAudit),
|
|
@@ -17372,7 +16012,6 @@ var defaultConfig = {
|
|
|
17372
16012
|
reg(WebmcpInputQualityAudit),
|
|
17373
16013
|
reg(WebmcpToolNamingAudit),
|
|
17374
16014
|
reg(WebmcpToolAnnotationsAudit),
|
|
17375
|
-
reg(WebmcpActionCoverageAudit),
|
|
17376
16015
|
reg(OpenApiDescriptionQualityAudit),
|
|
17377
16016
|
reg(FormActionabilityAudit)
|
|
17378
16017
|
],
|
|
@@ -17388,18 +16027,15 @@ var defaultConfig = {
|
|
|
17388
16027
|
reg(DataTablesAudit),
|
|
17389
16028
|
reg(CodeLanguageAudit),
|
|
17390
16029
|
reg(TimeElementAudit),
|
|
17391
|
-
reg(AddressElementAudit),
|
|
17392
16030
|
reg(DefinitionElementsAudit),
|
|
17393
16031
|
reg(ContentDepthAudit),
|
|
17394
16032
|
reg(ImageAltTextAudit),
|
|
17395
|
-
reg(DecorativeImagesAudit),
|
|
17396
16033
|
reg(FigureFigcaptionAudit),
|
|
17397
16034
|
reg(SvgBloatAudit),
|
|
17398
16035
|
reg(TokenRatioAudit),
|
|
17399
16036
|
reg(FakeHeadingsAudit)
|
|
17400
16037
|
],
|
|
17401
16038
|
accessibility: [
|
|
17402
|
-
reg(SkipNavAudit),
|
|
17403
16039
|
reg(AriaLandmarksAudit),
|
|
17404
16040
|
reg(NavAriaLabelAudit),
|
|
17405
16041
|
reg(FormErrorMessagesAudit),
|
|
@@ -17427,8 +16063,6 @@ var defaultConfig = {
|
|
|
17427
16063
|
reg(HstsHeaderAudit),
|
|
17428
16064
|
reg(CspHeaderAudit),
|
|
17429
16065
|
reg(ContentTypeOptionsAudit),
|
|
17430
|
-
reg(ReferrerPolicyAudit),
|
|
17431
|
-
reg(PermissionsPolicyAudit),
|
|
17432
16066
|
reg(SecurityTxtAudit),
|
|
17433
16067
|
reg(CorsAiFilesAudit),
|
|
17434
16068
|
reg(CorsApiRoutesAudit),
|
|
@@ -17439,11 +16073,9 @@ var defaultConfig = {
|
|
|
17439
16073
|
reg(NoRenderBlockingAudit),
|
|
17440
16074
|
reg(ImageDimensionsAudit),
|
|
17441
16075
|
reg(LcpNotLazyAudit),
|
|
17442
|
-
reg(PreconnectHintsAudit),
|
|
17443
16076
|
reg(NoBrokenAiEndpointsAudit),
|
|
17444
16077
|
reg(PrivacyPolicyAudit),
|
|
17445
|
-
reg(TermsOfServiceAudit)
|
|
17446
|
-
reg(FrameworkDetectionAudit)
|
|
16078
|
+
reg(TermsOfServiceAudit)
|
|
17447
16079
|
],
|
|
17448
16080
|
"answer-engine": [
|
|
17449
16081
|
reg(FaqSectionsAudit),
|
|
@@ -17470,7 +16102,6 @@ var defaultConfig = {
|
|
|
17470
16102
|
reg(PublicationDateAudit),
|
|
17471
16103
|
reg(LastModifiedSchemaAudit),
|
|
17472
16104
|
reg(InternalCrossLinkingAudit),
|
|
17473
|
-
reg(PaginationLinksAudit),
|
|
17474
16105
|
reg(UniqueDataAudit),
|
|
17475
16106
|
reg(BlockquoteUsageAudit),
|
|
17476
16107
|
reg(DescriptiveUrlsAudit)
|
|
@@ -17492,7 +16123,8 @@ function stubCheck(meta, tag, explanation) {
|
|
|
17492
16123
|
priority: meta.defaultPriority,
|
|
17493
16124
|
impact: meta.guidance?.impact ?? "",
|
|
17494
16125
|
fix: meta.guidance?.fix ?? "",
|
|
17495
|
-
tags: [tag]
|
|
16126
|
+
tags: [tag],
|
|
16127
|
+
deprecated: meta.deprecated
|
|
17496
16128
|
};
|
|
17497
16129
|
}
|
|
17498
16130
|
function planAudits(ctx, config) {
|
|
@@ -23014,6 +21646,34 @@ function generateScanSummary(report) {
|
|
|
23014
21646
|
return summary;
|
|
23015
21647
|
}
|
|
23016
21648
|
|
|
21649
|
+
// src/scorer.ts
|
|
21650
|
+
function isInformative(check) {
|
|
21651
|
+
return check.scoreDisplayMode === "informative";
|
|
21652
|
+
}
|
|
21653
|
+
function calculateCategoryScore(checks2) {
|
|
21654
|
+
const scored = checks2.filter((c) => c.status !== "na" && !isInformative(c));
|
|
21655
|
+
if (scored.length === 0) return 0;
|
|
21656
|
+
const total = scored.reduce((sum, c) => sum + c.score, 0);
|
|
21657
|
+
return Math.round(total / scored.length * 100);
|
|
21658
|
+
}
|
|
21659
|
+
function buildCategoryResult(id, checks2) {
|
|
21660
|
+
return {
|
|
21661
|
+
id,
|
|
21662
|
+
name: CATEGORY_NAMES[id] ?? id,
|
|
21663
|
+
weight: CATEGORY_WEIGHTS[id] ?? 0,
|
|
21664
|
+
score: calculateCategoryScore(checks2),
|
|
21665
|
+
checks: checks2,
|
|
21666
|
+
passCount: checks2.filter((c) => c.status === "pass").length,
|
|
21667
|
+
warnCount: checks2.filter((c) => c.status === "warn").length,
|
|
21668
|
+
failCount: checks2.filter((c) => c.status === "fail").length
|
|
21669
|
+
};
|
|
21670
|
+
}
|
|
21671
|
+
function calculateOverallScore(categories) {
|
|
21672
|
+
return Math.round(
|
|
21673
|
+
categories.reduce((sum, cat) => sum + cat.score * cat.weight, 0)
|
|
21674
|
+
);
|
|
21675
|
+
}
|
|
21676
|
+
|
|
23017
21677
|
// src/waf-detector.ts
|
|
23018
21678
|
function detectWafProtection(targetUrl, homepageResult, rootFiles, scannedPagesCount) {
|
|
23019
21679
|
const allResults = [];
|
|
@@ -23376,7 +22036,7 @@ async function runScan(url, options) {
|
|
|
23376
22036
|
tracker.phaseStart("report", 1);
|
|
23377
22037
|
logger.debug("[orchestrator] Phase 4: Building final report");
|
|
23378
22038
|
const durationMs = Math.round(performance.now() - start);
|
|
23379
|
-
const recommendations = allChecks.filter((c) => c.status !== "pass").slice().sort((a, b) => {
|
|
22039
|
+
const recommendations = allChecks.filter((c) => c.status !== "pass" && !isInformative(c)).slice().sort((a, b) => {
|
|
23380
22040
|
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
23381
22041
|
return (order[a.priority] ?? 3) - (order[b.priority] ?? 3);
|
|
23382
22042
|
});
|
|
@@ -23387,10 +22047,12 @@ async function runScan(url, options) {
|
|
|
23387
22047
|
weightMap.set(reg2.meta.id, reg2.meta.weight);
|
|
23388
22048
|
}
|
|
23389
22049
|
}
|
|
23390
|
-
const topPasses = allChecks.filter((c) => c.status === "pass").slice().sort(
|
|
22050
|
+
const topPasses = allChecks.filter((c) => c.status === "pass" && !isInformative(c)).slice().sort(
|
|
23391
22051
|
(a, b) => (weightMap.get(b.id) ?? 1) - (weightMap.get(a.id) ?? 1)
|
|
23392
22052
|
).slice(0, 10);
|
|
23393
|
-
const readinessVitals = calculateReadinessVitals(
|
|
22053
|
+
const readinessVitals = calculateReadinessVitals(
|
|
22054
|
+
allChecks.filter((c) => !isInformative(c))
|
|
22055
|
+
);
|
|
23394
22056
|
const readinessScore = Math.round(
|
|
23395
22057
|
readinessVitals.commerce * READINESS_WEIGHTS.commerce + readinessVitals.content * READINESS_WEIGHTS.content + readinessVitals.botAccessibility * READINESS_WEIGHTS.botAccessibility + readinessVitals.technical * READINESS_WEIGHTS.technical
|
|
23396
22058
|
);
|
|
@@ -23470,31 +22132,6 @@ function calculateReadinessVitals(checks2) {
|
|
|
23470
22132
|
};
|
|
23471
22133
|
}
|
|
23472
22134
|
|
|
23473
|
-
// src/scorer.ts
|
|
23474
|
-
function calculateCategoryScore(checks2) {
|
|
23475
|
-
const scored = checks2.filter((c) => c.status !== "na");
|
|
23476
|
-
if (scored.length === 0) return 0;
|
|
23477
|
-
const total = scored.reduce((sum, c) => sum + c.score, 0);
|
|
23478
|
-
return Math.round(total / scored.length * 100);
|
|
23479
|
-
}
|
|
23480
|
-
function buildCategoryResult(id, checks2) {
|
|
23481
|
-
return {
|
|
23482
|
-
id,
|
|
23483
|
-
name: CATEGORY_NAMES[id] ?? id,
|
|
23484
|
-
weight: CATEGORY_WEIGHTS[id] ?? 0,
|
|
23485
|
-
score: calculateCategoryScore(checks2),
|
|
23486
|
-
checks: checks2,
|
|
23487
|
-
passCount: checks2.filter((c) => c.status === "pass").length,
|
|
23488
|
-
warnCount: checks2.filter((c) => c.status === "warn").length,
|
|
23489
|
-
failCount: checks2.filter((c) => c.status === "fail").length
|
|
23490
|
-
};
|
|
23491
|
-
}
|
|
23492
|
-
function calculateOverallScore(categories) {
|
|
23493
|
-
return Math.round(
|
|
23494
|
-
categories.reduce((sum, cat) => sum + cat.score * cat.weight, 0)
|
|
23495
|
-
);
|
|
23496
|
-
}
|
|
23497
|
-
|
|
23498
22135
|
// src/types.ts
|
|
23499
22136
|
var PAGE_TYPE_LABELS = {
|
|
23500
22137
|
homepage: "Homepage",
|
|
@@ -23602,6 +22239,7 @@ function loadConfigFile(customPath) {
|
|
|
23602
22239
|
CheckResultSchema,
|
|
23603
22240
|
CheckStatusSchema,
|
|
23604
22241
|
DEFAULT_SCAN_LIMIT,
|
|
22242
|
+
DeprecationNoticeSchema,
|
|
23605
22243
|
FixEffortSchema,
|
|
23606
22244
|
MAX_CONCURRENT_REQUESTS,
|
|
23607
22245
|
MAX_PAGES_PER_SCAN,
|
|
@@ -23646,6 +22284,7 @@ function loadConfigFile(customPath) {
|
|
|
23646
22284
|
getTierColor,
|
|
23647
22285
|
getTierLabel,
|
|
23648
22286
|
getWordCount,
|
|
22287
|
+
isInformative,
|
|
23649
22288
|
isPrivateIp,
|
|
23650
22289
|
isSafeUrl,
|
|
23651
22290
|
joinUrl,
|