lighthouse 13.5.0-dev.20261008 → 13.5.0-dev.20261009

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.
@@ -4,17 +4,112 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ /**
8
+ * @fileoverview Gatherer that discovers and fetches a site's Agentic Resource Discovery (ARD)
9
+ * manifest. It follows the discovery mechanisms in ARD §5.1 and also consults the predecessor
10
+ * `ai-catalog` names so that sites that have not yet migrated are still audited.
11
+ * @see https://agenticresourcediscovery.org/spec/
12
+ */
13
+
7
14
  import log from 'lighthouse-logger';
8
15
  import LinkHeader from 'http-link-header';
9
16
 
10
17
  import BaseGatherer from '../../base-gatherer.js';
11
18
  import RobotsTxt from '../seo/robots-txt.js';
12
- import {pageFunctions} from '../../../lib/page-functions.js';
13
- function getAiCatalogLinkInDOM() {
14
- const link = document.querySelector('link[rel~="ai-catalog" i]');
15
- return link instanceof HTMLLinkElement ? link.href : null;
19
+
20
+ /**
21
+ * Link relations for ARD entry sources. `ai-catalog` is the predecessor relation, which
22
+ * consumers MAY consult but are not required to (ARD §5.1).
23
+ */
24
+ const REL_ARD = 'ard';
25
+ const REL_LEGACY = 'ai-catalog';
26
+
27
+ const WELL_KNOWN_PATH = '/.well-known/ard.json';
28
+ const LEGACY_WELL_KNOWN_PATH = '/.well-known/ai-catalog.json';
29
+
30
+ const HTTP_OK = 200;
31
+
32
+ /** @typedef {{ard: string|null, legacy: string|null}} ArdLinks */
33
+
34
+ /** @typedef {LH.Artifacts['AgentResourceDiscovery']['discoverySignals']} DiscoverySignals */
35
+
36
+ /**
37
+ * @typedef FetchResult
38
+ * @property {number|null} status
39
+ * @property {string|null} content
40
+ * @property {Record<string, string>|null} headers
41
+ * @property {string} [errorMessage]
42
+ */
43
+
44
+ /**
45
+ * A location to try when resolving the manifest. `advertised` locations are ones the site
46
+ * points at (robots.txt, link tags, Link headers); the well-known paths are probed by
47
+ * Lighthouse, so a failure there is not the site's fault and is not reported.
48
+ * @typedef ManifestCandidate
49
+ * @property {LH.Artifacts.ArdDiscoverySource} source
50
+ * @property {string|null} url
51
+ * @property {boolean} advertised
52
+ */
53
+
54
+ /** @type {ReadonlyArray<LH.Artifacts.ArdDiscoverySource>} */
55
+ const SOURCE_PRIORITY = [
56
+ 'robotsTxtAgentmap',
57
+ 'htmlLink',
58
+ 'httpHeaderLink',
59
+ 'wellKnown',
60
+ 'legacyHtmlLink',
61
+ 'legacyHttpHeaderLink',
62
+ 'legacyWellKnown',
63
+ ];
64
+
65
+ /** @type {Set<LH.Artifacts.ArdDiscoverySource>} */
66
+ const PROBED_SOURCES = new Set(['wellKnown', 'legacyWellKnown']);
67
+
68
+ /**
69
+ * @param {DiscoverySignals} discoverySignals
70
+ * @return {ManifestCandidate[]}
71
+ */
72
+ function getManifestCandidates(discoverySignals) {
73
+ return SOURCE_PRIORITY.map(source => ({
74
+ source,
75
+ url: discoverySignals[source],
76
+ advertised: !PROBED_SOURCES.has(source),
77
+ }));
78
+ }
79
+
80
+ /**
81
+ * Resolves `href` against `baseUrl`, returning null for empty or unparseable values.
82
+ * @param {string|null|undefined} href
83
+ * @param {string} baseUrl
84
+ * @return {string|null}
85
+ */
86
+ function resolveUrl(href, baseUrl) {
87
+ if (!href) return null;
88
+ try {
89
+ return new URL(href, baseUrl).href;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Runs in the page. Returns the `href` of the first `<link>` for each relation, or null.
97
+ * @param {string} relArd
98
+ * @param {string} relLegacy
99
+ * @return {ArdLinks}
100
+ */
101
+ function getArdLinksInDOM(relArd, relLegacy) {
102
+ /** @param {string} rel */
103
+ const getHref = rel => {
104
+ const link = document.querySelector(`link[rel~="${rel}" i]`);
105
+ return link instanceof HTMLLinkElement ? link.href : null;
106
+ };
107
+ return {ard: getHref(relArd), legacy: getHref(relLegacy)};
16
108
  }
17
109
 
110
+ /** @type {ArdLinks} */
111
+ const NO_LINKS = {ard: null, legacy: null};
112
+
18
113
  class AgentResourceDiscovery extends BaseGatherer {
19
114
  /** @type {LH.Gatherer.GathererMeta<'RobotsTxt'>} */
20
115
  meta = {
@@ -23,6 +118,7 @@ class AgentResourceDiscovery extends BaseGatherer {
23
118
  };
24
119
 
25
120
  /**
121
+ * Returns the absolute URL from the first `Agentmap:` directive in robots.txt, if any.
26
122
  * @param {LH.Artifacts['RobotsTxt']|null|undefined} robotsTxt
27
123
  * @param {string} finalDisplayedUrl
28
124
  * @return {string|null}
@@ -31,93 +127,159 @@ class AgentResourceDiscovery extends BaseGatherer {
31
127
  if (!robotsTxt?.content) return null;
32
128
  const match = robotsTxt.content.match(/^\s*Agentmap:\s*(\S+)/im);
33
129
  if (!match) return null;
34
- try {
35
- return new URL(match[1], finalDisplayedUrl).href;
36
- } catch {
37
- return null;
38
- }
130
+ return resolveUrl(match[1], finalDisplayedUrl);
39
131
  }
40
132
 
41
133
  /**
134
+ * Finds `<link rel="ard">` and `<link rel="ai-catalog">` in the page.
42
135
  * @param {LH.Gatherer.Context} context
43
136
  * @param {string} finalDisplayedUrl
44
- * @return {Promise<string|null>}
137
+ * @return {Promise<ArdLinks>}
45
138
  */
46
- static async getHtmlLinkFromDom(context, finalDisplayedUrl) {
139
+ static async getHtmlLinksFromDom(context, finalDisplayedUrl) {
47
140
  try {
48
- const href = await context.driver.executionContext.evaluate(getAiCatalogLinkInDOM, {
49
- args: [],
141
+ const links = await context.driver.executionContext.evaluate(getArdLinksInDOM, {
142
+ args: [REL_ARD, REL_LEGACY],
50
143
  useIsolation: true,
51
- deps: [pageFunctions.getNodeDetails],
52
144
  });
53
- if (!href) return null;
54
- return new URL(href, finalDisplayedUrl).href;
55
- } catch {
56
- return null;
145
+ return {
146
+ ard: resolveUrl(links?.ard, finalDisplayedUrl),
147
+ legacy: resolveUrl(links?.legacy, finalDisplayedUrl),
148
+ };
149
+ } catch (err) {
150
+ log.verbose('AgentResourceDiscovery', `Could not read link tags: ${err.message}`);
151
+ return NO_LINKS;
57
152
  }
58
153
  }
59
154
 
60
155
  /**
156
+ * Finds `rel="ard"` and `rel="ai-catalog"` in the main document's HTTP `Link` header.
61
157
  * @param {LH.Gatherer.Context} context
62
158
  * @param {string} finalDisplayedUrl
63
- * @return {Promise<string|null>}
159
+ * @return {Promise<ArdLinks>}
64
160
  */
65
- static async getHttpHeaderLink(context, finalDisplayedUrl) {
66
- // Only check in navigation mode
67
- if (context.gatherMode !== 'navigation') return null;
161
+ static async getHttpHeaderLinks(context, finalDisplayedUrl) {
162
+ if (context.gatherMode !== 'navigation') return NO_LINKS;
68
163
 
69
164
  try {
70
165
  const mainResourceResponse = await context.driver.fetcher.fetchResource(finalDisplayedUrl);
71
- const linkHeader = mainResourceResponse.headers?.['link'];
72
- if (!linkHeader) return null;
166
+ const linkHeader = mainResourceResponse.headers?.link;
167
+ if (!linkHeader) return NO_LINKS;
168
+
169
+ const parsed = LinkHeader.parse(linkHeader.replace(/\n/g, ','));
170
+ /** @param {string} rel */
171
+ const getUri = rel =>
172
+ resolveUrl(parsed.refs.find(ref => ref.rel?.toLowerCase() === rel)?.uri, finalDisplayedUrl);
173
+ return {ard: getUri(REL_ARD), legacy: getUri(REL_LEGACY)};
174
+ } catch (err) {
175
+ log.verbose('AgentResourceDiscovery', `Could not read Link header: ${err.message}`);
176
+ return NO_LINKS;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Fetches a candidate manifest URL. Never throws; network errors are returned as a
182
+ * null status with an `errorMessage`.
183
+ * @param {LH.Gatherer.Context} context
184
+ * @param {string} url
185
+ * @return {Promise<FetchResult>}
186
+ */
187
+ static async fetchManifest(context, url) {
188
+ try {
189
+ const {status, content, headers} = await context.driver.fetcher.fetchResource(url);
190
+ return {status, content, headers: headers || null};
191
+ } catch (err) {
192
+ log.error('AgentResourceDiscovery', err);
193
+ return {status: null, content: null, headers: null, errorMessage: err.message};
194
+ }
195
+ }
73
196
 
74
- const parsed = LinkHeader.parse(linkHeader);
75
- const aiCatalogRef = parsed.get('rel', 'ai-catalog')[0];
76
- if (!aiCatalogRef?.uri) return null;
197
+ /**
198
+ * Tries each candidate in order and returns the first that loads. Advertised candidates
199
+ * that fail are recorded in `failedSources` and skipped. If nothing loads, the result
200
+ * describes the first advertised location that failed, or else the `/.well-known/ard.json`
201
+ * path consumers are required to fetch.
202
+ * @param {LH.Gatherer.Context} context
203
+ * @param {ManifestCandidate[]} candidates
204
+ * @return {Promise<{
205
+ * discoverySource: LH.Artifacts.ArdDiscoverySource,
206
+ * catalogUrl: string,
207
+ * fetchResult: FetchResult,
208
+ * failedSources: LH.Artifacts['AgentResourceDiscovery']['failedSources'],
209
+ * }>}
210
+ */
211
+ static async resolveManifest(context, candidates) {
212
+ /** @type {Array<{source: LH.Artifacts.ArdDiscoverySource, url: string, advertised: boolean, fetchResult: FetchResult}>} */
213
+ const attempts = [];
214
+ const failedSources = () => attempts
215
+ .filter(attempt => attempt.advertised)
216
+ .map(({source, url, fetchResult}) => ({
217
+ source,
218
+ url,
219
+ status: fetchResult.status,
220
+ errorMessage: fetchResult.errorMessage,
221
+ }));
222
+
223
+ for (const {source, url, advertised} of candidates) {
224
+ if (!url || attempts.some(attempt => attempt.url === url)) continue;
77
225
 
78
- return new URL(aiCatalogRef.uri, finalDisplayedUrl).href;
79
- } catch {
80
- return null;
226
+ const fetchResult = await AgentResourceDiscovery.fetchManifest(context, url);
227
+ if (fetchResult.status === HTTP_OK && fetchResult.content) {
228
+ return {discoverySource: source, catalogUrl: url, fetchResult,
229
+ failedSources: failedSources()};
230
+ }
231
+ attempts.push({source, url, advertised, fetchResult});
81
232
  }
233
+
234
+ const fallback = attempts.find(attempt => attempt.advertised) ||
235
+ attempts.find(attempt => attempt.source === 'wellKnown');
236
+ if (!fallback) throw new Error('ARD discovery attempted no candidates');
237
+
238
+ return {
239
+ discoverySource: fallback.source,
240
+ catalogUrl: fallback.url,
241
+ fetchResult: fallback.fetchResult,
242
+ failedSources: failedSources(),
243
+ };
82
244
  }
83
245
 
84
246
  /**
247
+ * Resolves the manifest following ARD §5.1: explicit ARD signals first, then the
248
+ * `/.well-known/ard.json` path consumers MUST fetch, and only then the predecessor
249
+ * `ai-catalog` relation and well-known path, which consumers MAY consult.
85
250
  * @param {LH.Gatherer.Context<'RobotsTxt'>} context
86
251
  * @return {Promise<LH.Artifacts['AgentResourceDiscovery']>}
87
252
  */
88
253
  async getArtifact(context) {
89
254
  const {finalDisplayedUrl} = context.baseArtifacts.URL;
90
- const robotsTxt = context.dependencies.RobotsTxt;
91
255
  const robotsTxtAgentmap = AgentResourceDiscovery.getRobotsTxtAgentmap(
92
- robotsTxt, finalDisplayedUrl);
93
- const [htmlLink, httpHeaderLink] = await Promise.all([
94
- AgentResourceDiscovery.getHtmlLinkFromDom(context, finalDisplayedUrl),
95
- AgentResourceDiscovery.getHttpHeaderLink(context, finalDisplayedUrl),
256
+ context.dependencies.RobotsTxt, finalDisplayedUrl);
257
+ const [htmlLinks, httpHeaderLinks] = await Promise.all([
258
+ AgentResourceDiscovery.getHtmlLinksFromDom(context, finalDisplayedUrl),
259
+ AgentResourceDiscovery.getHttpHeaderLinks(context, finalDisplayedUrl),
96
260
  ]);
97
- const wellKnown = new URL('/.well-known/ai-catalog.json', finalDisplayedUrl).href;
98
- const catalogUrl = robotsTxtAgentmap || htmlLink || httpHeaderLink || wellKnown;
99
-
100
- /** @type {string|undefined} */
101
- let errorMessage;
102
- const fetchResult = await context.driver.fetcher.fetchResource(catalogUrl)
103
- .catch(err => {
104
- log.error('AgentResourceDiscovery', err);
105
- errorMessage = err.message;
106
- return {status: null, content: null, headers: null};
107
- });
261
+
262
+ /** @type {DiscoverySignals} */
263
+ const discoverySignals = {
264
+ robotsTxtAgentmap,
265
+ htmlLink: htmlLinks.ard,
266
+ httpHeaderLink: httpHeaderLinks.ard,
267
+ legacyHtmlLink: htmlLinks.legacy,
268
+ legacyHttpHeaderLink: httpHeaderLinks.legacy,
269
+ wellKnown: new URL(WELL_KNOWN_PATH, finalDisplayedUrl).href,
270
+ legacyWellKnown: new URL(LEGACY_WELL_KNOWN_PATH, finalDisplayedUrl).href,
271
+ };
272
+
273
+ const candidates = getManifestCandidates(discoverySignals);
274
+ const {discoverySource, catalogUrl, fetchResult, failedSources} =
275
+ await AgentResourceDiscovery.resolveManifest(context, candidates);
108
276
 
109
277
  return {
110
- status: fetchResult.status,
111
- content: fetchResult.content,
112
- headers: fetchResult.headers || null,
278
+ ...fetchResult,
113
279
  catalogUrl,
114
- discoverySignals: {
115
- robotsTxtAgentmap,
116
- htmlLink,
117
- httpHeaderLink,
118
- wellKnown,
119
- },
120
- errorMessage,
280
+ discoverySource,
281
+ discoverySignals,
282
+ failedSources,
121
283
  };
122
284
  }
123
285
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "13.5.0-dev.20261008",
4
+ "version": "13.5.0-dev.20261009",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
@@ -38,6 +38,7 @@
38
38
  "clean": "rm -r dist proto/scripts/*.json proto/scripts/*_pb2.* proto/scripts/*_pb.* proto/scripts/__pycache__ proto/scripts/*.pyc *.report.html *.report.dom.html *.report.json *.devtoolslog.json *.trace.json shared/localization/locales/*.ctc.json || true",
39
39
  "clean-types": "git clean -xfq '*.d.ts' '*.d.cts' -e 'node_modules/' -e 'dist/' -e '.tmp/' -e '**/types/'",
40
40
  "lint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .",
41
+ "zizmor": "uvx zizmor@1.25.2 --gh-token=\"$(gh auth token)\" .github/workflows",
41
42
  "smoke": "node -r source-map-support/register cli/test/smokehouse/frontends/smokehouse-bin.js",
42
43
  "debug": "node --inspect-brk ./cli/index.js",
43
44
  "start": "yarn build-report --standalone && node ./cli/index.js",
@@ -53,14 +54,13 @@
53
54
  "test-proto": "yarn compile-proto && yarn build-proto-roundtrip",
54
55
  "unit-core": "yarn mocha core",
55
56
  "unit-cli": "yarn mocha --testMatch cli/**/*-test.js",
56
- "unit-lantern-trace": "INTERNAL_LANTERN_USE_TRACE=1 yarn mocha core/test/computed/metrics core/test/audits",
57
+ "unit-lantern-trace": "INTERNAL_LANTERN_USE_TRACE=1 yarn mocha core/test/computed/metrics core/test/computed/page-dependency-graph-test.js core/test/computed/tbt-impact-tasks-test.js core/test/audits",
57
58
  "unit-report": "yarn mocha --testMatch report/**/*-test.js",
58
59
  "unit-treemap": "yarn mocha --testMatch treemap/**/*-test.js",
59
60
  "unit-viewer": "yarn mocha --testMatch viewer/**/*-test.js",
60
61
  "unit-flow": "bash flow-report/test/run-flow-report-tests.sh",
61
62
  "unit": "yarn unit-flow && yarn mocha && yarn unit-lantern-trace",
62
63
  "unit:ci": "NODE_OPTIONS=--max-old-space-size=8192 npm run unit",
63
- "unit-lantern-trace:ci": "NODE_OPTIONS=--max-old-space-size=8192 npm run unit-lantern-trace",
64
64
  "core-unit": "yarn unit-core",
65
65
  "cli-unit": "yarn unit-cli",
66
66
  "viewer-unit": "yarn unit-viewer",
@@ -71,7 +71,6 @@
71
71
  "deploy-treemap": "yarn build-treemap --deploy",
72
72
  "deploy-viewer": "yarn build-viewer --deploy",
73
73
  "vercel-build": "yarn build-sample-reports && yarn build-viewer && yarn build-treemap",
74
- "dogfood-lhci": "./core/scripts/dogfood-lhci.sh",
75
74
  "timing-trace": "node core/scripts/generate-timing-trace.js",
76
75
  "changelog": "conventional-changelog --config ./build/changelog-generator/index.cjs --infile changelog.md --same-file",
77
76
  "type-check": "tsc --build ./tsconfig-all.json",
@@ -111,7 +110,7 @@
111
110
  "@jest/fake-timers": "^29.7.0",
112
111
  "@testing-library/preact": "^3.1.1",
113
112
  "@testing-library/preact-hooks": "^1.1.0",
114
- "@types/archiver": "^2.1.2",
113
+ "@types/archiver": "^8.0.0",
115
114
  "@types/chrome": "^0.1.42",
116
115
  "@types/configstore": "^6.0.2",
117
116
  "@types/cpy": "^5.1.0",
@@ -138,7 +137,7 @@
138
137
  "@typescript-eslint/parser": "^8.60.1",
139
138
  "acorn": "^8.5.0",
140
139
  "angular": "^1.7.4",
141
- "archiver": "^7.0.1",
140
+ "archiver": "^8.0.0",
142
141
  "builtin-modules": "^3.3.0",
143
142
  "chalk": "^2.4.1",
144
143
  "chrome-devtools-frontend": "1.0.1705227",
@@ -624,19 +624,28 @@
624
624
  "message": "Severity"
625
625
  },
626
626
  "core/audits/agentic/ard-schema.js | description": {
627
- "message": "Valid `ai-catalog.json` manifests are required for autonomous AI agents and registries to discover and verify your resources. [Learn more about the ARD specification](https://agenticresourcediscovery.org/spec/)."
627
+ "message": "A valid Agentic Resource Discovery (ARD) manifest, served at `/.well-known/ard.json` or linked with `rel=\"ard\"`, lets AI agents and registries discover and verify your resources. [Learn more about the ARD specification](https://agenticresourcediscovery.org/spec/)."
628
628
  },
629
629
  "core/audits/agentic/ard-schema.js | explanation": {
630
- "message": "Catalog file could not be loaded for schema validation."
630
+ "message": "ARD manifest could not be loaded for schema validation."
631
631
  },
632
632
  "core/audits/agentic/ard-schema.js | failureTitle": {
633
- "message": "`ai-catalog.json` schema is invalid"
633
+ "message": "ARD manifest is invalid"
634
634
  },
635
635
  "core/audits/agentic/ard-schema.js | itemSeverityError": {
636
636
  "message": "Error"
637
637
  },
638
+ "core/audits/agentic/ard-schema.js | legacyDiscovery": {
639
+ "message": "Manifest was only found through a legacy location (`/.well-known/ai-catalog.json` or `rel=\"ai-catalog\"`). ARD consumers are not required to check these, so ARD consumers may not discover your resources. Serve the manifest at `/.well-known/ard.json` or link it with `rel=\"ard\"`."
640
+ },
641
+ "core/audits/agentic/ard-schema.js | sourceFetchFailed": {
642
+ "message": "Advertised ARD manifest location could not be loaded."
643
+ },
644
+ "core/audits/agentic/ard-schema.js | sourceUnavailable": {
645
+ "message": "Advertised ARD manifest location could not be loaded (HTTP status {statusCode})."
646
+ },
638
647
  "core/audits/agentic/ard-schema.js | title": {
639
- "message": "`ai-catalog.json` schema is valid"
648
+ "message": "ARD manifest is valid"
640
649
  },
641
650
  "core/audits/agentic/llms-txt.js | description": {
642
651
  "message": "If your llms.txt file does not follow recommendations, large language models may not be able to understand how you want your website to be crawled or used for training. The [llms.txt](https://llmstxt.org/) file should be a Markdown file containing at least one H1 header. [Learn more about the llms.txt audit](https://developer.chrome.com/docs/lighthouse/agentic-browsing/llms-txt)."
@@ -624,19 +624,28 @@
624
624
  "message": "Ŝév̂ér̂ít̂ý"
625
625
  },
626
626
  "core/audits/agentic/ard-schema.js | description": {
627
- "message": "V̂ál̂íd̂ `ai-catalog.json` ḿâńîf́êśt̂ś âŕê ŕêq́ûír̂éd̂ f́ôŕ âút̂ón̂óm̂óûś ÂÍ âǵêńt̂ś âńd̂ ŕêǵîśt̂ŕîéŝ t́ô d́îśĉóv̂ér̂ án̂d́ v̂ér̂íf̂ý ŷóûŕ r̂éŝóûŕĉéŝ. [Ĺêár̂ń m̂ór̂é âb́ôút̂ t́ĥé ÂŔD̂ śp̂éĉíf̂íĉát̂íôń](https://agenticresourcediscovery.org/spec/)."
627
+ "message": "Â v́âĺîd́ Âǵêńt̂íĉ Ŕêśôúr̂ćê D́îśĉóv̂ér̂ý (ÂŔD̂) ḿâńîf́êśt̂, śêŕv̂éd̂ át̂ `/.well-known/ard.json` ór̂ ĺîńk̂éd̂ ẃît́ĥ `rel=\"ard\"`, ĺêt́ŝ ÁÎ áĝén̂t́ŝ án̂d́ r̂éĝíŝt́r̂íêś d̂íŝćôv́êŕ âńd̂ v́êŕîf́ŷ ýôúr̂ ŕêśôúr̂ćêś. [L̂éâŕn̂ ḿôŕê áb̂óût́ t̂h́ê ÁR̂D́ ŝṕêćîf́îćât́îón̂](https://agenticresourcediscovery.org/spec/)."
628
628
  },
629
629
  "core/audits/agentic/ard-schema.js | explanation": {
630
- "message": "Ĉát̂ál̂óĝ f́îĺê ćôúl̂d́ n̂ót̂ b́ê ĺôád̂éd̂ f́ôŕ ŝćĥém̂á v̂ál̂íd̂át̂íôń."
630
+ "message": "ÂŔD̂ ḿâńîf́êśt̂ ćôúl̂d́ n̂ót̂ b́ê ĺôád̂éd̂ f́ôŕ ŝćĥém̂á v̂ál̂íd̂át̂íôń."
631
631
  },
632
632
  "core/audits/agentic/ard-schema.js | failureTitle": {
633
- "message": "`ai-catalog.json` ŝćĥém̂á îś îńv̂ál̂íd̂"
633
+ "message": "ÂŔD̂ ḿâńîf́êśt̂ íŝ ín̂v́âĺîd́"
634
634
  },
635
635
  "core/audits/agentic/ard-schema.js | itemSeverityError": {
636
636
  "message": "Êŕr̂ór̂"
637
637
  },
638
+ "core/audits/agentic/ard-schema.js | legacyDiscovery": {
639
+ "message": "M̂án̂íf̂éŝt́ ŵáŝ ón̂ĺŷ f́ôún̂d́ t̂h́r̂óûǵĥ á l̂éĝáĉý l̂óĉát̂íôń (`/.well-known/ai-catalog.json` ôŕ `rel=\"ai-catalog\"`). ÂŔD̂ ćôńŝúm̂ér̂ś âŕê ńôt́ r̂éq̂úîŕêd́ t̂ó ĉh́êćk̂ t́ĥéŝé, ŝó ÂŔD̂ ćôńŝúm̂ér̂ś m̂áŷ ńôt́ d̂íŝćôv́êŕ ŷóûŕ r̂éŝóûŕĉéŝ. Śêŕv̂é t̂h́ê ḿâńîf́êśt̂ át̂ `/.well-known/ard.json` ór̂ ĺîńk̂ ít̂ ẃît́ĥ `rel=\"ard\"`."
640
+ },
641
+ "core/audits/agentic/ard-schema.js | sourceFetchFailed": {
642
+ "message": "Âd́v̂ér̂t́îśêd́ ÂŔD̂ ḿâńîf́êśt̂ ĺôćât́îón̂ ćôúl̂d́ n̂ót̂ b́ê ĺôád̂éd̂."
643
+ },
644
+ "core/audits/agentic/ard-schema.js | sourceUnavailable": {
645
+ "message": "Âd́v̂ér̂t́îśêd́ ÂŔD̂ ḿâńîf́êśt̂ ĺôćât́îón̂ ćôúl̂d́ n̂ót̂ b́ê ĺôád̂éd̂ (H́T̂T́P̂ śt̂át̂úŝ {statusCode})."
646
+ },
638
647
  "core/audits/agentic/ard-schema.js | title": {
639
- "message": "`ai-catalog.json` ŝćĥém̂á îś v̂ál̂íd̂"
648
+ "message": "ÂŔD̂ ḿâńîf́êśt̂ íŝ v́âĺîd́"
640
649
  },
641
650
  "core/audits/agentic/llms-txt.js | description": {
642
651
  "message": "Îf́ ŷóûŕ l̂ĺm̂ś.t̂x́t̂ f́îĺê d́ôéŝ ńôt́ f̂ól̂ĺôẃ r̂éĉóm̂ḿêńd̂át̂íôńŝ, ĺâŕĝé l̂án̂ǵûáĝé m̂ód̂él̂ś m̂áŷ ńôt́ b̂é âb́l̂é t̂ó ûńd̂ér̂śt̂án̂d́ ĥóŵ ýôú ŵán̂t́ ŷóûŕ ŵéb̂śît́ê t́ô b́ê ćr̂áŵĺêd́ ôŕ ûśêd́ f̂ór̂ t́r̂áîńîńĝ. T́ĥé [l̂ĺm̂ś.t̂x́t̂](https://llmstxt.org/) f́îĺê śĥóûĺd̂ b́ê á M̂ár̂ḱd̂óŵń f̂íl̂é ĉón̂t́âín̂ín̂ǵ ât́ l̂éâśt̂ ón̂é Ĥ1 h́êád̂ér̂. [Ĺêár̂ń m̂ór̂é âb́ôút̂ t́ĥé l̂ĺm̂ś.t̂x́t̂ áûd́ît́](https://developer.chrome.com/docs/lighthouse/agentic-browsing/llms-txt)."
@@ -5,23 +5,32 @@ This is a JavaScript port of the Agentic Resource Discovery (ARD) Conformance Te
5
5
  The original Python script is located in the `ards-project/ard-spec` repository:
6
6
  - **Upstream Repository**: https://github.com/ards-project/ard-spec
7
7
  - **Source Script**: `conformance/bin/conformance-test`
8
- - **Schema**: `spec/schemas/ai-catalog.schema.json`
8
+ - **Schema**: `spec/schemas/ard-entry.schema.json` (`$defs/ArdManifest` and `$defs/ArdEntry`)
9
9
  - **Pinned Commit SHA**: `b76f235a8f461876ad4f1e77abd0eb0eb302b48d`
10
+ - **Reviewed Upstream Files** (SHA-256 of the upstream content the port was last verified against; maintained by `yarn update:ard-spec --ack-port`, do not edit by hand):
11
+ - `conformance/bin/conformance-test`: `ac2f3cae1c4ac75261fe45786a117aaad275322c92b74110529ad03a794275b4`
12
+ - `spec/ard.md`: `6deb2cc58216fb279569d8ff61bd4595ccdf6ceaeef6766a1a1c9f25b6b68a62`
10
13
 
11
14
  ## Modifications for Lighthouse:
12
15
  While the validation rules and test suite maintain 1:1 parity with the reference suite, the following adaptations were made for Lighthouse integration:
13
- - **Structured Error Return:** `ConformanceTester` stores structured objects (`{ element, message }`) in `errors` and `warnings` instead of formatted ANSI CLI strings.
14
- - **Precompiled Standalone Validator (`schema-validator.js`):** Uses `build/build-ard-schema.js` (`yarn build-ard-schema`) to precompile `ai-catalog.schema.json` into a pure, standalone JavaScript validator (`schema-validator.js`) ahead of time using `Ajv2020` and `ajv/dist/standalone`. This eliminates runtime dynamic compilation (`new Function()` / `eval()`), preventing Content Security Policy (CSP) `unsafe-eval` violations when running inside Chrome DevTools frontend, avoiding runtime `fs.readFileSync` calls in browser/bundled contexts, and keeping `Ajv` out of the client bundle.
16
+ - **Structured Error Return:** `ConformanceTester` stores structured objects (`{ element, message }`) in `errors`, `warnings` and `infos` instead of formatted ANSI CLI strings. The `[label]` prefix upstream puts on entry messages is reported as `element` instead.
17
+ - **Precompiled Standalone Validator (`schema-validator.js`):** Uses `build/build-ard-schema.js` (`yarn build-ard-schema`) to precompile the `ArdManifest` and `ArdEntry` definitions of `ard-entry.schema.json` into a pure, standalone JavaScript module (`schema-validator.js`) ahead of time using `Ajv2020` and `ajv/dist/standalone`. This eliminates runtime dynamic compilation (`new Function()` / `eval()`), preventing Content Security Policy (CSP) `unsafe-eval` violations when running inside Chrome DevTools frontend, avoiding runtime `fs.readFileSync` calls in browser/bundled contexts, and keeping `Ajv` out of the client bundle.
15
18
  - **ESM Format Validation:** Imports `uri` and `date-time` format validators statically from `ajv-formats/dist/formats.js` rather than bundling the full `ajv-formats` dynamic plugin.
19
+ - **Schema Error Messages:** Ajv reports different messages than Python's `jsonschema`. The first error not nested inside a `oneOf`/`anyOf` branch is reported, with `'x' is a required property` phrasing kept for `required` errors.
20
+ - **Non-object Input:** Upstream assumes the manifest root and each entry are JSON objects. The port treats anything else as empty, so it reports errors instead of throwing.
16
21
  - **Lighthouse Logger:** Replaced raw `console.log` with `lighthouse-logger`.
17
22
 
23
+ Publisher resolution (upstream's `resolve_publisher`, spec §5.1) is implemented by the gatherer in `core/gather/gatherers/agentic/ard.js`, and the resulting legacy-location warning by the `ard-schema` audit. `infos` are not surfaced in the audit.
24
+
18
25
  ## Updating Conformance Script
19
26
 
20
- Upstream schema and conformance test synchronization is checked regularly during dependency upgrades (via `core/scripts/upgrade-deps.sh`) and monitored weekly via CI (`.github/workflows/cron-weekly.yml` with `node core/scripts/update-ard-spec.js --check`).
27
+ `yarn check:ard-spec` (`core/scripts/update-ard-spec.js --check`) compares upstream `main` against what Lighthouse has: the vendored schema must match byte-for-byte, and the hand-ported files must match the **Reviewed Upstream Files** hashes above. It runs weekly in CI (`.github/workflows/cron-weekly.yml`) and during dependency upgrades (`core/scripts/upgrade-deps.sh`). Neither of those can mark upstream changes as handled; only `--ack-port` does.
21
28
 
22
- When upstream changes are detected:
23
- 1. Run `yarn update:ard-spec` to fetch the latest `ai-catalog.schema.json` and bump the pinned commit SHA.
24
- 2. Review the printed diff of `conformance/bin/conformance-test` and adapt `third-party/ard/ard.js` to match any updated validation rules.
25
- 3. Run `yarn build-ard-schema` to regenerate the standalone validator (`schema-validator.js`).
26
- 4. Run `yarn mocha third-party/ard/ard-test.js` to verify test conformance.
29
+ When it reports changes (the `update-ard-port` agent skill walks through this in detail):
30
+ 1. Run `yarn update:ard-spec`. This syncs `ard-entry.schema.json` verbatim and prints the upstream `conformance-test` diff. If a hand-ported file changed, it exits 1 **without** bumping the pinned SHA.
31
+ 2. Port `conformance/bin/conformance-test` changes into `third-party/ard/ard.js` (keep rules, severities and message text 1:1; see modifications above) and update `third-party/ard/ard-test.js`.
32
+ 3. If `spec/ard.md` changed, check §5.1 (Discovery Mechanisms) against `core/gather/gatherers/agentic/ard.js`.
33
+ 4. If the schema changed, run `yarn build-ard-schema` to regenerate `schema-validator.js`.
34
+ 5. Verify: `yarn mocha third-party/ard/ard-test.js core/test/gather/gatherers/agentic/ard-test.js core/test/audits/agentic/ard-schema-test.js` and `yarn smoke ard ardInvalid`.
35
+ 6. Run `yarn update:ard-spec --ack-port` to record the reviewed hashes and bump the pinned SHA. `yarn check:ard-spec` should now pass.
27
36