@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.mjs
CHANGED
|
@@ -702,6 +702,10 @@ var AuditGuidanceSchema = z.object({
|
|
|
702
702
|
tags: z.array(z.string().max(50)).max(20).optional()
|
|
703
703
|
});
|
|
704
704
|
var ScoreDisplayModeSchema = z.enum(["binary", "ternary", "informative"]);
|
|
705
|
+
var DeprecationNoticeSchema = z.object({
|
|
706
|
+
notice: z.string().min(1).max(500),
|
|
707
|
+
link: z.string().url()
|
|
708
|
+
});
|
|
705
709
|
var AuditMetaSchema = z.object({
|
|
706
710
|
id: z.string(),
|
|
707
711
|
category: z.string(),
|
|
@@ -709,10 +713,12 @@ var AuditMetaSchema = z.object({
|
|
|
709
713
|
failureTitle: z.string(),
|
|
710
714
|
description: z.string(),
|
|
711
715
|
scoreDisplayMode: ScoreDisplayModeSchema,
|
|
712
|
-
weight
|
|
716
|
+
// Deprecated audits carry weight 0 (excluded from scoring).
|
|
717
|
+
weight: z.number().nonnegative(),
|
|
713
718
|
applicablePageTypes: z.array(z.string()).optional(),
|
|
714
719
|
defaultPriority: CheckPrioritySchema,
|
|
715
|
-
guidance: AuditGuidanceSchema.optional()
|
|
720
|
+
guidance: AuditGuidanceSchema.optional(),
|
|
721
|
+
deprecated: DeprecationNoticeSchema.optional()
|
|
716
722
|
});
|
|
717
723
|
var CheckResultSchema = z.object({
|
|
718
724
|
id: z.string().max(20),
|
|
@@ -734,7 +740,8 @@ var CheckResultSchema = z.object({
|
|
|
734
740
|
code: z.string().max(1e4).optional(),
|
|
735
741
|
docsUrl: z.string().max(2048).url().optional().or(z.string().length(0))
|
|
736
742
|
}).optional(),
|
|
737
|
-
tags: z.array(z.string().max(50)).max(20).optional()
|
|
743
|
+
tags: z.array(z.string().max(50)).max(20).optional(),
|
|
744
|
+
deprecated: DeprecationNoticeSchema.optional()
|
|
738
745
|
});
|
|
739
746
|
|
|
740
747
|
// src/audit.ts
|
|
@@ -827,7 +834,8 @@ var Audit = class _Audit {
|
|
|
827
834
|
docsUrl: meta.guidance?.docsUrl,
|
|
828
835
|
effort: meta.guidance?.effort
|
|
829
836
|
},
|
|
830
|
-
tags: meta.guidance?.tags
|
|
837
|
+
tags: meta.guidance?.tags,
|
|
838
|
+
deprecated: meta.deprecated
|
|
831
839
|
};
|
|
832
840
|
}
|
|
833
841
|
};
|
|
@@ -2530,90 +2538,16 @@ var NoBrokenLinksAudit = class _NoBrokenLinksAudit extends Audit {
|
|
|
2530
2538
|
}
|
|
2531
2539
|
};
|
|
2532
2540
|
|
|
2533
|
-
// src/audits/content-discoverability/navigation-json.ts
|
|
2534
|
-
function isOk14(result) {
|
|
2535
|
-
return result.status === 200;
|
|
2536
|
-
}
|
|
2537
|
-
var NavigationJsonAudit = class extends Audit {
|
|
2538
|
-
static meta = {
|
|
2539
|
-
id: "1.21",
|
|
2540
|
-
category: "content-discoverability",
|
|
2541
|
-
title: "navigation.json present",
|
|
2542
|
-
failureTitle: "navigation.json present",
|
|
2543
|
-
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.",
|
|
2544
|
-
scoreDisplayMode: "binary",
|
|
2545
|
-
weight: 1,
|
|
2546
|
-
defaultPriority: "medium",
|
|
2547
|
-
guidance: {
|
|
2548
|
-
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.",
|
|
2549
|
-
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.",
|
|
2550
|
-
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}',
|
|
2551
|
-
effort: "easy",
|
|
2552
|
-
tags: ["navigation", "structured-data", "discoverability"]
|
|
2553
|
-
}
|
|
2554
|
-
};
|
|
2555
|
-
audit(ctx) {
|
|
2556
|
-
const result = ctx.rootFiles["/navigation.json"];
|
|
2557
|
-
if (!result || !isOk14(result)) {
|
|
2558
|
-
return this.fail(
|
|
2559
|
-
"No navigation.json found at the site root.",
|
|
2560
|
-
"GET /navigation.json returns 200 with valid JSON",
|
|
2561
|
-
result ? `HTTP ${result.status}` : "No response",
|
|
2562
|
-
{
|
|
2563
|
-
priority: "medium",
|
|
2564
|
-
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.",
|
|
2565
|
-
code: `{
|
|
2566
|
-
"name": "Your Site",
|
|
2567
|
-
"items": [
|
|
2568
|
-
{ "label": "Home", "url": "/" },
|
|
2569
|
-
{ "label": "Products", "url": "/products", "children": [
|
|
2570
|
-
{ "label": "Product A", "url": "/products/a" },
|
|
2571
|
-
{ "label": "Product B", "url": "/products/b" }
|
|
2572
|
-
]},
|
|
2573
|
-
{ "label": "About", "url": "/about" }
|
|
2574
|
-
]
|
|
2575
|
-
}`
|
|
2576
|
-
}
|
|
2577
|
-
);
|
|
2578
|
-
}
|
|
2579
|
-
try {
|
|
2580
|
-
JSON.parse(result.body);
|
|
2581
|
-
} catch {
|
|
2582
|
-
return this.fail(
|
|
2583
|
-
"navigation.json exists but contains invalid JSON.",
|
|
2584
|
-
"Valid JSON content",
|
|
2585
|
-
"Invalid JSON",
|
|
2586
|
-
{
|
|
2587
|
-
priority: "medium",
|
|
2588
|
-
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.).",
|
|
2589
|
-
code: `{
|
|
2590
|
-
"name": "Your Site",
|
|
2591
|
-
"items": [
|
|
2592
|
-
{ "label": "Home", "url": "/" },
|
|
2593
|
-
{ "label": "About", "url": "/about" }
|
|
2594
|
-
]
|
|
2595
|
-
}`
|
|
2596
|
-
}
|
|
2597
|
-
);
|
|
2598
|
-
}
|
|
2599
|
-
return this.pass(
|
|
2600
|
-
"navigation.json exists with valid JSON.",
|
|
2601
|
-
"HTTP 200 with valid JSON",
|
|
2602
|
-
"Valid JSON"
|
|
2603
|
-
);
|
|
2604
|
-
}
|
|
2605
|
-
};
|
|
2606
|
-
|
|
2607
2541
|
// src/audits/content-discoverability/no-orphan-pages.ts
|
|
2608
2542
|
import * as cheerio7 from "cheerio";
|
|
2609
|
-
function
|
|
2543
|
+
function isOk14(result) {
|
|
2610
2544
|
return result.status === 200;
|
|
2611
2545
|
}
|
|
2612
2546
|
function getSitemapResult5(ctx) {
|
|
2613
2547
|
const sitemap = ctx.rootFiles["/sitemap.xml"];
|
|
2614
|
-
if (sitemap &&
|
|
2548
|
+
if (sitemap && isOk14(sitemap)) return sitemap;
|
|
2615
2549
|
const index = ctx.rootFiles["/sitemap-index.xml"];
|
|
2616
|
-
if (index &&
|
|
2550
|
+
if (index && isOk14(index)) return index;
|
|
2617
2551
|
return null;
|
|
2618
2552
|
}
|
|
2619
2553
|
var NoOrphanPagesAudit = class extends Audit {
|
|
@@ -2655,7 +2589,7 @@ var NoOrphanPagesAudit = class extends Audit {
|
|
|
2655
2589
|
}
|
|
2656
2590
|
const llmsUrls = /* @__PURE__ */ new Set();
|
|
2657
2591
|
const llmsResult = ctx.rootFiles["/llms.txt"];
|
|
2658
|
-
if (llmsResult &&
|
|
2592
|
+
if (llmsResult && isOk14(llmsResult)) {
|
|
2659
2593
|
const links = extractMarkdownLinks(llmsResult.body);
|
|
2660
2594
|
for (const link of links) {
|
|
2661
2595
|
try {
|
|
@@ -4966,82 +4900,6 @@ var SpeakableSchemaAudit = class extends Audit {
|
|
|
4966
4900
|
}
|
|
4967
4901
|
};
|
|
4968
4902
|
|
|
4969
|
-
// src/audits/structured-data/potential-action.ts
|
|
4970
|
-
function matchesAnyType3(schema, types) {
|
|
4971
|
-
return types.some((t) => {
|
|
4972
|
-
const st = schema["@type"];
|
|
4973
|
-
if (typeof st === "string") return st === t;
|
|
4974
|
-
if (Array.isArray(st)) return st.includes(t);
|
|
4975
|
-
return false;
|
|
4976
|
-
});
|
|
4977
|
-
}
|
|
4978
|
-
function allSchemas5(ctx) {
|
|
4979
|
-
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
4980
|
-
}
|
|
4981
|
-
var PotentialActionAudit = class extends Audit {
|
|
4982
|
-
static meta = {
|
|
4983
|
-
id: "3.10",
|
|
4984
|
-
category: "structured-data",
|
|
4985
|
-
title: "potentialAction on service pages",
|
|
4986
|
-
failureTitle: "potentialAction on service pages",
|
|
4987
|
-
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.",
|
|
4988
|
-
scoreDisplayMode: "binary",
|
|
4989
|
-
weight: 1,
|
|
4990
|
-
applicablePageTypes: ["homepage", "product"],
|
|
4991
|
-
defaultPriority: "medium",
|
|
4992
|
-
guidance: {
|
|
4993
|
-
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.",
|
|
4994
|
-
fix: "Add a potentialAction property to your Organization or Service schema with a ContactAction, OrderAction, or BookAction type and a target URL.",
|
|
4995
|
-
code: `{
|
|
4996
|
-
"@context": "https://schema.org",
|
|
4997
|
-
"@type": "Organization",
|
|
4998
|
-
"name": "Your Company",
|
|
4999
|
-
"potentialAction": {
|
|
5000
|
-
"@type": "OrderAction",
|
|
5001
|
-
"target": "https://yoursite.com/order"
|
|
5002
|
-
}
|
|
5003
|
-
}`,
|
|
5004
|
-
effort: "easy",
|
|
5005
|
-
docsUrl: "https://schema.org/potentialAction",
|
|
5006
|
-
tags: ["json-ld", "schema", "actions", "agentic-commerce"]
|
|
5007
|
-
}
|
|
5008
|
-
};
|
|
5009
|
-
audit(ctx) {
|
|
5010
|
-
const actionTypes = ["ContactAction", "OrderAction", "BookAction"];
|
|
5011
|
-
const schemas = allSchemas5(ctx);
|
|
5012
|
-
const withAction = schemas.filter((s) => {
|
|
5013
|
-
const obj = s;
|
|
5014
|
-
const action = obj["potentialAction"];
|
|
5015
|
-
if (!action) return false;
|
|
5016
|
-
const actions = Array.isArray(action) ? action : [action];
|
|
5017
|
-
return actions.some(
|
|
5018
|
-
(a) => a && typeof a === "object" && matchesAnyType3(a, actionTypes)
|
|
5019
|
-
);
|
|
5020
|
-
});
|
|
5021
|
-
const found = withAction.length > 0;
|
|
5022
|
-
if (found) {
|
|
5023
|
-
return this.pass(
|
|
5024
|
-
`potentialAction (ContactAction/OrderAction/BookAction) found on ${withAction.length} schema(s).`,
|
|
5025
|
-
"At least one page with potentialAction (ContactAction, OrderAction, or BookAction).",
|
|
5026
|
-
`${withAction.length} schema(s) with qualifying potentialAction`
|
|
5027
|
-
);
|
|
5028
|
-
}
|
|
5029
|
-
return this.fail(
|
|
5030
|
-
"No potentialAction with ContactAction, OrderAction, or BookAction found.",
|
|
5031
|
-
"At least one page with potentialAction (ContactAction, OrderAction, or BookAction).",
|
|
5032
|
-
"None",
|
|
5033
|
-
{
|
|
5034
|
-
priority: "medium",
|
|
5035
|
-
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.",
|
|
5036
|
-
code: `"potentialAction": {
|
|
5037
|
-
"@type": "OrderAction",
|
|
5038
|
-
"target": "https://yoursite.com/order"
|
|
5039
|
-
}`
|
|
5040
|
-
}
|
|
5041
|
-
);
|
|
5042
|
-
}
|
|
5043
|
-
};
|
|
5044
|
-
|
|
5045
4903
|
// src/audits/structured-data/howto-schema.ts
|
|
5046
4904
|
function matchesType4(schema, type) {
|
|
5047
4905
|
const t = schema["@type"];
|
|
@@ -5178,7 +5036,7 @@ var HowToSchemaAudit = class extends Audit {
|
|
|
5178
5036
|
};
|
|
5179
5037
|
|
|
5180
5038
|
// src/audits/structured-data/local-business-schema.ts
|
|
5181
|
-
function
|
|
5039
|
+
function matchesAnyType3(schema, types) {
|
|
5182
5040
|
return types.some((t) => {
|
|
5183
5041
|
const st = schema["@type"];
|
|
5184
5042
|
if (typeof st === "string") return st === t;
|
|
@@ -5186,12 +5044,12 @@ function matchesAnyType4(schema, types) {
|
|
|
5186
5044
|
return false;
|
|
5187
5045
|
});
|
|
5188
5046
|
}
|
|
5189
|
-
function
|
|
5047
|
+
function allSchemas5(ctx) {
|
|
5190
5048
|
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5191
5049
|
}
|
|
5192
5050
|
function hasPostalAddressBlock(page) {
|
|
5193
5051
|
const schemas = flattenJsonLd(page.structuredData ?? page.jsonLd);
|
|
5194
|
-
if (schemas.some((s) =>
|
|
5052
|
+
if (schemas.some((s) => matchesAnyType3(s, ["PostalAddress"]))) {
|
|
5195
5053
|
return true;
|
|
5196
5054
|
}
|
|
5197
5055
|
return page.$('[itemtype*="PostalAddress"]').length > 0;
|
|
@@ -5257,9 +5115,9 @@ var LocalBusinessSchemaAudit = class extends Audit {
|
|
|
5257
5115
|
"No physical location indicators found."
|
|
5258
5116
|
);
|
|
5259
5117
|
}
|
|
5260
|
-
const schemas =
|
|
5118
|
+
const schemas = allSchemas5(ctx);
|
|
5261
5119
|
const localSchemas = schemas.filter(
|
|
5262
|
-
(s) =>
|
|
5120
|
+
(s) => matchesAnyType3(s, ["LocalBusiness", "ProfessionalService"])
|
|
5263
5121
|
);
|
|
5264
5122
|
const found = localSchemas.length > 0;
|
|
5265
5123
|
if (found) {
|
|
@@ -5289,7 +5147,7 @@ var LocalBusinessSchemaAudit = class extends Audit {
|
|
|
5289
5147
|
};
|
|
5290
5148
|
|
|
5291
5149
|
// src/audits/structured-data/review-schema.ts
|
|
5292
|
-
function
|
|
5150
|
+
function matchesAnyType4(schema, types) {
|
|
5293
5151
|
return types.some((t) => {
|
|
5294
5152
|
const st = schema["@type"];
|
|
5295
5153
|
if (typeof st === "string") return st === t;
|
|
@@ -5297,7 +5155,7 @@ function matchesAnyType5(schema, types) {
|
|
|
5297
5155
|
return false;
|
|
5298
5156
|
});
|
|
5299
5157
|
}
|
|
5300
|
-
function
|
|
5158
|
+
function allSchemas6(ctx) {
|
|
5301
5159
|
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5302
5160
|
}
|
|
5303
5161
|
function hasTestimonialContent(page) {
|
|
@@ -5366,9 +5224,9 @@ var ReviewSchemaAudit = class extends Audit {
|
|
|
5366
5224
|
}
|
|
5367
5225
|
);
|
|
5368
5226
|
}
|
|
5369
|
-
const schemas =
|
|
5227
|
+
const schemas = allSchemas6(ctx);
|
|
5370
5228
|
const reviewSchemas = schemas.filter(
|
|
5371
|
-
(s) =>
|
|
5229
|
+
(s) => matchesAnyType4(s, ["Review", "AggregateRating"])
|
|
5372
5230
|
);
|
|
5373
5231
|
const schemasWithReviewProp = schemas.filter((s) => {
|
|
5374
5232
|
const obj = s;
|
|
@@ -5400,7 +5258,7 @@ var ReviewSchemaAudit = class extends Audit {
|
|
|
5400
5258
|
};
|
|
5401
5259
|
|
|
5402
5260
|
// src/audits/structured-data/offer-schema.ts
|
|
5403
|
-
function
|
|
5261
|
+
function matchesAnyType5(schema, types) {
|
|
5404
5262
|
return types.some((t) => {
|
|
5405
5263
|
const st = schema["@type"];
|
|
5406
5264
|
if (typeof st === "string") return st === t;
|
|
@@ -5448,7 +5306,7 @@ var OfferSchemaAudit = class extends Audit {
|
|
|
5448
5306
|
const pagesWithOffer = productPages.filter((p) => {
|
|
5449
5307
|
const schemas = flattenJsonLd(p.structuredData ?? p.jsonLd);
|
|
5450
5308
|
const hasOfferSchema = schemas.some(
|
|
5451
|
-
(s) =>
|
|
5309
|
+
(s) => matchesAnyType5(s, ["Offer", "AggregateOffer"])
|
|
5452
5310
|
);
|
|
5453
5311
|
const hasOfferProp = schemas.some((s) => {
|
|
5454
5312
|
const obj = s;
|
|
@@ -5462,7 +5320,7 @@ var OfferSchemaAudit = class extends Audit {
|
|
|
5462
5320
|
});
|
|
5463
5321
|
if (hasOfferSchema) {
|
|
5464
5322
|
const first2 = schemas.find(
|
|
5465
|
-
(s) =>
|
|
5323
|
+
(s) => matchesAnyType5(s, ["Offer", "AggregateOffer"])
|
|
5466
5324
|
);
|
|
5467
5325
|
return first2 && first2["price"] !== void 0 && !!first2["priceCurrency"];
|
|
5468
5326
|
}
|
|
@@ -5521,10 +5379,10 @@ function matchesType5(schema, type) {
|
|
|
5521
5379
|
if (Array.isArray(t)) return t.includes(type);
|
|
5522
5380
|
return false;
|
|
5523
5381
|
}
|
|
5524
|
-
function
|
|
5382
|
+
function matchesAnyType6(schema, types) {
|
|
5525
5383
|
return types.some((t) => matchesType5(schema, t));
|
|
5526
5384
|
}
|
|
5527
|
-
function
|
|
5385
|
+
function allSchemas7(ctx) {
|
|
5528
5386
|
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5529
5387
|
}
|
|
5530
5388
|
function hasProps3(obj, keys) {
|
|
@@ -5564,13 +5422,13 @@ var AuthorSchemaAudit = class extends Audit {
|
|
|
5564
5422
|
}
|
|
5565
5423
|
};
|
|
5566
5424
|
audit(ctx) {
|
|
5567
|
-
const schemas =
|
|
5425
|
+
const schemas = allSchemas7(ctx);
|
|
5568
5426
|
const personSchemas = schemas.filter(
|
|
5569
5427
|
(s) => matchesType5(s, "Person")
|
|
5570
5428
|
);
|
|
5571
5429
|
const authorFromArticles = [];
|
|
5572
5430
|
for (const s of schemas) {
|
|
5573
|
-
if (
|
|
5431
|
+
if (matchesAnyType6(s, ["Article", "NewsArticle", "BlogPosting"])) {
|
|
5574
5432
|
const author = s["author"];
|
|
5575
5433
|
if (Array.isArray(author)) {
|
|
5576
5434
|
for (const a of author) {
|
|
@@ -5640,120 +5498,8 @@ var AuthorSchemaAudit = class extends Audit {
|
|
|
5640
5498
|
}
|
|
5641
5499
|
};
|
|
5642
5500
|
|
|
5643
|
-
// src/audits/structured-data/action-schema.ts
|
|
5644
|
-
function matchesAnyType8(schema, types) {
|
|
5645
|
-
return types.some((t) => {
|
|
5646
|
-
const st = schema["@type"];
|
|
5647
|
-
if (typeof st === "string") return st === t;
|
|
5648
|
-
if (Array.isArray(st)) return st.includes(t);
|
|
5649
|
-
return false;
|
|
5650
|
-
});
|
|
5651
|
-
}
|
|
5652
|
-
function isConfirmationUrl(url) {
|
|
5653
|
-
return /\/(thank-?you|confirmation|success|order-complete)\b/i.test(url);
|
|
5654
|
-
}
|
|
5655
|
-
var ActionSchemaAudit = class extends Audit {
|
|
5656
|
-
static meta = {
|
|
5657
|
-
id: "3.16",
|
|
5658
|
-
category: "structured-data",
|
|
5659
|
-
title: "ConfirmAction/ReserveAction schema",
|
|
5660
|
-
failureTitle: "ConfirmAction/ReserveAction schema",
|
|
5661
|
-
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.",
|
|
5662
|
-
scoreDisplayMode: "ternary",
|
|
5663
|
-
weight: 1,
|
|
5664
|
-
defaultPriority: "low",
|
|
5665
|
-
guidance: {
|
|
5666
|
-
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.",
|
|
5667
|
-
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.",
|
|
5668
|
-
code: `{
|
|
5669
|
-
"@context": "https://schema.org",
|
|
5670
|
-
"@type": "WebPage",
|
|
5671
|
-
"potentialAction": {
|
|
5672
|
-
"@type": "ConfirmAction",
|
|
5673
|
-
"target": "https://yoursite.com/confirm"
|
|
5674
|
-
}
|
|
5675
|
-
}`,
|
|
5676
|
-
effort: "moderate",
|
|
5677
|
-
docsUrl: "https://schema.org/ConfirmAction",
|
|
5678
|
-
tags: ["json-ld", "schema", "agentic-commerce", "actions"]
|
|
5679
|
-
}
|
|
5680
|
-
};
|
|
5681
|
-
audit(ctx) {
|
|
5682
|
-
const confirmationPages = ctx.pages.filter((p) => isConfirmationUrl(p.url));
|
|
5683
|
-
if (confirmationPages.length === 0) {
|
|
5684
|
-
return this.warn(
|
|
5685
|
-
"No thank-you or confirmation pages detected to evaluate.",
|
|
5686
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5687
|
-
"No confirmation pages detected (URLs containing /thank-you/, /confirmation/, /success/).",
|
|
5688
|
-
{
|
|
5689
|
-
priority: "low",
|
|
5690
|
-
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.",
|
|
5691
|
-
code: `"potentialAction": {
|
|
5692
|
-
"@type": "ConfirmAction",
|
|
5693
|
-
"target": "https://yoursite.com/confirm"
|
|
5694
|
-
}`
|
|
5695
|
-
}
|
|
5696
|
-
);
|
|
5697
|
-
}
|
|
5698
|
-
const actionTypes = ["ConfirmAction", "ReserveAction"];
|
|
5699
|
-
const pagesWithAction = confirmationPages.filter((p) => {
|
|
5700
|
-
const schemas = flattenJsonLd(p.structuredData ?? p.jsonLd);
|
|
5701
|
-
const hasActionSchema = schemas.some(
|
|
5702
|
-
(s) => matchesAnyType8(s, actionTypes)
|
|
5703
|
-
);
|
|
5704
|
-
const hasActionProp = schemas.some((s) => {
|
|
5705
|
-
const obj = s;
|
|
5706
|
-
const action = obj["potentialAction"];
|
|
5707
|
-
if (!action) return false;
|
|
5708
|
-
const actions = Array.isArray(action) ? action : [action];
|
|
5709
|
-
return actions.some(
|
|
5710
|
-
(a) => a && typeof a === "object" && matchesAnyType8(a, actionTypes)
|
|
5711
|
-
);
|
|
5712
|
-
});
|
|
5713
|
-
return hasActionSchema || hasActionProp;
|
|
5714
|
-
});
|
|
5715
|
-
const allHave = pagesWithAction.length === confirmationPages.length;
|
|
5716
|
-
const someHave = pagesWithAction.length > 0;
|
|
5717
|
-
if (allHave) {
|
|
5718
|
-
return this.pass(
|
|
5719
|
-
`ConfirmAction/ReserveAction found on all ${confirmationPages.length} confirmation page(s).`,
|
|
5720
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5721
|
-
`${pagesWithAction.length}/${confirmationPages.length} confirmation pages with action schema`
|
|
5722
|
-
);
|
|
5723
|
-
}
|
|
5724
|
-
if (someHave) {
|
|
5725
|
-
return this.warn(
|
|
5726
|
-
`ConfirmAction/ReserveAction found on ${pagesWithAction.length} of ${confirmationPages.length} confirmation page(s).`,
|
|
5727
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5728
|
-
`${pagesWithAction.length}/${confirmationPages.length} confirmation pages with action schema`,
|
|
5729
|
-
{
|
|
5730
|
-
priority: "low",
|
|
5731
|
-
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.",
|
|
5732
|
-
code: `"potentialAction": {
|
|
5733
|
-
"@type": "ConfirmAction",
|
|
5734
|
-
"target": "https://yoursite.com/confirm"
|
|
5735
|
-
}`
|
|
5736
|
-
}
|
|
5737
|
-
);
|
|
5738
|
-
}
|
|
5739
|
-
return this.fail(
|
|
5740
|
-
`No ConfirmAction/ReserveAction found on ${confirmationPages.length} confirmation page(s).`,
|
|
5741
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5742
|
-
`${pagesWithAction.length}/${confirmationPages.length} confirmation pages with action schema`,
|
|
5743
|
-
{
|
|
5744
|
-
priority: "low",
|
|
5745
|
-
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.",
|
|
5746
|
-
code: `"potentialAction": {
|
|
5747
|
-
"@type": "ConfirmAction",
|
|
5748
|
-
"target": "https://yoursite.com/confirm"
|
|
5749
|
-
}`
|
|
5750
|
-
}
|
|
5751
|
-
);
|
|
5752
|
-
}
|
|
5753
|
-
};
|
|
5754
|
-
|
|
5755
5501
|
// src/audits/structured-data/product-identifiers.ts
|
|
5756
|
-
function
|
|
5502
|
+
function matchesAnyType7(schema, types) {
|
|
5757
5503
|
return types.some((t) => {
|
|
5758
5504
|
const st = schema["@type"];
|
|
5759
5505
|
if (typeof st === "string") return st === t;
|
|
@@ -5791,7 +5537,7 @@ var ProductIdentifiersAudit = class extends Audit {
|
|
|
5791
5537
|
audit(ctx) {
|
|
5792
5538
|
const schemas = ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5793
5539
|
const products = schemas.filter(
|
|
5794
|
-
(s) =>
|
|
5540
|
+
(s) => matchesAnyType7(s, [
|
|
5795
5541
|
"Product",
|
|
5796
5542
|
"IndividualProduct",
|
|
5797
5543
|
"ProductModel"
|
|
@@ -5856,7 +5602,7 @@ var ProductIdentifiersAudit = class extends Audit {
|
|
|
5856
5602
|
};
|
|
5857
5603
|
|
|
5858
5604
|
// src/audits/structured-data/product-details.ts
|
|
5859
|
-
function
|
|
5605
|
+
function matchesAnyType8(schema, types) {
|
|
5860
5606
|
return types.some((t) => {
|
|
5861
5607
|
const st = schema["@type"];
|
|
5862
5608
|
if (typeof st === "string") return st === t;
|
|
@@ -5900,7 +5646,7 @@ var ProductDetailsAudit = class extends Audit {
|
|
|
5900
5646
|
audit(ctx) {
|
|
5901
5647
|
const schemas = ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5902
5648
|
const products = schemas.filter(
|
|
5903
|
-
(s) =>
|
|
5649
|
+
(s) => matchesAnyType8(s, [
|
|
5904
5650
|
"Product",
|
|
5905
5651
|
"IndividualProduct",
|
|
5906
5652
|
"ProductModel"
|
|
@@ -6019,7 +5765,7 @@ var ProductReviewsAudit = class extends Audit {
|
|
|
6019
5765
|
};
|
|
6020
5766
|
|
|
6021
5767
|
// src/audits/structured-data/product-transaction-certainty.ts
|
|
6022
|
-
function
|
|
5768
|
+
function matchesAnyType9(schema, types) {
|
|
6023
5769
|
return types.some((t) => {
|
|
6024
5770
|
const st = schema["@type"];
|
|
6025
5771
|
if (typeof st === "string") return st === t;
|
|
@@ -6078,7 +5824,7 @@ var ProductTransactionCertaintyAudit = class extends Audit {
|
|
|
6078
5824
|
audit(ctx) {
|
|
6079
5825
|
const schemas = ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
6080
5826
|
const products = schemas.filter(
|
|
6081
|
-
(s) =>
|
|
5827
|
+
(s) => matchesAnyType9(s, [
|
|
6082
5828
|
"Product",
|
|
6083
5829
|
"IndividualProduct",
|
|
6084
5830
|
"ProductModel"
|
|
@@ -6783,53 +6529,6 @@ var LlmsTxtLinkAudit = class extends Audit {
|
|
|
6783
6529
|
}
|
|
6784
6530
|
};
|
|
6785
6531
|
|
|
6786
|
-
// src/audits/meta-tags/llms-full-txt-link.ts
|
|
6787
|
-
var LlmsFullTxtLinkAudit = class extends Audit {
|
|
6788
|
-
static meta = {
|
|
6789
|
-
id: "4.12",
|
|
6790
|
-
category: "meta-tags",
|
|
6791
|
-
title: "llms-full.txt link in head",
|
|
6792
|
-
failureTitle: "llms-full.txt link in head",
|
|
6793
|
-
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.",
|
|
6794
|
-
scoreDisplayMode: "binary",
|
|
6795
|
-
weight: 1,
|
|
6796
|
-
defaultPriority: "medium",
|
|
6797
|
-
guidance: {
|
|
6798
|
-
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.",
|
|
6799
|
-
fix: 'Create a llms-full.txt file with comprehensive content and add a <link rel="alternate"> tag in <head> pointing to it.',
|
|
6800
|
-
code: '<link rel="alternate" type="text/plain" href="/llms-full.txt" title="LLMs-full.txt">',
|
|
6801
|
-
effort: "moderate",
|
|
6802
|
-
docsUrl: "https://llmstxt.org/",
|
|
6803
|
-
tags: ["meta-tags", "llms-txt", "ai-discovery"]
|
|
6804
|
-
}
|
|
6805
|
-
};
|
|
6806
|
-
audit(ctx) {
|
|
6807
|
-
const page = ctx.pages[0];
|
|
6808
|
-
const link = page?.headLinks?.find(
|
|
6809
|
-
(l) => l.rel === "alternate" && l.type === "text/plain" && (l.title ?? "").toLowerCase().includes("llms-full")
|
|
6810
|
-
);
|
|
6811
|
-
if (link) {
|
|
6812
|
-
return this.pass(
|
|
6813
|
-
`llms-full.txt link found: "${link.href}".`,
|
|
6814
|
-
'<link rel="alternate" type="text/plain" title="...LLMs-full...">',
|
|
6815
|
-
`href="${link.href}" title="${link.title}"`,
|
|
6816
|
-
page.url
|
|
6817
|
-
);
|
|
6818
|
-
}
|
|
6819
|
-
return this.fail(
|
|
6820
|
-
"No llms-full.txt link found in <head>.",
|
|
6821
|
-
'<link rel="alternate" type="text/plain" title="...LLMs-full...">',
|
|
6822
|
-
"Not found",
|
|
6823
|
-
{
|
|
6824
|
-
priority: "medium",
|
|
6825
|
-
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.",
|
|
6826
|
-
code: '<link rel="alternate" type="text/plain" href="/llms-full.txt" title="LLMs-full.txt">'
|
|
6827
|
-
},
|
|
6828
|
-
page?.url
|
|
6829
|
-
);
|
|
6830
|
-
}
|
|
6831
|
-
};
|
|
6832
|
-
|
|
6833
6532
|
// src/audits/meta-tags/ai-content-declaration.ts
|
|
6834
6533
|
var AiContentDeclarationAudit = class extends Audit {
|
|
6835
6534
|
static meta = {
|
|
@@ -6887,50 +6586,6 @@ var AiContentDeclarationAudit = class extends Audit {
|
|
|
6887
6586
|
}
|
|
6888
6587
|
};
|
|
6889
6588
|
|
|
6890
|
-
// src/audits/meta-tags/ai-instructions.ts
|
|
6891
|
-
var AiInstructionsAudit = class extends Audit {
|
|
6892
|
-
static meta = {
|
|
6893
|
-
id: "4.14",
|
|
6894
|
-
category: "meta-tags",
|
|
6895
|
-
title: "ai-instructions meta",
|
|
6896
|
-
failureTitle: "ai-instructions meta",
|
|
6897
|
-
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.",
|
|
6898
|
-
scoreDisplayMode: "binary",
|
|
6899
|
-
weight: 1,
|
|
6900
|
-
defaultPriority: "medium",
|
|
6901
|
-
guidance: {
|
|
6902
|
-
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.",
|
|
6903
|
-
fix: 'Add a <meta name="ai-instructions"> tag with plain-English instructions telling AI agents how to summarize and represent your content.',
|
|
6904
|
-
code: '<meta name="ai-instructions" content="Summarise this page as a product overview. Focus on features and pricing. Do not speculate about unreleased features.">',
|
|
6905
|
-
effort: "trivial",
|
|
6906
|
-
tags: ["meta-tags", "ai-policy", "ai-discovery"]
|
|
6907
|
-
}
|
|
6908
|
-
};
|
|
6909
|
-
audit(ctx) {
|
|
6910
|
-
const page = ctx.pages[0];
|
|
6911
|
-
const value = (page?.meta?.["ai-instructions"] ?? "").trim();
|
|
6912
|
-
if (value) {
|
|
6913
|
-
return this.pass(
|
|
6914
|
-
`ai-instructions meta tag is present.`,
|
|
6915
|
-
"meta[ai-instructions] with non-empty content",
|
|
6916
|
-
value.length > 80 ? value.slice(0, 80) + "..." : value,
|
|
6917
|
-
page.url
|
|
6918
|
-
);
|
|
6919
|
-
}
|
|
6920
|
-
return this.fail(
|
|
6921
|
-
"No ai-instructions meta tag found.",
|
|
6922
|
-
"meta[ai-instructions] with non-empty content",
|
|
6923
|
-
"Not found",
|
|
6924
|
-
{
|
|
6925
|
-
priority: "medium",
|
|
6926
|
-
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.",
|
|
6927
|
-
code: '<meta name="ai-instructions" content="Summarise this page as a product overview. Focus on features and pricing. Do not speculate about unreleased features.">'
|
|
6928
|
-
},
|
|
6929
|
-
page?.url
|
|
6930
|
-
);
|
|
6931
|
-
}
|
|
6932
|
-
};
|
|
6933
|
-
|
|
6934
6589
|
// src/audits/meta-tags/markdown-alternate.ts
|
|
6935
6590
|
var MarkdownAlternateAudit = class extends Audit {
|
|
6936
6591
|
static meta = {
|
|
@@ -7021,94 +6676,47 @@ var RssFeedLinkAudit = class extends Audit {
|
|
|
7021
6676
|
}
|
|
7022
6677
|
};
|
|
7023
6678
|
|
|
7024
|
-
// src/audits/meta-tags/
|
|
7025
|
-
var
|
|
6679
|
+
// src/audits/meta-tags/openapi-link.ts
|
|
6680
|
+
var OpenApiLinkAudit = class extends Audit {
|
|
7026
6681
|
static meta = {
|
|
7027
|
-
id: "4.
|
|
6682
|
+
id: "4.18",
|
|
7028
6683
|
category: "meta-tags",
|
|
7029
|
-
title: "
|
|
7030
|
-
failureTitle: "
|
|
7031
|
-
description: "
|
|
6684
|
+
title: "OpenAPI spec link in head",
|
|
6685
|
+
failureTitle: "OpenAPI spec link in head",
|
|
6686
|
+
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.",
|
|
7032
6687
|
scoreDisplayMode: "binary",
|
|
7033
6688
|
weight: 1,
|
|
7034
6689
|
defaultPriority: "low",
|
|
7035
6690
|
guidance: {
|
|
7036
|
-
impact: "Without an
|
|
7037
|
-
fix: 'If your site
|
|
7038
|
-
code: '<link rel="alternate" type="application/json" href="/
|
|
6691
|
+
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.",
|
|
6692
|
+
fix: 'If your site has an API, create an OpenAPI specification and add a <link rel="alternate"> tag in <head> pointing to it.',
|
|
6693
|
+
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">',
|
|
7039
6694
|
effort: "complex",
|
|
7040
|
-
docsUrl: "https://
|
|
7041
|
-
tags: ["meta-tags", "
|
|
6695
|
+
docsUrl: "https://swagger.io/specification/",
|
|
6696
|
+
tags: ["meta-tags", "openapi", "api", "ai-discovery"]
|
|
7042
6697
|
}
|
|
7043
6698
|
};
|
|
7044
6699
|
audit(ctx) {
|
|
7045
6700
|
const page = ctx.pages[0];
|
|
7046
6701
|
const link = page?.headLinks?.find(
|
|
7047
|
-
(l) => l.rel === "alternate" && l.type === "application/json" && (l.title ?? "").toLowerCase().includes("
|
|
6702
|
+
(l) => l.rel === "alternate" && l.type === "application/json" && (l.title ?? "").toLowerCase().includes("openapi")
|
|
7048
6703
|
);
|
|
7049
6704
|
if (link) {
|
|
7050
6705
|
return this.pass(
|
|
7051
|
-
`
|
|
7052
|
-
'<link rel="alternate" type="application/json" title="...
|
|
6706
|
+
`OpenAPI spec link found: "${link.href}".`,
|
|
6707
|
+
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7053
6708
|
`href="${link.href}" title="${link.title}"`,
|
|
7054
6709
|
page.url
|
|
7055
6710
|
);
|
|
7056
6711
|
}
|
|
7057
6712
|
return this.fail(
|
|
7058
|
-
"No
|
|
7059
|
-
'<link rel="alternate" type="application/json" title="...
|
|
6713
|
+
"No OpenAPI spec link found in <head>.",
|
|
6714
|
+
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7060
6715
|
"Not found",
|
|
7061
6716
|
{
|
|
7062
6717
|
priority: "low",
|
|
7063
|
-
description: "
|
|
7064
|
-
code: '<link rel="alternate" type="application/json" href="/
|
|
7065
|
-
},
|
|
7066
|
-
page?.url
|
|
7067
|
-
);
|
|
7068
|
-
}
|
|
7069
|
-
};
|
|
7070
|
-
|
|
7071
|
-
// src/audits/meta-tags/openapi-link.ts
|
|
7072
|
-
var OpenApiLinkAudit = class extends Audit {
|
|
7073
|
-
static meta = {
|
|
7074
|
-
id: "4.18",
|
|
7075
|
-
category: "meta-tags",
|
|
7076
|
-
title: "OpenAPI spec link in head",
|
|
7077
|
-
failureTitle: "OpenAPI spec link in head",
|
|
7078
|
-
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.",
|
|
7079
|
-
scoreDisplayMode: "binary",
|
|
7080
|
-
weight: 1,
|
|
7081
|
-
defaultPriority: "low",
|
|
7082
|
-
guidance: {
|
|
7083
|
-
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.",
|
|
7084
|
-
fix: 'If your site has an API, create an OpenAPI specification and add a <link rel="alternate"> tag in <head> pointing to it.',
|
|
7085
|
-
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">',
|
|
7086
|
-
effort: "complex",
|
|
7087
|
-
docsUrl: "https://swagger.io/specification/",
|
|
7088
|
-
tags: ["meta-tags", "openapi", "api", "ai-discovery"]
|
|
7089
|
-
}
|
|
7090
|
-
};
|
|
7091
|
-
audit(ctx) {
|
|
7092
|
-
const page = ctx.pages[0];
|
|
7093
|
-
const link = page?.headLinks?.find(
|
|
7094
|
-
(l) => l.rel === "alternate" && l.type === "application/json" && (l.title ?? "").toLowerCase().includes("openapi")
|
|
7095
|
-
);
|
|
7096
|
-
if (link) {
|
|
7097
|
-
return this.pass(
|
|
7098
|
-
`OpenAPI spec link found: "${link.href}".`,
|
|
7099
|
-
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7100
|
-
`href="${link.href}" title="${link.title}"`,
|
|
7101
|
-
page.url
|
|
7102
|
-
);
|
|
7103
|
-
}
|
|
7104
|
-
return this.fail(
|
|
7105
|
-
"No OpenAPI spec link found in <head>.",
|
|
7106
|
-
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7107
|
-
"Not found",
|
|
7108
|
-
{
|
|
7109
|
-
priority: "low",
|
|
7110
|
-
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.",
|
|
7111
|
-
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">'
|
|
6718
|
+
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.",
|
|
6719
|
+
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">'
|
|
7112
6720
|
},
|
|
7113
6721
|
page?.url
|
|
7114
6722
|
);
|
|
@@ -7633,7 +7241,7 @@ var OpenApiOperationIdsAudit = class _OpenApiOperationIdsAudit extends Audit {
|
|
|
7633
7241
|
}
|
|
7634
7242
|
};
|
|
7635
7243
|
|
|
7636
|
-
// src/audits/agent-tools/openapi-
|
|
7244
|
+
// src/audits/agent-tools/openapi-servers.ts
|
|
7637
7245
|
function tryParseJson4(body) {
|
|
7638
7246
|
try {
|
|
7639
7247
|
return JSON.parse(body);
|
|
@@ -7652,90 +7260,6 @@ function getOpenApiSpec3(ctx) {
|
|
|
7652
7260
|
}
|
|
7653
7261
|
return void 0;
|
|
7654
7262
|
}
|
|
7655
|
-
var OpenApiAiInstructionsAudit = class _OpenApiAiInstructionsAudit extends Audit {
|
|
7656
|
-
static meta = {
|
|
7657
|
-
id: "5.4",
|
|
7658
|
-
category: "agent-tools",
|
|
7659
|
-
title: "x-ai-instructions in OpenAPI",
|
|
7660
|
-
failureTitle: "x-ai-instructions in OpenAPI",
|
|
7661
|
-
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.",
|
|
7662
|
-
scoreDisplayMode: "binary",
|
|
7663
|
-
weight: 1,
|
|
7664
|
-
defaultPriority: "medium",
|
|
7665
|
-
guidance: {
|
|
7666
|
-
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.",
|
|
7667
|
-
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.",
|
|
7668
|
-
code: `"info": {
|
|
7669
|
-
"title": "Your Site API",
|
|
7670
|
-
"version": "1.0.0",
|
|
7671
|
-
"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."
|
|
7672
|
-
}`,
|
|
7673
|
-
effort: "trivial",
|
|
7674
|
-
tags: ["openapi", "ai-instructions", "api"]
|
|
7675
|
-
}
|
|
7676
|
-
};
|
|
7677
|
-
audit(ctx) {
|
|
7678
|
-
const spec = getOpenApiSpec3(ctx);
|
|
7679
|
-
if (!spec) {
|
|
7680
|
-
return this.fail(
|
|
7681
|
-
"No parseable OpenAPI JSON spec found.",
|
|
7682
|
-
"info object has x-ai-instructions field",
|
|
7683
|
-
"No spec",
|
|
7684
|
-
{
|
|
7685
|
-
priority: "medium",
|
|
7686
|
-
description: _OpenApiAiInstructionsAudit.meta.description,
|
|
7687
|
-
code: `"info": {
|
|
7688
|
-
"title": "Your Site API",
|
|
7689
|
-
"version": "1.0.0",
|
|
7690
|
-
"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."
|
|
7691
|
-
}`
|
|
7692
|
-
}
|
|
7693
|
-
);
|
|
7694
|
-
}
|
|
7695
|
-
const info = spec["info"];
|
|
7696
|
-
if (isObject4(info) && typeof info["x-ai-instructions"] === "string" && info["x-ai-instructions"]) {
|
|
7697
|
-
return this.pass(
|
|
7698
|
-
"OpenAPI info object contains x-ai-instructions.",
|
|
7699
|
-
"info object has x-ai-instructions field",
|
|
7700
|
-
"x-ai-instructions present"
|
|
7701
|
-
);
|
|
7702
|
-
}
|
|
7703
|
-
return this.fail(
|
|
7704
|
-
"OpenAPI info object does not contain x-ai-instructions.",
|
|
7705
|
-
"info object has x-ai-instructions field",
|
|
7706
|
-
"x-ai-instructions missing",
|
|
7707
|
-
{
|
|
7708
|
-
priority: "medium",
|
|
7709
|
-
description: _OpenApiAiInstructionsAudit.meta.description,
|
|
7710
|
-
code: `"info": {
|
|
7711
|
-
"title": "Your Site API",
|
|
7712
|
-
"version": "1.0.0",
|
|
7713
|
-
"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."
|
|
7714
|
-
}`
|
|
7715
|
-
}
|
|
7716
|
-
);
|
|
7717
|
-
}
|
|
7718
|
-
};
|
|
7719
|
-
|
|
7720
|
-
// src/audits/agent-tools/openapi-servers.ts
|
|
7721
|
-
function tryParseJson5(body) {
|
|
7722
|
-
try {
|
|
7723
|
-
return JSON.parse(body);
|
|
7724
|
-
} catch {
|
|
7725
|
-
return void 0;
|
|
7726
|
-
}
|
|
7727
|
-
}
|
|
7728
|
-
function isObject5(val) {
|
|
7729
|
-
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
7730
|
-
}
|
|
7731
|
-
function getOpenApiSpec4(ctx) {
|
|
7732
|
-
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
7733
|
-
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
7734
|
-
const parsed = tryParseJson5(jsonResult.body);
|
|
7735
|
-
if (isObject5(parsed)) return parsed;
|
|
7736
|
-
}
|
|
7737
|
-
return void 0;
|
|
7738
|
-
}
|
|
7739
7263
|
var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
7740
7264
|
static meta = {
|
|
7741
7265
|
id: "5.5",
|
|
@@ -7761,7 +7285,7 @@ var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
|
7761
7285
|
}
|
|
7762
7286
|
};
|
|
7763
7287
|
async audit(ctx) {
|
|
7764
|
-
const spec =
|
|
7288
|
+
const spec = getOpenApiSpec3(ctx);
|
|
7765
7289
|
if (!spec) {
|
|
7766
7290
|
return this.fail(
|
|
7767
7291
|
"No parseable OpenAPI JSON spec found.",
|
|
@@ -7798,7 +7322,7 @@ var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
|
7798
7322
|
);
|
|
7799
7323
|
}
|
|
7800
7324
|
const firstWithUrl = servers.find(
|
|
7801
|
-
(s) =>
|
|
7325
|
+
(s) => isObject4(s) && typeof s["url"] === "string" && s["url"]
|
|
7802
7326
|
);
|
|
7803
7327
|
if (!firstWithUrl) {
|
|
7804
7328
|
return this.fail(
|
|
@@ -7863,34 +7387,34 @@ var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
|
7863
7387
|
};
|
|
7864
7388
|
|
|
7865
7389
|
// src/audits/agent-tools/openapi-schemas.ts
|
|
7866
|
-
function
|
|
7390
|
+
function tryParseJson5(body) {
|
|
7867
7391
|
try {
|
|
7868
7392
|
return JSON.parse(body);
|
|
7869
7393
|
} catch {
|
|
7870
7394
|
return void 0;
|
|
7871
7395
|
}
|
|
7872
7396
|
}
|
|
7873
|
-
function
|
|
7397
|
+
function isObject5(val) {
|
|
7874
7398
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
7875
7399
|
}
|
|
7876
7400
|
var HTTP_METHODS3 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
7877
|
-
function
|
|
7401
|
+
function getOpenApiSpec4(ctx) {
|
|
7878
7402
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
7879
7403
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
7880
|
-
const parsed =
|
|
7881
|
-
if (
|
|
7404
|
+
const parsed = tryParseJson5(jsonResult.body);
|
|
7405
|
+
if (isObject5(parsed)) return parsed;
|
|
7882
7406
|
}
|
|
7883
7407
|
return void 0;
|
|
7884
7408
|
}
|
|
7885
7409
|
function getOperations3(spec) {
|
|
7886
7410
|
const paths = spec["paths"];
|
|
7887
|
-
if (!
|
|
7411
|
+
if (!isObject5(paths)) return [];
|
|
7888
7412
|
const ops = [];
|
|
7889
7413
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
7890
|
-
if (!
|
|
7414
|
+
if (!isObject5(pathItem)) continue;
|
|
7891
7415
|
for (const method of HTTP_METHODS3) {
|
|
7892
7416
|
const op = pathItem[method];
|
|
7893
|
-
if (
|
|
7417
|
+
if (isObject5(op)) {
|
|
7894
7418
|
ops.push({ path, method, op });
|
|
7895
7419
|
}
|
|
7896
7420
|
}
|
|
@@ -7951,7 +7475,7 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
7951
7475
|
}
|
|
7952
7476
|
};
|
|
7953
7477
|
audit(ctx) {
|
|
7954
|
-
const spec =
|
|
7478
|
+
const spec = getOpenApiSpec4(ctx);
|
|
7955
7479
|
if (!spec) {
|
|
7956
7480
|
return this.fail(
|
|
7957
7481
|
"No parseable OpenAPI JSON spec found.",
|
|
@@ -8052,11 +7576,11 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
8052
7576
|
if (["post", "put", "patch"].includes(method)) {
|
|
8053
7577
|
writeMethods++;
|
|
8054
7578
|
const rb = op["requestBody"];
|
|
8055
|
-
if (
|
|
7579
|
+
if (isObject5(rb)) {
|
|
8056
7580
|
const content = rb["content"];
|
|
8057
|
-
if (
|
|
7581
|
+
if (isObject5(content)) {
|
|
8058
7582
|
for (const mediaType of Object.values(content)) {
|
|
8059
|
-
if (
|
|
7583
|
+
if (isObject5(mediaType) && mediaType["schema"]) {
|
|
8060
7584
|
withRequestSchema++;
|
|
8061
7585
|
break;
|
|
8062
7586
|
}
|
|
@@ -8065,13 +7589,13 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
8065
7589
|
}
|
|
8066
7590
|
}
|
|
8067
7591
|
const responses = op["responses"];
|
|
8068
|
-
if (
|
|
7592
|
+
if (isObject5(responses)) {
|
|
8069
7593
|
for (const resp of Object.values(responses)) {
|
|
8070
|
-
if (
|
|
7594
|
+
if (isObject5(resp)) {
|
|
8071
7595
|
const content = resp["content"];
|
|
8072
|
-
if (
|
|
7596
|
+
if (isObject5(content)) {
|
|
8073
7597
|
for (const mediaType of Object.values(content)) {
|
|
8074
|
-
if (
|
|
7598
|
+
if (isObject5(mediaType) && mediaType["schema"]) {
|
|
8075
7599
|
withResponseSchema++;
|
|
8076
7600
|
break;
|
|
8077
7601
|
}
|
|
@@ -8148,14 +7672,14 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
8148
7672
|
};
|
|
8149
7673
|
|
|
8150
7674
|
// src/audits/agent-tools/ai-catalog-exists.ts
|
|
8151
|
-
function
|
|
7675
|
+
function tryParseJson6(body) {
|
|
8152
7676
|
try {
|
|
8153
7677
|
return JSON.parse(body);
|
|
8154
7678
|
} catch {
|
|
8155
7679
|
return void 0;
|
|
8156
7680
|
}
|
|
8157
7681
|
}
|
|
8158
|
-
function
|
|
7682
|
+
function isObject6(val) {
|
|
8159
7683
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8160
7684
|
}
|
|
8161
7685
|
var AiCatalogExistsAudit = class _AiCatalogExistsAudit extends Audit {
|
|
@@ -8227,8 +7751,8 @@ var AiCatalogExistsAudit = class _AiCatalogExistsAudit extends Audit {
|
|
|
8227
7751
|
}
|
|
8228
7752
|
);
|
|
8229
7753
|
}
|
|
8230
|
-
const parsed =
|
|
8231
|
-
if (!
|
|
7754
|
+
const parsed = tryParseJson6(result.body);
|
|
7755
|
+
if (!isObject6(parsed)) {
|
|
8232
7756
|
return this.fail(
|
|
8233
7757
|
"ai-catalog.json is not valid JSON.",
|
|
8234
7758
|
"/.well-known/ai-catalog.json returns 200 with valid JSON containing services array",
|
|
@@ -8307,14 +7831,14 @@ var AiCatalogExistsAudit = class _AiCatalogExistsAudit extends Audit {
|
|
|
8307
7831
|
};
|
|
8308
7832
|
|
|
8309
7833
|
// src/audits/agent-tools/ai-catalog-metadata.ts
|
|
8310
|
-
function
|
|
7834
|
+
function tryParseJson7(body) {
|
|
8311
7835
|
try {
|
|
8312
7836
|
return JSON.parse(body);
|
|
8313
7837
|
} catch {
|
|
8314
7838
|
return void 0;
|
|
8315
7839
|
}
|
|
8316
7840
|
}
|
|
8317
|
-
function
|
|
7841
|
+
function isObject7(val) {
|
|
8318
7842
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8319
7843
|
}
|
|
8320
7844
|
var AiCatalogMetadataAudit = class _AiCatalogMetadataAudit extends Audit {
|
|
@@ -8367,8 +7891,8 @@ var AiCatalogMetadataAudit = class _AiCatalogMetadataAudit extends Audit {
|
|
|
8367
7891
|
}
|
|
8368
7892
|
);
|
|
8369
7893
|
}
|
|
8370
|
-
const parsed =
|
|
8371
|
-
if (!
|
|
7894
|
+
const parsed = tryParseJson7(result.body);
|
|
7895
|
+
if (!isObject7(parsed)) {
|
|
8372
7896
|
return this.fail(
|
|
8373
7897
|
"ai-catalog.json is not valid JSON.",
|
|
8374
7898
|
"Has version, name, description, capabilities, owner, contact, lastUpdated",
|
|
@@ -8441,14 +7965,14 @@ var AiCatalogMetadataAudit = class _AiCatalogMetadataAudit extends Audit {
|
|
|
8441
7965
|
};
|
|
8442
7966
|
|
|
8443
7967
|
// src/audits/agent-tools/ai-catalog-urls.ts
|
|
8444
|
-
function
|
|
7968
|
+
function tryParseJson8(body) {
|
|
8445
7969
|
try {
|
|
8446
7970
|
return JSON.parse(body);
|
|
8447
7971
|
} catch {
|
|
8448
7972
|
return void 0;
|
|
8449
7973
|
}
|
|
8450
7974
|
}
|
|
8451
|
-
function
|
|
7975
|
+
function isObject8(val) {
|
|
8452
7976
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8453
7977
|
}
|
|
8454
7978
|
var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
@@ -8497,8 +8021,8 @@ var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
|
8497
8021
|
}
|
|
8498
8022
|
);
|
|
8499
8023
|
}
|
|
8500
|
-
const parsed =
|
|
8501
|
-
if (!
|
|
8024
|
+
const parsed = tryParseJson8(result.body);
|
|
8025
|
+
if (!isObject8(parsed) || !Array.isArray(parsed["services"])) {
|
|
8502
8026
|
return this.fail(
|
|
8503
8027
|
"ai-catalog.json has no services array.",
|
|
8504
8028
|
"Each service URL returns HTTP 200",
|
|
@@ -8520,7 +8044,7 @@ var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
|
8520
8044
|
const services = parsed["services"];
|
|
8521
8045
|
const urls = [];
|
|
8522
8046
|
for (const svc of services) {
|
|
8523
|
-
if (
|
|
8047
|
+
if (isObject8(svc) && typeof svc["url"] === "string" && svc["url"]) {
|
|
8524
8048
|
urls.push(svc["url"]);
|
|
8525
8049
|
}
|
|
8526
8050
|
}
|
|
@@ -8592,14 +8116,14 @@ var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
|
8592
8116
|
};
|
|
8593
8117
|
|
|
8594
8118
|
// src/audits/agent-tools/agents-json.ts
|
|
8595
|
-
function
|
|
8119
|
+
function tryParseJson9(body) {
|
|
8596
8120
|
try {
|
|
8597
8121
|
return JSON.parse(body);
|
|
8598
8122
|
} catch {
|
|
8599
8123
|
return void 0;
|
|
8600
8124
|
}
|
|
8601
8125
|
}
|
|
8602
|
-
function
|
|
8126
|
+
function isObject9(val) {
|
|
8603
8127
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8604
8128
|
}
|
|
8605
8129
|
var AgentsJsonAudit = class _AgentsJsonAudit extends Audit {
|
|
@@ -8667,8 +8191,8 @@ var AgentsJsonAudit = class _AgentsJsonAudit extends Audit {
|
|
|
8667
8191
|
}
|
|
8668
8192
|
);
|
|
8669
8193
|
}
|
|
8670
|
-
const parsed =
|
|
8671
|
-
if (!
|
|
8194
|
+
const parsed = tryParseJson9(result.body);
|
|
8195
|
+
if (!isObject9(parsed) && !Array.isArray(parsed)) {
|
|
8672
8196
|
return this.fail(
|
|
8673
8197
|
"agents.json is not valid JSON.",
|
|
8674
8198
|
"/.well-known/agents.json returns 200 with valid JSON",
|
|
@@ -8706,160 +8230,15 @@ var AgentsJsonAudit = class _AgentsJsonAudit extends Audit {
|
|
|
8706
8230
|
}
|
|
8707
8231
|
};
|
|
8708
8232
|
|
|
8709
|
-
// src/audits/agent-tools/ai-plugin-json.ts
|
|
8710
|
-
function tryParseJson11(body) {
|
|
8711
|
-
try {
|
|
8712
|
-
return JSON.parse(body);
|
|
8713
|
-
} catch {
|
|
8714
|
-
return void 0;
|
|
8715
|
-
}
|
|
8716
|
-
}
|
|
8717
|
-
function isObject11(val) {
|
|
8718
|
-
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8719
|
-
}
|
|
8720
|
-
var AiPluginJsonAudit = class _AiPluginJsonAudit extends Audit {
|
|
8721
|
-
static meta = {
|
|
8722
|
-
id: "5.11",
|
|
8723
|
-
category: "agent-tools",
|
|
8724
|
-
title: "ai-plugin.json exists",
|
|
8725
|
-
failureTitle: "ai-plugin.json exists",
|
|
8726
|
-
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.",
|
|
8727
|
-
scoreDisplayMode: "ternary",
|
|
8728
|
-
weight: 1,
|
|
8729
|
-
defaultPriority: "medium",
|
|
8730
|
-
guidance: {
|
|
8731
|
-
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.",
|
|
8732
|
-
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.",
|
|
8733
|
-
code: `// /.well-known/ai-plugin.json
|
|
8734
|
-
{
|
|
8735
|
-
"schema_version": "v1",
|
|
8736
|
-
"name_for_human": "Your Site Name",
|
|
8737
|
-
"name_for_model": "your_site",
|
|
8738
|
-
"description_for_human": "What your site does for users.",
|
|
8739
|
-
"description_for_model": "Use this plugin to search content and submit inquiries.",
|
|
8740
|
-
"auth": { "type": "none" },
|
|
8741
|
-
"api": {
|
|
8742
|
-
"type": "openapi",
|
|
8743
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8744
|
-
},
|
|
8745
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8746
|
-
"contact_email": "hello@yoursite.com"
|
|
8747
|
-
}`,
|
|
8748
|
-
effort: "easy",
|
|
8749
|
-
docsUrl: "https://platform.openai.com/docs/plugins/getting-started/plugin-manifest",
|
|
8750
|
-
tags: ["ai-plugin", "chatgpt", "discovery", "agent-protocol"]
|
|
8751
|
-
}
|
|
8752
|
-
};
|
|
8753
|
-
audit(ctx) {
|
|
8754
|
-
const result = ctx.rootFiles["/.well-known/ai-plugin.json"];
|
|
8755
|
-
if (!result || result.status !== 200 || !result.body) {
|
|
8756
|
-
return this.fail(
|
|
8757
|
-
"/.well-known/ai-plugin.json not found or not accessible.",
|
|
8758
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8759
|
-
result ? `HTTP ${result.status}` : "Not fetched",
|
|
8760
|
-
{
|
|
8761
|
-
priority: "medium",
|
|
8762
|
-
description: _AiPluginJsonAudit.meta.description,
|
|
8763
|
-
code: `// /.well-known/ai-plugin.json
|
|
8764
|
-
{
|
|
8765
|
-
"schema_version": "v1",
|
|
8766
|
-
"name_for_human": "Your Site Name",
|
|
8767
|
-
"name_for_model": "your_site",
|
|
8768
|
-
"description_for_human": "What your site does for users.",
|
|
8769
|
-
"description_for_model": "Use this plugin to search content, submit inquiries, and get product details from Your Site.",
|
|
8770
|
-
"auth": { "type": "none" },
|
|
8771
|
-
"api": {
|
|
8772
|
-
"type": "openapi",
|
|
8773
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8774
|
-
},
|
|
8775
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8776
|
-
"contact_email": "hello@yoursite.com"
|
|
8777
|
-
}`
|
|
8778
|
-
}
|
|
8779
|
-
);
|
|
8780
|
-
}
|
|
8781
|
-
const parsed = tryParseJson11(result.body);
|
|
8782
|
-
if (!isObject11(parsed)) {
|
|
8783
|
-
return this.fail(
|
|
8784
|
-
"ai-plugin.json is not valid JSON.",
|
|
8785
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8786
|
-
"Invalid JSON",
|
|
8787
|
-
{
|
|
8788
|
-
priority: "medium",
|
|
8789
|
-
description: _AiPluginJsonAudit.meta.description,
|
|
8790
|
-
code: `// /.well-known/ai-plugin.json
|
|
8791
|
-
{
|
|
8792
|
-
"schema_version": "v1",
|
|
8793
|
-
"name_for_human": "Your Site Name",
|
|
8794
|
-
"name_for_model": "your_site",
|
|
8795
|
-
"description_for_human": "What your site does for users.",
|
|
8796
|
-
"description_for_model": "Use this plugin to search content, submit inquiries, and get product details from Your Site.",
|
|
8797
|
-
"auth": { "type": "none" },
|
|
8798
|
-
"api": {
|
|
8799
|
-
"type": "openapi",
|
|
8800
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8801
|
-
},
|
|
8802
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8803
|
-
"contact_email": "hello@yoursite.com"
|
|
8804
|
-
}`
|
|
8805
|
-
}
|
|
8806
|
-
);
|
|
8807
|
-
}
|
|
8808
|
-
const requiredFields = ["schema_version", "name_for_human", "name_for_model"];
|
|
8809
|
-
const missing = requiredFields.filter((f) => typeof parsed[f] !== "string" || !parsed[f]);
|
|
8810
|
-
if (missing.length === 0) {
|
|
8811
|
-
return this.pass(
|
|
8812
|
-
"ai-plugin.json found with all required fields.",
|
|
8813
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8814
|
-
// requiredFields guarantees these three are non-empty strings here.
|
|
8815
|
-
`schema_version=${parsed["schema_version"]}, name_for_human=${parsed["name_for_human"]}, name_for_model=${parsed["name_for_model"]}`
|
|
8816
|
-
);
|
|
8817
|
-
}
|
|
8818
|
-
const recommendation = {
|
|
8819
|
-
priority: "medium",
|
|
8820
|
-
description: _AiPluginJsonAudit.meta.description,
|
|
8821
|
-
code: `// /.well-known/ai-plugin.json
|
|
8822
|
-
{
|
|
8823
|
-
"schema_version": "v1",
|
|
8824
|
-
"name_for_human": "Your Site Name",
|
|
8825
|
-
"name_for_model": "your_site",
|
|
8826
|
-
"description_for_human": "What your site does for users.",
|
|
8827
|
-
"description_for_model": "Use this plugin to search content, submit inquiries, and get product details from Your Site.",
|
|
8828
|
-
"auth": { "type": "none" },
|
|
8829
|
-
"api": {
|
|
8830
|
-
"type": "openapi",
|
|
8831
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8832
|
-
},
|
|
8833
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8834
|
-
"contact_email": "hello@yoursite.com"
|
|
8835
|
-
}`
|
|
8836
|
-
};
|
|
8837
|
-
if (missing.length < requiredFields.length) {
|
|
8838
|
-
return this.warn(
|
|
8839
|
-
`ai-plugin.json is missing fields: ${missing.join(", ")}.`,
|
|
8840
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8841
|
-
`Missing: ${missing.join(", ")}`,
|
|
8842
|
-
recommendation
|
|
8843
|
-
);
|
|
8844
|
-
}
|
|
8845
|
-
return this.fail(
|
|
8846
|
-
`ai-plugin.json is missing all required fields: ${missing.join(", ")}.`,
|
|
8847
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8848
|
-
`Missing: ${missing.join(", ")}`,
|
|
8849
|
-
recommendation
|
|
8850
|
-
);
|
|
8851
|
-
}
|
|
8852
|
-
};
|
|
8853
|
-
|
|
8854
8233
|
// src/audits/agent-tools/mcp-discovery.ts
|
|
8855
|
-
function
|
|
8234
|
+
function tryParseJson10(body) {
|
|
8856
8235
|
try {
|
|
8857
8236
|
return JSON.parse(body);
|
|
8858
8237
|
} catch {
|
|
8859
8238
|
return void 0;
|
|
8860
8239
|
}
|
|
8861
8240
|
}
|
|
8862
|
-
function
|
|
8241
|
+
function isObject10(val) {
|
|
8863
8242
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8864
8243
|
}
|
|
8865
8244
|
var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
@@ -8898,8 +8277,8 @@ var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
|
8898
8277
|
audit(ctx) {
|
|
8899
8278
|
const result = ctx.rootFiles["/.well-known/mcp/servers.json"];
|
|
8900
8279
|
if (result && result.status === 200 && result.body) {
|
|
8901
|
-
const parsed =
|
|
8902
|
-
if (!
|
|
8280
|
+
const parsed = tryParseJson10(result.body);
|
|
8281
|
+
if (!isObject10(parsed)) {
|
|
8903
8282
|
return this.fail(
|
|
8904
8283
|
"mcp/servers.json is not valid JSON.",
|
|
8905
8284
|
"/.well-known/mcp/servers.json returns 200 with valid JSON containing servers array",
|
|
@@ -8932,8 +8311,8 @@ var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
|
8932
8311
|
}
|
|
8933
8312
|
const ucpResult = ctx.rootFiles["/.well-known/ucp"];
|
|
8934
8313
|
if (ucpResult && ucpResult.status === 200 && ucpResult.body) {
|
|
8935
|
-
const ucpParsed =
|
|
8936
|
-
if (
|
|
8314
|
+
const ucpParsed = tryParseJson10(ucpResult.body);
|
|
8315
|
+
if (isObject10(ucpParsed)) {
|
|
8937
8316
|
const ucpObj = ucpParsed["ucp"] ?? ucpParsed;
|
|
8938
8317
|
const services = ucpParsed["services"] || ucpObj["services"];
|
|
8939
8318
|
const capabilities = ucpParsed["capabilities"] || ucpObj["capabilities"];
|
|
@@ -8960,14 +8339,14 @@ var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
|
8960
8339
|
};
|
|
8961
8340
|
|
|
8962
8341
|
// src/audits/agent-tools/mcp-endpoint.ts
|
|
8963
|
-
function
|
|
8342
|
+
function tryParseJson11(body) {
|
|
8964
8343
|
try {
|
|
8965
8344
|
return JSON.parse(body);
|
|
8966
8345
|
} catch {
|
|
8967
8346
|
return void 0;
|
|
8968
8347
|
}
|
|
8969
8348
|
}
|
|
8970
|
-
function
|
|
8349
|
+
function isObject11(val) {
|
|
8971
8350
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8972
8351
|
}
|
|
8973
8352
|
var McpEndpointAudit = class _McpEndpointAudit extends Audit {
|
|
@@ -9017,8 +8396,8 @@ Content-Type: application/json
|
|
|
9017
8396
|
let targetEndpointUrl;
|
|
9018
8397
|
const result = ctx.rootFiles["/.well-known/mcp/servers.json"];
|
|
9019
8398
|
if (result && result.status === 200 && result.body) {
|
|
9020
|
-
const parsed =
|
|
9021
|
-
if (!
|
|
8399
|
+
const parsed = tryParseJson11(result.body);
|
|
8400
|
+
if (!isObject11(parsed) || !Array.isArray(parsed["servers"])) {
|
|
9022
8401
|
return this.fail(
|
|
9023
8402
|
"servers.json has no servers array.",
|
|
9024
8403
|
"MCP server URL responds to JSON-RPC initialize request",
|
|
@@ -9031,8 +8410,8 @@ Content-Type: application/json
|
|
|
9031
8410
|
);
|
|
9032
8411
|
}
|
|
9033
8412
|
const servers = parsed["servers"];
|
|
9034
|
-
const serverUrl = servers.find((s) =>
|
|
9035
|
-
if (!serverUrl || !
|
|
8413
|
+
const serverUrl = servers.find((s) => isObject11(s) && typeof s["url"] === "string" && s["url"]);
|
|
8414
|
+
if (!serverUrl || !isObject11(serverUrl)) {
|
|
9036
8415
|
return this.fail(
|
|
9037
8416
|
"No server URL found in servers.json.",
|
|
9038
8417
|
"MCP server URL responds to JSON-RPC initialize request",
|
|
@@ -9049,8 +8428,8 @@ Content-Type: application/json
|
|
|
9049
8428
|
if (!targetEndpointUrl) {
|
|
9050
8429
|
const ucpResult = ctx.rootFiles["/.well-known/ucp"];
|
|
9051
8430
|
if (ucpResult && ucpResult.status === 200 && ucpResult.body) {
|
|
9052
|
-
const ucpParsed =
|
|
9053
|
-
if (
|
|
8431
|
+
const ucpParsed = tryParseJson11(ucpResult.body);
|
|
8432
|
+
if (isObject11(ucpParsed)) {
|
|
9054
8433
|
const ucpObj = ucpParsed["ucp"] ?? ucpParsed;
|
|
9055
8434
|
const services = ucpParsed["services"] || ucpObj["services"];
|
|
9056
8435
|
if (services) {
|
|
@@ -9058,7 +8437,7 @@ Content-Type: application/json
|
|
|
9058
8437
|
const svcList = services[key];
|
|
9059
8438
|
if (Array.isArray(svcList)) {
|
|
9060
8439
|
for (const svc of svcList) {
|
|
9061
|
-
if (
|
|
8440
|
+
if (isObject11(svc) && svc["transport"] === "mcp" && typeof svc["endpoint"] === "string") {
|
|
9062
8441
|
targetEndpointUrl = svc["endpoint"];
|
|
9063
8442
|
break;
|
|
9064
8443
|
}
|
|
@@ -9101,8 +8480,8 @@ Content-Type: application/json
|
|
|
9101
8480
|
contentType: "application/json"
|
|
9102
8481
|
});
|
|
9103
8482
|
if (response.status === 200) {
|
|
9104
|
-
const respBody =
|
|
9105
|
-
if (
|
|
8483
|
+
const respBody = tryParseJson11(response.body);
|
|
8484
|
+
if (isObject11(respBody) && respBody["jsonrpc"] === "2.0" && !("error" in respBody) && isObject11(respBody["result"]) && typeof respBody["result"]["protocolVersion"] === "string") {
|
|
9106
8485
|
return this.pass(
|
|
9107
8486
|
`MCP endpoint at ${url} responded with valid JSON-RPC initialize result.`,
|
|
9108
8487
|
"MCP server URL responds to JSON-RPC initialize request",
|
|
@@ -9218,14 +8597,14 @@ Content-Type: application/json
|
|
|
9218
8597
|
};
|
|
9219
8598
|
|
|
9220
8599
|
// src/audits/agent-tools/mcp-capabilities.ts
|
|
9221
|
-
function
|
|
8600
|
+
function tryParseJson12(body) {
|
|
9222
8601
|
try {
|
|
9223
8602
|
return JSON.parse(body);
|
|
9224
8603
|
} catch {
|
|
9225
8604
|
return void 0;
|
|
9226
8605
|
}
|
|
9227
8606
|
}
|
|
9228
|
-
function
|
|
8607
|
+
function isObject12(val) {
|
|
9229
8608
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9230
8609
|
}
|
|
9231
8610
|
var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
@@ -9263,8 +8642,8 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9263
8642
|
const result = ctx.rootFiles["/.well-known/mcp/servers.json"];
|
|
9264
8643
|
const ucpResult = ctx.rootFiles["/.well-known/ucp"];
|
|
9265
8644
|
if (result && result.status === 200 && result.body) {
|
|
9266
|
-
const parsed =
|
|
9267
|
-
if (!
|
|
8645
|
+
const parsed = tryParseJson12(result.body);
|
|
8646
|
+
if (!isObject12(parsed) || !Array.isArray(parsed["servers"])) {
|
|
9268
8647
|
return this.fail(
|
|
9269
8648
|
"servers.json has no servers array.",
|
|
9270
8649
|
"servers.json or MCP response declares tools, resources, or prompts",
|
|
@@ -9280,14 +8659,14 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9280
8659
|
const capabilityKeys = ["tools", "resources", "prompts"];
|
|
9281
8660
|
const foundCapabilities = [];
|
|
9282
8661
|
for (const server of servers) {
|
|
9283
|
-
if (!
|
|
8662
|
+
if (!isObject12(server)) continue;
|
|
9284
8663
|
for (const key of capabilityKeys) {
|
|
9285
8664
|
if (server[key] !== void 0 && server[key] !== false) {
|
|
9286
8665
|
foundCapabilities.push(key);
|
|
9287
8666
|
}
|
|
9288
8667
|
}
|
|
9289
8668
|
const caps = server["capabilities"];
|
|
9290
|
-
if (
|
|
8669
|
+
if (isObject12(caps)) {
|
|
9291
8670
|
for (const key of capabilityKeys) {
|
|
9292
8671
|
if (caps[key] !== void 0 && caps[key] !== false && !foundCapabilities.includes(key)) {
|
|
9293
8672
|
foundCapabilities.push(key);
|
|
@@ -9315,11 +8694,11 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9315
8694
|
);
|
|
9316
8695
|
}
|
|
9317
8696
|
if (ucpResult && ucpResult.status === 200 && ucpResult.body) {
|
|
9318
|
-
const ucpParsed =
|
|
9319
|
-
if (
|
|
8697
|
+
const ucpParsed = tryParseJson12(ucpResult.body);
|
|
8698
|
+
if (isObject12(ucpParsed)) {
|
|
9320
8699
|
const ucpObj = ucpParsed["ucp"] ?? ucpParsed;
|
|
9321
8700
|
const capabilities = ucpParsed["capabilities"] || ucpObj["capabilities"];
|
|
9322
|
-
if (capabilities &&
|
|
8701
|
+
if (capabilities && isObject12(capabilities)) {
|
|
9323
8702
|
const capNames = Object.keys(capabilities).map((cap) => cap.split(".").pop() || cap);
|
|
9324
8703
|
if (capNames.length > 0) {
|
|
9325
8704
|
const unique = [...new Set(capNames)];
|
|
@@ -9346,34 +8725,34 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9346
8725
|
};
|
|
9347
8726
|
|
|
9348
8727
|
// src/audits/agent-tools/contact-form.ts
|
|
9349
|
-
function
|
|
8728
|
+
function tryParseJson13(body) {
|
|
9350
8729
|
try {
|
|
9351
8730
|
return JSON.parse(body);
|
|
9352
8731
|
} catch {
|
|
9353
8732
|
return void 0;
|
|
9354
8733
|
}
|
|
9355
8734
|
}
|
|
9356
|
-
function
|
|
8735
|
+
function isObject13(val) {
|
|
9357
8736
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9358
8737
|
}
|
|
9359
8738
|
var HTTP_METHODS4 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
9360
|
-
function
|
|
8739
|
+
function getOpenApiSpec5(ctx) {
|
|
9361
8740
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
9362
8741
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
9363
|
-
const parsed =
|
|
9364
|
-
if (
|
|
8742
|
+
const parsed = tryParseJson13(jsonResult.body);
|
|
8743
|
+
if (isObject13(parsed)) return parsed;
|
|
9365
8744
|
}
|
|
9366
8745
|
return void 0;
|
|
9367
8746
|
}
|
|
9368
8747
|
function getOperations4(spec) {
|
|
9369
8748
|
const paths = spec["paths"];
|
|
9370
|
-
if (!
|
|
8749
|
+
if (!isObject13(paths)) return [];
|
|
9371
8750
|
const ops = [];
|
|
9372
8751
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
9373
|
-
if (!
|
|
8752
|
+
if (!isObject13(pathItem)) continue;
|
|
9374
8753
|
for (const method of HTTP_METHODS4) {
|
|
9375
8754
|
const op = pathItem[method];
|
|
9376
|
-
if (
|
|
8755
|
+
if (isObject13(op)) {
|
|
9377
8756
|
ops.push({ path, method, op });
|
|
9378
8757
|
}
|
|
9379
8758
|
}
|
|
@@ -9442,7 +8821,7 @@ var ContactFormAudit = class _ContactFormAudit extends Audit {
|
|
|
9442
8821
|
}
|
|
9443
8822
|
}
|
|
9444
8823
|
}
|
|
9445
|
-
const spec =
|
|
8824
|
+
const spec = getOpenApiSpec5(ctx);
|
|
9446
8825
|
if (spec) {
|
|
9447
8826
|
const ops = getOperations4(spec);
|
|
9448
8827
|
for (const { path, method } of ops) {
|
|
@@ -9489,34 +8868,34 @@ var ContactFormAudit = class _ContactFormAudit extends Audit {
|
|
|
9489
8868
|
};
|
|
9490
8869
|
|
|
9491
8870
|
// src/audits/agent-tools/search-endpoint.ts
|
|
9492
|
-
function
|
|
8871
|
+
function tryParseJson14(body) {
|
|
9493
8872
|
try {
|
|
9494
8873
|
return JSON.parse(body);
|
|
9495
8874
|
} catch {
|
|
9496
8875
|
return void 0;
|
|
9497
8876
|
}
|
|
9498
8877
|
}
|
|
9499
|
-
function
|
|
8878
|
+
function isObject14(val) {
|
|
9500
8879
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9501
8880
|
}
|
|
9502
8881
|
var HTTP_METHODS5 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
9503
|
-
function
|
|
8882
|
+
function getOpenApiSpec6(ctx) {
|
|
9504
8883
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
9505
8884
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
9506
|
-
const parsed =
|
|
9507
|
-
if (
|
|
8885
|
+
const parsed = tryParseJson14(jsonResult.body);
|
|
8886
|
+
if (isObject14(parsed)) return parsed;
|
|
9508
8887
|
}
|
|
9509
8888
|
return void 0;
|
|
9510
8889
|
}
|
|
9511
8890
|
function getOperations5(spec) {
|
|
9512
8891
|
const paths = spec["paths"];
|
|
9513
|
-
if (!
|
|
8892
|
+
if (!isObject14(paths)) return [];
|
|
9514
8893
|
const ops = [];
|
|
9515
8894
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
9516
|
-
if (!
|
|
8895
|
+
if (!isObject14(pathItem)) continue;
|
|
9517
8896
|
for (const method of HTTP_METHODS5) {
|
|
9518
8897
|
const op = pathItem[method];
|
|
9519
|
-
if (
|
|
8898
|
+
if (isObject14(op)) {
|
|
9520
8899
|
ops.push({ path, method, op });
|
|
9521
8900
|
}
|
|
9522
8901
|
}
|
|
@@ -9524,10 +8903,10 @@ function getOperations5(spec) {
|
|
|
9524
8903
|
return ops;
|
|
9525
8904
|
}
|
|
9526
8905
|
function findSearchActionUrl(obj) {
|
|
9527
|
-
if (!
|
|
9528
|
-
if ((obj["@type"] === "SearchAction" || obj["@type"] === "WebSite") &&
|
|
8906
|
+
if (!isObject14(obj)) return void 0;
|
|
8907
|
+
if ((obj["@type"] === "SearchAction" || obj["@type"] === "WebSite") && isObject14(obj["potentialAction"])) {
|
|
9529
8908
|
const action = obj["potentialAction"];
|
|
9530
|
-
if (action["@type"] === "SearchAction" &&
|
|
8909
|
+
if (action["@type"] === "SearchAction" && isObject14(action["target"])) {
|
|
9531
8910
|
const target = action["target"];
|
|
9532
8911
|
if (typeof target["urlTemplate"] === "string") return target["urlTemplate"];
|
|
9533
8912
|
}
|
|
@@ -9536,7 +8915,7 @@ function findSearchActionUrl(obj) {
|
|
|
9536
8915
|
}
|
|
9537
8916
|
}
|
|
9538
8917
|
if (obj["@type"] === "SearchAction") {
|
|
9539
|
-
if (
|
|
8918
|
+
if (isObject14(obj["target"])) {
|
|
9540
8919
|
const target = obj["target"];
|
|
9541
8920
|
if (typeof target["urlTemplate"] === "string") return target["urlTemplate"];
|
|
9542
8921
|
}
|
|
@@ -9657,7 +9036,7 @@ var SearchEndpointAudit = class _SearchEndpointAudit extends Audit {
|
|
|
9657
9036
|
}
|
|
9658
9037
|
}
|
|
9659
9038
|
}
|
|
9660
|
-
const spec =
|
|
9039
|
+
const spec = getOpenApiSpec6(ctx);
|
|
9661
9040
|
if (spec) {
|
|
9662
9041
|
const ops = getOperations5(spec);
|
|
9663
9042
|
for (const { path, method } of ops) {
|
|
@@ -9698,96 +9077,6 @@ var SearchEndpointAudit = class _SearchEndpointAudit extends Audit {
|
|
|
9698
9077
|
}
|
|
9699
9078
|
};
|
|
9700
9079
|
|
|
9701
|
-
// src/audits/agent-tools/data-action-ctas.ts
|
|
9702
|
-
var DataActionCtasAudit = class _DataActionCtasAudit extends Audit {
|
|
9703
|
-
static meta = {
|
|
9704
|
-
id: "5.17",
|
|
9705
|
-
category: "agent-tools",
|
|
9706
|
-
title: "data-action attributes on CTAs",
|
|
9707
|
-
failureTitle: "data-action attributes on CTAs",
|
|
9708
|
-
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.",
|
|
9709
|
-
scoreDisplayMode: "ternary",
|
|
9710
|
-
weight: 1,
|
|
9711
|
-
defaultPriority: "low",
|
|
9712
|
-
guidance: {
|
|
9713
|
-
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.',
|
|
9714
|
-
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").',
|
|
9715
|
-
code: `<button data-action="book-demo" data-action-type="conversion"
|
|
9716
|
-
data-action-label="Book a Demo">
|
|
9717
|
-
Book a Demo
|
|
9718
|
-
</button>
|
|
9719
|
-
|
|
9720
|
-
<a href="/pricing" data-action="view-pricing" data-action-type="navigation"
|
|
9721
|
-
data-action-label="See Pricing">
|
|
9722
|
-
See Pricing
|
|
9723
|
-
</a>`,
|
|
9724
|
-
effort: "easy",
|
|
9725
|
-
tags: ["html", "cta", "browser-agent", "accessibility"]
|
|
9726
|
-
}
|
|
9727
|
-
};
|
|
9728
|
-
audit(ctx) {
|
|
9729
|
-
let totalDataAction = 0;
|
|
9730
|
-
let totalDataActionType = 0;
|
|
9731
|
-
let foundPage = "";
|
|
9732
|
-
for (const page of ctx.pages) {
|
|
9733
|
-
const withDataAction = page.$("[data-action]");
|
|
9734
|
-
const withDataActionType = page.$("[data-action-type]");
|
|
9735
|
-
if (withDataAction.length > 0 || withDataActionType.length > 0) {
|
|
9736
|
-
totalDataAction += withDataAction.length;
|
|
9737
|
-
totalDataActionType += withDataActionType.length;
|
|
9738
|
-
if (!foundPage) foundPage = page.url;
|
|
9739
|
-
}
|
|
9740
|
-
}
|
|
9741
|
-
if (totalDataAction > 0 && totalDataActionType > 0) {
|
|
9742
|
-
return this.pass(
|
|
9743
|
-
`Found ${totalDataAction} element(s) with data-action and ${totalDataActionType} with data-action-type.`,
|
|
9744
|
-
"Elements with data-action and data-action-type attributes",
|
|
9745
|
-
`${totalDataAction} data-action, ${totalDataActionType} data-action-type`,
|
|
9746
|
-
foundPage
|
|
9747
|
-
);
|
|
9748
|
-
}
|
|
9749
|
-
if (totalDataAction > 0 || totalDataActionType > 0) {
|
|
9750
|
-
return this.warn(
|
|
9751
|
-
`Partial data-action markup: ${totalDataAction} data-action, ${totalDataActionType} data-action-type.`,
|
|
9752
|
-
"Elements with data-action and data-action-type attributes",
|
|
9753
|
-
`${totalDataAction} data-action, ${totalDataActionType} data-action-type`,
|
|
9754
|
-
{
|
|
9755
|
-
priority: "low",
|
|
9756
|
-
description: _DataActionCtasAudit.meta.description,
|
|
9757
|
-
code: `<button data-action="book-demo" data-action-type="conversion"
|
|
9758
|
-
data-action-label="Book a Demo">
|
|
9759
|
-
Book a Demo
|
|
9760
|
-
</button>
|
|
9761
|
-
|
|
9762
|
-
<a href="/pricing" data-action="view-pricing" data-action-type="navigation"
|
|
9763
|
-
data-action-label="See Pricing">
|
|
9764
|
-
See Pricing
|
|
9765
|
-
</a>`
|
|
9766
|
-
},
|
|
9767
|
-
foundPage
|
|
9768
|
-
);
|
|
9769
|
-
}
|
|
9770
|
-
return this.fail(
|
|
9771
|
-
"No elements with data-action or data-action-type attributes found.",
|
|
9772
|
-
"Elements with data-action and data-action-type attributes",
|
|
9773
|
-
"None",
|
|
9774
|
-
{
|
|
9775
|
-
priority: "low",
|
|
9776
|
-
description: _DataActionCtasAudit.meta.description,
|
|
9777
|
-
code: `<button data-action="book-demo" data-action-type="conversion"
|
|
9778
|
-
data-action-label="Book a Demo">
|
|
9779
|
-
Book a Demo
|
|
9780
|
-
</button>
|
|
9781
|
-
|
|
9782
|
-
<a href="/pricing" data-action="view-pricing" data-action-type="navigation"
|
|
9783
|
-
data-action-label="See Pricing">
|
|
9784
|
-
See Pricing
|
|
9785
|
-
</a>`
|
|
9786
|
-
}
|
|
9787
|
-
);
|
|
9788
|
-
}
|
|
9789
|
-
};
|
|
9790
|
-
|
|
9791
9080
|
// src/audits/agent-tools/no-blocking-captcha.ts
|
|
9792
9081
|
var CAPTCHA_PATTERNS = [
|
|
9793
9082
|
"recaptcha",
|
|
@@ -9958,14 +9247,14 @@ var FormsNoJsAudit = class _FormsNoJsAudit extends Audit {
|
|
|
9958
9247
|
};
|
|
9959
9248
|
|
|
9960
9249
|
// src/audits/agent-tools/webmcp-manifest.ts
|
|
9961
|
-
function
|
|
9250
|
+
function tryParseJson15(body) {
|
|
9962
9251
|
try {
|
|
9963
9252
|
return JSON.parse(body);
|
|
9964
9253
|
} catch {
|
|
9965
9254
|
return void 0;
|
|
9966
9255
|
}
|
|
9967
9256
|
}
|
|
9968
|
-
function
|
|
9257
|
+
function isObject15(val) {
|
|
9969
9258
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9970
9259
|
}
|
|
9971
9260
|
var WebmcpManifestAudit = class extends Audit {
|
|
@@ -10015,8 +9304,8 @@ var WebmcpManifestAudit = class extends Audit {
|
|
|
10015
9304
|
"high"
|
|
10016
9305
|
);
|
|
10017
9306
|
}
|
|
10018
|
-
const parsed =
|
|
10019
|
-
if (!
|
|
9307
|
+
const parsed = tryParseJson15(result.body);
|
|
9308
|
+
if (!isObject15(parsed)) {
|
|
10020
9309
|
return this.fail(
|
|
10021
9310
|
"/.well-known/webmcp is not valid JSON.",
|
|
10022
9311
|
"Valid JSON object with tools array",
|
|
@@ -10034,7 +9323,7 @@ var WebmcpManifestAudit = class extends Audit {
|
|
|
10034
9323
|
}
|
|
10035
9324
|
const rawTools = parsed["tools"];
|
|
10036
9325
|
const tools = rawTools.filter(
|
|
10037
|
-
(t) =>
|
|
9326
|
+
(t) => isObject15(t) && typeof t["name"] === "string"
|
|
10038
9327
|
);
|
|
10039
9328
|
if (tools.length === 0) {
|
|
10040
9329
|
return this.fail(
|
|
@@ -10251,13 +9540,13 @@ var WebmcpInputQualityAudit = class extends Audit {
|
|
|
10251
9540
|
};
|
|
10252
9541
|
|
|
10253
9542
|
// src/audits/agent-tools/webmcp-tool-naming.ts
|
|
10254
|
-
function
|
|
9543
|
+
function isObject16(val) {
|
|
10255
9544
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10256
9545
|
}
|
|
10257
9546
|
function asString(val) {
|
|
10258
9547
|
return typeof val === "string" ? val : "";
|
|
10259
9548
|
}
|
|
10260
|
-
function
|
|
9549
|
+
function tryParseJson16(body) {
|
|
10261
9550
|
try {
|
|
10262
9551
|
return JSON.parse(body);
|
|
10263
9552
|
} catch {
|
|
@@ -10300,10 +9589,10 @@ var WebmcpToolNamingAudit = class extends Audit {
|
|
|
10300
9589
|
const tools = [];
|
|
10301
9590
|
const manifestResult = ctx.rootFiles["/.well-known/webmcp"];
|
|
10302
9591
|
if (manifestResult?.status === 200 && manifestResult.body) {
|
|
10303
|
-
const parsed =
|
|
10304
|
-
if (
|
|
9592
|
+
const parsed = tryParseJson16(manifestResult.body);
|
|
9593
|
+
if (isObject16(parsed) && Array.isArray(parsed["tools"])) {
|
|
10305
9594
|
for (const tool of parsed["tools"]) {
|
|
10306
|
-
if (
|
|
9595
|
+
if (isObject16(tool)) {
|
|
10307
9596
|
const name = asString(tool["name"]);
|
|
10308
9597
|
tools.push({
|
|
10309
9598
|
name,
|
|
@@ -10380,14 +9669,14 @@ var WebmcpToolNamingAudit = class extends Audit {
|
|
|
10380
9669
|
};
|
|
10381
9670
|
|
|
10382
9671
|
// src/audits/agent-tools/webmcp-tool-annotations.ts
|
|
10383
|
-
function
|
|
9672
|
+
function tryParseJson17(body) {
|
|
10384
9673
|
try {
|
|
10385
9674
|
return JSON.parse(body);
|
|
10386
9675
|
} catch {
|
|
10387
9676
|
return void 0;
|
|
10388
9677
|
}
|
|
10389
9678
|
}
|
|
10390
|
-
function
|
|
9679
|
+
function isObject17(val) {
|
|
10391
9680
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10392
9681
|
}
|
|
10393
9682
|
function asString2(val) {
|
|
@@ -10450,15 +9739,15 @@ var WebmcpToolAnnotationsAudit = class extends Audit {
|
|
|
10450
9739
|
const seen = /* @__PURE__ */ new Set();
|
|
10451
9740
|
const manifestResult = ctx.rootFiles["/.well-known/webmcp"];
|
|
10452
9741
|
if (manifestResult?.status === 200 && manifestResult.body) {
|
|
10453
|
-
const parsed =
|
|
10454
|
-
if (
|
|
9742
|
+
const parsed = tryParseJson17(manifestResult.body);
|
|
9743
|
+
if (isObject17(parsed) && Array.isArray(parsed["tools"])) {
|
|
10455
9744
|
for (const tool of parsed["tools"]) {
|
|
10456
|
-
if (!
|
|
9745
|
+
if (!isObject17(tool)) continue;
|
|
10457
9746
|
const name = asString2(tool["name"]);
|
|
10458
9747
|
totalTools++;
|
|
10459
9748
|
if (name) seen.add(name);
|
|
10460
9749
|
const annotations = tool["annotations"];
|
|
10461
|
-
if (
|
|
9750
|
+
if (isObject17(annotations)) {
|
|
10462
9751
|
const found = SAFETY_ANNOTATIONS.filter((a) => a in annotations);
|
|
10463
9752
|
if (found.length > 0) {
|
|
10464
9753
|
toolsWithAnnotations++;
|
|
@@ -10520,203 +9809,24 @@ var WebmcpToolAnnotationsAudit = class extends Audit {
|
|
|
10520
9809
|
}
|
|
10521
9810
|
};
|
|
10522
9811
|
|
|
10523
|
-
// src/audits/agent-tools/webmcp-action-coverage.ts
|
|
10524
|
-
function tryParseJson20(body) {
|
|
10525
|
-
try {
|
|
10526
|
-
return JSON.parse(body);
|
|
10527
|
-
} catch {
|
|
10528
|
-
return void 0;
|
|
10529
|
-
}
|
|
10530
|
-
}
|
|
10531
|
-
function isObject20(val) {
|
|
10532
|
-
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10533
|
-
}
|
|
10534
|
-
function asString3(val) {
|
|
10535
|
-
return typeof val === "string" ? val : "";
|
|
10536
|
-
}
|
|
10537
|
-
var COMMERCE_ACTIONS = [
|
|
10538
|
-
{
|
|
10539
|
-
label: "Product Search",
|
|
10540
|
-
keywords: ["search", "find", "query", "browse", "filter", "lookup", "catalog"]
|
|
10541
|
-
},
|
|
10542
|
-
{
|
|
10543
|
-
label: "Product Detail",
|
|
10544
|
-
keywords: ["product", "detail", "productdetail", "getproduct", "viewproduct", "viewitem"]
|
|
10545
|
-
},
|
|
10546
|
-
{ label: "Add to Cart", keywords: ["cart", "addtocart", "basket", "additem"] },
|
|
10547
|
-
{
|
|
10548
|
-
label: "Checkout",
|
|
10549
|
-
keywords: ["checkout", "purchase", "placeorder", "completepurchase", "buyproduct"]
|
|
10550
|
-
},
|
|
10551
|
-
{
|
|
10552
|
-
label: "Account/Auth",
|
|
10553
|
-
keywords: ["login", "register", "signup", "signin", "authenticate", "createaccount"]
|
|
10554
|
-
},
|
|
10555
|
-
{
|
|
10556
|
-
label: "Contact/Support",
|
|
10557
|
-
keywords: ["contact", "support", "inquiry", "submitinquiry", "sendmessage", "helpdesk"]
|
|
10558
|
-
}
|
|
10559
|
-
];
|
|
10560
|
-
var MIN_COVERAGE = 2;
|
|
10561
|
-
var WebmcpActionCoverageAudit = class extends Audit {
|
|
10562
|
-
static meta = {
|
|
10563
|
-
id: "5.25",
|
|
10564
|
-
category: "agent-tools",
|
|
10565
|
-
title: "WebMCP commerce action coverage",
|
|
10566
|
-
failureTitle: "WebMCP commerce action coverage",
|
|
10567
|
-
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.",
|
|
10568
|
-
scoreDisplayMode: "ternary",
|
|
10569
|
-
weight: 1,
|
|
10570
|
-
defaultPriority: "medium",
|
|
10571
|
-
guidance: {
|
|
10572
|
-
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.",
|
|
10573
|
-
fix: "Expose WebMCP tools for the key commerce actions: product search, product detail/view, add to cart, checkout/purchase, and contact/support.",
|
|
10574
|
-
code: `// /.well-known/webmcp \u2014 full commerce coverage
|
|
10575
|
-
{
|
|
10576
|
-
"tools": [
|
|
10577
|
-
{
|
|
10578
|
-
"name": "searchProducts",
|
|
10579
|
-
"description": "Search the product catalog by keyword, category, or filters",
|
|
10580
|
-
"annotations": { "readOnlyHint": true },
|
|
10581
|
-
"inputSchema": {
|
|
10582
|
-
"type": "object",
|
|
10583
|
-
"properties": {
|
|
10584
|
-
"query": { "type": "string" },
|
|
10585
|
-
"category": { "type": "string" },
|
|
10586
|
-
"maxPrice": { "type": "number" }
|
|
10587
|
-
},
|
|
10588
|
-
"required": ["query"]
|
|
10589
|
-
}
|
|
10590
|
-
},
|
|
10591
|
-
{
|
|
10592
|
-
"name": "getProductDetails",
|
|
10593
|
-
"description": "Get full details for a specific product by ID or URL",
|
|
10594
|
-
"annotations": { "readOnlyHint": true },
|
|
10595
|
-
"inputSchema": {
|
|
10596
|
-
"type": "object",
|
|
10597
|
-
"properties": { "productId": { "type": "string" } },
|
|
10598
|
-
"required": ["productId"]
|
|
10599
|
-
}
|
|
10600
|
-
},
|
|
10601
|
-
{
|
|
10602
|
-
"name": "addToCart",
|
|
10603
|
-
"description": "Add a product to the shopping cart with quantity",
|
|
10604
|
-
"annotations": { "readOnlyHint": false, "idempotentHint": false },
|
|
10605
|
-
"inputSchema": {
|
|
10606
|
-
"type": "object",
|
|
10607
|
-
"properties": {
|
|
10608
|
-
"productId": { "type": "string" },
|
|
10609
|
-
"quantity": { "type": "integer", "minimum": 1 }
|
|
10610
|
-
},
|
|
10611
|
-
"required": ["productId"]
|
|
10612
|
-
}
|
|
10613
|
-
},
|
|
10614
|
-
{
|
|
10615
|
-
"name": "checkout",
|
|
10616
|
-
"description": "Initiate checkout for the current cart",
|
|
10617
|
-
"annotations": { "readOnlyHint": false, "confirmationRequired": true },
|
|
10618
|
-
"inputSchema": {
|
|
10619
|
-
"type": "object",
|
|
10620
|
-
"properties": { "shippingMethod": { "type": "string" } }
|
|
10621
|
-
}
|
|
10622
|
-
}
|
|
10623
|
-
]
|
|
10624
|
-
}`,
|
|
10625
|
-
effort: "moderate",
|
|
10626
|
-
docsUrl: "https://webmcp.link/",
|
|
10627
|
-
tags: ["webmcp", "commerce", "coverage", "chrome-146"]
|
|
10628
|
-
}
|
|
10629
|
-
};
|
|
10630
|
-
audit(ctx) {
|
|
10631
|
-
const toolSignatures = [];
|
|
10632
|
-
const seen = /* @__PURE__ */ new Set();
|
|
10633
|
-
const manifestResult = ctx.rootFiles["/.well-known/webmcp"];
|
|
10634
|
-
if (manifestResult?.status === 200 && manifestResult.body) {
|
|
10635
|
-
const parsed = tryParseJson20(manifestResult.body);
|
|
10636
|
-
if (isObject20(parsed) && Array.isArray(parsed["tools"])) {
|
|
10637
|
-
for (const tool of parsed["tools"]) {
|
|
10638
|
-
if (isObject20(tool)) {
|
|
10639
|
-
const name = asString3(tool["name"]);
|
|
10640
|
-
const sig = `${name} ${asString3(tool["description"])}`.toLowerCase();
|
|
10641
|
-
toolSignatures.push(sig);
|
|
10642
|
-
if (name) seen.add(name);
|
|
10643
|
-
}
|
|
10644
|
-
}
|
|
10645
|
-
}
|
|
10646
|
-
}
|
|
10647
|
-
for (const page of ctx.pages) {
|
|
10648
|
-
page.$("form[toolname]").each((_, el) => {
|
|
10649
|
-
const name = page.$(el).attr("toolname") || "";
|
|
10650
|
-
if (name && seen.has(name)) return;
|
|
10651
|
-
const desc = page.$(el).attr("tooldescription") || "";
|
|
10652
|
-
const action = page.$(el).attr("action") || "";
|
|
10653
|
-
toolSignatures.push(`${name} ${desc} ${action}`.toLowerCase());
|
|
10654
|
-
if (name) seen.add(name);
|
|
10655
|
-
});
|
|
10656
|
-
}
|
|
10657
|
-
if (toolSignatures.length === 0) {
|
|
10658
|
-
return this.notApplicable(
|
|
10659
|
-
"No WebMCP tools found \u2014 commerce action coverage cannot be assessed.",
|
|
10660
|
-
`At least ${MIN_COVERAGE} commerce actions covered (search, product, cart, checkout, contact)`,
|
|
10661
|
-
"No WebMCP tools"
|
|
10662
|
-
);
|
|
10663
|
-
}
|
|
10664
|
-
const coveredActions = [];
|
|
10665
|
-
const missingActions = [];
|
|
10666
|
-
for (const action of COMMERCE_ACTIONS) {
|
|
10667
|
-
const matched = toolSignatures.some(
|
|
10668
|
-
(sig) => action.keywords.some((kw) => new RegExp(`\\b${kw}\\b`).test(sig))
|
|
10669
|
-
);
|
|
10670
|
-
if (matched) {
|
|
10671
|
-
coveredActions.push(action.label);
|
|
10672
|
-
} else {
|
|
10673
|
-
missingActions.push(action.label);
|
|
10674
|
-
}
|
|
10675
|
-
}
|
|
10676
|
-
const coverage = coveredActions.length;
|
|
10677
|
-
const total = COMMERCE_ACTIONS.length;
|
|
10678
|
-
if (coverage >= 4) {
|
|
10679
|
-
return this.pass(
|
|
10680
|
-
`${coverage}/${total} commerce actions covered: ${coveredActions.join(", ")}.`,
|
|
10681
|
-
`At least ${MIN_COVERAGE} commerce actions covered`,
|
|
10682
|
-
`${coverage}/${total} covered`
|
|
10683
|
-
);
|
|
10684
|
-
}
|
|
10685
|
-
if (coverage >= MIN_COVERAGE) {
|
|
10686
|
-
return this.warn(
|
|
10687
|
-
`${coverage}/${total} commerce actions covered: ${coveredActions.join(", ")}. Missing: ${missingActions.join(", ")}.`,
|
|
10688
|
-
`At least 4 commerce actions covered for strong agent support`,
|
|
10689
|
-
`${coverage}/${total} covered`,
|
|
10690
|
-
"medium"
|
|
10691
|
-
);
|
|
10692
|
-
}
|
|
10693
|
-
return this.fail(
|
|
10694
|
-
`Only ${coverage}/${total} commerce actions covered: ${coveredActions.length > 0 ? coveredActions.join(", ") : "none"}. Missing: ${missingActions.join(", ")}.`,
|
|
10695
|
-
`At least ${MIN_COVERAGE} commerce actions covered`,
|
|
10696
|
-
`${coverage}/${total} covered`,
|
|
10697
|
-
"medium"
|
|
10698
|
-
);
|
|
10699
|
-
}
|
|
10700
|
-
};
|
|
10701
|
-
|
|
10702
9812
|
// src/audits/agent-tools/openapi-description-quality.ts
|
|
10703
|
-
function
|
|
9813
|
+
function tryParseJson18(body) {
|
|
10704
9814
|
try {
|
|
10705
9815
|
return JSON.parse(body);
|
|
10706
9816
|
} catch {
|
|
10707
9817
|
return void 0;
|
|
10708
9818
|
}
|
|
10709
9819
|
}
|
|
10710
|
-
function
|
|
9820
|
+
function isObject18(val) {
|
|
10711
9821
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10712
9822
|
}
|
|
10713
9823
|
var HTTP_METHODS6 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
10714
9824
|
var MIN_DESCRIPTION_LENGTH2 = 15;
|
|
10715
|
-
function
|
|
9825
|
+
function getOpenApiSpec7(ctx) {
|
|
10716
9826
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
10717
9827
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
10718
|
-
const parsed =
|
|
10719
|
-
if (
|
|
9828
|
+
const parsed = tryParseJson18(jsonResult.body);
|
|
9829
|
+
if (isObject18(parsed)) return parsed;
|
|
10720
9830
|
}
|
|
10721
9831
|
return void 0;
|
|
10722
9832
|
}
|
|
@@ -10725,13 +9835,13 @@ function hasGoodDescription(val) {
|
|
|
10725
9835
|
}
|
|
10726
9836
|
function getCheckableItems(spec) {
|
|
10727
9837
|
const paths = spec["paths"];
|
|
10728
|
-
if (!
|
|
9838
|
+
if (!isObject18(paths)) return [];
|
|
10729
9839
|
const items = [];
|
|
10730
9840
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
10731
|
-
if (!
|
|
9841
|
+
if (!isObject18(pathItem)) continue;
|
|
10732
9842
|
for (const method of HTTP_METHODS6) {
|
|
10733
9843
|
const op = pathItem[method];
|
|
10734
|
-
if (!
|
|
9844
|
+
if (!isObject18(op)) continue;
|
|
10735
9845
|
const operation = op;
|
|
10736
9846
|
const opLabel = `${method.toUpperCase()} ${path}`;
|
|
10737
9847
|
items.push({
|
|
@@ -10741,7 +9851,7 @@ function getCheckableItems(spec) {
|
|
|
10741
9851
|
const parameters = operation["parameters"];
|
|
10742
9852
|
if (Array.isArray(parameters)) {
|
|
10743
9853
|
for (const param of parameters) {
|
|
10744
|
-
if (!
|
|
9854
|
+
if (!isObject18(param)) continue;
|
|
10745
9855
|
const name = typeof param["name"] === "string" ? param["name"] : "(unnamed)";
|
|
10746
9856
|
items.push({
|
|
10747
9857
|
label: `${opLabel} param '${name}'`,
|
|
@@ -10785,7 +9895,7 @@ var OpenApiDescriptionQualityAudit = class _OpenApiDescriptionQualityAudit exten
|
|
|
10785
9895
|
}
|
|
10786
9896
|
};
|
|
10787
9897
|
audit(ctx) {
|
|
10788
|
-
const spec =
|
|
9898
|
+
const spec = getOpenApiSpec7(ctx);
|
|
10789
9899
|
if (!spec) {
|
|
10790
9900
|
return this.notApplicable(
|
|
10791
9901
|
"No parseable OpenAPI JSON spec found at /openapi.json.",
|
|
@@ -11762,53 +10872,6 @@ var TimeElementAudit = class extends Audit {
|
|
|
11762
10872
|
}
|
|
11763
10873
|
};
|
|
11764
10874
|
|
|
11765
|
-
// src/audits/semantic-html/address-element.ts
|
|
11766
|
-
var AddressElementAudit = class extends Audit {
|
|
11767
|
-
static meta = {
|
|
11768
|
-
id: "6.12",
|
|
11769
|
-
category: "semantic-html",
|
|
11770
|
-
title: "<address> for contact info",
|
|
11771
|
-
failureTitle: "<address> for contact info",
|
|
11772
|
-
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.',
|
|
11773
|
-
scoreDisplayMode: "binary",
|
|
11774
|
-
weight: 1,
|
|
11775
|
-
applicablePageTypes: ["homepage"],
|
|
11776
|
-
defaultPriority: "low",
|
|
11777
|
-
guidance: {
|
|
11778
|
-
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.',
|
|
11779
|
-
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.",
|
|
11780
|
-
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>',
|
|
11781
|
-
effort: "trivial",
|
|
11782
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address",
|
|
11783
|
-
tags: ["contact", "semantic", "html"]
|
|
11784
|
-
}
|
|
11785
|
-
};
|
|
11786
|
-
audit(ctx) {
|
|
11787
|
-
let pagesWithAddress = 0;
|
|
11788
|
-
for (const page of ctx.pages) {
|
|
11789
|
-
if (page.$("address").length > 0) pagesWithAddress++;
|
|
11790
|
-
}
|
|
11791
|
-
const hasAddress = pagesWithAddress > 0;
|
|
11792
|
-
if (hasAddress) {
|
|
11793
|
-
return this.pass(
|
|
11794
|
-
`${pagesWithAddress}/${ctx.pages.length} page(s) use <address> for contact information.`,
|
|
11795
|
-
"<address> element used for contact information",
|
|
11796
|
-
`${pagesWithAddress} page(s) with <address>`
|
|
11797
|
-
);
|
|
11798
|
-
}
|
|
11799
|
-
return this.warn(
|
|
11800
|
-
"No <address> elements found. If contact information exists, consider using <address>.",
|
|
11801
|
-
"<address> element used for contact information",
|
|
11802
|
-
"No <address> elements found",
|
|
11803
|
-
{
|
|
11804
|
-
priority: "low",
|
|
11805
|
-
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.',
|
|
11806
|
-
code: '<address>\n <a href="mailto:info@yoursite.com">info@yoursite.com</a><br>\n 123 Main St, City, ST 12345\n</address>'
|
|
11807
|
-
}
|
|
11808
|
-
);
|
|
11809
|
-
}
|
|
11810
|
-
};
|
|
11811
|
-
|
|
11812
10875
|
// src/audits/semantic-html/definition-elements.ts
|
|
11813
10876
|
var DefinitionElementsAudit = class extends Audit {
|
|
11814
10877
|
static meta = {
|
|
@@ -11969,106 +11032,31 @@ var ImageAltTextAudit = class extends Audit {
|
|
|
11969
11032
|
const mostCovered = coverage >= 0.8;
|
|
11970
11033
|
if (allCovered) {
|
|
11971
11034
|
return this.pass(
|
|
11972
|
-
`All ${totalImages} non-decorative image(s) have descriptive alt text.`,
|
|
11973
|
-
"100% of non-decorative images have non-empty descriptive alt text",
|
|
11974
|
-
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`
|
|
11975
|
-
);
|
|
11976
|
-
}
|
|
11977
|
-
if (mostCovered) {
|
|
11978
|
-
return this.warn(
|
|
11979
|
-
`${imagesWithAlt}/${totalImages} non-decorative image(s) have alt text (${Math.round(coverage * 100)}%).`,
|
|
11980
|
-
"100% of non-decorative images have non-empty descriptive alt text",
|
|
11981
|
-
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`,
|
|
11982
|
-
{
|
|
11983
|
-
priority: "high",
|
|
11984
|
-
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.",
|
|
11985
|
-
code: '<img src="product.jpg" alt="Product name shown from the front, featuring key design element">'
|
|
11986
|
-
}
|
|
11987
|
-
);
|
|
11988
|
-
}
|
|
11989
|
-
return this.fail(
|
|
11990
|
-
`${imagesWithAlt}/${totalImages} non-decorative image(s) have alt text (${Math.round(coverage * 100)}%).`,
|
|
11991
|
-
"100% of non-decorative images have non-empty descriptive alt text",
|
|
11992
|
-
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`,
|
|
11993
|
-
{
|
|
11994
|
-
priority: "high",
|
|
11995
|
-
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.",
|
|
11996
|
-
code: '<img src="product.jpg" alt="Product name shown from the front, featuring key design element">'
|
|
11997
|
-
}
|
|
11998
|
-
);
|
|
11999
|
-
}
|
|
12000
|
-
};
|
|
12001
|
-
|
|
12002
|
-
// src/audits/semantic-html/decorative-images.ts
|
|
12003
|
-
var DecorativeImagesAudit = class extends Audit {
|
|
12004
|
-
static meta = {
|
|
12005
|
-
id: "6.16",
|
|
12006
|
-
category: "semantic-html",
|
|
12007
|
-
title: "Decorative images marked correctly",
|
|
12008
|
-
failureTitle: "Decorative images marked correctly",
|
|
12009
|
-
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.',
|
|
12010
|
-
scoreDisplayMode: "ternary",
|
|
12011
|
-
weight: 1,
|
|
12012
|
-
defaultPriority: "medium",
|
|
12013
|
-
guidance: {
|
|
12014
|
-
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.',
|
|
12015
|
-
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.',
|
|
12016
|
-
code: '<img src="decorative-border.png" alt="" role="presentation">',
|
|
12017
|
-
effort: "trivial",
|
|
12018
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/presentation_role",
|
|
12019
|
-
tags: ["images", "decorative", "accessibility", "semantic"]
|
|
12020
|
-
}
|
|
12021
|
-
};
|
|
12022
|
-
audit(ctx) {
|
|
12023
|
-
let decorativeCount = 0;
|
|
12024
|
-
let correctlyMarked = 0;
|
|
12025
|
-
for (const page of ctx.pages) {
|
|
12026
|
-
const images = extractImages(page.$);
|
|
12027
|
-
for (const img of images) {
|
|
12028
|
-
if (img.alt === "") {
|
|
12029
|
-
decorativeCount++;
|
|
12030
|
-
if (img.role === "presentation" || img.role === "none" || img.ariaHidden === "true") {
|
|
12031
|
-
correctlyMarked++;
|
|
12032
|
-
}
|
|
12033
|
-
}
|
|
12034
|
-
}
|
|
12035
|
-
}
|
|
12036
|
-
if (decorativeCount === 0) {
|
|
12037
|
-
return this.pass(
|
|
12038
|
-
"No decorative images (empty alt) found \u2014 check not applicable.",
|
|
12039
|
-
'Images with empty alt have role="presentation"',
|
|
12040
|
-
"No images with empty alt"
|
|
12041
|
-
);
|
|
12042
|
-
}
|
|
12043
|
-
const allCorrect = correctlyMarked === decorativeCount;
|
|
12044
|
-
const majorityCorrect = correctlyMarked > decorativeCount / 2;
|
|
12045
|
-
if (allCorrect) {
|
|
12046
|
-
return this.pass(
|
|
12047
|
-
`All ${decorativeCount} decorative image(s) have role="presentation".`,
|
|
12048
|
-
'Images with empty alt have role="presentation"',
|
|
12049
|
-
`${correctlyMarked}/${decorativeCount} correctly marked`
|
|
11035
|
+
`All ${totalImages} non-decorative image(s) have descriptive alt text.`,
|
|
11036
|
+
"100% of non-decorative images have non-empty descriptive alt text",
|
|
11037
|
+
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`
|
|
12050
11038
|
);
|
|
12051
11039
|
}
|
|
12052
|
-
if (
|
|
11040
|
+
if (mostCovered) {
|
|
12053
11041
|
return this.warn(
|
|
12054
|
-
`${
|
|
12055
|
-
|
|
12056
|
-
`${
|
|
11042
|
+
`${imagesWithAlt}/${totalImages} non-decorative image(s) have alt text (${Math.round(coverage * 100)}%).`,
|
|
11043
|
+
"100% of non-decorative images have non-empty descriptive alt text",
|
|
11044
|
+
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`,
|
|
12057
11045
|
{
|
|
12058
|
-
priority: "
|
|
12059
|
-
description:
|
|
12060
|
-
code: '<img src="
|
|
11046
|
+
priority: "high",
|
|
11047
|
+
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.",
|
|
11048
|
+
code: '<img src="product.jpg" alt="Product name shown from the front, featuring key design element">'
|
|
12061
11049
|
}
|
|
12062
11050
|
);
|
|
12063
11051
|
}
|
|
12064
11052
|
return this.fail(
|
|
12065
|
-
`${
|
|
12066
|
-
|
|
12067
|
-
`${
|
|
11053
|
+
`${imagesWithAlt}/${totalImages} non-decorative image(s) have alt text (${Math.round(coverage * 100)}%).`,
|
|
11054
|
+
"100% of non-decorative images have non-empty descriptive alt text",
|
|
11055
|
+
`${imagesWithAlt}/${totalImages} images with alt text (${Math.round(coverage * 100)}%)`,
|
|
12068
11056
|
{
|
|
12069
|
-
priority: "
|
|
12070
|
-
description:
|
|
12071
|
-
code: '<img src="
|
|
11057
|
+
priority: "high",
|
|
11058
|
+
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.",
|
|
11059
|
+
code: '<img src="product.jpg" alt="Product name shown from the front, featuring key design element">'
|
|
12072
11060
|
}
|
|
12073
11061
|
);
|
|
12074
11062
|
}
|
|
@@ -12450,67 +11438,6 @@ var FakeHeadingsAudit = class extends Audit {
|
|
|
12450
11438
|
}
|
|
12451
11439
|
};
|
|
12452
11440
|
|
|
12453
|
-
// src/audits/accessibility/skip-nav.ts
|
|
12454
|
-
var SkipNavAudit = class extends Audit {
|
|
12455
|
-
static meta = {
|
|
12456
|
-
id: "7.1",
|
|
12457
|
-
category: "accessibility",
|
|
12458
|
-
title: "Skip navigation link",
|
|
12459
|
-
failureTitle: "Skip navigation link",
|
|
12460
|
-
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.",
|
|
12461
|
-
scoreDisplayMode: "binary",
|
|
12462
|
-
weight: 1,
|
|
12463
|
-
defaultPriority: "medium",
|
|
12464
|
-
guidance: {
|
|
12465
|
-
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.",
|
|
12466
|
-
fix: 'Add a "Skip to main content" link as the first focusable element in <body>, pointing to an anchor on your <main> element.',
|
|
12467
|
-
code: '<a href="#main-content" class="skip-link">Skip to main content</a>\n<!-- ... navigation ... -->\n<main id="main-content">...</main>',
|
|
12468
|
-
effort: "trivial",
|
|
12469
|
-
docsUrl: "https://www.w3.org/WAI/WCAG21/Techniques/general/G1",
|
|
12470
|
-
tags: ["a11y", "navigation", "accessibility"]
|
|
12471
|
-
}
|
|
12472
|
-
};
|
|
12473
|
-
audit(ctx) {
|
|
12474
|
-
if (!ctx.pages || ctx.pages.length === 0) {
|
|
12475
|
-
return this.warn(
|
|
12476
|
-
"No pages scanned to check for skip navigation link.",
|
|
12477
|
-
"A skip-to-content link among the first links in <body>",
|
|
12478
|
-
"No pages scanned"
|
|
12479
|
-
);
|
|
12480
|
-
}
|
|
12481
|
-
for (const page of ctx.pages) {
|
|
12482
|
-
const $ = page.$;
|
|
12483
|
-
const bodyLinks = $("body a").slice(0, 5);
|
|
12484
|
-
let found = false;
|
|
12485
|
-
bodyLinks.each((_, el) => {
|
|
12486
|
-
const text = $(el).text().toLowerCase().trim();
|
|
12487
|
-
const href = ($(el).attr("href") ?? "").toLowerCase();
|
|
12488
|
-
if ((text.includes("skip") || text.includes("jump to") || text.includes("go to main")) && (href.includes("#main") || href.includes("#content") || href.includes("#skip"))) {
|
|
12489
|
-
found = true;
|
|
12490
|
-
}
|
|
12491
|
-
});
|
|
12492
|
-
if (found) {
|
|
12493
|
-
return this.pass(
|
|
12494
|
-
"Skip navigation link found among the first links in <body>.",
|
|
12495
|
-
"A skip-to-content link among the first links in <body>",
|
|
12496
|
-
"Skip navigation link detected",
|
|
12497
|
-
page.url
|
|
12498
|
-
);
|
|
12499
|
-
}
|
|
12500
|
-
}
|
|
12501
|
-
return this.fail(
|
|
12502
|
-
"No skip navigation link found. Screen reader and keyboard users rely on skip links to bypass repeated navigation.",
|
|
12503
|
-
"A skip-to-content link among the first links in <body>",
|
|
12504
|
-
"No skip navigation link detected in the first few <body> links",
|
|
12505
|
-
{
|
|
12506
|
-
priority: "medium",
|
|
12507
|
-
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.",
|
|
12508
|
-
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>'
|
|
12509
|
-
}
|
|
12510
|
-
);
|
|
12511
|
-
}
|
|
12512
|
-
};
|
|
12513
|
-
|
|
12514
11441
|
// src/audits/accessibility/aria-landmarks.ts
|
|
12515
11442
|
var REQUIRED_LANDMARKS = [
|
|
12516
11443
|
{
|
|
@@ -13377,98 +12304,6 @@ var ContentTypeOptionsAudit = class extends Audit {
|
|
|
13377
12304
|
}
|
|
13378
12305
|
};
|
|
13379
12306
|
|
|
13380
|
-
// src/audits/technical-readiness/referrer-policy.ts
|
|
13381
|
-
var ReferrerPolicyAudit = class extends Audit {
|
|
13382
|
-
static meta = {
|
|
13383
|
-
id: "8.5",
|
|
13384
|
-
category: "technical-readiness",
|
|
13385
|
-
title: "Referrer-Policy header",
|
|
13386
|
-
failureTitle: "Referrer-Policy header",
|
|
13387
|
-
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.",
|
|
13388
|
-
scoreDisplayMode: "binary",
|
|
13389
|
-
weight: 1,
|
|
13390
|
-
defaultPriority: "medium",
|
|
13391
|
-
guidance: {
|
|
13392
|
-
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.",
|
|
13393
|
-
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.',
|
|
13394
|
-
code: "Referrer-Policy: strict-origin-when-cross-origin",
|
|
13395
|
-
effort: "trivial",
|
|
13396
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy",
|
|
13397
|
-
tags: ["security", "headers", "privacy"]
|
|
13398
|
-
}
|
|
13399
|
-
};
|
|
13400
|
-
audit(ctx) {
|
|
13401
|
-
const page = ctx.pages?.[0];
|
|
13402
|
-
const headers = page?.fetchResult.headers ?? {};
|
|
13403
|
-
const value = headers["referrer-policy"];
|
|
13404
|
-
if (value) {
|
|
13405
|
-
return this.pass(
|
|
13406
|
-
`Referrer-Policy header is present: ${value}`,
|
|
13407
|
-
"Referrer-Policy header present on homepage response",
|
|
13408
|
-
`referrer-policy: ${value}`,
|
|
13409
|
-
page?.url
|
|
13410
|
-
);
|
|
13411
|
-
}
|
|
13412
|
-
return this.fail(
|
|
13413
|
-
"Referrer-Policy header is missing from the homepage response.",
|
|
13414
|
-
"Referrer-Policy header present on homepage response",
|
|
13415
|
-
"Header not found",
|
|
13416
|
-
{
|
|
13417
|
-
priority: "medium",
|
|
13418
|
-
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.",
|
|
13419
|
-
code: "Referrer-Policy: strict-origin-when-cross-origin"
|
|
13420
|
-
},
|
|
13421
|
-
page?.url
|
|
13422
|
-
);
|
|
13423
|
-
}
|
|
13424
|
-
};
|
|
13425
|
-
|
|
13426
|
-
// src/audits/technical-readiness/permissions-policy.ts
|
|
13427
|
-
var PermissionsPolicyAudit = class extends Audit {
|
|
13428
|
-
static meta = {
|
|
13429
|
-
id: "8.6",
|
|
13430
|
-
category: "technical-readiness",
|
|
13431
|
-
title: "Permissions-Policy header",
|
|
13432
|
-
failureTitle: "Permissions-Policy header",
|
|
13433
|
-
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.",
|
|
13434
|
-
scoreDisplayMode: "binary",
|
|
13435
|
-
weight: 1,
|
|
13436
|
-
defaultPriority: "medium",
|
|
13437
|
-
guidance: {
|
|
13438
|
-
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.",
|
|
13439
|
-
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.",
|
|
13440
|
-
code: "Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()",
|
|
13441
|
-
effort: "trivial",
|
|
13442
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy",
|
|
13443
|
-
tags: ["security", "headers", "privacy"]
|
|
13444
|
-
}
|
|
13445
|
-
};
|
|
13446
|
-
audit(ctx) {
|
|
13447
|
-
const page = ctx.pages?.[0];
|
|
13448
|
-
const headers = page?.fetchResult.headers ?? {};
|
|
13449
|
-
const value = headers["permissions-policy"];
|
|
13450
|
-
if (value) {
|
|
13451
|
-
return this.pass(
|
|
13452
|
-
`Permissions-Policy header is present: ${value.length > 120 ? value.slice(0, 120) + "..." : value}`,
|
|
13453
|
-
"Permissions-Policy header present on homepage response",
|
|
13454
|
-
`permissions-policy: ${value.length > 120 ? value.slice(0, 120) + "..." : value}`,
|
|
13455
|
-
page?.url
|
|
13456
|
-
);
|
|
13457
|
-
}
|
|
13458
|
-
return this.fail(
|
|
13459
|
-
"Permissions-Policy header is missing from the homepage response.",
|
|
13460
|
-
"Permissions-Policy header present on homepage response",
|
|
13461
|
-
"Header not found",
|
|
13462
|
-
{
|
|
13463
|
-
priority: "medium",
|
|
13464
|
-
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.",
|
|
13465
|
-
code: "Permissions-Policy: camera=(), microphone=(), geolocation=()"
|
|
13466
|
-
},
|
|
13467
|
-
page?.url
|
|
13468
|
-
);
|
|
13469
|
-
}
|
|
13470
|
-
};
|
|
13471
|
-
|
|
13472
12307
|
// src/audits/technical-readiness/security-txt.ts
|
|
13473
12308
|
var SecurityTxtAudit = class extends Audit {
|
|
13474
12309
|
static meta = {
|
|
@@ -14208,66 +13043,6 @@ var LcpNotLazyAudit = class extends Audit {
|
|
|
14208
13043
|
}
|
|
14209
13044
|
};
|
|
14210
13045
|
|
|
14211
|
-
// src/audits/technical-readiness/preconnect-hints.ts
|
|
14212
|
-
var PreconnectHintsAudit = class extends Audit {
|
|
14213
|
-
static meta = {
|
|
14214
|
-
id: "8.17",
|
|
14215
|
-
category: "technical-readiness",
|
|
14216
|
-
title: "Preconnect hints",
|
|
14217
|
-
failureTitle: "Preconnect hints",
|
|
14218
|
-
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.",
|
|
14219
|
-
scoreDisplayMode: "binary",
|
|
14220
|
-
weight: 1,
|
|
14221
|
-
defaultPriority: "low",
|
|
14222
|
-
guidance: {
|
|
14223
|
-
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.",
|
|
14224
|
-
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.',
|
|
14225
|
-
code: '<link rel="preconnect" href="https://fonts.googleapis.com">\n<link rel="preconnect" href="https://cdn.yoursite.com" crossorigin>',
|
|
14226
|
-
effort: "trivial",
|
|
14227
|
-
docsUrl: "https://web.dev/articles/uses-rel-preconnect",
|
|
14228
|
-
tags: ["performance", "speed", "resource-hints"]
|
|
14229
|
-
}
|
|
14230
|
-
};
|
|
14231
|
-
audit(ctx) {
|
|
14232
|
-
const page = ctx.pages?.[0];
|
|
14233
|
-
if (!page) {
|
|
14234
|
-
return this.warn(
|
|
14235
|
-
"No homepage data available to check preconnect hints.",
|
|
14236
|
-
'At least one <link rel="preconnect"> tag present',
|
|
14237
|
-
"No homepage fetched",
|
|
14238
|
-
void 0,
|
|
14239
|
-
void 0
|
|
14240
|
-
);
|
|
14241
|
-
}
|
|
14242
|
-
const $ = page.$;
|
|
14243
|
-
const preconnects = $('link[rel="preconnect"]');
|
|
14244
|
-
if (preconnects.length > 0) {
|
|
14245
|
-
const hrefs = [];
|
|
14246
|
-
preconnects.each((_, el) => {
|
|
14247
|
-
const href = $(el).attr("href");
|
|
14248
|
-
if (href) hrefs.push(href);
|
|
14249
|
-
});
|
|
14250
|
-
return this.pass(
|
|
14251
|
-
`Found ${preconnects.length} preconnect hint(s): ${hrefs.slice(0, 5).join(", ")}`,
|
|
14252
|
-
'At least one <link rel="preconnect"> tag present',
|
|
14253
|
-
`${preconnects.length} preconnect hint(s)`,
|
|
14254
|
-
page.url
|
|
14255
|
-
);
|
|
14256
|
-
}
|
|
14257
|
-
return this.fail(
|
|
14258
|
-
'No <link rel="preconnect"> hints found. Preconnect hints speed up connections to critical third-party origins.',
|
|
14259
|
-
'At least one <link rel="preconnect"> tag present',
|
|
14260
|
-
"No preconnect hints found",
|
|
14261
|
-
{
|
|
14262
|
-
priority: "low",
|
|
14263
|
-
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.",
|
|
14264
|
-
code: '<link rel="preconnect" href="https://fonts.googleapis.com">\n<link rel="preconnect" href="https://cdn.yoursite.com">'
|
|
14265
|
-
},
|
|
14266
|
-
page.url
|
|
14267
|
-
);
|
|
14268
|
-
}
|
|
14269
|
-
};
|
|
14270
|
-
|
|
14271
13046
|
// src/audits/technical-readiness/no-broken-ai-endpoints.ts
|
|
14272
13047
|
var NoBrokenAiEndpointsAudit = class extends Audit {
|
|
14273
13048
|
static meta = {
|
|
@@ -14581,71 +13356,6 @@ var TermsOfServiceAudit = class extends Audit {
|
|
|
14581
13356
|
}
|
|
14582
13357
|
};
|
|
14583
13358
|
|
|
14584
|
-
// src/audits/technical-readiness/framework-detection.ts
|
|
14585
|
-
var FrameworkDetectionAudit = class extends Audit {
|
|
14586
|
-
static meta = {
|
|
14587
|
-
id: "8.21",
|
|
14588
|
-
category: "technical-readiness",
|
|
14589
|
-
title: "Frontend framework detection",
|
|
14590
|
-
failureTitle: "Frontend framework detection",
|
|
14591
|
-
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.",
|
|
14592
|
-
scoreDisplayMode: "informative",
|
|
14593
|
-
weight: 1,
|
|
14594
|
-
defaultPriority: "low",
|
|
14595
|
-
guidance: {
|
|
14596
|
-
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.",
|
|
14597
|
-
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.",
|
|
14598
|
-
effort: "trivial",
|
|
14599
|
-
tags: ["informational", "framework"]
|
|
14600
|
-
}
|
|
14601
|
-
};
|
|
14602
|
-
audit(ctx) {
|
|
14603
|
-
const page = ctx.pages[0];
|
|
14604
|
-
if (!page) {
|
|
14605
|
-
return this.fail("No pages available for analysis.", "", "None");
|
|
14606
|
-
}
|
|
14607
|
-
const { meta, $ } = page;
|
|
14608
|
-
const frameworks = [];
|
|
14609
|
-
if (meta["generator"]) {
|
|
14610
|
-
frameworks.push(`Generator: ${meta["generator"]}`);
|
|
14611
|
-
}
|
|
14612
|
-
if (meta["next-head-count"] || $('script[id="__NEXT_DATA__"]').length > 0) {
|
|
14613
|
-
frameworks.push("Next.js");
|
|
14614
|
-
}
|
|
14615
|
-
if ($('script[src*="nuxt"]').length > 0 || globalThis.window?.__NUXT__) {
|
|
14616
|
-
frameworks.push("Nuxt.js");
|
|
14617
|
-
}
|
|
14618
|
-
if ($("[data-reactroot], [data-reactid]").length > 0 || $('script[src*="react"]').length > 0) {
|
|
14619
|
-
frameworks.push("React");
|
|
14620
|
-
}
|
|
14621
|
-
if ($("[data-v-field], [data-v-]").length > 0 || $('script[src*="vue"]').length > 0) {
|
|
14622
|
-
frameworks.push("Vue.js");
|
|
14623
|
-
}
|
|
14624
|
-
if ($("app-root, [ng-version]").length > 0) {
|
|
14625
|
-
frameworks.push("Angular");
|
|
14626
|
-
}
|
|
14627
|
-
if ($('script[src*="astro"]').length > 0 || $("style[data-astro-cid]").length > 0) {
|
|
14628
|
-
frameworks.push("Astro");
|
|
14629
|
-
}
|
|
14630
|
-
if ($('script[src*="svelte"]').length > 0) {
|
|
14631
|
-
frameworks.push("Svelte");
|
|
14632
|
-
}
|
|
14633
|
-
if (frameworks.length > 0) {
|
|
14634
|
-
const unique = Array.from(new Set(frameworks));
|
|
14635
|
-
return this.pass(
|
|
14636
|
-
`Detected frameworks: ${unique.join(", ")}.`,
|
|
14637
|
-
"Identify the frontend framework used by the site.",
|
|
14638
|
-
unique.join(", ")
|
|
14639
|
-
);
|
|
14640
|
-
}
|
|
14641
|
-
return this.pass(
|
|
14642
|
-
"No specific frontend framework clearly detected.",
|
|
14643
|
-
"Identify the frontend framework used by the site.",
|
|
14644
|
-
"Generic/Unknown"
|
|
14645
|
-
);
|
|
14646
|
-
}
|
|
14647
|
-
};
|
|
14648
|
-
|
|
14649
13359
|
// src/audits/answer-engine/faq-sections.ts
|
|
14650
13360
|
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;
|
|
14651
13361
|
function hasFaqJsonLd(p) {
|
|
@@ -15610,7 +14320,7 @@ var MetaDescriptionAeoAudit = class _MetaDescriptionAeoAudit extends Audit {
|
|
|
15610
14320
|
};
|
|
15611
14321
|
|
|
15612
14322
|
// src/audits/generative-engine/named-author.ts
|
|
15613
|
-
function
|
|
14323
|
+
function asString3(val) {
|
|
15614
14324
|
return typeof val === "string" ? val : "";
|
|
15615
14325
|
}
|
|
15616
14326
|
function findJsonLdByType(jsonLd, types) {
|
|
@@ -15697,7 +14407,7 @@ var NamedAuthorAudit = class extends Audit {
|
|
|
15697
14407
|
if (!author) continue;
|
|
15698
14408
|
const authors = Array.isArray(author) ? author : [author];
|
|
15699
14409
|
for (const a of authors) {
|
|
15700
|
-
const name = typeof a === "string" ? a : typeof a === "object" && a !== null ?
|
|
14410
|
+
const name = typeof a === "string" ? a : typeof a === "object" && a !== null ? asString3(a["name"]) : "";
|
|
15701
14411
|
const lower = name.trim().toLowerCase();
|
|
15702
14412
|
if (lower && !GENERIC_AUTHOR_NAMES.has(lower)) {
|
|
15703
14413
|
return this.pass(
|
|
@@ -16636,7 +15346,7 @@ var PublicationDateAudit = class extends Audit {
|
|
|
16636
15346
|
};
|
|
16637
15347
|
|
|
16638
15348
|
// src/audits/generative-engine/last-modified-schema.ts
|
|
16639
|
-
function
|
|
15349
|
+
function asString4(val) {
|
|
16640
15350
|
return typeof val === "string" ? val : "";
|
|
16641
15351
|
}
|
|
16642
15352
|
function findJsonLdByType5(jsonLd, types) {
|
|
@@ -16722,7 +15432,7 @@ var LastModifiedSchemaAudit = class extends Audit {
|
|
|
16722
15432
|
return this.warn(
|
|
16723
15433
|
datePublished ? `dateModified equals datePublished ("${dateModified}"). Update dateModified when content changes.` : `dateModified is set ("${dateModified}") but no datePublished for comparison.`,
|
|
16724
15434
|
"JSON-LD dateModified present and different from datePublished",
|
|
16725
|
-
`dateModified: ${dateModified}${datePublished ? `, datePublished: ${
|
|
15435
|
+
`dateModified: ${dateModified}${datePublished ? `, datePublished: ${asString4(datePublished)}` : ""}`,
|
|
16726
15436
|
{
|
|
16727
15437
|
priority: "low",
|
|
16728
15438
|
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."
|
|
@@ -16832,69 +15542,6 @@ var InternalCrossLinkingAudit = class extends Audit {
|
|
|
16832
15542
|
}
|
|
16833
15543
|
};
|
|
16834
15544
|
|
|
16835
|
-
// src/audits/generative-engine/pagination-links.ts
|
|
16836
|
-
var PaginationLinksAudit = class extends Audit {
|
|
16837
|
-
static meta = {
|
|
16838
|
-
id: "10.12",
|
|
16839
|
-
category: "generative-engine",
|
|
16840
|
-
title: "Pagination links",
|
|
16841
|
-
failureTitle: "Pagination links",
|
|
16842
|
-
description: 'AI crawlers use rel="prev" and rel="next" to navigate paginated content series without missing pages.',
|
|
16843
|
-
scoreDisplayMode: "ternary",
|
|
16844
|
-
weight: 1,
|
|
16845
|
-
applicablePageTypes: ["category"],
|
|
16846
|
-
defaultPriority: "low",
|
|
16847
|
-
guidance: {
|
|
16848
|
-
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.',
|
|
16849
|
-
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.',
|
|
16850
|
-
code: '<link rel="prev" href="/blog/page/1">\n<link rel="next" href="/blog/page/3">',
|
|
16851
|
-
effort: "easy",
|
|
16852
|
-
tags: ["pagination", "html", "generative-engine"]
|
|
16853
|
-
}
|
|
16854
|
-
};
|
|
16855
|
-
audit(ctx) {
|
|
16856
|
-
const page = ctx.pages[0];
|
|
16857
|
-
if (!page) {
|
|
16858
|
-
return this.fail(
|
|
16859
|
-
"No pages scanned.",
|
|
16860
|
-
'<link rel="prev"> and <link rel="next"> in head',
|
|
16861
|
-
"No pages scanned",
|
|
16862
|
-
{
|
|
16863
|
-
priority: "low",
|
|
16864
|
-
description: 'AI crawlers use rel="prev" and rel="next" to navigate paginated content series without missing pages.',
|
|
16865
|
-
code: '<link rel="prev" href="/blog/page/1">\n<link rel="next" href="/blog/page/3">'
|
|
16866
|
-
}
|
|
16867
|
-
);
|
|
16868
|
-
}
|
|
16869
|
-
for (const p of ctx.pages) {
|
|
16870
|
-
const hasPrev = p.headLinks.some((l) => l.rel === "prev");
|
|
16871
|
-
const hasNext = p.headLinks.some((l) => l.rel === "next");
|
|
16872
|
-
if (hasPrev || hasNext) {
|
|
16873
|
-
const found = [];
|
|
16874
|
-
if (hasPrev) found.push('rel="prev"');
|
|
16875
|
-
if (hasNext) found.push('rel="next"');
|
|
16876
|
-
return this.pass(
|
|
16877
|
-
`Pagination links found: ${found.join(" and ")}.`,
|
|
16878
|
-
'<link rel="prev"> and <link rel="next"> in head',
|
|
16879
|
-
found.join(", "),
|
|
16880
|
-
p.url
|
|
16881
|
-
);
|
|
16882
|
-
}
|
|
16883
|
-
}
|
|
16884
|
-
return this.warn(
|
|
16885
|
-
'No <link rel="prev"> or <link rel="next"> found on any page.',
|
|
16886
|
-
'<link rel="prev"> and <link rel="next"> in head',
|
|
16887
|
-
"Not found",
|
|
16888
|
-
{
|
|
16889
|
-
priority: "low",
|
|
16890
|
-
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.',
|
|
16891
|
-
code: '<link rel="prev" href="/blog/page/1">\n<link rel="next" href="/blog/page/3">'
|
|
16892
|
-
},
|
|
16893
|
-
page.url
|
|
16894
|
-
);
|
|
16895
|
-
}
|
|
16896
|
-
};
|
|
16897
|
-
|
|
16898
15545
|
// src/audits/generative-engine/unique-data.ts
|
|
16899
15546
|
var STAT_PATTERN = /\d+(?:\.\d+)?%|\$[\d,]+(?:\.\d{2})?|\b\d{1,3}(?:,\d{3})+\b|\b\d+(?:\.\d+)?x\b/;
|
|
16900
15547
|
var UniqueDataAudit = class extends Audit {
|
|
@@ -17170,7 +15817,6 @@ var defaultConfig = {
|
|
|
17170
15817
|
reg(MobileFriendlyAudit),
|
|
17171
15818
|
reg(FastPageLoadAudit),
|
|
17172
15819
|
reg(NoBrokenLinksAudit),
|
|
17173
|
-
reg(NavigationJsonAudit),
|
|
17174
15820
|
reg(NoOrphanPagesAudit),
|
|
17175
15821
|
reg(CommerceLinksAudit)
|
|
17176
15822
|
],
|
|
@@ -17214,13 +15860,11 @@ var defaultConfig = {
|
|
|
17214
15860
|
reg(FaqPageSchemaAudit),
|
|
17215
15861
|
reg(ServiceProductSchemaAudit),
|
|
17216
15862
|
reg(SpeakableSchemaAudit),
|
|
17217
|
-
reg(PotentialActionAudit),
|
|
17218
15863
|
reg(HowToSchemaAudit),
|
|
17219
15864
|
reg(LocalBusinessSchemaAudit),
|
|
17220
15865
|
reg(ReviewSchemaAudit),
|
|
17221
15866
|
reg(OfferSchemaAudit),
|
|
17222
15867
|
reg(AuthorSchemaAudit),
|
|
17223
|
-
reg(ActionSchemaAudit),
|
|
17224
15868
|
reg(ProductIdentifiersAudit),
|
|
17225
15869
|
reg(ProductDetailsAudit),
|
|
17226
15870
|
reg(ProductReviewsAudit),
|
|
@@ -17238,12 +15882,9 @@ var defaultConfig = {
|
|
|
17238
15882
|
reg(OgImageAltAudit),
|
|
17239
15883
|
reg(TwitterCardAudit),
|
|
17240
15884
|
reg(LlmsTxtLinkAudit),
|
|
17241
|
-
reg(LlmsFullTxtLinkAudit),
|
|
17242
15885
|
reg(AiContentDeclarationAudit),
|
|
17243
|
-
reg(AiInstructionsAudit),
|
|
17244
15886
|
reg(MarkdownAlternateAudit),
|
|
17245
15887
|
reg(RssFeedLinkAudit),
|
|
17246
|
-
reg(McpDiscoveryLinkAudit),
|
|
17247
15888
|
reg(OpenApiLinkAudit),
|
|
17248
15889
|
reg(AiCatalogLinkAudit),
|
|
17249
15890
|
reg(MetaRobotsAudit)
|
|
@@ -17252,20 +15893,17 @@ var defaultConfig = {
|
|
|
17252
15893
|
reg(OpenApiExistsAudit),
|
|
17253
15894
|
reg(OpenApiEndpointsAudit),
|
|
17254
15895
|
reg(OpenApiOperationIdsAudit),
|
|
17255
|
-
reg(OpenApiAiInstructionsAudit),
|
|
17256
15896
|
reg(OpenApiServersAudit),
|
|
17257
15897
|
reg(OpenApiSchemasAudit),
|
|
17258
15898
|
reg(AiCatalogExistsAudit),
|
|
17259
15899
|
reg(AiCatalogMetadataAudit),
|
|
17260
15900
|
reg(AiCatalogUrlsAudit),
|
|
17261
15901
|
reg(AgentsJsonAudit),
|
|
17262
|
-
reg(AiPluginJsonAudit),
|
|
17263
15902
|
reg(McpDiscoveryAudit),
|
|
17264
15903
|
reg(McpEndpointAudit),
|
|
17265
15904
|
reg(McpCapabilitiesAudit),
|
|
17266
15905
|
reg(ContactFormAudit),
|
|
17267
15906
|
reg(SearchEndpointAudit),
|
|
17268
|
-
reg(DataActionCtasAudit),
|
|
17269
15907
|
reg(NoBlockingCaptchaAudit),
|
|
17270
15908
|
reg(FormsNoJsAudit),
|
|
17271
15909
|
reg(WebmcpManifestAudit),
|
|
@@ -17273,7 +15911,6 @@ var defaultConfig = {
|
|
|
17273
15911
|
reg(WebmcpInputQualityAudit),
|
|
17274
15912
|
reg(WebmcpToolNamingAudit),
|
|
17275
15913
|
reg(WebmcpToolAnnotationsAudit),
|
|
17276
|
-
reg(WebmcpActionCoverageAudit),
|
|
17277
15914
|
reg(OpenApiDescriptionQualityAudit),
|
|
17278
15915
|
reg(FormActionabilityAudit)
|
|
17279
15916
|
],
|
|
@@ -17289,18 +15926,15 @@ var defaultConfig = {
|
|
|
17289
15926
|
reg(DataTablesAudit),
|
|
17290
15927
|
reg(CodeLanguageAudit),
|
|
17291
15928
|
reg(TimeElementAudit),
|
|
17292
|
-
reg(AddressElementAudit),
|
|
17293
15929
|
reg(DefinitionElementsAudit),
|
|
17294
15930
|
reg(ContentDepthAudit),
|
|
17295
15931
|
reg(ImageAltTextAudit),
|
|
17296
|
-
reg(DecorativeImagesAudit),
|
|
17297
15932
|
reg(FigureFigcaptionAudit),
|
|
17298
15933
|
reg(SvgBloatAudit),
|
|
17299
15934
|
reg(TokenRatioAudit),
|
|
17300
15935
|
reg(FakeHeadingsAudit)
|
|
17301
15936
|
],
|
|
17302
15937
|
accessibility: [
|
|
17303
|
-
reg(SkipNavAudit),
|
|
17304
15938
|
reg(AriaLandmarksAudit),
|
|
17305
15939
|
reg(NavAriaLabelAudit),
|
|
17306
15940
|
reg(FormErrorMessagesAudit),
|
|
@@ -17328,8 +15962,6 @@ var defaultConfig = {
|
|
|
17328
15962
|
reg(HstsHeaderAudit),
|
|
17329
15963
|
reg(CspHeaderAudit),
|
|
17330
15964
|
reg(ContentTypeOptionsAudit),
|
|
17331
|
-
reg(ReferrerPolicyAudit),
|
|
17332
|
-
reg(PermissionsPolicyAudit),
|
|
17333
15965
|
reg(SecurityTxtAudit),
|
|
17334
15966
|
reg(CorsAiFilesAudit),
|
|
17335
15967
|
reg(CorsApiRoutesAudit),
|
|
@@ -17340,11 +15972,9 @@ var defaultConfig = {
|
|
|
17340
15972
|
reg(NoRenderBlockingAudit),
|
|
17341
15973
|
reg(ImageDimensionsAudit),
|
|
17342
15974
|
reg(LcpNotLazyAudit),
|
|
17343
|
-
reg(PreconnectHintsAudit),
|
|
17344
15975
|
reg(NoBrokenAiEndpointsAudit),
|
|
17345
15976
|
reg(PrivacyPolicyAudit),
|
|
17346
|
-
reg(TermsOfServiceAudit)
|
|
17347
|
-
reg(FrameworkDetectionAudit)
|
|
15977
|
+
reg(TermsOfServiceAudit)
|
|
17348
15978
|
],
|
|
17349
15979
|
"answer-engine": [
|
|
17350
15980
|
reg(FaqSectionsAudit),
|
|
@@ -17371,7 +16001,6 @@ var defaultConfig = {
|
|
|
17371
16001
|
reg(PublicationDateAudit),
|
|
17372
16002
|
reg(LastModifiedSchemaAudit),
|
|
17373
16003
|
reg(InternalCrossLinkingAudit),
|
|
17374
|
-
reg(PaginationLinksAudit),
|
|
17375
16004
|
reg(UniqueDataAudit),
|
|
17376
16005
|
reg(BlockquoteUsageAudit),
|
|
17377
16006
|
reg(DescriptiveUrlsAudit)
|
|
@@ -17393,7 +16022,8 @@ function stubCheck(meta, tag, explanation) {
|
|
|
17393
16022
|
priority: meta.defaultPriority,
|
|
17394
16023
|
impact: meta.guidance?.impact ?? "",
|
|
17395
16024
|
fix: meta.guidance?.fix ?? "",
|
|
17396
|
-
tags: [tag]
|
|
16025
|
+
tags: [tag],
|
|
16026
|
+
deprecated: meta.deprecated
|
|
17397
16027
|
};
|
|
17398
16028
|
}
|
|
17399
16029
|
function planAudits(ctx, config) {
|
|
@@ -22915,6 +21545,34 @@ function generateScanSummary(report) {
|
|
|
22915
21545
|
return summary;
|
|
22916
21546
|
}
|
|
22917
21547
|
|
|
21548
|
+
// src/scorer.ts
|
|
21549
|
+
function isInformative(check) {
|
|
21550
|
+
return check.scoreDisplayMode === "informative";
|
|
21551
|
+
}
|
|
21552
|
+
function calculateCategoryScore(checks2) {
|
|
21553
|
+
const scored = checks2.filter((c) => c.status !== "na" && !isInformative(c));
|
|
21554
|
+
if (scored.length === 0) return 0;
|
|
21555
|
+
const total = scored.reduce((sum, c) => sum + c.score, 0);
|
|
21556
|
+
return Math.round(total / scored.length * 100);
|
|
21557
|
+
}
|
|
21558
|
+
function buildCategoryResult(id, checks2) {
|
|
21559
|
+
return {
|
|
21560
|
+
id,
|
|
21561
|
+
name: CATEGORY_NAMES[id] ?? id,
|
|
21562
|
+
weight: CATEGORY_WEIGHTS[id] ?? 0,
|
|
21563
|
+
score: calculateCategoryScore(checks2),
|
|
21564
|
+
checks: checks2,
|
|
21565
|
+
passCount: checks2.filter((c) => c.status === "pass").length,
|
|
21566
|
+
warnCount: checks2.filter((c) => c.status === "warn").length,
|
|
21567
|
+
failCount: checks2.filter((c) => c.status === "fail").length
|
|
21568
|
+
};
|
|
21569
|
+
}
|
|
21570
|
+
function calculateOverallScore(categories) {
|
|
21571
|
+
return Math.round(
|
|
21572
|
+
categories.reduce((sum, cat) => sum + cat.score * cat.weight, 0)
|
|
21573
|
+
);
|
|
21574
|
+
}
|
|
21575
|
+
|
|
22918
21576
|
// src/waf-detector.ts
|
|
22919
21577
|
function detectWafProtection(targetUrl, homepageResult, rootFiles, scannedPagesCount) {
|
|
22920
21578
|
const allResults = [];
|
|
@@ -23277,7 +21935,7 @@ async function runScan(url, options) {
|
|
|
23277
21935
|
tracker.phaseStart("report", 1);
|
|
23278
21936
|
logger.debug("[orchestrator] Phase 4: Building final report");
|
|
23279
21937
|
const durationMs = Math.round(performance.now() - start);
|
|
23280
|
-
const recommendations = allChecks.filter((c) => c.status !== "pass").slice().sort((a, b) => {
|
|
21938
|
+
const recommendations = allChecks.filter((c) => c.status !== "pass" && !isInformative(c)).slice().sort((a, b) => {
|
|
23281
21939
|
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
23282
21940
|
return (order[a.priority] ?? 3) - (order[b.priority] ?? 3);
|
|
23283
21941
|
});
|
|
@@ -23288,10 +21946,12 @@ async function runScan(url, options) {
|
|
|
23288
21946
|
weightMap.set(reg2.meta.id, reg2.meta.weight);
|
|
23289
21947
|
}
|
|
23290
21948
|
}
|
|
23291
|
-
const topPasses = allChecks.filter((c) => c.status === "pass").slice().sort(
|
|
21949
|
+
const topPasses = allChecks.filter((c) => c.status === "pass" && !isInformative(c)).slice().sort(
|
|
23292
21950
|
(a, b) => (weightMap.get(b.id) ?? 1) - (weightMap.get(a.id) ?? 1)
|
|
23293
21951
|
).slice(0, 10);
|
|
23294
|
-
const readinessVitals = calculateReadinessVitals(
|
|
21952
|
+
const readinessVitals = calculateReadinessVitals(
|
|
21953
|
+
allChecks.filter((c) => !isInformative(c))
|
|
21954
|
+
);
|
|
23295
21955
|
const readinessScore = Math.round(
|
|
23296
21956
|
readinessVitals.commerce * READINESS_WEIGHTS.commerce + readinessVitals.content * READINESS_WEIGHTS.content + readinessVitals.botAccessibility * READINESS_WEIGHTS.botAccessibility + readinessVitals.technical * READINESS_WEIGHTS.technical
|
|
23297
21957
|
);
|
|
@@ -23371,31 +22031,6 @@ function calculateReadinessVitals(checks2) {
|
|
|
23371
22031
|
};
|
|
23372
22032
|
}
|
|
23373
22033
|
|
|
23374
|
-
// src/scorer.ts
|
|
23375
|
-
function calculateCategoryScore(checks2) {
|
|
23376
|
-
const scored = checks2.filter((c) => c.status !== "na");
|
|
23377
|
-
if (scored.length === 0) return 0;
|
|
23378
|
-
const total = scored.reduce((sum, c) => sum + c.score, 0);
|
|
23379
|
-
return Math.round(total / scored.length * 100);
|
|
23380
|
-
}
|
|
23381
|
-
function buildCategoryResult(id, checks2) {
|
|
23382
|
-
return {
|
|
23383
|
-
id,
|
|
23384
|
-
name: CATEGORY_NAMES[id] ?? id,
|
|
23385
|
-
weight: CATEGORY_WEIGHTS[id] ?? 0,
|
|
23386
|
-
score: calculateCategoryScore(checks2),
|
|
23387
|
-
checks: checks2,
|
|
23388
|
-
passCount: checks2.filter((c) => c.status === "pass").length,
|
|
23389
|
-
warnCount: checks2.filter((c) => c.status === "warn").length,
|
|
23390
|
-
failCount: checks2.filter((c) => c.status === "fail").length
|
|
23391
|
-
};
|
|
23392
|
-
}
|
|
23393
|
-
function calculateOverallScore(categories) {
|
|
23394
|
-
return Math.round(
|
|
23395
|
-
categories.reduce((sum, cat) => sum + cat.score * cat.weight, 0)
|
|
23396
|
-
);
|
|
23397
|
-
}
|
|
23398
|
-
|
|
23399
22034
|
// src/types.ts
|
|
23400
22035
|
var PAGE_TYPE_LABELS = {
|
|
23401
22036
|
homepage: "Homepage",
|
|
@@ -23502,6 +22137,7 @@ export {
|
|
|
23502
22137
|
CheckResultSchema,
|
|
23503
22138
|
CheckStatusSchema,
|
|
23504
22139
|
DEFAULT_SCAN_LIMIT,
|
|
22140
|
+
DeprecationNoticeSchema,
|
|
23505
22141
|
FixEffortSchema,
|
|
23506
22142
|
MAX_CONCURRENT_REQUESTS,
|
|
23507
22143
|
MAX_PAGES_PER_SCAN,
|
|
@@ -23546,6 +22182,7 @@ export {
|
|
|
23546
22182
|
getTierColor,
|
|
23547
22183
|
getTierLabel,
|
|
23548
22184
|
getWordCount,
|
|
22185
|
+
isInformative,
|
|
23549
22186
|
isPrivateIp,
|
|
23550
22187
|
isSafeUrl,
|
|
23551
22188
|
joinUrl,
|