@forkpoint/agent-lighthouse-core 0.3.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 +177 -5
- package/dist/index.d.ts +177 -5
- package/dist/index.js +401 -1618
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +396 -1618
- package/dist/index.mjs.map +1 -1
- package/migration-map.json +110 -0
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -40,12 +40,15 @@ __export(index_exports, {
|
|
|
40
40
|
CheckResultSchema: () => CheckResultSchema,
|
|
41
41
|
CheckStatusSchema: () => CheckStatusSchema,
|
|
42
42
|
DEFAULT_SCAN_LIMIT: () => DEFAULT_SCAN_LIMIT,
|
|
43
|
+
DeprecationNoticeSchema: () => DeprecationNoticeSchema,
|
|
43
44
|
FixEffortSchema: () => FixEffortSchema,
|
|
44
45
|
MAX_CONCURRENT_REQUESTS: () => MAX_CONCURRENT_REQUESTS,
|
|
45
46
|
MAX_PAGES_PER_SCAN: () => MAX_PAGES_PER_SCAN,
|
|
46
47
|
MAX_RESPONSE_BODY_BYTES: () => MAX_RESPONSE_BODY_BYTES,
|
|
47
48
|
PAGE_TYPE_LABELS: () => PAGE_TYPE_LABELS,
|
|
49
|
+
PHASE_WEIGHTS: () => PHASE_WEIGHTS,
|
|
48
50
|
PRESETS: () => PRESETS,
|
|
51
|
+
ProgressTracker: () => ProgressTracker,
|
|
49
52
|
READINESS_WEIGHTS: () => READINESS_WEIGHTS,
|
|
50
53
|
REQUEST_TIMEOUT_MS: () => REQUEST_TIMEOUT_MS,
|
|
51
54
|
SCANNER_USER_AGENT: () => SCANNER_USER_AGENT,
|
|
@@ -82,6 +85,7 @@ __export(index_exports, {
|
|
|
82
85
|
getTierColor: () => getTierColor,
|
|
83
86
|
getTierLabel: () => getTierLabel,
|
|
84
87
|
getWordCount: () => getWordCount,
|
|
88
|
+
isInformative: () => isInformative,
|
|
85
89
|
isPrivateIp: () => isPrivateIp,
|
|
86
90
|
isSafeUrl: () => isSafeUrl,
|
|
87
91
|
joinUrl: () => joinUrl,
|
|
@@ -89,6 +93,7 @@ __export(index_exports, {
|
|
|
89
93
|
logger: () => logger,
|
|
90
94
|
normalizeUrl: () => normalizeUrl,
|
|
91
95
|
parseHtml: () => parseHtml,
|
|
96
|
+
planAudits: () => planAudits,
|
|
92
97
|
runAudits: () => runAudits,
|
|
93
98
|
runScan: () => runScan
|
|
94
99
|
});
|
|
@@ -798,6 +803,10 @@ var AuditGuidanceSchema = import_zod.z.object({
|
|
|
798
803
|
tags: import_zod.z.array(import_zod.z.string().max(50)).max(20).optional()
|
|
799
804
|
});
|
|
800
805
|
var ScoreDisplayModeSchema = import_zod.z.enum(["binary", "ternary", "informative"]);
|
|
806
|
+
var DeprecationNoticeSchema = import_zod.z.object({
|
|
807
|
+
notice: import_zod.z.string().min(1).max(500),
|
|
808
|
+
link: import_zod.z.string().url()
|
|
809
|
+
});
|
|
801
810
|
var AuditMetaSchema = import_zod.z.object({
|
|
802
811
|
id: import_zod.z.string(),
|
|
803
812
|
category: import_zod.z.string(),
|
|
@@ -805,10 +814,12 @@ var AuditMetaSchema = import_zod.z.object({
|
|
|
805
814
|
failureTitle: import_zod.z.string(),
|
|
806
815
|
description: import_zod.z.string(),
|
|
807
816
|
scoreDisplayMode: ScoreDisplayModeSchema,
|
|
808
|
-
weight
|
|
817
|
+
// Deprecated audits carry weight 0 (excluded from scoring).
|
|
818
|
+
weight: import_zod.z.number().nonnegative(),
|
|
809
819
|
applicablePageTypes: import_zod.z.array(import_zod.z.string()).optional(),
|
|
810
820
|
defaultPriority: CheckPrioritySchema,
|
|
811
|
-
guidance: AuditGuidanceSchema.optional()
|
|
821
|
+
guidance: AuditGuidanceSchema.optional(),
|
|
822
|
+
deprecated: DeprecationNoticeSchema.optional()
|
|
812
823
|
});
|
|
813
824
|
var CheckResultSchema = import_zod.z.object({
|
|
814
825
|
id: import_zod.z.string().max(20),
|
|
@@ -830,7 +841,8 @@ var CheckResultSchema = import_zod.z.object({
|
|
|
830
841
|
code: import_zod.z.string().max(1e4).optional(),
|
|
831
842
|
docsUrl: import_zod.z.string().max(2048).url().optional().or(import_zod.z.string().length(0))
|
|
832
843
|
}).optional(),
|
|
833
|
-
tags: import_zod.z.array(import_zod.z.string().max(50)).max(20).optional()
|
|
844
|
+
tags: import_zod.z.array(import_zod.z.string().max(50)).max(20).optional(),
|
|
845
|
+
deprecated: DeprecationNoticeSchema.optional()
|
|
834
846
|
});
|
|
835
847
|
|
|
836
848
|
// src/audit.ts
|
|
@@ -923,7 +935,8 @@ var Audit = class _Audit {
|
|
|
923
935
|
docsUrl: meta.guidance?.docsUrl,
|
|
924
936
|
effort: meta.guidance?.effort
|
|
925
937
|
},
|
|
926
|
-
tags: meta.guidance?.tags
|
|
938
|
+
tags: meta.guidance?.tags,
|
|
939
|
+
deprecated: meta.deprecated
|
|
927
940
|
};
|
|
928
941
|
}
|
|
929
942
|
};
|
|
@@ -2626,90 +2639,16 @@ var NoBrokenLinksAudit = class _NoBrokenLinksAudit extends Audit {
|
|
|
2626
2639
|
}
|
|
2627
2640
|
};
|
|
2628
2641
|
|
|
2629
|
-
// src/audits/content-discoverability/navigation-json.ts
|
|
2630
|
-
function isOk14(result) {
|
|
2631
|
-
return result.status === 200;
|
|
2632
|
-
}
|
|
2633
|
-
var NavigationJsonAudit = class extends Audit {
|
|
2634
|
-
static meta = {
|
|
2635
|
-
id: "1.21",
|
|
2636
|
-
category: "content-discoverability",
|
|
2637
|
-
title: "navigation.json present",
|
|
2638
|
-
failureTitle: "navigation.json present",
|
|
2639
|
-
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.",
|
|
2640
|
-
scoreDisplayMode: "binary",
|
|
2641
|
-
weight: 1,
|
|
2642
|
-
defaultPriority: "medium",
|
|
2643
|
-
guidance: {
|
|
2644
|
-
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.",
|
|
2645
|
-
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.",
|
|
2646
|
-
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}',
|
|
2647
|
-
effort: "easy",
|
|
2648
|
-
tags: ["navigation", "structured-data", "discoverability"]
|
|
2649
|
-
}
|
|
2650
|
-
};
|
|
2651
|
-
audit(ctx) {
|
|
2652
|
-
const result = ctx.rootFiles["/navigation.json"];
|
|
2653
|
-
if (!result || !isOk14(result)) {
|
|
2654
|
-
return this.fail(
|
|
2655
|
-
"No navigation.json found at the site root.",
|
|
2656
|
-
"GET /navigation.json returns 200 with valid JSON",
|
|
2657
|
-
result ? `HTTP ${result.status}` : "No response",
|
|
2658
|
-
{
|
|
2659
|
-
priority: "medium",
|
|
2660
|
-
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.",
|
|
2661
|
-
code: `{
|
|
2662
|
-
"name": "Your Site",
|
|
2663
|
-
"items": [
|
|
2664
|
-
{ "label": "Home", "url": "/" },
|
|
2665
|
-
{ "label": "Products", "url": "/products", "children": [
|
|
2666
|
-
{ "label": "Product A", "url": "/products/a" },
|
|
2667
|
-
{ "label": "Product B", "url": "/products/b" }
|
|
2668
|
-
]},
|
|
2669
|
-
{ "label": "About", "url": "/about" }
|
|
2670
|
-
]
|
|
2671
|
-
}`
|
|
2672
|
-
}
|
|
2673
|
-
);
|
|
2674
|
-
}
|
|
2675
|
-
try {
|
|
2676
|
-
JSON.parse(result.body);
|
|
2677
|
-
} catch {
|
|
2678
|
-
return this.fail(
|
|
2679
|
-
"navigation.json exists but contains invalid JSON.",
|
|
2680
|
-
"Valid JSON content",
|
|
2681
|
-
"Invalid JSON",
|
|
2682
|
-
{
|
|
2683
|
-
priority: "medium",
|
|
2684
|
-
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.).",
|
|
2685
|
-
code: `{
|
|
2686
|
-
"name": "Your Site",
|
|
2687
|
-
"items": [
|
|
2688
|
-
{ "label": "Home", "url": "/" },
|
|
2689
|
-
{ "label": "About", "url": "/about" }
|
|
2690
|
-
]
|
|
2691
|
-
}`
|
|
2692
|
-
}
|
|
2693
|
-
);
|
|
2694
|
-
}
|
|
2695
|
-
return this.pass(
|
|
2696
|
-
"navigation.json exists with valid JSON.",
|
|
2697
|
-
"HTTP 200 with valid JSON",
|
|
2698
|
-
"Valid JSON"
|
|
2699
|
-
);
|
|
2700
|
-
}
|
|
2701
|
-
};
|
|
2702
|
-
|
|
2703
2642
|
// src/audits/content-discoverability/no-orphan-pages.ts
|
|
2704
2643
|
var cheerio7 = __toESM(require("cheerio"));
|
|
2705
|
-
function
|
|
2644
|
+
function isOk14(result) {
|
|
2706
2645
|
return result.status === 200;
|
|
2707
2646
|
}
|
|
2708
2647
|
function getSitemapResult5(ctx) {
|
|
2709
2648
|
const sitemap = ctx.rootFiles["/sitemap.xml"];
|
|
2710
|
-
if (sitemap &&
|
|
2649
|
+
if (sitemap && isOk14(sitemap)) return sitemap;
|
|
2711
2650
|
const index = ctx.rootFiles["/sitemap-index.xml"];
|
|
2712
|
-
if (index &&
|
|
2651
|
+
if (index && isOk14(index)) return index;
|
|
2713
2652
|
return null;
|
|
2714
2653
|
}
|
|
2715
2654
|
var NoOrphanPagesAudit = class extends Audit {
|
|
@@ -2751,7 +2690,7 @@ var NoOrphanPagesAudit = class extends Audit {
|
|
|
2751
2690
|
}
|
|
2752
2691
|
const llmsUrls = /* @__PURE__ */ new Set();
|
|
2753
2692
|
const llmsResult = ctx.rootFiles["/llms.txt"];
|
|
2754
|
-
if (llmsResult &&
|
|
2693
|
+
if (llmsResult && isOk14(llmsResult)) {
|
|
2755
2694
|
const links = extractMarkdownLinks(llmsResult.body);
|
|
2756
2695
|
for (const link of links) {
|
|
2757
2696
|
try {
|
|
@@ -5062,82 +5001,6 @@ var SpeakableSchemaAudit = class extends Audit {
|
|
|
5062
5001
|
}
|
|
5063
5002
|
};
|
|
5064
5003
|
|
|
5065
|
-
// src/audits/structured-data/potential-action.ts
|
|
5066
|
-
function matchesAnyType3(schema, types) {
|
|
5067
|
-
return types.some((t) => {
|
|
5068
|
-
const st = schema["@type"];
|
|
5069
|
-
if (typeof st === "string") return st === t;
|
|
5070
|
-
if (Array.isArray(st)) return st.includes(t);
|
|
5071
|
-
return false;
|
|
5072
|
-
});
|
|
5073
|
-
}
|
|
5074
|
-
function allSchemas5(ctx) {
|
|
5075
|
-
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5076
|
-
}
|
|
5077
|
-
var PotentialActionAudit = class extends Audit {
|
|
5078
|
-
static meta = {
|
|
5079
|
-
id: "3.10",
|
|
5080
|
-
category: "structured-data",
|
|
5081
|
-
title: "potentialAction on service pages",
|
|
5082
|
-
failureTitle: "potentialAction on service pages",
|
|
5083
|
-
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.",
|
|
5084
|
-
scoreDisplayMode: "binary",
|
|
5085
|
-
weight: 1,
|
|
5086
|
-
applicablePageTypes: ["homepage", "product"],
|
|
5087
|
-
defaultPriority: "medium",
|
|
5088
|
-
guidance: {
|
|
5089
|
-
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.",
|
|
5090
|
-
fix: "Add a potentialAction property to your Organization or Service schema with a ContactAction, OrderAction, or BookAction type and a target URL.",
|
|
5091
|
-
code: `{
|
|
5092
|
-
"@context": "https://schema.org",
|
|
5093
|
-
"@type": "Organization",
|
|
5094
|
-
"name": "Your Company",
|
|
5095
|
-
"potentialAction": {
|
|
5096
|
-
"@type": "OrderAction",
|
|
5097
|
-
"target": "https://yoursite.com/order"
|
|
5098
|
-
}
|
|
5099
|
-
}`,
|
|
5100
|
-
effort: "easy",
|
|
5101
|
-
docsUrl: "https://schema.org/potentialAction",
|
|
5102
|
-
tags: ["json-ld", "schema", "actions", "agentic-commerce"]
|
|
5103
|
-
}
|
|
5104
|
-
};
|
|
5105
|
-
audit(ctx) {
|
|
5106
|
-
const actionTypes = ["ContactAction", "OrderAction", "BookAction"];
|
|
5107
|
-
const schemas = allSchemas5(ctx);
|
|
5108
|
-
const withAction = schemas.filter((s) => {
|
|
5109
|
-
const obj = s;
|
|
5110
|
-
const action = obj["potentialAction"];
|
|
5111
|
-
if (!action) return false;
|
|
5112
|
-
const actions = Array.isArray(action) ? action : [action];
|
|
5113
|
-
return actions.some(
|
|
5114
|
-
(a) => a && typeof a === "object" && matchesAnyType3(a, actionTypes)
|
|
5115
|
-
);
|
|
5116
|
-
});
|
|
5117
|
-
const found = withAction.length > 0;
|
|
5118
|
-
if (found) {
|
|
5119
|
-
return this.pass(
|
|
5120
|
-
`potentialAction (ContactAction/OrderAction/BookAction) found on ${withAction.length} schema(s).`,
|
|
5121
|
-
"At least one page with potentialAction (ContactAction, OrderAction, or BookAction).",
|
|
5122
|
-
`${withAction.length} schema(s) with qualifying potentialAction`
|
|
5123
|
-
);
|
|
5124
|
-
}
|
|
5125
|
-
return this.fail(
|
|
5126
|
-
"No potentialAction with ContactAction, OrderAction, or BookAction found.",
|
|
5127
|
-
"At least one page with potentialAction (ContactAction, OrderAction, or BookAction).",
|
|
5128
|
-
"None",
|
|
5129
|
-
{
|
|
5130
|
-
priority: "medium",
|
|
5131
|
-
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.",
|
|
5132
|
-
code: `"potentialAction": {
|
|
5133
|
-
"@type": "OrderAction",
|
|
5134
|
-
"target": "https://yoursite.com/order"
|
|
5135
|
-
}`
|
|
5136
|
-
}
|
|
5137
|
-
);
|
|
5138
|
-
}
|
|
5139
|
-
};
|
|
5140
|
-
|
|
5141
5004
|
// src/audits/structured-data/howto-schema.ts
|
|
5142
5005
|
function matchesType4(schema, type) {
|
|
5143
5006
|
const t = schema["@type"];
|
|
@@ -5274,7 +5137,7 @@ var HowToSchemaAudit = class extends Audit {
|
|
|
5274
5137
|
};
|
|
5275
5138
|
|
|
5276
5139
|
// src/audits/structured-data/local-business-schema.ts
|
|
5277
|
-
function
|
|
5140
|
+
function matchesAnyType3(schema, types) {
|
|
5278
5141
|
return types.some((t) => {
|
|
5279
5142
|
const st = schema["@type"];
|
|
5280
5143
|
if (typeof st === "string") return st === t;
|
|
@@ -5282,12 +5145,12 @@ function matchesAnyType4(schema, types) {
|
|
|
5282
5145
|
return false;
|
|
5283
5146
|
});
|
|
5284
5147
|
}
|
|
5285
|
-
function
|
|
5148
|
+
function allSchemas5(ctx) {
|
|
5286
5149
|
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5287
5150
|
}
|
|
5288
5151
|
function hasPostalAddressBlock(page) {
|
|
5289
5152
|
const schemas = flattenJsonLd(page.structuredData ?? page.jsonLd);
|
|
5290
|
-
if (schemas.some((s) =>
|
|
5153
|
+
if (schemas.some((s) => matchesAnyType3(s, ["PostalAddress"]))) {
|
|
5291
5154
|
return true;
|
|
5292
5155
|
}
|
|
5293
5156
|
return page.$('[itemtype*="PostalAddress"]').length > 0;
|
|
@@ -5353,9 +5216,9 @@ var LocalBusinessSchemaAudit = class extends Audit {
|
|
|
5353
5216
|
"No physical location indicators found."
|
|
5354
5217
|
);
|
|
5355
5218
|
}
|
|
5356
|
-
const schemas =
|
|
5219
|
+
const schemas = allSchemas5(ctx);
|
|
5357
5220
|
const localSchemas = schemas.filter(
|
|
5358
|
-
(s) =>
|
|
5221
|
+
(s) => matchesAnyType3(s, ["LocalBusiness", "ProfessionalService"])
|
|
5359
5222
|
);
|
|
5360
5223
|
const found = localSchemas.length > 0;
|
|
5361
5224
|
if (found) {
|
|
@@ -5385,7 +5248,7 @@ var LocalBusinessSchemaAudit = class extends Audit {
|
|
|
5385
5248
|
};
|
|
5386
5249
|
|
|
5387
5250
|
// src/audits/structured-data/review-schema.ts
|
|
5388
|
-
function
|
|
5251
|
+
function matchesAnyType4(schema, types) {
|
|
5389
5252
|
return types.some((t) => {
|
|
5390
5253
|
const st = schema["@type"];
|
|
5391
5254
|
if (typeof st === "string") return st === t;
|
|
@@ -5393,7 +5256,7 @@ function matchesAnyType5(schema, types) {
|
|
|
5393
5256
|
return false;
|
|
5394
5257
|
});
|
|
5395
5258
|
}
|
|
5396
|
-
function
|
|
5259
|
+
function allSchemas6(ctx) {
|
|
5397
5260
|
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5398
5261
|
}
|
|
5399
5262
|
function hasTestimonialContent(page) {
|
|
@@ -5462,9 +5325,9 @@ var ReviewSchemaAudit = class extends Audit {
|
|
|
5462
5325
|
}
|
|
5463
5326
|
);
|
|
5464
5327
|
}
|
|
5465
|
-
const schemas =
|
|
5328
|
+
const schemas = allSchemas6(ctx);
|
|
5466
5329
|
const reviewSchemas = schemas.filter(
|
|
5467
|
-
(s) =>
|
|
5330
|
+
(s) => matchesAnyType4(s, ["Review", "AggregateRating"])
|
|
5468
5331
|
);
|
|
5469
5332
|
const schemasWithReviewProp = schemas.filter((s) => {
|
|
5470
5333
|
const obj = s;
|
|
@@ -5496,7 +5359,7 @@ var ReviewSchemaAudit = class extends Audit {
|
|
|
5496
5359
|
};
|
|
5497
5360
|
|
|
5498
5361
|
// src/audits/structured-data/offer-schema.ts
|
|
5499
|
-
function
|
|
5362
|
+
function matchesAnyType5(schema, types) {
|
|
5500
5363
|
return types.some((t) => {
|
|
5501
5364
|
const st = schema["@type"];
|
|
5502
5365
|
if (typeof st === "string") return st === t;
|
|
@@ -5544,7 +5407,7 @@ var OfferSchemaAudit = class extends Audit {
|
|
|
5544
5407
|
const pagesWithOffer = productPages.filter((p) => {
|
|
5545
5408
|
const schemas = flattenJsonLd(p.structuredData ?? p.jsonLd);
|
|
5546
5409
|
const hasOfferSchema = schemas.some(
|
|
5547
|
-
(s) =>
|
|
5410
|
+
(s) => matchesAnyType5(s, ["Offer", "AggregateOffer"])
|
|
5548
5411
|
);
|
|
5549
5412
|
const hasOfferProp = schemas.some((s) => {
|
|
5550
5413
|
const obj = s;
|
|
@@ -5558,7 +5421,7 @@ var OfferSchemaAudit = class extends Audit {
|
|
|
5558
5421
|
});
|
|
5559
5422
|
if (hasOfferSchema) {
|
|
5560
5423
|
const first2 = schemas.find(
|
|
5561
|
-
(s) =>
|
|
5424
|
+
(s) => matchesAnyType5(s, ["Offer", "AggregateOffer"])
|
|
5562
5425
|
);
|
|
5563
5426
|
return first2 && first2["price"] !== void 0 && !!first2["priceCurrency"];
|
|
5564
5427
|
}
|
|
@@ -5617,10 +5480,10 @@ function matchesType5(schema, type) {
|
|
|
5617
5480
|
if (Array.isArray(t)) return t.includes(type);
|
|
5618
5481
|
return false;
|
|
5619
5482
|
}
|
|
5620
|
-
function
|
|
5483
|
+
function matchesAnyType6(schema, types) {
|
|
5621
5484
|
return types.some((t) => matchesType5(schema, t));
|
|
5622
5485
|
}
|
|
5623
|
-
function
|
|
5486
|
+
function allSchemas7(ctx) {
|
|
5624
5487
|
return ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5625
5488
|
}
|
|
5626
5489
|
function hasProps3(obj, keys) {
|
|
@@ -5660,13 +5523,13 @@ var AuthorSchemaAudit = class extends Audit {
|
|
|
5660
5523
|
}
|
|
5661
5524
|
};
|
|
5662
5525
|
audit(ctx) {
|
|
5663
|
-
const schemas =
|
|
5526
|
+
const schemas = allSchemas7(ctx);
|
|
5664
5527
|
const personSchemas = schemas.filter(
|
|
5665
5528
|
(s) => matchesType5(s, "Person")
|
|
5666
5529
|
);
|
|
5667
5530
|
const authorFromArticles = [];
|
|
5668
5531
|
for (const s of schemas) {
|
|
5669
|
-
if (
|
|
5532
|
+
if (matchesAnyType6(s, ["Article", "NewsArticle", "BlogPosting"])) {
|
|
5670
5533
|
const author = s["author"];
|
|
5671
5534
|
if (Array.isArray(author)) {
|
|
5672
5535
|
for (const a of author) {
|
|
@@ -5736,120 +5599,8 @@ var AuthorSchemaAudit = class extends Audit {
|
|
|
5736
5599
|
}
|
|
5737
5600
|
};
|
|
5738
5601
|
|
|
5739
|
-
// src/audits/structured-data/action-schema.ts
|
|
5740
|
-
function matchesAnyType8(schema, types) {
|
|
5741
|
-
return types.some((t) => {
|
|
5742
|
-
const st = schema["@type"];
|
|
5743
|
-
if (typeof st === "string") return st === t;
|
|
5744
|
-
if (Array.isArray(st)) return st.includes(t);
|
|
5745
|
-
return false;
|
|
5746
|
-
});
|
|
5747
|
-
}
|
|
5748
|
-
function isConfirmationUrl(url) {
|
|
5749
|
-
return /\/(thank-?you|confirmation|success|order-complete)\b/i.test(url);
|
|
5750
|
-
}
|
|
5751
|
-
var ActionSchemaAudit = class extends Audit {
|
|
5752
|
-
static meta = {
|
|
5753
|
-
id: "3.16",
|
|
5754
|
-
category: "structured-data",
|
|
5755
|
-
title: "ConfirmAction/ReserveAction schema",
|
|
5756
|
-
failureTitle: "ConfirmAction/ReserveAction schema",
|
|
5757
|
-
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.",
|
|
5758
|
-
scoreDisplayMode: "ternary",
|
|
5759
|
-
weight: 1,
|
|
5760
|
-
defaultPriority: "low",
|
|
5761
|
-
guidance: {
|
|
5762
|
-
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.",
|
|
5763
|
-
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.",
|
|
5764
|
-
code: `{
|
|
5765
|
-
"@context": "https://schema.org",
|
|
5766
|
-
"@type": "WebPage",
|
|
5767
|
-
"potentialAction": {
|
|
5768
|
-
"@type": "ConfirmAction",
|
|
5769
|
-
"target": "https://yoursite.com/confirm"
|
|
5770
|
-
}
|
|
5771
|
-
}`,
|
|
5772
|
-
effort: "moderate",
|
|
5773
|
-
docsUrl: "https://schema.org/ConfirmAction",
|
|
5774
|
-
tags: ["json-ld", "schema", "agentic-commerce", "actions"]
|
|
5775
|
-
}
|
|
5776
|
-
};
|
|
5777
|
-
audit(ctx) {
|
|
5778
|
-
const confirmationPages = ctx.pages.filter((p) => isConfirmationUrl(p.url));
|
|
5779
|
-
if (confirmationPages.length === 0) {
|
|
5780
|
-
return this.warn(
|
|
5781
|
-
"No thank-you or confirmation pages detected to evaluate.",
|
|
5782
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5783
|
-
"No confirmation pages detected (URLs containing /thank-you/, /confirmation/, /success/).",
|
|
5784
|
-
{
|
|
5785
|
-
priority: "low",
|
|
5786
|
-
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.",
|
|
5787
|
-
code: `"potentialAction": {
|
|
5788
|
-
"@type": "ConfirmAction",
|
|
5789
|
-
"target": "https://yoursite.com/confirm"
|
|
5790
|
-
}`
|
|
5791
|
-
}
|
|
5792
|
-
);
|
|
5793
|
-
}
|
|
5794
|
-
const actionTypes = ["ConfirmAction", "ReserveAction"];
|
|
5795
|
-
const pagesWithAction = confirmationPages.filter((p) => {
|
|
5796
|
-
const schemas = flattenJsonLd(p.structuredData ?? p.jsonLd);
|
|
5797
|
-
const hasActionSchema = schemas.some(
|
|
5798
|
-
(s) => matchesAnyType8(s, actionTypes)
|
|
5799
|
-
);
|
|
5800
|
-
const hasActionProp = schemas.some((s) => {
|
|
5801
|
-
const obj = s;
|
|
5802
|
-
const action = obj["potentialAction"];
|
|
5803
|
-
if (!action) return false;
|
|
5804
|
-
const actions = Array.isArray(action) ? action : [action];
|
|
5805
|
-
return actions.some(
|
|
5806
|
-
(a) => a && typeof a === "object" && matchesAnyType8(a, actionTypes)
|
|
5807
|
-
);
|
|
5808
|
-
});
|
|
5809
|
-
return hasActionSchema || hasActionProp;
|
|
5810
|
-
});
|
|
5811
|
-
const allHave = pagesWithAction.length === confirmationPages.length;
|
|
5812
|
-
const someHave = pagesWithAction.length > 0;
|
|
5813
|
-
if (allHave) {
|
|
5814
|
-
return this.pass(
|
|
5815
|
-
`ConfirmAction/ReserveAction found on all ${confirmationPages.length} confirmation page(s).`,
|
|
5816
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5817
|
-
`${pagesWithAction.length}/${confirmationPages.length} confirmation pages with action schema`
|
|
5818
|
-
);
|
|
5819
|
-
}
|
|
5820
|
-
if (someHave) {
|
|
5821
|
-
return this.warn(
|
|
5822
|
-
`ConfirmAction/ReserveAction found on ${pagesWithAction.length} of ${confirmationPages.length} confirmation page(s).`,
|
|
5823
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5824
|
-
`${pagesWithAction.length}/${confirmationPages.length} confirmation pages with action schema`,
|
|
5825
|
-
{
|
|
5826
|
-
priority: "low",
|
|
5827
|
-
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.",
|
|
5828
|
-
code: `"potentialAction": {
|
|
5829
|
-
"@type": "ConfirmAction",
|
|
5830
|
-
"target": "https://yoursite.com/confirm"
|
|
5831
|
-
}`
|
|
5832
|
-
}
|
|
5833
|
-
);
|
|
5834
|
-
}
|
|
5835
|
-
return this.fail(
|
|
5836
|
-
`No ConfirmAction/ReserveAction found on ${confirmationPages.length} confirmation page(s).`,
|
|
5837
|
-
"ConfirmAction or ReserveAction schema on thank-you/confirmation pages.",
|
|
5838
|
-
`${pagesWithAction.length}/${confirmationPages.length} confirmation pages with action schema`,
|
|
5839
|
-
{
|
|
5840
|
-
priority: "low",
|
|
5841
|
-
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.",
|
|
5842
|
-
code: `"potentialAction": {
|
|
5843
|
-
"@type": "ConfirmAction",
|
|
5844
|
-
"target": "https://yoursite.com/confirm"
|
|
5845
|
-
}`
|
|
5846
|
-
}
|
|
5847
|
-
);
|
|
5848
|
-
}
|
|
5849
|
-
};
|
|
5850
|
-
|
|
5851
5602
|
// src/audits/structured-data/product-identifiers.ts
|
|
5852
|
-
function
|
|
5603
|
+
function matchesAnyType7(schema, types) {
|
|
5853
5604
|
return types.some((t) => {
|
|
5854
5605
|
const st = schema["@type"];
|
|
5855
5606
|
if (typeof st === "string") return st === t;
|
|
@@ -5887,7 +5638,7 @@ var ProductIdentifiersAudit = class extends Audit {
|
|
|
5887
5638
|
audit(ctx) {
|
|
5888
5639
|
const schemas = ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5889
5640
|
const products = schemas.filter(
|
|
5890
|
-
(s) =>
|
|
5641
|
+
(s) => matchesAnyType7(s, [
|
|
5891
5642
|
"Product",
|
|
5892
5643
|
"IndividualProduct",
|
|
5893
5644
|
"ProductModel"
|
|
@@ -5952,7 +5703,7 @@ var ProductIdentifiersAudit = class extends Audit {
|
|
|
5952
5703
|
};
|
|
5953
5704
|
|
|
5954
5705
|
// src/audits/structured-data/product-details.ts
|
|
5955
|
-
function
|
|
5706
|
+
function matchesAnyType8(schema, types) {
|
|
5956
5707
|
return types.some((t) => {
|
|
5957
5708
|
const st = schema["@type"];
|
|
5958
5709
|
if (typeof st === "string") return st === t;
|
|
@@ -5996,7 +5747,7 @@ var ProductDetailsAudit = class extends Audit {
|
|
|
5996
5747
|
audit(ctx) {
|
|
5997
5748
|
const schemas = ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
5998
5749
|
const products = schemas.filter(
|
|
5999
|
-
(s) =>
|
|
5750
|
+
(s) => matchesAnyType8(s, [
|
|
6000
5751
|
"Product",
|
|
6001
5752
|
"IndividualProduct",
|
|
6002
5753
|
"ProductModel"
|
|
@@ -6115,7 +5866,7 @@ var ProductReviewsAudit = class extends Audit {
|
|
|
6115
5866
|
};
|
|
6116
5867
|
|
|
6117
5868
|
// src/audits/structured-data/product-transaction-certainty.ts
|
|
6118
|
-
function
|
|
5869
|
+
function matchesAnyType9(schema, types) {
|
|
6119
5870
|
return types.some((t) => {
|
|
6120
5871
|
const st = schema["@type"];
|
|
6121
5872
|
if (typeof st === "string") return st === t;
|
|
@@ -6174,7 +5925,7 @@ var ProductTransactionCertaintyAudit = class extends Audit {
|
|
|
6174
5925
|
audit(ctx) {
|
|
6175
5926
|
const schemas = ctx.pages.flatMap((p) => flattenJsonLd(p.structuredData ?? p.jsonLd));
|
|
6176
5927
|
const products = schemas.filter(
|
|
6177
|
-
(s) =>
|
|
5928
|
+
(s) => matchesAnyType9(s, [
|
|
6178
5929
|
"Product",
|
|
6179
5930
|
"IndividualProduct",
|
|
6180
5931
|
"ProductModel"
|
|
@@ -6879,53 +6630,6 @@ var LlmsTxtLinkAudit = class extends Audit {
|
|
|
6879
6630
|
}
|
|
6880
6631
|
};
|
|
6881
6632
|
|
|
6882
|
-
// src/audits/meta-tags/llms-full-txt-link.ts
|
|
6883
|
-
var LlmsFullTxtLinkAudit = class extends Audit {
|
|
6884
|
-
static meta = {
|
|
6885
|
-
id: "4.12",
|
|
6886
|
-
category: "meta-tags",
|
|
6887
|
-
title: "llms-full.txt link in head",
|
|
6888
|
-
failureTitle: "llms-full.txt link in head",
|
|
6889
|
-
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.",
|
|
6890
|
-
scoreDisplayMode: "binary",
|
|
6891
|
-
weight: 1,
|
|
6892
|
-
defaultPriority: "medium",
|
|
6893
|
-
guidance: {
|
|
6894
|
-
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.",
|
|
6895
|
-
fix: 'Create a llms-full.txt file with comprehensive content and add a <link rel="alternate"> tag in <head> pointing to it.',
|
|
6896
|
-
code: '<link rel="alternate" type="text/plain" href="/llms-full.txt" title="LLMs-full.txt">',
|
|
6897
|
-
effort: "moderate",
|
|
6898
|
-
docsUrl: "https://llmstxt.org/",
|
|
6899
|
-
tags: ["meta-tags", "llms-txt", "ai-discovery"]
|
|
6900
|
-
}
|
|
6901
|
-
};
|
|
6902
|
-
audit(ctx) {
|
|
6903
|
-
const page = ctx.pages[0];
|
|
6904
|
-
const link = page?.headLinks?.find(
|
|
6905
|
-
(l) => l.rel === "alternate" && l.type === "text/plain" && (l.title ?? "").toLowerCase().includes("llms-full")
|
|
6906
|
-
);
|
|
6907
|
-
if (link) {
|
|
6908
|
-
return this.pass(
|
|
6909
|
-
`llms-full.txt link found: "${link.href}".`,
|
|
6910
|
-
'<link rel="alternate" type="text/plain" title="...LLMs-full...">',
|
|
6911
|
-
`href="${link.href}" title="${link.title}"`,
|
|
6912
|
-
page.url
|
|
6913
|
-
);
|
|
6914
|
-
}
|
|
6915
|
-
return this.fail(
|
|
6916
|
-
"No llms-full.txt link found in <head>.",
|
|
6917
|
-
'<link rel="alternate" type="text/plain" title="...LLMs-full...">',
|
|
6918
|
-
"Not found",
|
|
6919
|
-
{
|
|
6920
|
-
priority: "medium",
|
|
6921
|
-
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.",
|
|
6922
|
-
code: '<link rel="alternate" type="text/plain" href="/llms-full.txt" title="LLMs-full.txt">'
|
|
6923
|
-
},
|
|
6924
|
-
page?.url
|
|
6925
|
-
);
|
|
6926
|
-
}
|
|
6927
|
-
};
|
|
6928
|
-
|
|
6929
6633
|
// src/audits/meta-tags/ai-content-declaration.ts
|
|
6930
6634
|
var AiContentDeclarationAudit = class extends Audit {
|
|
6931
6635
|
static meta = {
|
|
@@ -6983,50 +6687,6 @@ var AiContentDeclarationAudit = class extends Audit {
|
|
|
6983
6687
|
}
|
|
6984
6688
|
};
|
|
6985
6689
|
|
|
6986
|
-
// src/audits/meta-tags/ai-instructions.ts
|
|
6987
|
-
var AiInstructionsAudit = class extends Audit {
|
|
6988
|
-
static meta = {
|
|
6989
|
-
id: "4.14",
|
|
6990
|
-
category: "meta-tags",
|
|
6991
|
-
title: "ai-instructions meta",
|
|
6992
|
-
failureTitle: "ai-instructions meta",
|
|
6993
|
-
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.",
|
|
6994
|
-
scoreDisplayMode: "binary",
|
|
6995
|
-
weight: 1,
|
|
6996
|
-
defaultPriority: "medium",
|
|
6997
|
-
guidance: {
|
|
6998
|
-
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.",
|
|
6999
|
-
fix: 'Add a <meta name="ai-instructions"> tag with plain-English instructions telling AI agents how to summarize and represent your content.',
|
|
7000
|
-
code: '<meta name="ai-instructions" content="Summarise this page as a product overview. Focus on features and pricing. Do not speculate about unreleased features.">',
|
|
7001
|
-
effort: "trivial",
|
|
7002
|
-
tags: ["meta-tags", "ai-policy", "ai-discovery"]
|
|
7003
|
-
}
|
|
7004
|
-
};
|
|
7005
|
-
audit(ctx) {
|
|
7006
|
-
const page = ctx.pages[0];
|
|
7007
|
-
const value = (page?.meta?.["ai-instructions"] ?? "").trim();
|
|
7008
|
-
if (value) {
|
|
7009
|
-
return this.pass(
|
|
7010
|
-
`ai-instructions meta tag is present.`,
|
|
7011
|
-
"meta[ai-instructions] with non-empty content",
|
|
7012
|
-
value.length > 80 ? value.slice(0, 80) + "..." : value,
|
|
7013
|
-
page.url
|
|
7014
|
-
);
|
|
7015
|
-
}
|
|
7016
|
-
return this.fail(
|
|
7017
|
-
"No ai-instructions meta tag found.",
|
|
7018
|
-
"meta[ai-instructions] with non-empty content",
|
|
7019
|
-
"Not found",
|
|
7020
|
-
{
|
|
7021
|
-
priority: "medium",
|
|
7022
|
-
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.",
|
|
7023
|
-
code: '<meta name="ai-instructions" content="Summarise this page as a product overview. Focus on features and pricing. Do not speculate about unreleased features.">'
|
|
7024
|
-
},
|
|
7025
|
-
page?.url
|
|
7026
|
-
);
|
|
7027
|
-
}
|
|
7028
|
-
};
|
|
7029
|
-
|
|
7030
6690
|
// src/audits/meta-tags/markdown-alternate.ts
|
|
7031
6691
|
var MarkdownAlternateAudit = class extends Audit {
|
|
7032
6692
|
static meta = {
|
|
@@ -7117,94 +6777,47 @@ var RssFeedLinkAudit = class extends Audit {
|
|
|
7117
6777
|
}
|
|
7118
6778
|
};
|
|
7119
6779
|
|
|
7120
|
-
// src/audits/meta-tags/
|
|
7121
|
-
var
|
|
6780
|
+
// src/audits/meta-tags/openapi-link.ts
|
|
6781
|
+
var OpenApiLinkAudit = class extends Audit {
|
|
7122
6782
|
static meta = {
|
|
7123
|
-
id: "4.
|
|
6783
|
+
id: "4.18",
|
|
7124
6784
|
category: "meta-tags",
|
|
7125
|
-
title: "
|
|
7126
|
-
failureTitle: "
|
|
7127
|
-
description: "
|
|
6785
|
+
title: "OpenAPI spec link in head",
|
|
6786
|
+
failureTitle: "OpenAPI spec link in head",
|
|
6787
|
+
description: "AI agents use OpenAPI specifications to understand your API endpoints, parameters, and response formats. An OpenAPI link in <head> enables agents to programmatically interact with your API without manual documentation parsing, powering agentic workflows that call your services.",
|
|
7128
6788
|
scoreDisplayMode: "binary",
|
|
7129
6789
|
weight: 1,
|
|
7130
6790
|
defaultPriority: "low",
|
|
7131
6791
|
guidance: {
|
|
7132
|
-
impact: "Without an
|
|
7133
|
-
fix: 'If your site
|
|
7134
|
-
code: '<link rel="alternate" type="application/json" href="/
|
|
6792
|
+
impact: "Without an OpenAPI spec link, AI agents cannot programmatically understand your API endpoints, parameters, and response formats. This blocks agentic workflows that could call your API services on behalf of users.",
|
|
6793
|
+
fix: 'If your site has an API, create an OpenAPI specification and add a <link rel="alternate"> tag in <head> pointing to it.',
|
|
6794
|
+
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">',
|
|
7135
6795
|
effort: "complex",
|
|
7136
|
-
docsUrl: "https://
|
|
7137
|
-
tags: ["meta-tags", "
|
|
6796
|
+
docsUrl: "https://swagger.io/specification/",
|
|
6797
|
+
tags: ["meta-tags", "openapi", "api", "ai-discovery"]
|
|
7138
6798
|
}
|
|
7139
6799
|
};
|
|
7140
6800
|
audit(ctx) {
|
|
7141
6801
|
const page = ctx.pages[0];
|
|
7142
6802
|
const link = page?.headLinks?.find(
|
|
7143
|
-
(l) => l.rel === "alternate" && l.type === "application/json" && (l.title ?? "").toLowerCase().includes("
|
|
6803
|
+
(l) => l.rel === "alternate" && l.type === "application/json" && (l.title ?? "").toLowerCase().includes("openapi")
|
|
7144
6804
|
);
|
|
7145
6805
|
if (link) {
|
|
7146
6806
|
return this.pass(
|
|
7147
|
-
`
|
|
7148
|
-
'<link rel="alternate" type="application/json" title="...
|
|
6807
|
+
`OpenAPI spec link found: "${link.href}".`,
|
|
6808
|
+
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7149
6809
|
`href="${link.href}" title="${link.title}"`,
|
|
7150
6810
|
page.url
|
|
7151
6811
|
);
|
|
7152
6812
|
}
|
|
7153
6813
|
return this.fail(
|
|
7154
|
-
"No
|
|
7155
|
-
'<link rel="alternate" type="application/json" title="...
|
|
6814
|
+
"No OpenAPI spec link found in <head>.",
|
|
6815
|
+
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7156
6816
|
"Not found",
|
|
7157
6817
|
{
|
|
7158
6818
|
priority: "low",
|
|
7159
|
-
description: "
|
|
7160
|
-
code: '<link rel="alternate" type="application/json" href="/
|
|
7161
|
-
},
|
|
7162
|
-
page?.url
|
|
7163
|
-
);
|
|
7164
|
-
}
|
|
7165
|
-
};
|
|
7166
|
-
|
|
7167
|
-
// src/audits/meta-tags/openapi-link.ts
|
|
7168
|
-
var OpenApiLinkAudit = class extends Audit {
|
|
7169
|
-
static meta = {
|
|
7170
|
-
id: "4.18",
|
|
7171
|
-
category: "meta-tags",
|
|
7172
|
-
title: "OpenAPI spec link in head",
|
|
7173
|
-
failureTitle: "OpenAPI spec link in head",
|
|
7174
|
-
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.",
|
|
7175
|
-
scoreDisplayMode: "binary",
|
|
7176
|
-
weight: 1,
|
|
7177
|
-
defaultPriority: "low",
|
|
7178
|
-
guidance: {
|
|
7179
|
-
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.",
|
|
7180
|
-
fix: 'If your site has an API, create an OpenAPI specification and add a <link rel="alternate"> tag in <head> pointing to it.',
|
|
7181
|
-
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">',
|
|
7182
|
-
effort: "complex",
|
|
7183
|
-
docsUrl: "https://swagger.io/specification/",
|
|
7184
|
-
tags: ["meta-tags", "openapi", "api", "ai-discovery"]
|
|
7185
|
-
}
|
|
7186
|
-
};
|
|
7187
|
-
audit(ctx) {
|
|
7188
|
-
const page = ctx.pages[0];
|
|
7189
|
-
const link = page?.headLinks?.find(
|
|
7190
|
-
(l) => l.rel === "alternate" && l.type === "application/json" && (l.title ?? "").toLowerCase().includes("openapi")
|
|
7191
|
-
);
|
|
7192
|
-
if (link) {
|
|
7193
|
-
return this.pass(
|
|
7194
|
-
`OpenAPI spec link found: "${link.href}".`,
|
|
7195
|
-
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7196
|
-
`href="${link.href}" title="${link.title}"`,
|
|
7197
|
-
page.url
|
|
7198
|
-
);
|
|
7199
|
-
}
|
|
7200
|
-
return this.fail(
|
|
7201
|
-
"No OpenAPI spec link found in <head>.",
|
|
7202
|
-
'<link rel="alternate" type="application/json" title="...OpenAPI...">',
|
|
7203
|
-
"Not found",
|
|
7204
|
-
{
|
|
7205
|
-
priority: "low",
|
|
7206
|
-
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.",
|
|
7207
|
-
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">'
|
|
6819
|
+
description: "AI agents use OpenAPI specifications to understand your API endpoints, parameters, and response formats. An OpenAPI link in <head> enables agents to programmatically interact with your API without manual documentation parsing, powering agentic workflows that call your services.",
|
|
6820
|
+
code: '<link rel="alternate" type="application/json" href="/openapi.json" title="OpenAPI Spec">'
|
|
7208
6821
|
},
|
|
7209
6822
|
page?.url
|
|
7210
6823
|
);
|
|
@@ -7729,7 +7342,7 @@ var OpenApiOperationIdsAudit = class _OpenApiOperationIdsAudit extends Audit {
|
|
|
7729
7342
|
}
|
|
7730
7343
|
};
|
|
7731
7344
|
|
|
7732
|
-
// src/audits/agent-tools/openapi-
|
|
7345
|
+
// src/audits/agent-tools/openapi-servers.ts
|
|
7733
7346
|
function tryParseJson4(body) {
|
|
7734
7347
|
try {
|
|
7735
7348
|
return JSON.parse(body);
|
|
@@ -7748,90 +7361,6 @@ function getOpenApiSpec3(ctx) {
|
|
|
7748
7361
|
}
|
|
7749
7362
|
return void 0;
|
|
7750
7363
|
}
|
|
7751
|
-
var OpenApiAiInstructionsAudit = class _OpenApiAiInstructionsAudit extends Audit {
|
|
7752
|
-
static meta = {
|
|
7753
|
-
id: "5.4",
|
|
7754
|
-
category: "agent-tools",
|
|
7755
|
-
title: "x-ai-instructions in OpenAPI",
|
|
7756
|
-
failureTitle: "x-ai-instructions in OpenAPI",
|
|
7757
|
-
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.",
|
|
7758
|
-
scoreDisplayMode: "binary",
|
|
7759
|
-
weight: 1,
|
|
7760
|
-
defaultPriority: "medium",
|
|
7761
|
-
guidance: {
|
|
7762
|
-
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.",
|
|
7763
|
-
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.",
|
|
7764
|
-
code: `"info": {
|
|
7765
|
-
"title": "Your Site API",
|
|
7766
|
-
"version": "1.0.0",
|
|
7767
|
-
"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."
|
|
7768
|
-
}`,
|
|
7769
|
-
effort: "trivial",
|
|
7770
|
-
tags: ["openapi", "ai-instructions", "api"]
|
|
7771
|
-
}
|
|
7772
|
-
};
|
|
7773
|
-
audit(ctx) {
|
|
7774
|
-
const spec = getOpenApiSpec3(ctx);
|
|
7775
|
-
if (!spec) {
|
|
7776
|
-
return this.fail(
|
|
7777
|
-
"No parseable OpenAPI JSON spec found.",
|
|
7778
|
-
"info object has x-ai-instructions field",
|
|
7779
|
-
"No spec",
|
|
7780
|
-
{
|
|
7781
|
-
priority: "medium",
|
|
7782
|
-
description: _OpenApiAiInstructionsAudit.meta.description,
|
|
7783
|
-
code: `"info": {
|
|
7784
|
-
"title": "Your Site API",
|
|
7785
|
-
"version": "1.0.0",
|
|
7786
|
-
"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."
|
|
7787
|
-
}`
|
|
7788
|
-
}
|
|
7789
|
-
);
|
|
7790
|
-
}
|
|
7791
|
-
const info = spec["info"];
|
|
7792
|
-
if (isObject4(info) && typeof info["x-ai-instructions"] === "string" && info["x-ai-instructions"]) {
|
|
7793
|
-
return this.pass(
|
|
7794
|
-
"OpenAPI info object contains x-ai-instructions.",
|
|
7795
|
-
"info object has x-ai-instructions field",
|
|
7796
|
-
"x-ai-instructions present"
|
|
7797
|
-
);
|
|
7798
|
-
}
|
|
7799
|
-
return this.fail(
|
|
7800
|
-
"OpenAPI info object does not contain x-ai-instructions.",
|
|
7801
|
-
"info object has x-ai-instructions field",
|
|
7802
|
-
"x-ai-instructions missing",
|
|
7803
|
-
{
|
|
7804
|
-
priority: "medium",
|
|
7805
|
-
description: _OpenApiAiInstructionsAudit.meta.description,
|
|
7806
|
-
code: `"info": {
|
|
7807
|
-
"title": "Your Site API",
|
|
7808
|
-
"version": "1.0.0",
|
|
7809
|
-
"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."
|
|
7810
|
-
}`
|
|
7811
|
-
}
|
|
7812
|
-
);
|
|
7813
|
-
}
|
|
7814
|
-
};
|
|
7815
|
-
|
|
7816
|
-
// src/audits/agent-tools/openapi-servers.ts
|
|
7817
|
-
function tryParseJson5(body) {
|
|
7818
|
-
try {
|
|
7819
|
-
return JSON.parse(body);
|
|
7820
|
-
} catch {
|
|
7821
|
-
return void 0;
|
|
7822
|
-
}
|
|
7823
|
-
}
|
|
7824
|
-
function isObject5(val) {
|
|
7825
|
-
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
7826
|
-
}
|
|
7827
|
-
function getOpenApiSpec4(ctx) {
|
|
7828
|
-
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
7829
|
-
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
7830
|
-
const parsed = tryParseJson5(jsonResult.body);
|
|
7831
|
-
if (isObject5(parsed)) return parsed;
|
|
7832
|
-
}
|
|
7833
|
-
return void 0;
|
|
7834
|
-
}
|
|
7835
7364
|
var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
7836
7365
|
static meta = {
|
|
7837
7366
|
id: "5.5",
|
|
@@ -7857,7 +7386,7 @@ var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
|
7857
7386
|
}
|
|
7858
7387
|
};
|
|
7859
7388
|
async audit(ctx) {
|
|
7860
|
-
const spec =
|
|
7389
|
+
const spec = getOpenApiSpec3(ctx);
|
|
7861
7390
|
if (!spec) {
|
|
7862
7391
|
return this.fail(
|
|
7863
7392
|
"No parseable OpenAPI JSON spec found.",
|
|
@@ -7894,7 +7423,7 @@ var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
|
7894
7423
|
);
|
|
7895
7424
|
}
|
|
7896
7425
|
const firstWithUrl = servers.find(
|
|
7897
|
-
(s) =>
|
|
7426
|
+
(s) => isObject4(s) && typeof s["url"] === "string" && s["url"]
|
|
7898
7427
|
);
|
|
7899
7428
|
if (!firstWithUrl) {
|
|
7900
7429
|
return this.fail(
|
|
@@ -7959,34 +7488,34 @@ var OpenApiServersAudit = class _OpenApiServersAudit extends Audit {
|
|
|
7959
7488
|
};
|
|
7960
7489
|
|
|
7961
7490
|
// src/audits/agent-tools/openapi-schemas.ts
|
|
7962
|
-
function
|
|
7491
|
+
function tryParseJson5(body) {
|
|
7963
7492
|
try {
|
|
7964
7493
|
return JSON.parse(body);
|
|
7965
7494
|
} catch {
|
|
7966
7495
|
return void 0;
|
|
7967
7496
|
}
|
|
7968
7497
|
}
|
|
7969
|
-
function
|
|
7498
|
+
function isObject5(val) {
|
|
7970
7499
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
7971
7500
|
}
|
|
7972
7501
|
var HTTP_METHODS3 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
7973
|
-
function
|
|
7502
|
+
function getOpenApiSpec4(ctx) {
|
|
7974
7503
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
7975
7504
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
7976
|
-
const parsed =
|
|
7977
|
-
if (
|
|
7505
|
+
const parsed = tryParseJson5(jsonResult.body);
|
|
7506
|
+
if (isObject5(parsed)) return parsed;
|
|
7978
7507
|
}
|
|
7979
7508
|
return void 0;
|
|
7980
7509
|
}
|
|
7981
7510
|
function getOperations3(spec) {
|
|
7982
7511
|
const paths = spec["paths"];
|
|
7983
|
-
if (!
|
|
7512
|
+
if (!isObject5(paths)) return [];
|
|
7984
7513
|
const ops = [];
|
|
7985
7514
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
7986
|
-
if (!
|
|
7515
|
+
if (!isObject5(pathItem)) continue;
|
|
7987
7516
|
for (const method of HTTP_METHODS3) {
|
|
7988
7517
|
const op = pathItem[method];
|
|
7989
|
-
if (
|
|
7518
|
+
if (isObject5(op)) {
|
|
7990
7519
|
ops.push({ path, method, op });
|
|
7991
7520
|
}
|
|
7992
7521
|
}
|
|
@@ -8047,7 +7576,7 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
8047
7576
|
}
|
|
8048
7577
|
};
|
|
8049
7578
|
audit(ctx) {
|
|
8050
|
-
const spec =
|
|
7579
|
+
const spec = getOpenApiSpec4(ctx);
|
|
8051
7580
|
if (!spec) {
|
|
8052
7581
|
return this.fail(
|
|
8053
7582
|
"No parseable OpenAPI JSON spec found.",
|
|
@@ -8148,11 +7677,11 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
8148
7677
|
if (["post", "put", "patch"].includes(method)) {
|
|
8149
7678
|
writeMethods++;
|
|
8150
7679
|
const rb = op["requestBody"];
|
|
8151
|
-
if (
|
|
7680
|
+
if (isObject5(rb)) {
|
|
8152
7681
|
const content = rb["content"];
|
|
8153
|
-
if (
|
|
7682
|
+
if (isObject5(content)) {
|
|
8154
7683
|
for (const mediaType of Object.values(content)) {
|
|
8155
|
-
if (
|
|
7684
|
+
if (isObject5(mediaType) && mediaType["schema"]) {
|
|
8156
7685
|
withRequestSchema++;
|
|
8157
7686
|
break;
|
|
8158
7687
|
}
|
|
@@ -8161,13 +7690,13 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
8161
7690
|
}
|
|
8162
7691
|
}
|
|
8163
7692
|
const responses = op["responses"];
|
|
8164
|
-
if (
|
|
7693
|
+
if (isObject5(responses)) {
|
|
8165
7694
|
for (const resp of Object.values(responses)) {
|
|
8166
|
-
if (
|
|
7695
|
+
if (isObject5(resp)) {
|
|
8167
7696
|
const content = resp["content"];
|
|
8168
|
-
if (
|
|
7697
|
+
if (isObject5(content)) {
|
|
8169
7698
|
for (const mediaType of Object.values(content)) {
|
|
8170
|
-
if (
|
|
7699
|
+
if (isObject5(mediaType) && mediaType["schema"]) {
|
|
8171
7700
|
withResponseSchema++;
|
|
8172
7701
|
break;
|
|
8173
7702
|
}
|
|
@@ -8244,14 +7773,14 @@ var OpenApiSchemasAudit = class _OpenApiSchemasAudit extends Audit {
|
|
|
8244
7773
|
};
|
|
8245
7774
|
|
|
8246
7775
|
// src/audits/agent-tools/ai-catalog-exists.ts
|
|
8247
|
-
function
|
|
7776
|
+
function tryParseJson6(body) {
|
|
8248
7777
|
try {
|
|
8249
7778
|
return JSON.parse(body);
|
|
8250
7779
|
} catch {
|
|
8251
7780
|
return void 0;
|
|
8252
7781
|
}
|
|
8253
7782
|
}
|
|
8254
|
-
function
|
|
7783
|
+
function isObject6(val) {
|
|
8255
7784
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8256
7785
|
}
|
|
8257
7786
|
var AiCatalogExistsAudit = class _AiCatalogExistsAudit extends Audit {
|
|
@@ -8323,8 +7852,8 @@ var AiCatalogExistsAudit = class _AiCatalogExistsAudit extends Audit {
|
|
|
8323
7852
|
}
|
|
8324
7853
|
);
|
|
8325
7854
|
}
|
|
8326
|
-
const parsed =
|
|
8327
|
-
if (!
|
|
7855
|
+
const parsed = tryParseJson6(result.body);
|
|
7856
|
+
if (!isObject6(parsed)) {
|
|
8328
7857
|
return this.fail(
|
|
8329
7858
|
"ai-catalog.json is not valid JSON.",
|
|
8330
7859
|
"/.well-known/ai-catalog.json returns 200 with valid JSON containing services array",
|
|
@@ -8403,14 +7932,14 @@ var AiCatalogExistsAudit = class _AiCatalogExistsAudit extends Audit {
|
|
|
8403
7932
|
};
|
|
8404
7933
|
|
|
8405
7934
|
// src/audits/agent-tools/ai-catalog-metadata.ts
|
|
8406
|
-
function
|
|
7935
|
+
function tryParseJson7(body) {
|
|
8407
7936
|
try {
|
|
8408
7937
|
return JSON.parse(body);
|
|
8409
7938
|
} catch {
|
|
8410
7939
|
return void 0;
|
|
8411
7940
|
}
|
|
8412
7941
|
}
|
|
8413
|
-
function
|
|
7942
|
+
function isObject7(val) {
|
|
8414
7943
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8415
7944
|
}
|
|
8416
7945
|
var AiCatalogMetadataAudit = class _AiCatalogMetadataAudit extends Audit {
|
|
@@ -8463,8 +7992,8 @@ var AiCatalogMetadataAudit = class _AiCatalogMetadataAudit extends Audit {
|
|
|
8463
7992
|
}
|
|
8464
7993
|
);
|
|
8465
7994
|
}
|
|
8466
|
-
const parsed =
|
|
8467
|
-
if (!
|
|
7995
|
+
const parsed = tryParseJson7(result.body);
|
|
7996
|
+
if (!isObject7(parsed)) {
|
|
8468
7997
|
return this.fail(
|
|
8469
7998
|
"ai-catalog.json is not valid JSON.",
|
|
8470
7999
|
"Has version, name, description, capabilities, owner, contact, lastUpdated",
|
|
@@ -8537,14 +8066,14 @@ var AiCatalogMetadataAudit = class _AiCatalogMetadataAudit extends Audit {
|
|
|
8537
8066
|
};
|
|
8538
8067
|
|
|
8539
8068
|
// src/audits/agent-tools/ai-catalog-urls.ts
|
|
8540
|
-
function
|
|
8069
|
+
function tryParseJson8(body) {
|
|
8541
8070
|
try {
|
|
8542
8071
|
return JSON.parse(body);
|
|
8543
8072
|
} catch {
|
|
8544
8073
|
return void 0;
|
|
8545
8074
|
}
|
|
8546
8075
|
}
|
|
8547
|
-
function
|
|
8076
|
+
function isObject8(val) {
|
|
8548
8077
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8549
8078
|
}
|
|
8550
8079
|
var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
@@ -8593,8 +8122,8 @@ var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
|
8593
8122
|
}
|
|
8594
8123
|
);
|
|
8595
8124
|
}
|
|
8596
|
-
const parsed =
|
|
8597
|
-
if (!
|
|
8125
|
+
const parsed = tryParseJson8(result.body);
|
|
8126
|
+
if (!isObject8(parsed) || !Array.isArray(parsed["services"])) {
|
|
8598
8127
|
return this.fail(
|
|
8599
8128
|
"ai-catalog.json has no services array.",
|
|
8600
8129
|
"Each service URL returns HTTP 200",
|
|
@@ -8616,7 +8145,7 @@ var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
|
8616
8145
|
const services = parsed["services"];
|
|
8617
8146
|
const urls = [];
|
|
8618
8147
|
for (const svc of services) {
|
|
8619
|
-
if (
|
|
8148
|
+
if (isObject8(svc) && typeof svc["url"] === "string" && svc["url"]) {
|
|
8620
8149
|
urls.push(svc["url"]);
|
|
8621
8150
|
}
|
|
8622
8151
|
}
|
|
@@ -8688,14 +8217,14 @@ var AiCatalogUrlsAudit = class _AiCatalogUrlsAudit extends Audit {
|
|
|
8688
8217
|
};
|
|
8689
8218
|
|
|
8690
8219
|
// src/audits/agent-tools/agents-json.ts
|
|
8691
|
-
function
|
|
8220
|
+
function tryParseJson9(body) {
|
|
8692
8221
|
try {
|
|
8693
8222
|
return JSON.parse(body);
|
|
8694
8223
|
} catch {
|
|
8695
8224
|
return void 0;
|
|
8696
8225
|
}
|
|
8697
8226
|
}
|
|
8698
|
-
function
|
|
8227
|
+
function isObject9(val) {
|
|
8699
8228
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8700
8229
|
}
|
|
8701
8230
|
var AgentsJsonAudit = class _AgentsJsonAudit extends Audit {
|
|
@@ -8763,8 +8292,8 @@ var AgentsJsonAudit = class _AgentsJsonAudit extends Audit {
|
|
|
8763
8292
|
}
|
|
8764
8293
|
);
|
|
8765
8294
|
}
|
|
8766
|
-
const parsed =
|
|
8767
|
-
if (!
|
|
8295
|
+
const parsed = tryParseJson9(result.body);
|
|
8296
|
+
if (!isObject9(parsed) && !Array.isArray(parsed)) {
|
|
8768
8297
|
return this.fail(
|
|
8769
8298
|
"agents.json is not valid JSON.",
|
|
8770
8299
|
"/.well-known/agents.json returns 200 with valid JSON",
|
|
@@ -8802,160 +8331,15 @@ var AgentsJsonAudit = class _AgentsJsonAudit extends Audit {
|
|
|
8802
8331
|
}
|
|
8803
8332
|
};
|
|
8804
8333
|
|
|
8805
|
-
// src/audits/agent-tools/ai-plugin-json.ts
|
|
8806
|
-
function tryParseJson11(body) {
|
|
8807
|
-
try {
|
|
8808
|
-
return JSON.parse(body);
|
|
8809
|
-
} catch {
|
|
8810
|
-
return void 0;
|
|
8811
|
-
}
|
|
8812
|
-
}
|
|
8813
|
-
function isObject11(val) {
|
|
8814
|
-
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8815
|
-
}
|
|
8816
|
-
var AiPluginJsonAudit = class _AiPluginJsonAudit extends Audit {
|
|
8817
|
-
static meta = {
|
|
8818
|
-
id: "5.11",
|
|
8819
|
-
category: "agent-tools",
|
|
8820
|
-
title: "ai-plugin.json exists",
|
|
8821
|
-
failureTitle: "ai-plugin.json exists",
|
|
8822
|
-
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.",
|
|
8823
|
-
scoreDisplayMode: "ternary",
|
|
8824
|
-
weight: 1,
|
|
8825
|
-
defaultPriority: "medium",
|
|
8826
|
-
guidance: {
|
|
8827
|
-
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.",
|
|
8828
|
-
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.",
|
|
8829
|
-
code: `// /.well-known/ai-plugin.json
|
|
8830
|
-
{
|
|
8831
|
-
"schema_version": "v1",
|
|
8832
|
-
"name_for_human": "Your Site Name",
|
|
8833
|
-
"name_for_model": "your_site",
|
|
8834
|
-
"description_for_human": "What your site does for users.",
|
|
8835
|
-
"description_for_model": "Use this plugin to search content and submit inquiries.",
|
|
8836
|
-
"auth": { "type": "none" },
|
|
8837
|
-
"api": {
|
|
8838
|
-
"type": "openapi",
|
|
8839
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8840
|
-
},
|
|
8841
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8842
|
-
"contact_email": "hello@yoursite.com"
|
|
8843
|
-
}`,
|
|
8844
|
-
effort: "easy",
|
|
8845
|
-
docsUrl: "https://platform.openai.com/docs/plugins/getting-started/plugin-manifest",
|
|
8846
|
-
tags: ["ai-plugin", "chatgpt", "discovery", "agent-protocol"]
|
|
8847
|
-
}
|
|
8848
|
-
};
|
|
8849
|
-
audit(ctx) {
|
|
8850
|
-
const result = ctx.rootFiles["/.well-known/ai-plugin.json"];
|
|
8851
|
-
if (!result || result.status !== 200 || !result.body) {
|
|
8852
|
-
return this.fail(
|
|
8853
|
-
"/.well-known/ai-plugin.json not found or not accessible.",
|
|
8854
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8855
|
-
result ? `HTTP ${result.status}` : "Not fetched",
|
|
8856
|
-
{
|
|
8857
|
-
priority: "medium",
|
|
8858
|
-
description: _AiPluginJsonAudit.meta.description,
|
|
8859
|
-
code: `// /.well-known/ai-plugin.json
|
|
8860
|
-
{
|
|
8861
|
-
"schema_version": "v1",
|
|
8862
|
-
"name_for_human": "Your Site Name",
|
|
8863
|
-
"name_for_model": "your_site",
|
|
8864
|
-
"description_for_human": "What your site does for users.",
|
|
8865
|
-
"description_for_model": "Use this plugin to search content, submit inquiries, and get product details from Your Site.",
|
|
8866
|
-
"auth": { "type": "none" },
|
|
8867
|
-
"api": {
|
|
8868
|
-
"type": "openapi",
|
|
8869
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8870
|
-
},
|
|
8871
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8872
|
-
"contact_email": "hello@yoursite.com"
|
|
8873
|
-
}`
|
|
8874
|
-
}
|
|
8875
|
-
);
|
|
8876
|
-
}
|
|
8877
|
-
const parsed = tryParseJson11(result.body);
|
|
8878
|
-
if (!isObject11(parsed)) {
|
|
8879
|
-
return this.fail(
|
|
8880
|
-
"ai-plugin.json is not valid JSON.",
|
|
8881
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8882
|
-
"Invalid JSON",
|
|
8883
|
-
{
|
|
8884
|
-
priority: "medium",
|
|
8885
|
-
description: _AiPluginJsonAudit.meta.description,
|
|
8886
|
-
code: `// /.well-known/ai-plugin.json
|
|
8887
|
-
{
|
|
8888
|
-
"schema_version": "v1",
|
|
8889
|
-
"name_for_human": "Your Site Name",
|
|
8890
|
-
"name_for_model": "your_site",
|
|
8891
|
-
"description_for_human": "What your site does for users.",
|
|
8892
|
-
"description_for_model": "Use this plugin to search content, submit inquiries, and get product details from Your Site.",
|
|
8893
|
-
"auth": { "type": "none" },
|
|
8894
|
-
"api": {
|
|
8895
|
-
"type": "openapi",
|
|
8896
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8897
|
-
},
|
|
8898
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8899
|
-
"contact_email": "hello@yoursite.com"
|
|
8900
|
-
}`
|
|
8901
|
-
}
|
|
8902
|
-
);
|
|
8903
|
-
}
|
|
8904
|
-
const requiredFields = ["schema_version", "name_for_human", "name_for_model"];
|
|
8905
|
-
const missing = requiredFields.filter((f) => typeof parsed[f] !== "string" || !parsed[f]);
|
|
8906
|
-
if (missing.length === 0) {
|
|
8907
|
-
return this.pass(
|
|
8908
|
-
"ai-plugin.json found with all required fields.",
|
|
8909
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8910
|
-
// requiredFields guarantees these three are non-empty strings here.
|
|
8911
|
-
`schema_version=${parsed["schema_version"]}, name_for_human=${parsed["name_for_human"]}, name_for_model=${parsed["name_for_model"]}`
|
|
8912
|
-
);
|
|
8913
|
-
}
|
|
8914
|
-
const recommendation = {
|
|
8915
|
-
priority: "medium",
|
|
8916
|
-
description: _AiPluginJsonAudit.meta.description,
|
|
8917
|
-
code: `// /.well-known/ai-plugin.json
|
|
8918
|
-
{
|
|
8919
|
-
"schema_version": "v1",
|
|
8920
|
-
"name_for_human": "Your Site Name",
|
|
8921
|
-
"name_for_model": "your_site",
|
|
8922
|
-
"description_for_human": "What your site does for users.",
|
|
8923
|
-
"description_for_model": "Use this plugin to search content, submit inquiries, and get product details from Your Site.",
|
|
8924
|
-
"auth": { "type": "none" },
|
|
8925
|
-
"api": {
|
|
8926
|
-
"type": "openapi",
|
|
8927
|
-
"url": "https://yoursite.com/openapi.json"
|
|
8928
|
-
},
|
|
8929
|
-
"logo_url": "https://yoursite.com/logo.png",
|
|
8930
|
-
"contact_email": "hello@yoursite.com"
|
|
8931
|
-
}`
|
|
8932
|
-
};
|
|
8933
|
-
if (missing.length < requiredFields.length) {
|
|
8934
|
-
return this.warn(
|
|
8935
|
-
`ai-plugin.json is missing fields: ${missing.join(", ")}.`,
|
|
8936
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8937
|
-
`Missing: ${missing.join(", ")}`,
|
|
8938
|
-
recommendation
|
|
8939
|
-
);
|
|
8940
|
-
}
|
|
8941
|
-
return this.fail(
|
|
8942
|
-
`ai-plugin.json is missing all required fields: ${missing.join(", ")}.`,
|
|
8943
|
-
"/.well-known/ai-plugin.json returns 200 with valid JSON containing schema_version, name_for_human, name_for_model",
|
|
8944
|
-
`Missing: ${missing.join(", ")}`,
|
|
8945
|
-
recommendation
|
|
8946
|
-
);
|
|
8947
|
-
}
|
|
8948
|
-
};
|
|
8949
|
-
|
|
8950
8334
|
// src/audits/agent-tools/mcp-discovery.ts
|
|
8951
|
-
function
|
|
8335
|
+
function tryParseJson10(body) {
|
|
8952
8336
|
try {
|
|
8953
8337
|
return JSON.parse(body);
|
|
8954
8338
|
} catch {
|
|
8955
8339
|
return void 0;
|
|
8956
8340
|
}
|
|
8957
8341
|
}
|
|
8958
|
-
function
|
|
8342
|
+
function isObject10(val) {
|
|
8959
8343
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
8960
8344
|
}
|
|
8961
8345
|
var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
@@ -8994,8 +8378,8 @@ var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
|
8994
8378
|
audit(ctx) {
|
|
8995
8379
|
const result = ctx.rootFiles["/.well-known/mcp/servers.json"];
|
|
8996
8380
|
if (result && result.status === 200 && result.body) {
|
|
8997
|
-
const parsed =
|
|
8998
|
-
if (!
|
|
8381
|
+
const parsed = tryParseJson10(result.body);
|
|
8382
|
+
if (!isObject10(parsed)) {
|
|
8999
8383
|
return this.fail(
|
|
9000
8384
|
"mcp/servers.json is not valid JSON.",
|
|
9001
8385
|
"/.well-known/mcp/servers.json returns 200 with valid JSON containing servers array",
|
|
@@ -9028,8 +8412,8 @@ var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
|
9028
8412
|
}
|
|
9029
8413
|
const ucpResult = ctx.rootFiles["/.well-known/ucp"];
|
|
9030
8414
|
if (ucpResult && ucpResult.status === 200 && ucpResult.body) {
|
|
9031
|
-
const ucpParsed =
|
|
9032
|
-
if (
|
|
8415
|
+
const ucpParsed = tryParseJson10(ucpResult.body);
|
|
8416
|
+
if (isObject10(ucpParsed)) {
|
|
9033
8417
|
const ucpObj = ucpParsed["ucp"] ?? ucpParsed;
|
|
9034
8418
|
const services = ucpParsed["services"] || ucpObj["services"];
|
|
9035
8419
|
const capabilities = ucpParsed["capabilities"] || ucpObj["capabilities"];
|
|
@@ -9056,14 +8440,14 @@ var McpDiscoveryAudit = class _McpDiscoveryAudit extends Audit {
|
|
|
9056
8440
|
};
|
|
9057
8441
|
|
|
9058
8442
|
// src/audits/agent-tools/mcp-endpoint.ts
|
|
9059
|
-
function
|
|
8443
|
+
function tryParseJson11(body) {
|
|
9060
8444
|
try {
|
|
9061
8445
|
return JSON.parse(body);
|
|
9062
8446
|
} catch {
|
|
9063
8447
|
return void 0;
|
|
9064
8448
|
}
|
|
9065
8449
|
}
|
|
9066
|
-
function
|
|
8450
|
+
function isObject11(val) {
|
|
9067
8451
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9068
8452
|
}
|
|
9069
8453
|
var McpEndpointAudit = class _McpEndpointAudit extends Audit {
|
|
@@ -9113,8 +8497,8 @@ Content-Type: application/json
|
|
|
9113
8497
|
let targetEndpointUrl;
|
|
9114
8498
|
const result = ctx.rootFiles["/.well-known/mcp/servers.json"];
|
|
9115
8499
|
if (result && result.status === 200 && result.body) {
|
|
9116
|
-
const parsed =
|
|
9117
|
-
if (!
|
|
8500
|
+
const parsed = tryParseJson11(result.body);
|
|
8501
|
+
if (!isObject11(parsed) || !Array.isArray(parsed["servers"])) {
|
|
9118
8502
|
return this.fail(
|
|
9119
8503
|
"servers.json has no servers array.",
|
|
9120
8504
|
"MCP server URL responds to JSON-RPC initialize request",
|
|
@@ -9127,8 +8511,8 @@ Content-Type: application/json
|
|
|
9127
8511
|
);
|
|
9128
8512
|
}
|
|
9129
8513
|
const servers = parsed["servers"];
|
|
9130
|
-
const serverUrl = servers.find((s) =>
|
|
9131
|
-
if (!serverUrl || !
|
|
8514
|
+
const serverUrl = servers.find((s) => isObject11(s) && typeof s["url"] === "string" && s["url"]);
|
|
8515
|
+
if (!serverUrl || !isObject11(serverUrl)) {
|
|
9132
8516
|
return this.fail(
|
|
9133
8517
|
"No server URL found in servers.json.",
|
|
9134
8518
|
"MCP server URL responds to JSON-RPC initialize request",
|
|
@@ -9145,8 +8529,8 @@ Content-Type: application/json
|
|
|
9145
8529
|
if (!targetEndpointUrl) {
|
|
9146
8530
|
const ucpResult = ctx.rootFiles["/.well-known/ucp"];
|
|
9147
8531
|
if (ucpResult && ucpResult.status === 200 && ucpResult.body) {
|
|
9148
|
-
const ucpParsed =
|
|
9149
|
-
if (
|
|
8532
|
+
const ucpParsed = tryParseJson11(ucpResult.body);
|
|
8533
|
+
if (isObject11(ucpParsed)) {
|
|
9150
8534
|
const ucpObj = ucpParsed["ucp"] ?? ucpParsed;
|
|
9151
8535
|
const services = ucpParsed["services"] || ucpObj["services"];
|
|
9152
8536
|
if (services) {
|
|
@@ -9154,7 +8538,7 @@ Content-Type: application/json
|
|
|
9154
8538
|
const svcList = services[key];
|
|
9155
8539
|
if (Array.isArray(svcList)) {
|
|
9156
8540
|
for (const svc of svcList) {
|
|
9157
|
-
if (
|
|
8541
|
+
if (isObject11(svc) && svc["transport"] === "mcp" && typeof svc["endpoint"] === "string") {
|
|
9158
8542
|
targetEndpointUrl = svc["endpoint"];
|
|
9159
8543
|
break;
|
|
9160
8544
|
}
|
|
@@ -9197,8 +8581,8 @@ Content-Type: application/json
|
|
|
9197
8581
|
contentType: "application/json"
|
|
9198
8582
|
});
|
|
9199
8583
|
if (response.status === 200) {
|
|
9200
|
-
const respBody =
|
|
9201
|
-
if (
|
|
8584
|
+
const respBody = tryParseJson11(response.body);
|
|
8585
|
+
if (isObject11(respBody) && respBody["jsonrpc"] === "2.0" && !("error" in respBody) && isObject11(respBody["result"]) && typeof respBody["result"]["protocolVersion"] === "string") {
|
|
9202
8586
|
return this.pass(
|
|
9203
8587
|
`MCP endpoint at ${url} responded with valid JSON-RPC initialize result.`,
|
|
9204
8588
|
"MCP server URL responds to JSON-RPC initialize request",
|
|
@@ -9314,14 +8698,14 @@ Content-Type: application/json
|
|
|
9314
8698
|
};
|
|
9315
8699
|
|
|
9316
8700
|
// src/audits/agent-tools/mcp-capabilities.ts
|
|
9317
|
-
function
|
|
8701
|
+
function tryParseJson12(body) {
|
|
9318
8702
|
try {
|
|
9319
8703
|
return JSON.parse(body);
|
|
9320
8704
|
} catch {
|
|
9321
8705
|
return void 0;
|
|
9322
8706
|
}
|
|
9323
8707
|
}
|
|
9324
|
-
function
|
|
8708
|
+
function isObject12(val) {
|
|
9325
8709
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9326
8710
|
}
|
|
9327
8711
|
var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
@@ -9359,8 +8743,8 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9359
8743
|
const result = ctx.rootFiles["/.well-known/mcp/servers.json"];
|
|
9360
8744
|
const ucpResult = ctx.rootFiles["/.well-known/ucp"];
|
|
9361
8745
|
if (result && result.status === 200 && result.body) {
|
|
9362
|
-
const parsed =
|
|
9363
|
-
if (!
|
|
8746
|
+
const parsed = tryParseJson12(result.body);
|
|
8747
|
+
if (!isObject12(parsed) || !Array.isArray(parsed["servers"])) {
|
|
9364
8748
|
return this.fail(
|
|
9365
8749
|
"servers.json has no servers array.",
|
|
9366
8750
|
"servers.json or MCP response declares tools, resources, or prompts",
|
|
@@ -9376,14 +8760,14 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9376
8760
|
const capabilityKeys = ["tools", "resources", "prompts"];
|
|
9377
8761
|
const foundCapabilities = [];
|
|
9378
8762
|
for (const server of servers) {
|
|
9379
|
-
if (!
|
|
8763
|
+
if (!isObject12(server)) continue;
|
|
9380
8764
|
for (const key of capabilityKeys) {
|
|
9381
8765
|
if (server[key] !== void 0 && server[key] !== false) {
|
|
9382
8766
|
foundCapabilities.push(key);
|
|
9383
8767
|
}
|
|
9384
8768
|
}
|
|
9385
8769
|
const caps = server["capabilities"];
|
|
9386
|
-
if (
|
|
8770
|
+
if (isObject12(caps)) {
|
|
9387
8771
|
for (const key of capabilityKeys) {
|
|
9388
8772
|
if (caps[key] !== void 0 && caps[key] !== false && !foundCapabilities.includes(key)) {
|
|
9389
8773
|
foundCapabilities.push(key);
|
|
@@ -9411,11 +8795,11 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9411
8795
|
);
|
|
9412
8796
|
}
|
|
9413
8797
|
if (ucpResult && ucpResult.status === 200 && ucpResult.body) {
|
|
9414
|
-
const ucpParsed =
|
|
9415
|
-
if (
|
|
8798
|
+
const ucpParsed = tryParseJson12(ucpResult.body);
|
|
8799
|
+
if (isObject12(ucpParsed)) {
|
|
9416
8800
|
const ucpObj = ucpParsed["ucp"] ?? ucpParsed;
|
|
9417
8801
|
const capabilities = ucpParsed["capabilities"] || ucpObj["capabilities"];
|
|
9418
|
-
if (capabilities &&
|
|
8802
|
+
if (capabilities && isObject12(capabilities)) {
|
|
9419
8803
|
const capNames = Object.keys(capabilities).map((cap) => cap.split(".").pop() || cap);
|
|
9420
8804
|
if (capNames.length > 0) {
|
|
9421
8805
|
const unique = [...new Set(capNames)];
|
|
@@ -9442,34 +8826,34 @@ var McpCapabilitiesAudit = class _McpCapabilitiesAudit extends Audit {
|
|
|
9442
8826
|
};
|
|
9443
8827
|
|
|
9444
8828
|
// src/audits/agent-tools/contact-form.ts
|
|
9445
|
-
function
|
|
8829
|
+
function tryParseJson13(body) {
|
|
9446
8830
|
try {
|
|
9447
8831
|
return JSON.parse(body);
|
|
9448
8832
|
} catch {
|
|
9449
8833
|
return void 0;
|
|
9450
8834
|
}
|
|
9451
8835
|
}
|
|
9452
|
-
function
|
|
8836
|
+
function isObject13(val) {
|
|
9453
8837
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9454
8838
|
}
|
|
9455
8839
|
var HTTP_METHODS4 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
9456
|
-
function
|
|
8840
|
+
function getOpenApiSpec5(ctx) {
|
|
9457
8841
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
9458
8842
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
9459
|
-
const parsed =
|
|
9460
|
-
if (
|
|
8843
|
+
const parsed = tryParseJson13(jsonResult.body);
|
|
8844
|
+
if (isObject13(parsed)) return parsed;
|
|
9461
8845
|
}
|
|
9462
8846
|
return void 0;
|
|
9463
8847
|
}
|
|
9464
8848
|
function getOperations4(spec) {
|
|
9465
8849
|
const paths = spec["paths"];
|
|
9466
|
-
if (!
|
|
8850
|
+
if (!isObject13(paths)) return [];
|
|
9467
8851
|
const ops = [];
|
|
9468
8852
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
9469
|
-
if (!
|
|
8853
|
+
if (!isObject13(pathItem)) continue;
|
|
9470
8854
|
for (const method of HTTP_METHODS4) {
|
|
9471
8855
|
const op = pathItem[method];
|
|
9472
|
-
if (
|
|
8856
|
+
if (isObject13(op)) {
|
|
9473
8857
|
ops.push({ path, method, op });
|
|
9474
8858
|
}
|
|
9475
8859
|
}
|
|
@@ -9538,7 +8922,7 @@ var ContactFormAudit = class _ContactFormAudit extends Audit {
|
|
|
9538
8922
|
}
|
|
9539
8923
|
}
|
|
9540
8924
|
}
|
|
9541
|
-
const spec =
|
|
8925
|
+
const spec = getOpenApiSpec5(ctx);
|
|
9542
8926
|
if (spec) {
|
|
9543
8927
|
const ops = getOperations4(spec);
|
|
9544
8928
|
for (const { path, method } of ops) {
|
|
@@ -9585,34 +8969,34 @@ var ContactFormAudit = class _ContactFormAudit extends Audit {
|
|
|
9585
8969
|
};
|
|
9586
8970
|
|
|
9587
8971
|
// src/audits/agent-tools/search-endpoint.ts
|
|
9588
|
-
function
|
|
8972
|
+
function tryParseJson14(body) {
|
|
9589
8973
|
try {
|
|
9590
8974
|
return JSON.parse(body);
|
|
9591
8975
|
} catch {
|
|
9592
8976
|
return void 0;
|
|
9593
8977
|
}
|
|
9594
8978
|
}
|
|
9595
|
-
function
|
|
8979
|
+
function isObject14(val) {
|
|
9596
8980
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
9597
8981
|
}
|
|
9598
8982
|
var HTTP_METHODS5 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
9599
|
-
function
|
|
8983
|
+
function getOpenApiSpec6(ctx) {
|
|
9600
8984
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
9601
8985
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
9602
|
-
const parsed =
|
|
9603
|
-
if (
|
|
8986
|
+
const parsed = tryParseJson14(jsonResult.body);
|
|
8987
|
+
if (isObject14(parsed)) return parsed;
|
|
9604
8988
|
}
|
|
9605
8989
|
return void 0;
|
|
9606
8990
|
}
|
|
9607
8991
|
function getOperations5(spec) {
|
|
9608
8992
|
const paths = spec["paths"];
|
|
9609
|
-
if (!
|
|
8993
|
+
if (!isObject14(paths)) return [];
|
|
9610
8994
|
const ops = [];
|
|
9611
8995
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
9612
|
-
if (!
|
|
8996
|
+
if (!isObject14(pathItem)) continue;
|
|
9613
8997
|
for (const method of HTTP_METHODS5) {
|
|
9614
8998
|
const op = pathItem[method];
|
|
9615
|
-
if (
|
|
8999
|
+
if (isObject14(op)) {
|
|
9616
9000
|
ops.push({ path, method, op });
|
|
9617
9001
|
}
|
|
9618
9002
|
}
|
|
@@ -9620,10 +9004,10 @@ function getOperations5(spec) {
|
|
|
9620
9004
|
return ops;
|
|
9621
9005
|
}
|
|
9622
9006
|
function findSearchActionUrl(obj) {
|
|
9623
|
-
if (!
|
|
9624
|
-
if ((obj["@type"] === "SearchAction" || obj["@type"] === "WebSite") &&
|
|
9007
|
+
if (!isObject14(obj)) return void 0;
|
|
9008
|
+
if ((obj["@type"] === "SearchAction" || obj["@type"] === "WebSite") && isObject14(obj["potentialAction"])) {
|
|
9625
9009
|
const action = obj["potentialAction"];
|
|
9626
|
-
if (action["@type"] === "SearchAction" &&
|
|
9010
|
+
if (action["@type"] === "SearchAction" && isObject14(action["target"])) {
|
|
9627
9011
|
const target = action["target"];
|
|
9628
9012
|
if (typeof target["urlTemplate"] === "string") return target["urlTemplate"];
|
|
9629
9013
|
}
|
|
@@ -9632,7 +9016,7 @@ function findSearchActionUrl(obj) {
|
|
|
9632
9016
|
}
|
|
9633
9017
|
}
|
|
9634
9018
|
if (obj["@type"] === "SearchAction") {
|
|
9635
|
-
if (
|
|
9019
|
+
if (isObject14(obj["target"])) {
|
|
9636
9020
|
const target = obj["target"];
|
|
9637
9021
|
if (typeof target["urlTemplate"] === "string") return target["urlTemplate"];
|
|
9638
9022
|
}
|
|
@@ -9753,7 +9137,7 @@ var SearchEndpointAudit = class _SearchEndpointAudit extends Audit {
|
|
|
9753
9137
|
}
|
|
9754
9138
|
}
|
|
9755
9139
|
}
|
|
9756
|
-
const spec =
|
|
9140
|
+
const spec = getOpenApiSpec6(ctx);
|
|
9757
9141
|
if (spec) {
|
|
9758
9142
|
const ops = getOperations5(spec);
|
|
9759
9143
|
for (const { path, method } of ops) {
|
|
@@ -9794,96 +9178,6 @@ var SearchEndpointAudit = class _SearchEndpointAudit extends Audit {
|
|
|
9794
9178
|
}
|
|
9795
9179
|
};
|
|
9796
9180
|
|
|
9797
|
-
// src/audits/agent-tools/data-action-ctas.ts
|
|
9798
|
-
var DataActionCtasAudit = class _DataActionCtasAudit extends Audit {
|
|
9799
|
-
static meta = {
|
|
9800
|
-
id: "5.17",
|
|
9801
|
-
category: "agent-tools",
|
|
9802
|
-
title: "data-action attributes on CTAs",
|
|
9803
|
-
failureTitle: "data-action attributes on CTAs",
|
|
9804
|
-
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.",
|
|
9805
|
-
scoreDisplayMode: "ternary",
|
|
9806
|
-
weight: 1,
|
|
9807
|
-
defaultPriority: "low",
|
|
9808
|
-
guidance: {
|
|
9809
|
-
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.',
|
|
9810
|
-
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").',
|
|
9811
|
-
code: `<button data-action="book-demo" data-action-type="conversion"
|
|
9812
|
-
data-action-label="Book a Demo">
|
|
9813
|
-
Book a Demo
|
|
9814
|
-
</button>
|
|
9815
|
-
|
|
9816
|
-
<a href="/pricing" data-action="view-pricing" data-action-type="navigation"
|
|
9817
|
-
data-action-label="See Pricing">
|
|
9818
|
-
See Pricing
|
|
9819
|
-
</a>`,
|
|
9820
|
-
effort: "easy",
|
|
9821
|
-
tags: ["html", "cta", "browser-agent", "accessibility"]
|
|
9822
|
-
}
|
|
9823
|
-
};
|
|
9824
|
-
audit(ctx) {
|
|
9825
|
-
let totalDataAction = 0;
|
|
9826
|
-
let totalDataActionType = 0;
|
|
9827
|
-
let foundPage = "";
|
|
9828
|
-
for (const page of ctx.pages) {
|
|
9829
|
-
const withDataAction = page.$("[data-action]");
|
|
9830
|
-
const withDataActionType = page.$("[data-action-type]");
|
|
9831
|
-
if (withDataAction.length > 0 || withDataActionType.length > 0) {
|
|
9832
|
-
totalDataAction += withDataAction.length;
|
|
9833
|
-
totalDataActionType += withDataActionType.length;
|
|
9834
|
-
if (!foundPage) foundPage = page.url;
|
|
9835
|
-
}
|
|
9836
|
-
}
|
|
9837
|
-
if (totalDataAction > 0 && totalDataActionType > 0) {
|
|
9838
|
-
return this.pass(
|
|
9839
|
-
`Found ${totalDataAction} element(s) with data-action and ${totalDataActionType} with data-action-type.`,
|
|
9840
|
-
"Elements with data-action and data-action-type attributes",
|
|
9841
|
-
`${totalDataAction} data-action, ${totalDataActionType} data-action-type`,
|
|
9842
|
-
foundPage
|
|
9843
|
-
);
|
|
9844
|
-
}
|
|
9845
|
-
if (totalDataAction > 0 || totalDataActionType > 0) {
|
|
9846
|
-
return this.warn(
|
|
9847
|
-
`Partial data-action markup: ${totalDataAction} data-action, ${totalDataActionType} data-action-type.`,
|
|
9848
|
-
"Elements with data-action and data-action-type attributes",
|
|
9849
|
-
`${totalDataAction} data-action, ${totalDataActionType} data-action-type`,
|
|
9850
|
-
{
|
|
9851
|
-
priority: "low",
|
|
9852
|
-
description: _DataActionCtasAudit.meta.description,
|
|
9853
|
-
code: `<button data-action="book-demo" data-action-type="conversion"
|
|
9854
|
-
data-action-label="Book a Demo">
|
|
9855
|
-
Book a Demo
|
|
9856
|
-
</button>
|
|
9857
|
-
|
|
9858
|
-
<a href="/pricing" data-action="view-pricing" data-action-type="navigation"
|
|
9859
|
-
data-action-label="See Pricing">
|
|
9860
|
-
See Pricing
|
|
9861
|
-
</a>`
|
|
9862
|
-
},
|
|
9863
|
-
foundPage
|
|
9864
|
-
);
|
|
9865
|
-
}
|
|
9866
|
-
return this.fail(
|
|
9867
|
-
"No elements with data-action or data-action-type attributes found.",
|
|
9868
|
-
"Elements with data-action and data-action-type attributes",
|
|
9869
|
-
"None",
|
|
9870
|
-
{
|
|
9871
|
-
priority: "low",
|
|
9872
|
-
description: _DataActionCtasAudit.meta.description,
|
|
9873
|
-
code: `<button data-action="book-demo" data-action-type="conversion"
|
|
9874
|
-
data-action-label="Book a Demo">
|
|
9875
|
-
Book a Demo
|
|
9876
|
-
</button>
|
|
9877
|
-
|
|
9878
|
-
<a href="/pricing" data-action="view-pricing" data-action-type="navigation"
|
|
9879
|
-
data-action-label="See Pricing">
|
|
9880
|
-
See Pricing
|
|
9881
|
-
</a>`
|
|
9882
|
-
}
|
|
9883
|
-
);
|
|
9884
|
-
}
|
|
9885
|
-
};
|
|
9886
|
-
|
|
9887
9181
|
// src/audits/agent-tools/no-blocking-captcha.ts
|
|
9888
9182
|
var CAPTCHA_PATTERNS = [
|
|
9889
9183
|
"recaptcha",
|
|
@@ -10054,14 +9348,14 @@ var FormsNoJsAudit = class _FormsNoJsAudit extends Audit {
|
|
|
10054
9348
|
};
|
|
10055
9349
|
|
|
10056
9350
|
// src/audits/agent-tools/webmcp-manifest.ts
|
|
10057
|
-
function
|
|
9351
|
+
function tryParseJson15(body) {
|
|
10058
9352
|
try {
|
|
10059
9353
|
return JSON.parse(body);
|
|
10060
9354
|
} catch {
|
|
10061
9355
|
return void 0;
|
|
10062
9356
|
}
|
|
10063
9357
|
}
|
|
10064
|
-
function
|
|
9358
|
+
function isObject15(val) {
|
|
10065
9359
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10066
9360
|
}
|
|
10067
9361
|
var WebmcpManifestAudit = class extends Audit {
|
|
@@ -10111,8 +9405,8 @@ var WebmcpManifestAudit = class extends Audit {
|
|
|
10111
9405
|
"high"
|
|
10112
9406
|
);
|
|
10113
9407
|
}
|
|
10114
|
-
const parsed =
|
|
10115
|
-
if (!
|
|
9408
|
+
const parsed = tryParseJson15(result.body);
|
|
9409
|
+
if (!isObject15(parsed)) {
|
|
10116
9410
|
return this.fail(
|
|
10117
9411
|
"/.well-known/webmcp is not valid JSON.",
|
|
10118
9412
|
"Valid JSON object with tools array",
|
|
@@ -10130,7 +9424,7 @@ var WebmcpManifestAudit = class extends Audit {
|
|
|
10130
9424
|
}
|
|
10131
9425
|
const rawTools = parsed["tools"];
|
|
10132
9426
|
const tools = rawTools.filter(
|
|
10133
|
-
(t) =>
|
|
9427
|
+
(t) => isObject15(t) && typeof t["name"] === "string"
|
|
10134
9428
|
);
|
|
10135
9429
|
if (tools.length === 0) {
|
|
10136
9430
|
return this.fail(
|
|
@@ -10347,13 +9641,13 @@ var WebmcpInputQualityAudit = class extends Audit {
|
|
|
10347
9641
|
};
|
|
10348
9642
|
|
|
10349
9643
|
// src/audits/agent-tools/webmcp-tool-naming.ts
|
|
10350
|
-
function
|
|
9644
|
+
function isObject16(val) {
|
|
10351
9645
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10352
9646
|
}
|
|
10353
9647
|
function asString(val) {
|
|
10354
9648
|
return typeof val === "string" ? val : "";
|
|
10355
9649
|
}
|
|
10356
|
-
function
|
|
9650
|
+
function tryParseJson16(body) {
|
|
10357
9651
|
try {
|
|
10358
9652
|
return JSON.parse(body);
|
|
10359
9653
|
} catch {
|
|
@@ -10396,10 +9690,10 @@ var WebmcpToolNamingAudit = class extends Audit {
|
|
|
10396
9690
|
const tools = [];
|
|
10397
9691
|
const manifestResult = ctx.rootFiles["/.well-known/webmcp"];
|
|
10398
9692
|
if (manifestResult?.status === 200 && manifestResult.body) {
|
|
10399
|
-
const parsed =
|
|
10400
|
-
if (
|
|
9693
|
+
const parsed = tryParseJson16(manifestResult.body);
|
|
9694
|
+
if (isObject16(parsed) && Array.isArray(parsed["tools"])) {
|
|
10401
9695
|
for (const tool of parsed["tools"]) {
|
|
10402
|
-
if (
|
|
9696
|
+
if (isObject16(tool)) {
|
|
10403
9697
|
const name = asString(tool["name"]);
|
|
10404
9698
|
tools.push({
|
|
10405
9699
|
name,
|
|
@@ -10476,14 +9770,14 @@ var WebmcpToolNamingAudit = class extends Audit {
|
|
|
10476
9770
|
};
|
|
10477
9771
|
|
|
10478
9772
|
// src/audits/agent-tools/webmcp-tool-annotations.ts
|
|
10479
|
-
function
|
|
9773
|
+
function tryParseJson17(body) {
|
|
10480
9774
|
try {
|
|
10481
9775
|
return JSON.parse(body);
|
|
10482
9776
|
} catch {
|
|
10483
9777
|
return void 0;
|
|
10484
9778
|
}
|
|
10485
9779
|
}
|
|
10486
|
-
function
|
|
9780
|
+
function isObject17(val) {
|
|
10487
9781
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10488
9782
|
}
|
|
10489
9783
|
function asString2(val) {
|
|
@@ -10546,15 +9840,15 @@ var WebmcpToolAnnotationsAudit = class extends Audit {
|
|
|
10546
9840
|
const seen = /* @__PURE__ */ new Set();
|
|
10547
9841
|
const manifestResult = ctx.rootFiles["/.well-known/webmcp"];
|
|
10548
9842
|
if (manifestResult?.status === 200 && manifestResult.body) {
|
|
10549
|
-
const parsed =
|
|
10550
|
-
if (
|
|
9843
|
+
const parsed = tryParseJson17(manifestResult.body);
|
|
9844
|
+
if (isObject17(parsed) && Array.isArray(parsed["tools"])) {
|
|
10551
9845
|
for (const tool of parsed["tools"]) {
|
|
10552
|
-
if (!
|
|
9846
|
+
if (!isObject17(tool)) continue;
|
|
10553
9847
|
const name = asString2(tool["name"]);
|
|
10554
9848
|
totalTools++;
|
|
10555
9849
|
if (name) seen.add(name);
|
|
10556
9850
|
const annotations = tool["annotations"];
|
|
10557
|
-
if (
|
|
9851
|
+
if (isObject17(annotations)) {
|
|
10558
9852
|
const found = SAFETY_ANNOTATIONS.filter((a) => a in annotations);
|
|
10559
9853
|
if (found.length > 0) {
|
|
10560
9854
|
toolsWithAnnotations++;
|
|
@@ -10616,203 +9910,24 @@ var WebmcpToolAnnotationsAudit = class extends Audit {
|
|
|
10616
9910
|
}
|
|
10617
9911
|
};
|
|
10618
9912
|
|
|
10619
|
-
// src/audits/agent-tools/webmcp-action-coverage.ts
|
|
10620
|
-
function tryParseJson20(body) {
|
|
10621
|
-
try {
|
|
10622
|
-
return JSON.parse(body);
|
|
10623
|
-
} catch {
|
|
10624
|
-
return void 0;
|
|
10625
|
-
}
|
|
10626
|
-
}
|
|
10627
|
-
function isObject20(val) {
|
|
10628
|
-
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10629
|
-
}
|
|
10630
|
-
function asString3(val) {
|
|
10631
|
-
return typeof val === "string" ? val : "";
|
|
10632
|
-
}
|
|
10633
|
-
var COMMERCE_ACTIONS = [
|
|
10634
|
-
{
|
|
10635
|
-
label: "Product Search",
|
|
10636
|
-
keywords: ["search", "find", "query", "browse", "filter", "lookup", "catalog"]
|
|
10637
|
-
},
|
|
10638
|
-
{
|
|
10639
|
-
label: "Product Detail",
|
|
10640
|
-
keywords: ["product", "detail", "productdetail", "getproduct", "viewproduct", "viewitem"]
|
|
10641
|
-
},
|
|
10642
|
-
{ label: "Add to Cart", keywords: ["cart", "addtocart", "basket", "additem"] },
|
|
10643
|
-
{
|
|
10644
|
-
label: "Checkout",
|
|
10645
|
-
keywords: ["checkout", "purchase", "placeorder", "completepurchase", "buyproduct"]
|
|
10646
|
-
},
|
|
10647
|
-
{
|
|
10648
|
-
label: "Account/Auth",
|
|
10649
|
-
keywords: ["login", "register", "signup", "signin", "authenticate", "createaccount"]
|
|
10650
|
-
},
|
|
10651
|
-
{
|
|
10652
|
-
label: "Contact/Support",
|
|
10653
|
-
keywords: ["contact", "support", "inquiry", "submitinquiry", "sendmessage", "helpdesk"]
|
|
10654
|
-
}
|
|
10655
|
-
];
|
|
10656
|
-
var MIN_COVERAGE = 2;
|
|
10657
|
-
var WebmcpActionCoverageAudit = class extends Audit {
|
|
10658
|
-
static meta = {
|
|
10659
|
-
id: "5.25",
|
|
10660
|
-
category: "agent-tools",
|
|
10661
|
-
title: "WebMCP commerce action coverage",
|
|
10662
|
-
failureTitle: "WebMCP commerce action coverage",
|
|
10663
|
-
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.",
|
|
10664
|
-
scoreDisplayMode: "ternary",
|
|
10665
|
-
weight: 1,
|
|
10666
|
-
defaultPriority: "medium",
|
|
10667
|
-
guidance: {
|
|
10668
|
-
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.",
|
|
10669
|
-
fix: "Expose WebMCP tools for the key commerce actions: product search, product detail/view, add to cart, checkout/purchase, and contact/support.",
|
|
10670
|
-
code: `// /.well-known/webmcp \u2014 full commerce coverage
|
|
10671
|
-
{
|
|
10672
|
-
"tools": [
|
|
10673
|
-
{
|
|
10674
|
-
"name": "searchProducts",
|
|
10675
|
-
"description": "Search the product catalog by keyword, category, or filters",
|
|
10676
|
-
"annotations": { "readOnlyHint": true },
|
|
10677
|
-
"inputSchema": {
|
|
10678
|
-
"type": "object",
|
|
10679
|
-
"properties": {
|
|
10680
|
-
"query": { "type": "string" },
|
|
10681
|
-
"category": { "type": "string" },
|
|
10682
|
-
"maxPrice": { "type": "number" }
|
|
10683
|
-
},
|
|
10684
|
-
"required": ["query"]
|
|
10685
|
-
}
|
|
10686
|
-
},
|
|
10687
|
-
{
|
|
10688
|
-
"name": "getProductDetails",
|
|
10689
|
-
"description": "Get full details for a specific product by ID or URL",
|
|
10690
|
-
"annotations": { "readOnlyHint": true },
|
|
10691
|
-
"inputSchema": {
|
|
10692
|
-
"type": "object",
|
|
10693
|
-
"properties": { "productId": { "type": "string" } },
|
|
10694
|
-
"required": ["productId"]
|
|
10695
|
-
}
|
|
10696
|
-
},
|
|
10697
|
-
{
|
|
10698
|
-
"name": "addToCart",
|
|
10699
|
-
"description": "Add a product to the shopping cart with quantity",
|
|
10700
|
-
"annotations": { "readOnlyHint": false, "idempotentHint": false },
|
|
10701
|
-
"inputSchema": {
|
|
10702
|
-
"type": "object",
|
|
10703
|
-
"properties": {
|
|
10704
|
-
"productId": { "type": "string" },
|
|
10705
|
-
"quantity": { "type": "integer", "minimum": 1 }
|
|
10706
|
-
},
|
|
10707
|
-
"required": ["productId"]
|
|
10708
|
-
}
|
|
10709
|
-
},
|
|
10710
|
-
{
|
|
10711
|
-
"name": "checkout",
|
|
10712
|
-
"description": "Initiate checkout for the current cart",
|
|
10713
|
-
"annotations": { "readOnlyHint": false, "confirmationRequired": true },
|
|
10714
|
-
"inputSchema": {
|
|
10715
|
-
"type": "object",
|
|
10716
|
-
"properties": { "shippingMethod": { "type": "string" } }
|
|
10717
|
-
}
|
|
10718
|
-
}
|
|
10719
|
-
]
|
|
10720
|
-
}`,
|
|
10721
|
-
effort: "moderate",
|
|
10722
|
-
docsUrl: "https://webmcp.link/",
|
|
10723
|
-
tags: ["webmcp", "commerce", "coverage", "chrome-146"]
|
|
10724
|
-
}
|
|
10725
|
-
};
|
|
10726
|
-
audit(ctx) {
|
|
10727
|
-
const toolSignatures = [];
|
|
10728
|
-
const seen = /* @__PURE__ */ new Set();
|
|
10729
|
-
const manifestResult = ctx.rootFiles["/.well-known/webmcp"];
|
|
10730
|
-
if (manifestResult?.status === 200 && manifestResult.body) {
|
|
10731
|
-
const parsed = tryParseJson20(manifestResult.body);
|
|
10732
|
-
if (isObject20(parsed) && Array.isArray(parsed["tools"])) {
|
|
10733
|
-
for (const tool of parsed["tools"]) {
|
|
10734
|
-
if (isObject20(tool)) {
|
|
10735
|
-
const name = asString3(tool["name"]);
|
|
10736
|
-
const sig = `${name} ${asString3(tool["description"])}`.toLowerCase();
|
|
10737
|
-
toolSignatures.push(sig);
|
|
10738
|
-
if (name) seen.add(name);
|
|
10739
|
-
}
|
|
10740
|
-
}
|
|
10741
|
-
}
|
|
10742
|
-
}
|
|
10743
|
-
for (const page of ctx.pages) {
|
|
10744
|
-
page.$("form[toolname]").each((_, el) => {
|
|
10745
|
-
const name = page.$(el).attr("toolname") || "";
|
|
10746
|
-
if (name && seen.has(name)) return;
|
|
10747
|
-
const desc = page.$(el).attr("tooldescription") || "";
|
|
10748
|
-
const action = page.$(el).attr("action") || "";
|
|
10749
|
-
toolSignatures.push(`${name} ${desc} ${action}`.toLowerCase());
|
|
10750
|
-
if (name) seen.add(name);
|
|
10751
|
-
});
|
|
10752
|
-
}
|
|
10753
|
-
if (toolSignatures.length === 0) {
|
|
10754
|
-
return this.notApplicable(
|
|
10755
|
-
"No WebMCP tools found \u2014 commerce action coverage cannot be assessed.",
|
|
10756
|
-
`At least ${MIN_COVERAGE} commerce actions covered (search, product, cart, checkout, contact)`,
|
|
10757
|
-
"No WebMCP tools"
|
|
10758
|
-
);
|
|
10759
|
-
}
|
|
10760
|
-
const coveredActions = [];
|
|
10761
|
-
const missingActions = [];
|
|
10762
|
-
for (const action of COMMERCE_ACTIONS) {
|
|
10763
|
-
const matched = toolSignatures.some(
|
|
10764
|
-
(sig) => action.keywords.some((kw) => new RegExp(`\\b${kw}\\b`).test(sig))
|
|
10765
|
-
);
|
|
10766
|
-
if (matched) {
|
|
10767
|
-
coveredActions.push(action.label);
|
|
10768
|
-
} else {
|
|
10769
|
-
missingActions.push(action.label);
|
|
10770
|
-
}
|
|
10771
|
-
}
|
|
10772
|
-
const coverage = coveredActions.length;
|
|
10773
|
-
const total = COMMERCE_ACTIONS.length;
|
|
10774
|
-
if (coverage >= 4) {
|
|
10775
|
-
return this.pass(
|
|
10776
|
-
`${coverage}/${total} commerce actions covered: ${coveredActions.join(", ")}.`,
|
|
10777
|
-
`At least ${MIN_COVERAGE} commerce actions covered`,
|
|
10778
|
-
`${coverage}/${total} covered`
|
|
10779
|
-
);
|
|
10780
|
-
}
|
|
10781
|
-
if (coverage >= MIN_COVERAGE) {
|
|
10782
|
-
return this.warn(
|
|
10783
|
-
`${coverage}/${total} commerce actions covered: ${coveredActions.join(", ")}. Missing: ${missingActions.join(", ")}.`,
|
|
10784
|
-
`At least 4 commerce actions covered for strong agent support`,
|
|
10785
|
-
`${coverage}/${total} covered`,
|
|
10786
|
-
"medium"
|
|
10787
|
-
);
|
|
10788
|
-
}
|
|
10789
|
-
return this.fail(
|
|
10790
|
-
`Only ${coverage}/${total} commerce actions covered: ${coveredActions.length > 0 ? coveredActions.join(", ") : "none"}. Missing: ${missingActions.join(", ")}.`,
|
|
10791
|
-
`At least ${MIN_COVERAGE} commerce actions covered`,
|
|
10792
|
-
`${coverage}/${total} covered`,
|
|
10793
|
-
"medium"
|
|
10794
|
-
);
|
|
10795
|
-
}
|
|
10796
|
-
};
|
|
10797
|
-
|
|
10798
9913
|
// src/audits/agent-tools/openapi-description-quality.ts
|
|
10799
|
-
function
|
|
9914
|
+
function tryParseJson18(body) {
|
|
10800
9915
|
try {
|
|
10801
9916
|
return JSON.parse(body);
|
|
10802
9917
|
} catch {
|
|
10803
9918
|
return void 0;
|
|
10804
9919
|
}
|
|
10805
9920
|
}
|
|
10806
|
-
function
|
|
9921
|
+
function isObject18(val) {
|
|
10807
9922
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10808
9923
|
}
|
|
10809
9924
|
var HTTP_METHODS6 = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
10810
9925
|
var MIN_DESCRIPTION_LENGTH2 = 15;
|
|
10811
|
-
function
|
|
9926
|
+
function getOpenApiSpec7(ctx) {
|
|
10812
9927
|
const jsonResult = ctx.rootFiles["/openapi.json"];
|
|
10813
9928
|
if (jsonResult && jsonResult.status === 200 && jsonResult.body) {
|
|
10814
|
-
const parsed =
|
|
10815
|
-
if (
|
|
9929
|
+
const parsed = tryParseJson18(jsonResult.body);
|
|
9930
|
+
if (isObject18(parsed)) return parsed;
|
|
10816
9931
|
}
|
|
10817
9932
|
return void 0;
|
|
10818
9933
|
}
|
|
@@ -10821,13 +9936,13 @@ function hasGoodDescription(val) {
|
|
|
10821
9936
|
}
|
|
10822
9937
|
function getCheckableItems(spec) {
|
|
10823
9938
|
const paths = spec["paths"];
|
|
10824
|
-
if (!
|
|
9939
|
+
if (!isObject18(paths)) return [];
|
|
10825
9940
|
const items = [];
|
|
10826
9941
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
10827
|
-
if (!
|
|
9942
|
+
if (!isObject18(pathItem)) continue;
|
|
10828
9943
|
for (const method of HTTP_METHODS6) {
|
|
10829
9944
|
const op = pathItem[method];
|
|
10830
|
-
if (!
|
|
9945
|
+
if (!isObject18(op)) continue;
|
|
10831
9946
|
const operation = op;
|
|
10832
9947
|
const opLabel = `${method.toUpperCase()} ${path}`;
|
|
10833
9948
|
items.push({
|
|
@@ -10837,7 +9952,7 @@ function getCheckableItems(spec) {
|
|
|
10837
9952
|
const parameters = operation["parameters"];
|
|
10838
9953
|
if (Array.isArray(parameters)) {
|
|
10839
9954
|
for (const param of parameters) {
|
|
10840
|
-
if (!
|
|
9955
|
+
if (!isObject18(param)) continue;
|
|
10841
9956
|
const name = typeof param["name"] === "string" ? param["name"] : "(unnamed)";
|
|
10842
9957
|
items.push({
|
|
10843
9958
|
label: `${opLabel} param '${name}'`,
|
|
@@ -10881,7 +9996,7 @@ var OpenApiDescriptionQualityAudit = class _OpenApiDescriptionQualityAudit exten
|
|
|
10881
9996
|
}
|
|
10882
9997
|
};
|
|
10883
9998
|
audit(ctx) {
|
|
10884
|
-
const spec =
|
|
9999
|
+
const spec = getOpenApiSpec7(ctx);
|
|
10885
10000
|
if (!spec) {
|
|
10886
10001
|
return this.notApplicable(
|
|
10887
10002
|
"No parseable OpenAPI JSON spec found at /openapi.json.",
|
|
@@ -11858,53 +10973,6 @@ var TimeElementAudit = class extends Audit {
|
|
|
11858
10973
|
}
|
|
11859
10974
|
};
|
|
11860
10975
|
|
|
11861
|
-
// src/audits/semantic-html/address-element.ts
|
|
11862
|
-
var AddressElementAudit = class extends Audit {
|
|
11863
|
-
static meta = {
|
|
11864
|
-
id: "6.12",
|
|
11865
|
-
category: "semantic-html",
|
|
11866
|
-
title: "<address> for contact info",
|
|
11867
|
-
failureTitle: "<address> for contact info",
|
|
11868
|
-
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.',
|
|
11869
|
-
scoreDisplayMode: "binary",
|
|
11870
|
-
weight: 1,
|
|
11871
|
-
applicablePageTypes: ["homepage"],
|
|
11872
|
-
defaultPriority: "low",
|
|
11873
|
-
guidance: {
|
|
11874
|
-
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.',
|
|
11875
|
-
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.",
|
|
11876
|
-
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>',
|
|
11877
|
-
effort: "trivial",
|
|
11878
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address",
|
|
11879
|
-
tags: ["contact", "semantic", "html"]
|
|
11880
|
-
}
|
|
11881
|
-
};
|
|
11882
|
-
audit(ctx) {
|
|
11883
|
-
let pagesWithAddress = 0;
|
|
11884
|
-
for (const page of ctx.pages) {
|
|
11885
|
-
if (page.$("address").length > 0) pagesWithAddress++;
|
|
11886
|
-
}
|
|
11887
|
-
const hasAddress = pagesWithAddress > 0;
|
|
11888
|
-
if (hasAddress) {
|
|
11889
|
-
return this.pass(
|
|
11890
|
-
`${pagesWithAddress}/${ctx.pages.length} page(s) use <address> for contact information.`,
|
|
11891
|
-
"<address> element used for contact information",
|
|
11892
|
-
`${pagesWithAddress} page(s) with <address>`
|
|
11893
|
-
);
|
|
11894
|
-
}
|
|
11895
|
-
return this.warn(
|
|
11896
|
-
"No <address> elements found. If contact information exists, consider using <address>.",
|
|
11897
|
-
"<address> element used for contact information",
|
|
11898
|
-
"No <address> elements found",
|
|
11899
|
-
{
|
|
11900
|
-
priority: "low",
|
|
11901
|
-
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.',
|
|
11902
|
-
code: '<address>\n <a href="mailto:info@yoursite.com">info@yoursite.com</a><br>\n 123 Main St, City, ST 12345\n</address>'
|
|
11903
|
-
}
|
|
11904
|
-
);
|
|
11905
|
-
}
|
|
11906
|
-
};
|
|
11907
|
-
|
|
11908
10976
|
// src/audits/semantic-html/definition-elements.ts
|
|
11909
10977
|
var DefinitionElementsAudit = class extends Audit {
|
|
11910
10978
|
static meta = {
|
|
@@ -12095,81 +11163,6 @@ var ImageAltTextAudit = class extends Audit {
|
|
|
12095
11163
|
}
|
|
12096
11164
|
};
|
|
12097
11165
|
|
|
12098
|
-
// src/audits/semantic-html/decorative-images.ts
|
|
12099
|
-
var DecorativeImagesAudit = class extends Audit {
|
|
12100
|
-
static meta = {
|
|
12101
|
-
id: "6.16",
|
|
12102
|
-
category: "semantic-html",
|
|
12103
|
-
title: "Decorative images marked correctly",
|
|
12104
|
-
failureTitle: "Decorative images marked correctly",
|
|
12105
|
-
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.',
|
|
12106
|
-
scoreDisplayMode: "ternary",
|
|
12107
|
-
weight: 1,
|
|
12108
|
-
defaultPriority: "medium",
|
|
12109
|
-
guidance: {
|
|
12110
|
-
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.',
|
|
12111
|
-
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.',
|
|
12112
|
-
code: '<img src="decorative-border.png" alt="" role="presentation">',
|
|
12113
|
-
effort: "trivial",
|
|
12114
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/presentation_role",
|
|
12115
|
-
tags: ["images", "decorative", "accessibility", "semantic"]
|
|
12116
|
-
}
|
|
12117
|
-
};
|
|
12118
|
-
audit(ctx) {
|
|
12119
|
-
let decorativeCount = 0;
|
|
12120
|
-
let correctlyMarked = 0;
|
|
12121
|
-
for (const page of ctx.pages) {
|
|
12122
|
-
const images = extractImages(page.$);
|
|
12123
|
-
for (const img of images) {
|
|
12124
|
-
if (img.alt === "") {
|
|
12125
|
-
decorativeCount++;
|
|
12126
|
-
if (img.role === "presentation" || img.role === "none" || img.ariaHidden === "true") {
|
|
12127
|
-
correctlyMarked++;
|
|
12128
|
-
}
|
|
12129
|
-
}
|
|
12130
|
-
}
|
|
12131
|
-
}
|
|
12132
|
-
if (decorativeCount === 0) {
|
|
12133
|
-
return this.pass(
|
|
12134
|
-
"No decorative images (empty alt) found \u2014 check not applicable.",
|
|
12135
|
-
'Images with empty alt have role="presentation"',
|
|
12136
|
-
"No images with empty alt"
|
|
12137
|
-
);
|
|
12138
|
-
}
|
|
12139
|
-
const allCorrect = correctlyMarked === decorativeCount;
|
|
12140
|
-
const majorityCorrect = correctlyMarked > decorativeCount / 2;
|
|
12141
|
-
if (allCorrect) {
|
|
12142
|
-
return this.pass(
|
|
12143
|
-
`All ${decorativeCount} decorative image(s) have role="presentation".`,
|
|
12144
|
-
'Images with empty alt have role="presentation"',
|
|
12145
|
-
`${correctlyMarked}/${decorativeCount} correctly marked`
|
|
12146
|
-
);
|
|
12147
|
-
}
|
|
12148
|
-
if (majorityCorrect) {
|
|
12149
|
-
return this.warn(
|
|
12150
|
-
`${correctlyMarked}/${decorativeCount} decorative image(s) have role="presentation".`,
|
|
12151
|
-
'Images with empty alt have role="presentation"',
|
|
12152
|
-
`${correctlyMarked}/${decorativeCount} correctly marked`,
|
|
12153
|
-
{
|
|
12154
|
-
priority: "medium",
|
|
12155
|
-
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.',
|
|
12156
|
-
code: '<img src="decorative-bg.png" alt="" role="presentation">'
|
|
12157
|
-
}
|
|
12158
|
-
);
|
|
12159
|
-
}
|
|
12160
|
-
return this.fail(
|
|
12161
|
-
`${correctlyMarked}/${decorativeCount} decorative image(s) have role="presentation".`,
|
|
12162
|
-
'Images with empty alt have role="presentation"',
|
|
12163
|
-
`${correctlyMarked}/${decorativeCount} correctly marked`,
|
|
12164
|
-
{
|
|
12165
|
-
priority: "medium",
|
|
12166
|
-
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.',
|
|
12167
|
-
code: '<img src="decorative-bg.png" alt="" role="presentation">'
|
|
12168
|
-
}
|
|
12169
|
-
);
|
|
12170
|
-
}
|
|
12171
|
-
};
|
|
12172
|
-
|
|
12173
11166
|
// src/audits/semantic-html/figure-figcaption.ts
|
|
12174
11167
|
var FigureFigcaptionAudit = class extends Audit {
|
|
12175
11168
|
static meta = {
|
|
@@ -12524,85 +11517,24 @@ var FakeHeadingsAudit = class extends Audit {
|
|
|
12524
11517
|
"No styled <div>/<span>/<p>/<b> elements impersonating headings"
|
|
12525
11518
|
);
|
|
12526
11519
|
}
|
|
12527
|
-
const recommendation = {
|
|
12528
|
-
priority: "medium",
|
|
12529
|
-
description: "AI agents chunk and outline page content by reading real <h1>\u2013<h6> tags. Elements styled to look like headings are invisible to that outline, so sections cannot be navigated, summarized, or cited correctly. Replace styled generic elements with the appropriate heading level.",
|
|
12530
|
-
code: '<!-- Before -->\n<div class="text-2xl font-bold">Pricing Plans</div>\n\n<!-- After -->\n<h2 class="text-2xl font-bold">Pricing Plans</h2>'
|
|
12531
|
-
};
|
|
12532
|
-
if (found.length >= 5) {
|
|
12533
|
-
return this.fail(
|
|
12534
|
-
`Found ${found.length} fake heading(s) \u2014 generic elements styled to look like headings instead of <h1>-<h6> tags.`,
|
|
12535
|
-
expected,
|
|
12536
|
-
foundSummary,
|
|
12537
|
-
recommendation
|
|
12538
|
-
);
|
|
12539
|
-
}
|
|
12540
|
-
return this.warn(
|
|
12541
|
-
`Found ${found.length} fake heading(s) \u2014 generic elements styled to look like headings instead of <h1>-<h6> tags.`,
|
|
12542
|
-
expected,
|
|
12543
|
-
foundSummary,
|
|
12544
|
-
recommendation
|
|
12545
|
-
);
|
|
12546
|
-
}
|
|
12547
|
-
};
|
|
12548
|
-
|
|
12549
|
-
// src/audits/accessibility/skip-nav.ts
|
|
12550
|
-
var SkipNavAudit = class extends Audit {
|
|
12551
|
-
static meta = {
|
|
12552
|
-
id: "7.1",
|
|
12553
|
-
category: "accessibility",
|
|
12554
|
-
title: "Skip navigation link",
|
|
12555
|
-
failureTitle: "Skip navigation link",
|
|
12556
|
-
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.",
|
|
12557
|
-
scoreDisplayMode: "binary",
|
|
12558
|
-
weight: 1,
|
|
12559
|
-
defaultPriority: "medium",
|
|
12560
|
-
guidance: {
|
|
12561
|
-
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.",
|
|
12562
|
-
fix: 'Add a "Skip to main content" link as the first focusable element in <body>, pointing to an anchor on your <main> element.',
|
|
12563
|
-
code: '<a href="#main-content" class="skip-link">Skip to main content</a>\n<!-- ... navigation ... -->\n<main id="main-content">...</main>',
|
|
12564
|
-
effort: "trivial",
|
|
12565
|
-
docsUrl: "https://www.w3.org/WAI/WCAG21/Techniques/general/G1",
|
|
12566
|
-
tags: ["a11y", "navigation", "accessibility"]
|
|
12567
|
-
}
|
|
12568
|
-
};
|
|
12569
|
-
audit(ctx) {
|
|
12570
|
-
if (!ctx.pages || ctx.pages.length === 0) {
|
|
12571
|
-
return this.warn(
|
|
12572
|
-
"No pages scanned to check for skip navigation link.",
|
|
12573
|
-
"A skip-to-content link among the first links in <body>",
|
|
12574
|
-
"No pages scanned"
|
|
12575
|
-
);
|
|
12576
|
-
}
|
|
12577
|
-
for (const page of ctx.pages) {
|
|
12578
|
-
const $ = page.$;
|
|
12579
|
-
const bodyLinks = $("body a").slice(0, 5);
|
|
12580
|
-
let found = false;
|
|
12581
|
-
bodyLinks.each((_, el) => {
|
|
12582
|
-
const text = $(el).text().toLowerCase().trim();
|
|
12583
|
-
const href = ($(el).attr("href") ?? "").toLowerCase();
|
|
12584
|
-
if ((text.includes("skip") || text.includes("jump to") || text.includes("go to main")) && (href.includes("#main") || href.includes("#content") || href.includes("#skip"))) {
|
|
12585
|
-
found = true;
|
|
12586
|
-
}
|
|
12587
|
-
});
|
|
12588
|
-
if (found) {
|
|
12589
|
-
return this.pass(
|
|
12590
|
-
"Skip navigation link found among the first links in <body>.",
|
|
12591
|
-
"A skip-to-content link among the first links in <body>",
|
|
12592
|
-
"Skip navigation link detected",
|
|
12593
|
-
page.url
|
|
12594
|
-
);
|
|
12595
|
-
}
|
|
11520
|
+
const recommendation = {
|
|
11521
|
+
priority: "medium",
|
|
11522
|
+
description: "AI agents chunk and outline page content by reading real <h1>\u2013<h6> tags. Elements styled to look like headings are invisible to that outline, so sections cannot be navigated, summarized, or cited correctly. Replace styled generic elements with the appropriate heading level.",
|
|
11523
|
+
code: '<!-- Before -->\n<div class="text-2xl font-bold">Pricing Plans</div>\n\n<!-- After -->\n<h2 class="text-2xl font-bold">Pricing Plans</h2>'
|
|
11524
|
+
};
|
|
11525
|
+
if (found.length >= 5) {
|
|
11526
|
+
return this.fail(
|
|
11527
|
+
`Found ${found.length} fake heading(s) \u2014 generic elements styled to look like headings instead of <h1>-<h6> tags.`,
|
|
11528
|
+
expected,
|
|
11529
|
+
foundSummary,
|
|
11530
|
+
recommendation
|
|
11531
|
+
);
|
|
12596
11532
|
}
|
|
12597
|
-
return this.
|
|
12598
|
-
|
|
12599
|
-
|
|
12600
|
-
|
|
12601
|
-
|
|
12602
|
-
priority: "medium",
|
|
12603
|
-
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.",
|
|
12604
|
-
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>'
|
|
12605
|
-
}
|
|
11533
|
+
return this.warn(
|
|
11534
|
+
`Found ${found.length} fake heading(s) \u2014 generic elements styled to look like headings instead of <h1>-<h6> tags.`,
|
|
11535
|
+
expected,
|
|
11536
|
+
foundSummary,
|
|
11537
|
+
recommendation
|
|
12606
11538
|
);
|
|
12607
11539
|
}
|
|
12608
11540
|
};
|
|
@@ -13473,98 +12405,6 @@ var ContentTypeOptionsAudit = class extends Audit {
|
|
|
13473
12405
|
}
|
|
13474
12406
|
};
|
|
13475
12407
|
|
|
13476
|
-
// src/audits/technical-readiness/referrer-policy.ts
|
|
13477
|
-
var ReferrerPolicyAudit = class extends Audit {
|
|
13478
|
-
static meta = {
|
|
13479
|
-
id: "8.5",
|
|
13480
|
-
category: "technical-readiness",
|
|
13481
|
-
title: "Referrer-Policy header",
|
|
13482
|
-
failureTitle: "Referrer-Policy header",
|
|
13483
|
-
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.",
|
|
13484
|
-
scoreDisplayMode: "binary",
|
|
13485
|
-
weight: 1,
|
|
13486
|
-
defaultPriority: "medium",
|
|
13487
|
-
guidance: {
|
|
13488
|
-
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.",
|
|
13489
|
-
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.',
|
|
13490
|
-
code: "Referrer-Policy: strict-origin-when-cross-origin",
|
|
13491
|
-
effort: "trivial",
|
|
13492
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy",
|
|
13493
|
-
tags: ["security", "headers", "privacy"]
|
|
13494
|
-
}
|
|
13495
|
-
};
|
|
13496
|
-
audit(ctx) {
|
|
13497
|
-
const page = ctx.pages?.[0];
|
|
13498
|
-
const headers = page?.fetchResult.headers ?? {};
|
|
13499
|
-
const value = headers["referrer-policy"];
|
|
13500
|
-
if (value) {
|
|
13501
|
-
return this.pass(
|
|
13502
|
-
`Referrer-Policy header is present: ${value}`,
|
|
13503
|
-
"Referrer-Policy header present on homepage response",
|
|
13504
|
-
`referrer-policy: ${value}`,
|
|
13505
|
-
page?.url
|
|
13506
|
-
);
|
|
13507
|
-
}
|
|
13508
|
-
return this.fail(
|
|
13509
|
-
"Referrer-Policy header is missing from the homepage response.",
|
|
13510
|
-
"Referrer-Policy header present on homepage response",
|
|
13511
|
-
"Header not found",
|
|
13512
|
-
{
|
|
13513
|
-
priority: "medium",
|
|
13514
|
-
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.",
|
|
13515
|
-
code: "Referrer-Policy: strict-origin-when-cross-origin"
|
|
13516
|
-
},
|
|
13517
|
-
page?.url
|
|
13518
|
-
);
|
|
13519
|
-
}
|
|
13520
|
-
};
|
|
13521
|
-
|
|
13522
|
-
// src/audits/technical-readiness/permissions-policy.ts
|
|
13523
|
-
var PermissionsPolicyAudit = class extends Audit {
|
|
13524
|
-
static meta = {
|
|
13525
|
-
id: "8.6",
|
|
13526
|
-
category: "technical-readiness",
|
|
13527
|
-
title: "Permissions-Policy header",
|
|
13528
|
-
failureTitle: "Permissions-Policy header",
|
|
13529
|
-
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.",
|
|
13530
|
-
scoreDisplayMode: "binary",
|
|
13531
|
-
weight: 1,
|
|
13532
|
-
defaultPriority: "medium",
|
|
13533
|
-
guidance: {
|
|
13534
|
-
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.",
|
|
13535
|
-
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.",
|
|
13536
|
-
code: "Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()",
|
|
13537
|
-
effort: "trivial",
|
|
13538
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy",
|
|
13539
|
-
tags: ["security", "headers", "privacy"]
|
|
13540
|
-
}
|
|
13541
|
-
};
|
|
13542
|
-
audit(ctx) {
|
|
13543
|
-
const page = ctx.pages?.[0];
|
|
13544
|
-
const headers = page?.fetchResult.headers ?? {};
|
|
13545
|
-
const value = headers["permissions-policy"];
|
|
13546
|
-
if (value) {
|
|
13547
|
-
return this.pass(
|
|
13548
|
-
`Permissions-Policy header is present: ${value.length > 120 ? value.slice(0, 120) + "..." : value}`,
|
|
13549
|
-
"Permissions-Policy header present on homepage response",
|
|
13550
|
-
`permissions-policy: ${value.length > 120 ? value.slice(0, 120) + "..." : value}`,
|
|
13551
|
-
page?.url
|
|
13552
|
-
);
|
|
13553
|
-
}
|
|
13554
|
-
return this.fail(
|
|
13555
|
-
"Permissions-Policy header is missing from the homepage response.",
|
|
13556
|
-
"Permissions-Policy header present on homepage response",
|
|
13557
|
-
"Header not found",
|
|
13558
|
-
{
|
|
13559
|
-
priority: "medium",
|
|
13560
|
-
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.",
|
|
13561
|
-
code: "Permissions-Policy: camera=(), microphone=(), geolocation=()"
|
|
13562
|
-
},
|
|
13563
|
-
page?.url
|
|
13564
|
-
);
|
|
13565
|
-
}
|
|
13566
|
-
};
|
|
13567
|
-
|
|
13568
12408
|
// src/audits/technical-readiness/security-txt.ts
|
|
13569
12409
|
var SecurityTxtAudit = class extends Audit {
|
|
13570
12410
|
static meta = {
|
|
@@ -14304,66 +13144,6 @@ var LcpNotLazyAudit = class extends Audit {
|
|
|
14304
13144
|
}
|
|
14305
13145
|
};
|
|
14306
13146
|
|
|
14307
|
-
// src/audits/technical-readiness/preconnect-hints.ts
|
|
14308
|
-
var PreconnectHintsAudit = class extends Audit {
|
|
14309
|
-
static meta = {
|
|
14310
|
-
id: "8.17",
|
|
14311
|
-
category: "technical-readiness",
|
|
14312
|
-
title: "Preconnect hints",
|
|
14313
|
-
failureTitle: "Preconnect hints",
|
|
14314
|
-
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.",
|
|
14315
|
-
scoreDisplayMode: "binary",
|
|
14316
|
-
weight: 1,
|
|
14317
|
-
defaultPriority: "low",
|
|
14318
|
-
guidance: {
|
|
14319
|
-
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.",
|
|
14320
|
-
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.',
|
|
14321
|
-
code: '<link rel="preconnect" href="https://fonts.googleapis.com">\n<link rel="preconnect" href="https://cdn.yoursite.com" crossorigin>',
|
|
14322
|
-
effort: "trivial",
|
|
14323
|
-
docsUrl: "https://web.dev/articles/uses-rel-preconnect",
|
|
14324
|
-
tags: ["performance", "speed", "resource-hints"]
|
|
14325
|
-
}
|
|
14326
|
-
};
|
|
14327
|
-
audit(ctx) {
|
|
14328
|
-
const page = ctx.pages?.[0];
|
|
14329
|
-
if (!page) {
|
|
14330
|
-
return this.warn(
|
|
14331
|
-
"No homepage data available to check preconnect hints.",
|
|
14332
|
-
'At least one <link rel="preconnect"> tag present',
|
|
14333
|
-
"No homepage fetched",
|
|
14334
|
-
void 0,
|
|
14335
|
-
void 0
|
|
14336
|
-
);
|
|
14337
|
-
}
|
|
14338
|
-
const $ = page.$;
|
|
14339
|
-
const preconnects = $('link[rel="preconnect"]');
|
|
14340
|
-
if (preconnects.length > 0) {
|
|
14341
|
-
const hrefs = [];
|
|
14342
|
-
preconnects.each((_, el) => {
|
|
14343
|
-
const href = $(el).attr("href");
|
|
14344
|
-
if (href) hrefs.push(href);
|
|
14345
|
-
});
|
|
14346
|
-
return this.pass(
|
|
14347
|
-
`Found ${preconnects.length} preconnect hint(s): ${hrefs.slice(0, 5).join(", ")}`,
|
|
14348
|
-
'At least one <link rel="preconnect"> tag present',
|
|
14349
|
-
`${preconnects.length} preconnect hint(s)`,
|
|
14350
|
-
page.url
|
|
14351
|
-
);
|
|
14352
|
-
}
|
|
14353
|
-
return this.fail(
|
|
14354
|
-
'No <link rel="preconnect"> hints found. Preconnect hints speed up connections to critical third-party origins.',
|
|
14355
|
-
'At least one <link rel="preconnect"> tag present',
|
|
14356
|
-
"No preconnect hints found",
|
|
14357
|
-
{
|
|
14358
|
-
priority: "low",
|
|
14359
|
-
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.",
|
|
14360
|
-
code: '<link rel="preconnect" href="https://fonts.googleapis.com">\n<link rel="preconnect" href="https://cdn.yoursite.com">'
|
|
14361
|
-
},
|
|
14362
|
-
page.url
|
|
14363
|
-
);
|
|
14364
|
-
}
|
|
14365
|
-
};
|
|
14366
|
-
|
|
14367
13147
|
// src/audits/technical-readiness/no-broken-ai-endpoints.ts
|
|
14368
13148
|
var NoBrokenAiEndpointsAudit = class extends Audit {
|
|
14369
13149
|
static meta = {
|
|
@@ -14677,71 +13457,6 @@ var TermsOfServiceAudit = class extends Audit {
|
|
|
14677
13457
|
}
|
|
14678
13458
|
};
|
|
14679
13459
|
|
|
14680
|
-
// src/audits/technical-readiness/framework-detection.ts
|
|
14681
|
-
var FrameworkDetectionAudit = class extends Audit {
|
|
14682
|
-
static meta = {
|
|
14683
|
-
id: "8.21",
|
|
14684
|
-
category: "technical-readiness",
|
|
14685
|
-
title: "Frontend framework detection",
|
|
14686
|
-
failureTitle: "Frontend framework detection",
|
|
14687
|
-
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.",
|
|
14688
|
-
scoreDisplayMode: "informative",
|
|
14689
|
-
weight: 1,
|
|
14690
|
-
defaultPriority: "low",
|
|
14691
|
-
guidance: {
|
|
14692
|
-
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.",
|
|
14693
|
-
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.",
|
|
14694
|
-
effort: "trivial",
|
|
14695
|
-
tags: ["informational", "framework"]
|
|
14696
|
-
}
|
|
14697
|
-
};
|
|
14698
|
-
audit(ctx) {
|
|
14699
|
-
const page = ctx.pages[0];
|
|
14700
|
-
if (!page) {
|
|
14701
|
-
return this.fail("No pages available for analysis.", "", "None");
|
|
14702
|
-
}
|
|
14703
|
-
const { meta, $ } = page;
|
|
14704
|
-
const frameworks = [];
|
|
14705
|
-
if (meta["generator"]) {
|
|
14706
|
-
frameworks.push(`Generator: ${meta["generator"]}`);
|
|
14707
|
-
}
|
|
14708
|
-
if (meta["next-head-count"] || $('script[id="__NEXT_DATA__"]').length > 0) {
|
|
14709
|
-
frameworks.push("Next.js");
|
|
14710
|
-
}
|
|
14711
|
-
if ($('script[src*="nuxt"]').length > 0 || globalThis.window?.__NUXT__) {
|
|
14712
|
-
frameworks.push("Nuxt.js");
|
|
14713
|
-
}
|
|
14714
|
-
if ($("[data-reactroot], [data-reactid]").length > 0 || $('script[src*="react"]').length > 0) {
|
|
14715
|
-
frameworks.push("React");
|
|
14716
|
-
}
|
|
14717
|
-
if ($("[data-v-field], [data-v-]").length > 0 || $('script[src*="vue"]').length > 0) {
|
|
14718
|
-
frameworks.push("Vue.js");
|
|
14719
|
-
}
|
|
14720
|
-
if ($("app-root, [ng-version]").length > 0) {
|
|
14721
|
-
frameworks.push("Angular");
|
|
14722
|
-
}
|
|
14723
|
-
if ($('script[src*="astro"]').length > 0 || $("style[data-astro-cid]").length > 0) {
|
|
14724
|
-
frameworks.push("Astro");
|
|
14725
|
-
}
|
|
14726
|
-
if ($('script[src*="svelte"]').length > 0) {
|
|
14727
|
-
frameworks.push("Svelte");
|
|
14728
|
-
}
|
|
14729
|
-
if (frameworks.length > 0) {
|
|
14730
|
-
const unique = Array.from(new Set(frameworks));
|
|
14731
|
-
return this.pass(
|
|
14732
|
-
`Detected frameworks: ${unique.join(", ")}.`,
|
|
14733
|
-
"Identify the frontend framework used by the site.",
|
|
14734
|
-
unique.join(", ")
|
|
14735
|
-
);
|
|
14736
|
-
}
|
|
14737
|
-
return this.pass(
|
|
14738
|
-
"No specific frontend framework clearly detected.",
|
|
14739
|
-
"Identify the frontend framework used by the site.",
|
|
14740
|
-
"Generic/Unknown"
|
|
14741
|
-
);
|
|
14742
|
-
}
|
|
14743
|
-
};
|
|
14744
|
-
|
|
14745
13460
|
// src/audits/answer-engine/faq-sections.ts
|
|
14746
13461
|
var FAQ_TEXT = /frequently\s+asked\s+questions|\bFAQ['']?s?\b|common\s+questions|questions?\s*(?:&|and)\s*answers|\bQ\s*&\s*A\b/i;
|
|
14747
13462
|
function hasFaqJsonLd(p) {
|
|
@@ -15706,7 +14421,7 @@ var MetaDescriptionAeoAudit = class _MetaDescriptionAeoAudit extends Audit {
|
|
|
15706
14421
|
};
|
|
15707
14422
|
|
|
15708
14423
|
// src/audits/generative-engine/named-author.ts
|
|
15709
|
-
function
|
|
14424
|
+
function asString3(val) {
|
|
15710
14425
|
return typeof val === "string" ? val : "";
|
|
15711
14426
|
}
|
|
15712
14427
|
function findJsonLdByType(jsonLd, types) {
|
|
@@ -15793,7 +14508,7 @@ var NamedAuthorAudit = class extends Audit {
|
|
|
15793
14508
|
if (!author) continue;
|
|
15794
14509
|
const authors = Array.isArray(author) ? author : [author];
|
|
15795
14510
|
for (const a of authors) {
|
|
15796
|
-
const name = typeof a === "string" ? a : typeof a === "object" && a !== null ?
|
|
14511
|
+
const name = typeof a === "string" ? a : typeof a === "object" && a !== null ? asString3(a["name"]) : "";
|
|
15797
14512
|
const lower = name.trim().toLowerCase();
|
|
15798
14513
|
if (lower && !GENERIC_AUTHOR_NAMES.has(lower)) {
|
|
15799
14514
|
return this.pass(
|
|
@@ -16732,7 +15447,7 @@ var PublicationDateAudit = class extends Audit {
|
|
|
16732
15447
|
};
|
|
16733
15448
|
|
|
16734
15449
|
// src/audits/generative-engine/last-modified-schema.ts
|
|
16735
|
-
function
|
|
15450
|
+
function asString4(val) {
|
|
16736
15451
|
return typeof val === "string" ? val : "";
|
|
16737
15452
|
}
|
|
16738
15453
|
function findJsonLdByType5(jsonLd, types) {
|
|
@@ -16818,7 +15533,7 @@ var LastModifiedSchemaAudit = class extends Audit {
|
|
|
16818
15533
|
return this.warn(
|
|
16819
15534
|
datePublished ? `dateModified equals datePublished ("${dateModified}"). Update dateModified when content changes.` : `dateModified is set ("${dateModified}") but no datePublished for comparison.`,
|
|
16820
15535
|
"JSON-LD dateModified present and different from datePublished",
|
|
16821
|
-
`dateModified: ${dateModified}${datePublished ? `, datePublished: ${
|
|
15536
|
+
`dateModified: ${dateModified}${datePublished ? `, datePublished: ${asString4(datePublished)}` : ""}`,
|
|
16822
15537
|
{
|
|
16823
15538
|
priority: "low",
|
|
16824
15539
|
description: "AI engines compare dateModified to datePublished to detect actively maintained content. When they match, agents treat the content as never-updated since publication. Update dateModified each time you revise content to signal freshness."
|
|
@@ -16928,69 +15643,6 @@ var InternalCrossLinkingAudit = class extends Audit {
|
|
|
16928
15643
|
}
|
|
16929
15644
|
};
|
|
16930
15645
|
|
|
16931
|
-
// src/audits/generative-engine/pagination-links.ts
|
|
16932
|
-
var PaginationLinksAudit = class extends Audit {
|
|
16933
|
-
static meta = {
|
|
16934
|
-
id: "10.12",
|
|
16935
|
-
category: "generative-engine",
|
|
16936
|
-
title: "Pagination links",
|
|
16937
|
-
failureTitle: "Pagination links",
|
|
16938
|
-
description: 'AI crawlers use rel="prev" and rel="next" to navigate paginated content series without missing pages.',
|
|
16939
|
-
scoreDisplayMode: "ternary",
|
|
16940
|
-
weight: 1,
|
|
16941
|
-
applicablePageTypes: ["category"],
|
|
16942
|
-
defaultPriority: "low",
|
|
16943
|
-
guidance: {
|
|
16944
|
-
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.',
|
|
16945
|
-
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.',
|
|
16946
|
-
code: '<link rel="prev" href="/blog/page/1">\n<link rel="next" href="/blog/page/3">',
|
|
16947
|
-
effort: "easy",
|
|
16948
|
-
tags: ["pagination", "html", "generative-engine"]
|
|
16949
|
-
}
|
|
16950
|
-
};
|
|
16951
|
-
audit(ctx) {
|
|
16952
|
-
const page = ctx.pages[0];
|
|
16953
|
-
if (!page) {
|
|
16954
|
-
return this.fail(
|
|
16955
|
-
"No pages scanned.",
|
|
16956
|
-
'<link rel="prev"> and <link rel="next"> in head',
|
|
16957
|
-
"No pages scanned",
|
|
16958
|
-
{
|
|
16959
|
-
priority: "low",
|
|
16960
|
-
description: 'AI crawlers use rel="prev" and rel="next" to navigate paginated content series without missing pages.',
|
|
16961
|
-
code: '<link rel="prev" href="/blog/page/1">\n<link rel="next" href="/blog/page/3">'
|
|
16962
|
-
}
|
|
16963
|
-
);
|
|
16964
|
-
}
|
|
16965
|
-
for (const p of ctx.pages) {
|
|
16966
|
-
const hasPrev = p.headLinks.some((l) => l.rel === "prev");
|
|
16967
|
-
const hasNext = p.headLinks.some((l) => l.rel === "next");
|
|
16968
|
-
if (hasPrev || hasNext) {
|
|
16969
|
-
const found = [];
|
|
16970
|
-
if (hasPrev) found.push('rel="prev"');
|
|
16971
|
-
if (hasNext) found.push('rel="next"');
|
|
16972
|
-
return this.pass(
|
|
16973
|
-
`Pagination links found: ${found.join(" and ")}.`,
|
|
16974
|
-
'<link rel="prev"> and <link rel="next"> in head',
|
|
16975
|
-
found.join(", "),
|
|
16976
|
-
p.url
|
|
16977
|
-
);
|
|
16978
|
-
}
|
|
16979
|
-
}
|
|
16980
|
-
return this.warn(
|
|
16981
|
-
'No <link rel="prev"> or <link rel="next"> found on any page.',
|
|
16982
|
-
'<link rel="prev"> and <link rel="next"> in head',
|
|
16983
|
-
"Not found",
|
|
16984
|
-
{
|
|
16985
|
-
priority: "low",
|
|
16986
|
-
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.',
|
|
16987
|
-
code: '<link rel="prev" href="/blog/page/1">\n<link rel="next" href="/blog/page/3">'
|
|
16988
|
-
},
|
|
16989
|
-
page.url
|
|
16990
|
-
);
|
|
16991
|
-
}
|
|
16992
|
-
};
|
|
16993
|
-
|
|
16994
15646
|
// src/audits/generative-engine/unique-data.ts
|
|
16995
15647
|
var STAT_PATTERN = /\d+(?:\.\d+)?%|\$[\d,]+(?:\.\d{2})?|\b\d{1,3}(?:,\d{3})+\b|\b\d+(?:\.\d+)?x\b/;
|
|
16996
15648
|
var UniqueDataAudit = class extends Audit {
|
|
@@ -17266,7 +15918,6 @@ var defaultConfig = {
|
|
|
17266
15918
|
reg(MobileFriendlyAudit),
|
|
17267
15919
|
reg(FastPageLoadAudit),
|
|
17268
15920
|
reg(NoBrokenLinksAudit),
|
|
17269
|
-
reg(NavigationJsonAudit),
|
|
17270
15921
|
reg(NoOrphanPagesAudit),
|
|
17271
15922
|
reg(CommerceLinksAudit)
|
|
17272
15923
|
],
|
|
@@ -17310,13 +15961,11 @@ var defaultConfig = {
|
|
|
17310
15961
|
reg(FaqPageSchemaAudit),
|
|
17311
15962
|
reg(ServiceProductSchemaAudit),
|
|
17312
15963
|
reg(SpeakableSchemaAudit),
|
|
17313
|
-
reg(PotentialActionAudit),
|
|
17314
15964
|
reg(HowToSchemaAudit),
|
|
17315
15965
|
reg(LocalBusinessSchemaAudit),
|
|
17316
15966
|
reg(ReviewSchemaAudit),
|
|
17317
15967
|
reg(OfferSchemaAudit),
|
|
17318
15968
|
reg(AuthorSchemaAudit),
|
|
17319
|
-
reg(ActionSchemaAudit),
|
|
17320
15969
|
reg(ProductIdentifiersAudit),
|
|
17321
15970
|
reg(ProductDetailsAudit),
|
|
17322
15971
|
reg(ProductReviewsAudit),
|
|
@@ -17334,12 +15983,9 @@ var defaultConfig = {
|
|
|
17334
15983
|
reg(OgImageAltAudit),
|
|
17335
15984
|
reg(TwitterCardAudit),
|
|
17336
15985
|
reg(LlmsTxtLinkAudit),
|
|
17337
|
-
reg(LlmsFullTxtLinkAudit),
|
|
17338
15986
|
reg(AiContentDeclarationAudit),
|
|
17339
|
-
reg(AiInstructionsAudit),
|
|
17340
15987
|
reg(MarkdownAlternateAudit),
|
|
17341
15988
|
reg(RssFeedLinkAudit),
|
|
17342
|
-
reg(McpDiscoveryLinkAudit),
|
|
17343
15989
|
reg(OpenApiLinkAudit),
|
|
17344
15990
|
reg(AiCatalogLinkAudit),
|
|
17345
15991
|
reg(MetaRobotsAudit)
|
|
@@ -17348,20 +15994,17 @@ var defaultConfig = {
|
|
|
17348
15994
|
reg(OpenApiExistsAudit),
|
|
17349
15995
|
reg(OpenApiEndpointsAudit),
|
|
17350
15996
|
reg(OpenApiOperationIdsAudit),
|
|
17351
|
-
reg(OpenApiAiInstructionsAudit),
|
|
17352
15997
|
reg(OpenApiServersAudit),
|
|
17353
15998
|
reg(OpenApiSchemasAudit),
|
|
17354
15999
|
reg(AiCatalogExistsAudit),
|
|
17355
16000
|
reg(AiCatalogMetadataAudit),
|
|
17356
16001
|
reg(AiCatalogUrlsAudit),
|
|
17357
16002
|
reg(AgentsJsonAudit),
|
|
17358
|
-
reg(AiPluginJsonAudit),
|
|
17359
16003
|
reg(McpDiscoveryAudit),
|
|
17360
16004
|
reg(McpEndpointAudit),
|
|
17361
16005
|
reg(McpCapabilitiesAudit),
|
|
17362
16006
|
reg(ContactFormAudit),
|
|
17363
16007
|
reg(SearchEndpointAudit),
|
|
17364
|
-
reg(DataActionCtasAudit),
|
|
17365
16008
|
reg(NoBlockingCaptchaAudit),
|
|
17366
16009
|
reg(FormsNoJsAudit),
|
|
17367
16010
|
reg(WebmcpManifestAudit),
|
|
@@ -17369,7 +16012,6 @@ var defaultConfig = {
|
|
|
17369
16012
|
reg(WebmcpInputQualityAudit),
|
|
17370
16013
|
reg(WebmcpToolNamingAudit),
|
|
17371
16014
|
reg(WebmcpToolAnnotationsAudit),
|
|
17372
|
-
reg(WebmcpActionCoverageAudit),
|
|
17373
16015
|
reg(OpenApiDescriptionQualityAudit),
|
|
17374
16016
|
reg(FormActionabilityAudit)
|
|
17375
16017
|
],
|
|
@@ -17385,18 +16027,15 @@ var defaultConfig = {
|
|
|
17385
16027
|
reg(DataTablesAudit),
|
|
17386
16028
|
reg(CodeLanguageAudit),
|
|
17387
16029
|
reg(TimeElementAudit),
|
|
17388
|
-
reg(AddressElementAudit),
|
|
17389
16030
|
reg(DefinitionElementsAudit),
|
|
17390
16031
|
reg(ContentDepthAudit),
|
|
17391
16032
|
reg(ImageAltTextAudit),
|
|
17392
|
-
reg(DecorativeImagesAudit),
|
|
17393
16033
|
reg(FigureFigcaptionAudit),
|
|
17394
16034
|
reg(SvgBloatAudit),
|
|
17395
16035
|
reg(TokenRatioAudit),
|
|
17396
16036
|
reg(FakeHeadingsAudit)
|
|
17397
16037
|
],
|
|
17398
16038
|
accessibility: [
|
|
17399
|
-
reg(SkipNavAudit),
|
|
17400
16039
|
reg(AriaLandmarksAudit),
|
|
17401
16040
|
reg(NavAriaLabelAudit),
|
|
17402
16041
|
reg(FormErrorMessagesAudit),
|
|
@@ -17424,8 +16063,6 @@ var defaultConfig = {
|
|
|
17424
16063
|
reg(HstsHeaderAudit),
|
|
17425
16064
|
reg(CspHeaderAudit),
|
|
17426
16065
|
reg(ContentTypeOptionsAudit),
|
|
17427
|
-
reg(ReferrerPolicyAudit),
|
|
17428
|
-
reg(PermissionsPolicyAudit),
|
|
17429
16066
|
reg(SecurityTxtAudit),
|
|
17430
16067
|
reg(CorsAiFilesAudit),
|
|
17431
16068
|
reg(CorsApiRoutesAudit),
|
|
@@ -17436,11 +16073,9 @@ var defaultConfig = {
|
|
|
17436
16073
|
reg(NoRenderBlockingAudit),
|
|
17437
16074
|
reg(ImageDimensionsAudit),
|
|
17438
16075
|
reg(LcpNotLazyAudit),
|
|
17439
|
-
reg(PreconnectHintsAudit),
|
|
17440
16076
|
reg(NoBrokenAiEndpointsAudit),
|
|
17441
16077
|
reg(PrivacyPolicyAudit),
|
|
17442
|
-
reg(TermsOfServiceAudit)
|
|
17443
|
-
reg(FrameworkDetectionAudit)
|
|
16078
|
+
reg(TermsOfServiceAudit)
|
|
17444
16079
|
],
|
|
17445
16080
|
"answer-engine": [
|
|
17446
16081
|
reg(FaqSectionsAudit),
|
|
@@ -17467,7 +16102,6 @@ var defaultConfig = {
|
|
|
17467
16102
|
reg(PublicationDateAudit),
|
|
17468
16103
|
reg(LastModifiedSchemaAudit),
|
|
17469
16104
|
reg(InternalCrossLinkingAudit),
|
|
17470
|
-
reg(PaginationLinksAudit),
|
|
17471
16105
|
reg(UniqueDataAudit),
|
|
17472
16106
|
reg(BlockquoteUsageAudit),
|
|
17473
16107
|
reg(DescriptiveUrlsAudit)
|
|
@@ -17489,20 +16123,21 @@ function stubCheck(meta, tag, explanation) {
|
|
|
17489
16123
|
priority: meta.defaultPriority,
|
|
17490
16124
|
impact: meta.guidance?.impact ?? "",
|
|
17491
16125
|
fix: meta.guidance?.fix ?? "",
|
|
17492
|
-
tags: [tag]
|
|
16126
|
+
tags: [tag],
|
|
16127
|
+
deprecated: meta.deprecated
|
|
17493
16128
|
};
|
|
17494
16129
|
}
|
|
17495
|
-
|
|
16130
|
+
function planAudits(ctx, config) {
|
|
17496
16131
|
const scannedPageTypes = new Set(ctx.pages.map((p) => p.pageType));
|
|
17497
|
-
const
|
|
17498
|
-
const
|
|
16132
|
+
const runnable = [];
|
|
16133
|
+
const skipped = [];
|
|
17499
16134
|
for (const cat of config.categories) {
|
|
17500
16135
|
const regs = config.audits[cat.id] ?? [];
|
|
17501
16136
|
for (const reg2 of regs) {
|
|
17502
16137
|
const applicable = reg2.meta.applicablePageTypes;
|
|
17503
16138
|
if (applicable && applicable.length > 0) {
|
|
17504
16139
|
if (!applicable.some((pt) => scannedPageTypes.has(pt))) {
|
|
17505
|
-
|
|
16140
|
+
skipped.push(
|
|
17506
16141
|
stubCheck(
|
|
17507
16142
|
reg2.meta,
|
|
17508
16143
|
TAG_SKIPPED_PAGE_TYPE,
|
|
@@ -17512,30 +16147,35 @@ async function runAudits(ctx, config, onProgress) {
|
|
|
17512
16147
|
continue;
|
|
17513
16148
|
}
|
|
17514
16149
|
}
|
|
17515
|
-
|
|
16150
|
+
runnable.push({ reg: reg2, categoryId: cat.id });
|
|
17516
16151
|
}
|
|
17517
16152
|
}
|
|
17518
|
-
|
|
17519
|
-
|
|
16153
|
+
return { runnable, skipped };
|
|
16154
|
+
}
|
|
16155
|
+
async function runAudits(ctx, config, onEvent, plan) {
|
|
16156
|
+
const { runnable, skipped } = plan ?? planAudits(ctx, config);
|
|
16157
|
+
const allChecks = [...skipped];
|
|
17520
16158
|
const batchSize = 20;
|
|
17521
|
-
for (let i = 0; i <
|
|
17522
|
-
const batch =
|
|
16159
|
+
for (let i = 0; i < runnable.length; i += batchSize) {
|
|
16160
|
+
const batch = runnable.slice(i, i + batchSize);
|
|
17523
16161
|
const batchResults = await Promise.all(
|
|
17524
16162
|
batch.map(async ({ reg: reg2 }) => {
|
|
16163
|
+
const label2 = `${reg2.meta.id} ${reg2.meta.title}`;
|
|
17525
16164
|
try {
|
|
17526
16165
|
const instance = reg2.create();
|
|
17527
16166
|
const result = await instance.audit(ctx);
|
|
17528
|
-
|
|
16167
|
+
const check = instance.toCheckResult(result);
|
|
16168
|
+
onEvent?.({ type: "unit:done", label: label2 });
|
|
16169
|
+
return check;
|
|
17529
16170
|
} catch (err) {
|
|
17530
16171
|
logger.error({ err, auditId: reg2.meta.id }, "[scanner] Audit error");
|
|
17531
16172
|
const message = err instanceof Error ? err.message : String(err);
|
|
16173
|
+
onEvent?.({ type: "unit:fail", label: label2, error: message });
|
|
17532
16174
|
return stubCheck(reg2.meta, TAG_SCAN_ERROR, `Audit failed to run: ${message}`);
|
|
17533
16175
|
}
|
|
17534
16176
|
})
|
|
17535
16177
|
);
|
|
17536
16178
|
allChecks.push(...batchResults);
|
|
17537
|
-
completed += batchResults.length;
|
|
17538
|
-
onProgress?.(completed, totalAudits);
|
|
17539
16179
|
}
|
|
17540
16180
|
const categories = config.categories.map((cat) => {
|
|
17541
16181
|
const catChecks = allChecks.filter((c) => c.category === cat.id);
|
|
@@ -17583,6 +16223,110 @@ function buildWeightedCategoryResult(cat, checks2, registrations) {
|
|
|
17583
16223
|
};
|
|
17584
16224
|
}
|
|
17585
16225
|
|
|
16226
|
+
// src/progress.ts
|
|
16227
|
+
var PHASE_WEIGHTS = {
|
|
16228
|
+
"fetch-root": 0.35,
|
|
16229
|
+
"fetch-pages": 0.2,
|
|
16230
|
+
analyze: 0.1,
|
|
16231
|
+
audits: 0.3,
|
|
16232
|
+
report: 0.05
|
|
16233
|
+
};
|
|
16234
|
+
var ProgressTracker = class {
|
|
16235
|
+
onEvent;
|
|
16236
|
+
startMs;
|
|
16237
|
+
doneWeight = 0;
|
|
16238
|
+
phase = null;
|
|
16239
|
+
phaseStartMs = 0;
|
|
16240
|
+
totalUnits = 0;
|
|
16241
|
+
completedUnits = 0;
|
|
16242
|
+
lastFraction = 0;
|
|
16243
|
+
constructor(onEvent) {
|
|
16244
|
+
this.onEvent = onEvent;
|
|
16245
|
+
this.startMs = performance.now();
|
|
16246
|
+
}
|
|
16247
|
+
/** Fraction of the whole scan that is complete, in [0, 1]. Never decreases. */
|
|
16248
|
+
get fraction() {
|
|
16249
|
+
let f = this.doneWeight;
|
|
16250
|
+
if (this.phase !== null) {
|
|
16251
|
+
const ratio = this.totalUnits > 0 ? Math.min(1, this.completedUnits / this.totalUnits) : 0;
|
|
16252
|
+
f += PHASE_WEIGHTS[this.phase] * ratio;
|
|
16253
|
+
}
|
|
16254
|
+
return Math.min(1, Math.max(f, this.lastFraction));
|
|
16255
|
+
}
|
|
16256
|
+
/** Stamp an event with the current fraction/elapsed and advance the floor. */
|
|
16257
|
+
stamp() {
|
|
16258
|
+
const fraction = this.fraction;
|
|
16259
|
+
this.lastFraction = Math.max(this.lastFraction, fraction);
|
|
16260
|
+
return { fraction, elapsedMs: this.elapsedMs() };
|
|
16261
|
+
}
|
|
16262
|
+
elapsedMs() {
|
|
16263
|
+
return Math.max(0, Math.round(performance.now() - this.startMs));
|
|
16264
|
+
}
|
|
16265
|
+
scanStart(url) {
|
|
16266
|
+
this.onEvent({ type: "scan:start", url, ...this.stamp() });
|
|
16267
|
+
}
|
|
16268
|
+
phaseStart(phase, totalUnits) {
|
|
16269
|
+
this.phase = phase;
|
|
16270
|
+
this.phaseStartMs = performance.now();
|
|
16271
|
+
this.totalUnits = Math.max(0, totalUnits);
|
|
16272
|
+
this.completedUnits = 0;
|
|
16273
|
+
this.onEvent({
|
|
16274
|
+
type: "phase:start",
|
|
16275
|
+
phase,
|
|
16276
|
+
totalUnits: this.totalUnits,
|
|
16277
|
+
...this.stamp()
|
|
16278
|
+
});
|
|
16279
|
+
}
|
|
16280
|
+
/** Correct the current phase's unit total (e.g. discovery finds pages mid-phase). */
|
|
16281
|
+
setPhaseTotal(totalUnits) {
|
|
16282
|
+
this.totalUnits = Math.max(this.completedUnits, totalUnits);
|
|
16283
|
+
}
|
|
16284
|
+
unitDone(label2) {
|
|
16285
|
+
const phase = this.phase;
|
|
16286
|
+
if (phase === null) return;
|
|
16287
|
+
this.completedUnits += 1;
|
|
16288
|
+
this.onEvent({
|
|
16289
|
+
type: "unit:done",
|
|
16290
|
+
phase,
|
|
16291
|
+
completed: this.completedUnits,
|
|
16292
|
+
total: this.totalUnits,
|
|
16293
|
+
label: label2,
|
|
16294
|
+
...this.stamp()
|
|
16295
|
+
});
|
|
16296
|
+
}
|
|
16297
|
+
/** A failed unit still counts as settled work so the phase can complete. */
|
|
16298
|
+
unitFail(label2, error) {
|
|
16299
|
+
const phase = this.phase;
|
|
16300
|
+
if (phase === null) return;
|
|
16301
|
+
this.completedUnits += 1;
|
|
16302
|
+
this.onEvent({
|
|
16303
|
+
type: "unit:fail",
|
|
16304
|
+
phase,
|
|
16305
|
+
label: label2,
|
|
16306
|
+
error,
|
|
16307
|
+
...this.stamp()
|
|
16308
|
+
});
|
|
16309
|
+
}
|
|
16310
|
+
phaseDone() {
|
|
16311
|
+
const phase = this.phase;
|
|
16312
|
+
if (phase === null) return;
|
|
16313
|
+
this.phase = null;
|
|
16314
|
+
this.doneWeight += PHASE_WEIGHTS[phase];
|
|
16315
|
+
const durationMs = Math.max(0, Math.round(performance.now() - this.phaseStartMs));
|
|
16316
|
+
this.totalUnits = 0;
|
|
16317
|
+
this.completedUnits = 0;
|
|
16318
|
+
this.onEvent({
|
|
16319
|
+
type: "phase:done",
|
|
16320
|
+
phase,
|
|
16321
|
+
durationMs,
|
|
16322
|
+
...this.stamp()
|
|
16323
|
+
});
|
|
16324
|
+
}
|
|
16325
|
+
scanDone(score) {
|
|
16326
|
+
this.onEvent({ type: "scan:done", durationMs: this.elapsedMs(), score, ...this.stamp() });
|
|
16327
|
+
}
|
|
16328
|
+
};
|
|
16329
|
+
|
|
17586
16330
|
// src/audits/accessibility/runner.ts
|
|
17587
16331
|
var import_jsdom = require("jsdom");
|
|
17588
16332
|
|
|
@@ -22902,6 +21646,34 @@ function generateScanSummary(report) {
|
|
|
22902
21646
|
return summary;
|
|
22903
21647
|
}
|
|
22904
21648
|
|
|
21649
|
+
// src/scorer.ts
|
|
21650
|
+
function isInformative(check) {
|
|
21651
|
+
return check.scoreDisplayMode === "informative";
|
|
21652
|
+
}
|
|
21653
|
+
function calculateCategoryScore(checks2) {
|
|
21654
|
+
const scored = checks2.filter((c) => c.status !== "na" && !isInformative(c));
|
|
21655
|
+
if (scored.length === 0) return 0;
|
|
21656
|
+
const total = scored.reduce((sum, c) => sum + c.score, 0);
|
|
21657
|
+
return Math.round(total / scored.length * 100);
|
|
21658
|
+
}
|
|
21659
|
+
function buildCategoryResult(id, checks2) {
|
|
21660
|
+
return {
|
|
21661
|
+
id,
|
|
21662
|
+
name: CATEGORY_NAMES[id] ?? id,
|
|
21663
|
+
weight: CATEGORY_WEIGHTS[id] ?? 0,
|
|
21664
|
+
score: calculateCategoryScore(checks2),
|
|
21665
|
+
checks: checks2,
|
|
21666
|
+
passCount: checks2.filter((c) => c.status === "pass").length,
|
|
21667
|
+
warnCount: checks2.filter((c) => c.status === "warn").length,
|
|
21668
|
+
failCount: checks2.filter((c) => c.status === "fail").length
|
|
21669
|
+
};
|
|
21670
|
+
}
|
|
21671
|
+
function calculateOverallScore(categories) {
|
|
21672
|
+
return Math.round(
|
|
21673
|
+
categories.reduce((sum, cat) => sum + cat.score * cat.weight, 0)
|
|
21674
|
+
);
|
|
21675
|
+
}
|
|
21676
|
+
|
|
22905
21677
|
// src/waf-detector.ts
|
|
22906
21678
|
function detectWafProtection(targetUrl, homepageResult, rootFiles, scannedPagesCount) {
|
|
22907
21679
|
const allResults = [];
|
|
@@ -23094,9 +21866,11 @@ function discoverPages(homepageUrl, domain, rootFiles, homepage$, exclude, maxAd
|
|
|
23094
21866
|
}
|
|
23095
21867
|
return selected;
|
|
23096
21868
|
}
|
|
23097
|
-
async function runScan(url,
|
|
23098
|
-
const
|
|
23099
|
-
|
|
21869
|
+
async function runScan(url, options) {
|
|
21870
|
+
const onEvent = options?.onEvent;
|
|
21871
|
+
const pageOverrides = options?.pages;
|
|
21872
|
+
const signal = options?.signal;
|
|
21873
|
+
const tracker = new ProgressTracker((event) => onEvent?.(event));
|
|
23100
21874
|
const start = performance.now();
|
|
23101
21875
|
const fetcher = createFetcher();
|
|
23102
21876
|
const baseUrl = new URL(url).origin;
|
|
@@ -23119,7 +21893,7 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23119
21893
|
}
|
|
23120
21894
|
logger.debug({ url, domain }, "[orchestrator] Starting runScan");
|
|
23121
21895
|
signal?.throwIfAborted();
|
|
23122
|
-
|
|
21896
|
+
tracker.scanStart(displayUrl);
|
|
23123
21897
|
const rootFilePaths = [
|
|
23124
21898
|
"/robots.txt",
|
|
23125
21899
|
"/llms.txt",
|
|
@@ -23157,19 +21931,26 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23157
21931
|
"/our-story"
|
|
23158
21932
|
];
|
|
23159
21933
|
logger.debug({ count: rootFilePaths.length }, "[orchestrator] Phase 1: Fetching root files");
|
|
21934
|
+
tracker.phaseStart("fetch-root", rootFilePaths.length);
|
|
23160
21935
|
const rootResults = await Promise.all(
|
|
23161
|
-
rootFilePaths.map(
|
|
21936
|
+
rootFilePaths.map(
|
|
21937
|
+
(path) => fetcher.fetch({ url: `${baseUrl}${path}`, signal }).then((result) => {
|
|
21938
|
+
tracker.unitDone(path);
|
|
21939
|
+
return result;
|
|
21940
|
+
})
|
|
21941
|
+
)
|
|
23162
21942
|
);
|
|
23163
21943
|
const rootFiles = {};
|
|
23164
21944
|
rootFilePaths.forEach((path, i) => {
|
|
23165
21945
|
rootFiles[path] = rootResults[i];
|
|
23166
21946
|
});
|
|
23167
|
-
|
|
21947
|
+
tracker.phaseDone();
|
|
23168
21948
|
logger.debug("[orchestrator] Phase 1 complete: Root files fetched");
|
|
23169
21949
|
signal?.throwIfAborted();
|
|
23170
|
-
await progress(30, "Fetching pages");
|
|
23171
21950
|
logger.debug("[orchestrator] Phase 2: Fetching pages");
|
|
21951
|
+
tracker.phaseStart("fetch-pages", 1);
|
|
23172
21952
|
const homepageResult = await fetcher.fetch({ url, signal });
|
|
21953
|
+
tracker.unitDone(displayUrl);
|
|
23173
21954
|
const homepage$ = homepageResult.status === 200 && homepageResult.body ? parseHtml(homepageResult.body) : null;
|
|
23174
21955
|
const discoverLimit = Math.max(0, MAX_PAGES_PER_SCAN - 1 - overrideUrls.length);
|
|
23175
21956
|
const discoveredUrls = homepage$ ? discoverPages(url, domain, rootFiles, homepage$, new Set(overrideTypeByKey.keys()), discoverLimit) : [];
|
|
@@ -23178,12 +21959,22 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23178
21959
|
"[orchestrator] Page set: overrides + discovered URLs"
|
|
23179
21960
|
);
|
|
23180
21961
|
const extraUrls = [...overrideUrls, ...discoveredUrls];
|
|
23181
|
-
|
|
21962
|
+
tracker.setPhaseTotal(1 + extraUrls.length);
|
|
23182
21963
|
const extraResults = await Promise.all(
|
|
23183
|
-
extraUrls.map(
|
|
21964
|
+
extraUrls.map(
|
|
21965
|
+
(pageUrl) => fetcher.fetch({ url: pageUrl, signal }).then((result) => {
|
|
21966
|
+
tracker.unitDone(pageUrl);
|
|
21967
|
+
return result;
|
|
21968
|
+
})
|
|
21969
|
+
)
|
|
23184
21970
|
);
|
|
21971
|
+
tracker.phaseDone();
|
|
23185
21972
|
const allPageResults = [homepageResult, ...extraResults];
|
|
23186
21973
|
const allPageUrls = [displayUrl, ...extraUrls];
|
|
21974
|
+
tracker.phaseStart(
|
|
21975
|
+
"analyze",
|
|
21976
|
+
allPageResults.filter((r) => r.status === 200 && r.body).length
|
|
21977
|
+
);
|
|
23187
21978
|
const pages = allPageResults.map((r, i) => ({ result: r, url: allPageUrls[i], index: i })).filter((p) => p.result.status === 200 && p.result.body).map((p) => {
|
|
23188
21979
|
const $ = parseHtml(p.result.body);
|
|
23189
21980
|
const jsonLd = extractJsonLd($);
|
|
@@ -23191,6 +21982,7 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23191
21982
|
const meta = extractMetaTags($);
|
|
23192
21983
|
const isFirstPage = p.index === 0;
|
|
23193
21984
|
const forcedType = overrideTypeByKey.get(p.url.replace(/\/$/, ""));
|
|
21985
|
+
tracker.unitDone(p.url);
|
|
23194
21986
|
return {
|
|
23195
21987
|
url: p.url,
|
|
23196
21988
|
pageType: forcedType ?? detectPageType(p.url, $, structuredData, meta, isFirstPage),
|
|
@@ -23208,13 +22000,12 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23208
22000
|
p.a11yResults = await runA11yForHtml(p.fetchResult.body, p.url, A11Y_RULES);
|
|
23209
22001
|
})
|
|
23210
22002
|
);
|
|
23211
|
-
|
|
22003
|
+
tracker.phaseDone();
|
|
23212
22004
|
logger.debug(
|
|
23213
22005
|
{ pagesAnalyzed: pages.length },
|
|
23214
22006
|
"[orchestrator] Phase 2 complete: Page analysis complete"
|
|
23215
22007
|
);
|
|
23216
22008
|
signal?.throwIfAborted();
|
|
23217
|
-
await progress(60, "Running audits");
|
|
23218
22009
|
logger.debug("[orchestrator] Phase 3: Running audits");
|
|
23219
22010
|
const wafProtection = detectWafProtection(url, homepageResult, rootFiles, pages.length);
|
|
23220
22011
|
const ctx = {
|
|
@@ -23222,22 +22013,30 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23222
22013
|
pages,
|
|
23223
22014
|
domain,
|
|
23224
22015
|
baseUrl,
|
|
23225
|
-
fetch: (
|
|
22016
|
+
fetch: (options2) => fetcher.fetch({ ...options2, signal }),
|
|
23226
22017
|
wafProtection: wafProtection ?? void 0
|
|
23227
22018
|
};
|
|
22019
|
+
const auditPlan = planAudits(ctx, defaultConfig);
|
|
22020
|
+
tracker.phaseStart("audits", auditPlan.runnable.length);
|
|
23228
22021
|
const {
|
|
23229
22022
|
checks: allChecks,
|
|
23230
22023
|
categories,
|
|
23231
22024
|
overallScore
|
|
23232
|
-
} = await runAudits(
|
|
23233
|
-
|
|
23234
|
-
|
|
23235
|
-
|
|
22025
|
+
} = await runAudits(
|
|
22026
|
+
ctx,
|
|
22027
|
+
defaultConfig,
|
|
22028
|
+
(event) => {
|
|
22029
|
+
if (event.type === "unit:done") tracker.unitDone(event.label);
|
|
22030
|
+
else tracker.unitFail(event.label, event.error);
|
|
22031
|
+
},
|
|
22032
|
+
auditPlan
|
|
22033
|
+
);
|
|
22034
|
+
tracker.phaseDone();
|
|
23236
22035
|
logger.debug("[orchestrator] Phase 3 complete: Audits finished");
|
|
23237
|
-
|
|
22036
|
+
tracker.phaseStart("report", 1);
|
|
23238
22037
|
logger.debug("[orchestrator] Phase 4: Building final report");
|
|
23239
22038
|
const durationMs = Math.round(performance.now() - start);
|
|
23240
|
-
const recommendations = allChecks.filter((c) => c.status !== "pass").slice().sort((a, b) => {
|
|
22039
|
+
const recommendations = allChecks.filter((c) => c.status !== "pass" && !isInformative(c)).slice().sort((a, b) => {
|
|
23241
22040
|
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
23242
22041
|
return (order[a.priority] ?? 3) - (order[b.priority] ?? 3);
|
|
23243
22042
|
});
|
|
@@ -23248,11 +22047,12 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23248
22047
|
weightMap.set(reg2.meta.id, reg2.meta.weight);
|
|
23249
22048
|
}
|
|
23250
22049
|
}
|
|
23251
|
-
const topPasses = allChecks.filter((c) => c.status === "pass").slice().sort(
|
|
22050
|
+
const topPasses = allChecks.filter((c) => c.status === "pass" && !isInformative(c)).slice().sort(
|
|
23252
22051
|
(a, b) => (weightMap.get(b.id) ?? 1) - (weightMap.get(a.id) ?? 1)
|
|
23253
22052
|
).slice(0, 10);
|
|
23254
|
-
|
|
23255
|
-
|
|
22053
|
+
const readinessVitals = calculateReadinessVitals(
|
|
22054
|
+
allChecks.filter((c) => !isInformative(c))
|
|
22055
|
+
);
|
|
23256
22056
|
const readinessScore = Math.round(
|
|
23257
22057
|
readinessVitals.commerce * READINESS_WEIGHTS.commerce + readinessVitals.content * READINESS_WEIGHTS.content + readinessVitals.botAccessibility * READINESS_WEIGHTS.botAccessibility + readinessVitals.technical * READINESS_WEIGHTS.technical
|
|
23258
22058
|
);
|
|
@@ -23281,6 +22081,9 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23281
22081
|
productFields: [...overrideTypeByKey.values()].includes("product") ? extractProductFieldVerification(pages) : void 0
|
|
23282
22082
|
};
|
|
23283
22083
|
report.summary = generateScanSummary(report);
|
|
22084
|
+
tracker.unitDone();
|
|
22085
|
+
tracker.phaseDone();
|
|
22086
|
+
tracker.scanDone(overallScore);
|
|
23284
22087
|
logger.debug({ durationMs, score: overallScore }, "[orchestrator] runScan complete");
|
|
23285
22088
|
return report;
|
|
23286
22089
|
}
|
|
@@ -23329,31 +22132,6 @@ function calculateReadinessVitals(checks2) {
|
|
|
23329
22132
|
};
|
|
23330
22133
|
}
|
|
23331
22134
|
|
|
23332
|
-
// src/scorer.ts
|
|
23333
|
-
function calculateCategoryScore(checks2) {
|
|
23334
|
-
const scored = checks2.filter((c) => c.status !== "na");
|
|
23335
|
-
if (scored.length === 0) return 0;
|
|
23336
|
-
const total = scored.reduce((sum, c) => sum + c.score, 0);
|
|
23337
|
-
return Math.round(total / scored.length * 100);
|
|
23338
|
-
}
|
|
23339
|
-
function buildCategoryResult(id, checks2) {
|
|
23340
|
-
return {
|
|
23341
|
-
id,
|
|
23342
|
-
name: CATEGORY_NAMES[id] ?? id,
|
|
23343
|
-
weight: CATEGORY_WEIGHTS[id] ?? 0,
|
|
23344
|
-
score: calculateCategoryScore(checks2),
|
|
23345
|
-
checks: checks2,
|
|
23346
|
-
passCount: checks2.filter((c) => c.status === "pass").length,
|
|
23347
|
-
warnCount: checks2.filter((c) => c.status === "warn").length,
|
|
23348
|
-
failCount: checks2.filter((c) => c.status === "fail").length
|
|
23349
|
-
};
|
|
23350
|
-
}
|
|
23351
|
-
function calculateOverallScore(categories) {
|
|
23352
|
-
return Math.round(
|
|
23353
|
-
categories.reduce((sum, cat) => sum + cat.score * cat.weight, 0)
|
|
23354
|
-
);
|
|
23355
|
-
}
|
|
23356
|
-
|
|
23357
22135
|
// src/types.ts
|
|
23358
22136
|
var PAGE_TYPE_LABELS = {
|
|
23359
22137
|
homepage: "Homepage",
|
|
@@ -23461,12 +22239,15 @@ function loadConfigFile(customPath) {
|
|
|
23461
22239
|
CheckResultSchema,
|
|
23462
22240
|
CheckStatusSchema,
|
|
23463
22241
|
DEFAULT_SCAN_LIMIT,
|
|
22242
|
+
DeprecationNoticeSchema,
|
|
23464
22243
|
FixEffortSchema,
|
|
23465
22244
|
MAX_CONCURRENT_REQUESTS,
|
|
23466
22245
|
MAX_PAGES_PER_SCAN,
|
|
23467
22246
|
MAX_RESPONSE_BODY_BYTES,
|
|
23468
22247
|
PAGE_TYPE_LABELS,
|
|
22248
|
+
PHASE_WEIGHTS,
|
|
23469
22249
|
PRESETS,
|
|
22250
|
+
ProgressTracker,
|
|
23470
22251
|
READINESS_WEIGHTS,
|
|
23471
22252
|
REQUEST_TIMEOUT_MS,
|
|
23472
22253
|
SCANNER_USER_AGENT,
|
|
@@ -23503,6 +22284,7 @@ function loadConfigFile(customPath) {
|
|
|
23503
22284
|
getTierColor,
|
|
23504
22285
|
getTierLabel,
|
|
23505
22286
|
getWordCount,
|
|
22287
|
+
isInformative,
|
|
23506
22288
|
isPrivateIp,
|
|
23507
22289
|
isSafeUrl,
|
|
23508
22290
|
joinUrl,
|
|
@@ -23510,6 +22292,7 @@ function loadConfigFile(customPath) {
|
|
|
23510
22292
|
logger,
|
|
23511
22293
|
normalizeUrl,
|
|
23512
22294
|
parseHtml,
|
|
22295
|
+
planAudits,
|
|
23513
22296
|
runAudits,
|
|
23514
22297
|
runScan
|
|
23515
22298
|
});
|