lighthouse 13.5.0-dev.20261007 → 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.
- package/.agents/skills/update-ard-port/SKILL.md +170 -0
- package/cli/test/smokehouse/config/exclusions.js +1 -1
- package/core/audits/agentic/ard-schema.d.ts +13 -0
- package/core/audits/agentic/ard-schema.js +81 -33
- package/core/audits/agentic/llms-txt.js +5 -2
- package/core/audits/insights/insight-audit.d.ts +9 -0
- package/core/audits/insights/insight-audit.js +2 -1
- package/core/audits/long-tasks.js +2 -1
- package/core/audits/seo/robots-txt.js +3 -8
- package/core/computed/page-dependency-graph.js +0 -4
- package/core/computed/trace-engine-result.js +3 -1
- package/core/config/constants.d.ts +2 -28
- package/core/gather/gatherers/agentic/ard.d.ts +59 -4
- package/core/gather/gatherers/agentic/ard.js +218 -56
- package/core/lib/trace-engine.d.ts +1 -0
- package/core/lib/trace-engine.js +2 -0
- package/package.json +6 -7
- package/shared/localization/locales/ar-XB.json +157 -136
- package/shared/localization/locales/ar.json +157 -136
- package/shared/localization/locales/bg.json +154 -133
- package/shared/localization/locales/ca.json +157 -136
- package/shared/localization/locales/cs.json +148 -127
- package/shared/localization/locales/da.json +153 -132
- package/shared/localization/locales/de.json +152 -131
- package/shared/localization/locales/el.json +153 -132
- package/shared/localization/locales/en-GB.json +160 -139
- package/shared/localization/locales/en-US.json +166 -145
- package/shared/localization/locales/en-XL.json +166 -145
- package/shared/localization/locales/es-419.json +150 -129
- package/shared/localization/locales/es.json +149 -128
- package/shared/localization/locales/fi.json +151 -130
- package/shared/localization/locales/fil.json +154 -133
- package/shared/localization/locales/fr.json +150 -129
- package/shared/localization/locales/he.json +160 -139
- package/shared/localization/locales/hi.json +156 -135
- package/shared/localization/locales/hr.json +154 -133
- package/shared/localization/locales/hu.json +111 -90
- package/shared/localization/locales/id.json +153 -132
- package/shared/localization/locales/it.json +153 -132
- package/shared/localization/locales/ja.json +150 -129
- package/shared/localization/locales/ko.json +42 -21
- package/shared/localization/locales/lt.json +152 -131
- package/shared/localization/locales/lv.json +93 -72
- package/shared/localization/locales/nl.json +149 -128
- package/shared/localization/locales/no.json +152 -131
- package/shared/localization/locales/pl.json +153 -132
- package/shared/localization/locales/pt-PT.json +152 -131
- package/shared/localization/locales/pt.json +157 -136
- package/shared/localization/locales/ro.json +149 -128
- package/shared/localization/locales/ru.json +73 -52
- package/shared/localization/locales/sk.json +153 -132
- package/shared/localization/locales/sl.json +149 -128
- package/shared/localization/locales/sr-Latn.json +151 -130
- package/shared/localization/locales/sr.json +151 -130
- package/shared/localization/locales/sv.json +152 -131
- package/shared/localization/locales/ta.json +156 -135
- package/shared/localization/locales/te.json +157 -136
- package/shared/localization/locales/th.json +111 -90
- package/shared/localization/locales/tr.json +153 -132
- package/shared/localization/locales/uk.json +149 -128
- package/shared/localization/locales/vi.json +162 -141
- package/shared/localization/locales/zh-HK.json +150 -129
- package/shared/localization/locales/zh-TW.json +154 -133
- package/shared/localization/locales/zh.json +152 -131
- package/third-party/ard/README.md +18 -9
- package/third-party/ard/ard-test.js +183 -266
- package/third-party/ard/ard.d.ts +17 -1
- package/third-party/ard/ard.js +276 -45
- package/third-party/ard/schema-validator.d.ts +14 -5
- package/third-party/ard/schema-validator.js +921 -1177
- package/third-party/ard/spec/schemas/ard-entry.schema.json +176 -0
- package/types/artifacts.d.ts +29 -5
- package/build-tracker.config.js +0 -16
- package/third-party/ard/spec/schemas/ai-catalog.schema.json +0 -292
package/third-party/ard/ard.js
CHANGED
|
@@ -4,12 +4,216 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview JavaScript port of the manifest validation in the ARD conformance test
|
|
9
|
+
* (`conformance/bin/conformance-test`, ARD v0.91). See README.md for the pinned upstream commit
|
|
10
|
+
* and the Lighthouse-specific adaptations.
|
|
11
|
+
*/
|
|
12
|
+
|
|
7
13
|
import log from 'lighthouse-logger';
|
|
8
|
-
import
|
|
14
|
+
import {ArdManifest as validateArdManifest, ArdEntry as validateArdEntry} from './schema-validator.js';
|
|
9
15
|
|
|
10
16
|
// Strict URN Regex matching urn:air:<publisher>:<namespace>:<agent-name>
|
|
11
17
|
const URN_REGEX = /^urn:air:([a-zA-Z0-9.-]+)(?::([a-zA-Z0-9._:-]+))?:([a-zA-Z0-9._-]+)$/;
|
|
12
18
|
|
|
19
|
+
// RFC 6838 restricted-name for type/subtype plus RFC 9110 parameters.
|
|
20
|
+
const RESTRICTED_NAME = '[0-9A-Za-z][0-9A-Za-z!#$&^_.+\\-]{0,126}';
|
|
21
|
+
const TOKEN = "[!#$%&'*+\\-.^_`|~0-9A-Za-z]+";
|
|
22
|
+
const QUOTED_STRING = '"(?:[\\t !#-\\[\\]-~]|\\\\[\\t !-~])*"';
|
|
23
|
+
const MEDIA_TYPE_REGEX = new RegExp(
|
|
24
|
+
`^(?<type>${RESTRICTED_NAME})/(?<subtype>${RESTRICTED_NAME})` +
|
|
25
|
+
`(?<parameters>(?:[ \\t]*;[ \\t]*${TOKEN}[ \\t]*=[ \\t]*(?:${TOKEN}|${QUOTED_STRING}))*)$`
|
|
26
|
+
);
|
|
27
|
+
const MEDIA_TYPE_PARAMETER_REGEX = new RegExp(
|
|
28
|
+
`[ \\t]*;[ \\t]*(?<name>${TOKEN})[ \\t]*=[ \\t]*` +
|
|
29
|
+
`(?<value>${TOKEN}|${QUOTED_STRING})`,
|
|
30
|
+
'g'
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {[string, Array<[string, string]>]} ParsedMediaType
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} media_type
|
|
39
|
+
* @return {ParsedMediaType|null}
|
|
40
|
+
*/
|
|
41
|
+
function parse_media_type(media_type) {
|
|
42
|
+
const match = MEDIA_TYPE_REGEX.exec(media_type);
|
|
43
|
+
if (!match?.groups) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const base_media_type =
|
|
48
|
+
`${match.groups.type.toLowerCase()}/${match.groups.subtype.toLowerCase()}`;
|
|
49
|
+
/** @type {Array<[string, string]>} */
|
|
50
|
+
const parameters = [...match.groups.parameters.matchAll(MEDIA_TYPE_PARAMETER_REGEX)]
|
|
51
|
+
.map(parameter => /** @type {[string, string]} */ ([
|
|
52
|
+
/** @type {Record<string, string>} */ (parameter.groups).name.toLowerCase(),
|
|
53
|
+
/** @type {Record<string, string>} */ (parameter.groups).value,
|
|
54
|
+
]))
|
|
55
|
+
// Python sorts (name, value) tuples by code point.
|
|
56
|
+
.sort((a, b) => {
|
|
57
|
+
if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
|
|
58
|
+
if (a[1] !== b[1]) return a[1] < b[1] ? -1 : 1;
|
|
59
|
+
return 0;
|
|
60
|
+
});
|
|
61
|
+
const parameter_names = parameters.map(([name]) => name);
|
|
62
|
+
if (parameter_names.length !== new Set(parameter_names).size) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return [base_media_type, parameters];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {ParsedMediaType} parsed */
|
|
69
|
+
const mediaTypeKey = parsed => JSON.stringify(parsed);
|
|
70
|
+
|
|
71
|
+
const STANDARD_MEDIA_TYPES = [
|
|
72
|
+
"application/ai-catalog+json",
|
|
73
|
+
"application/agent-card+json",
|
|
74
|
+
"application/a2a-agent-card+json",
|
|
75
|
+
"application/mcp-server-card+json",
|
|
76
|
+
"application/agent-skills+zip",
|
|
77
|
+
"application/agent-skills+gzip",
|
|
78
|
+
'text/markdown; profile="urn:air:agent-skills"',
|
|
79
|
+
"application/ai-registry",
|
|
80
|
+
"application/ai-registry+json",
|
|
81
|
+
];
|
|
82
|
+
const STANDARD_MEDIA_TYPES_PARSED = STANDARD_MEDIA_TYPES.map(media_type =>
|
|
83
|
+
/** @type {ParsedMediaType} */ (parse_media_type(media_type)));
|
|
84
|
+
const STANDARD_MEDIA_TYPE_KEYS = new Set(STANDARD_MEDIA_TYPES_PARSED.map(mediaTypeKey));
|
|
85
|
+
const STANDARD_BASE_MEDIA_TYPES = new Set(STANDARD_MEDIA_TYPES_PARSED.map(([base]) => base));
|
|
86
|
+
/** @type {Map<string, Map<string, string>>} */
|
|
87
|
+
const STANDARD_PARAMETERS_BY_BASE = new Map(STANDARD_MEDIA_TYPES_PARSED.map(
|
|
88
|
+
([base, parameters]) => [base, new Map(parameters)]));
|
|
89
|
+
/** @type {Record<string, string>} */
|
|
90
|
+
const DEPRECATED_MEDIA_TYPES = {
|
|
91
|
+
"application/mcp-server+json": "application/mcp-server-card+json",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Python's `repr()` of the list of standard media types, to keep messages identical to upstream.
|
|
96
|
+
*/
|
|
97
|
+
const STANDARD_MEDIA_TYPES_REPR = `[${STANDARD_MEDIA_TYPES.map(t => `'${t}'`).join(', ')}]`;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Name of the Python type a JSON value would parse to, for messages matching upstream.
|
|
101
|
+
* @param {unknown} value
|
|
102
|
+
* @return {string}
|
|
103
|
+
*/
|
|
104
|
+
function python_type_name(value) {
|
|
105
|
+
if (value === null) return 'NoneType';
|
|
106
|
+
if (Array.isArray(value)) return 'list';
|
|
107
|
+
if (typeof value === 'boolean') return 'bool';
|
|
108
|
+
if (typeof value === 'number') return Number.isInteger(value) ? 'int' : 'float';
|
|
109
|
+
if (typeof value === 'object') return 'dict';
|
|
110
|
+
return typeof value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {unknown} media_type
|
|
115
|
+
* @return {['warning'|'info', string]|null}
|
|
116
|
+
*/
|
|
117
|
+
function classify_media_type(media_type) {
|
|
118
|
+
if (typeof media_type !== 'string') {
|
|
119
|
+
return [
|
|
120
|
+
"warning",
|
|
121
|
+
`Media type must be a string; got ${python_type_name(media_type)}.`,
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const parsed_media_type = parse_media_type(media_type);
|
|
126
|
+
if (parsed_media_type === null) {
|
|
127
|
+
return [
|
|
128
|
+
"warning",
|
|
129
|
+
`Media type '${media_type}' is not a valid IANA media type. ` +
|
|
130
|
+
"Expected 'type/subtype' with optional parameters.",
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const [base_media_type, parameters] = parsed_media_type;
|
|
135
|
+
if (STANDARD_MEDIA_TYPE_KEYS.has(mediaTypeKey(parsed_media_type))) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const replacement = DEPRECATED_MEDIA_TYPES[base_media_type];
|
|
140
|
+
if (replacement !== undefined) {
|
|
141
|
+
return [
|
|
142
|
+
"warning",
|
|
143
|
+
`Media type '${media_type}' was renamed by ADR-0008. ` +
|
|
144
|
+
`Use '${replacement}'.`,
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const expected_parameters = STANDARD_PARAMETERS_BY_BASE.get(base_media_type);
|
|
149
|
+
if (STANDARD_BASE_MEDIA_TYPES.has(base_media_type) && expected_parameters) {
|
|
150
|
+
const actual_parameters = new Map(parameters);
|
|
151
|
+
const missing_parameters = [...expected_parameters]
|
|
152
|
+
.filter(([name, value]) => actual_parameters.get(name) !== value)
|
|
153
|
+
.map(([name, value]) => `${name}=${value}`);
|
|
154
|
+
const unrecognized_parameters = parameters
|
|
155
|
+
.filter(([name, value]) => expected_parameters.get(name) !== value)
|
|
156
|
+
.map(([name]) => name);
|
|
157
|
+
const differences = [];
|
|
158
|
+
if (missing_parameters.length) {
|
|
159
|
+
differences.push(
|
|
160
|
+
"missing required parameters: " + missing_parameters.join(", ")
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (unrecognized_parameters.length) {
|
|
164
|
+
differences.push(
|
|
165
|
+
"unrecognized parameters: " + unrecognized_parameters.join(", ")
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return [
|
|
169
|
+
"warning",
|
|
170
|
+
`Media type '${media_type}' is based on standard discovery type ` +
|
|
171
|
+
`'${base_media_type}' but has ` +
|
|
172
|
+
`${differences.join('; ')}.`,
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (base_media_type.startsWith("application/")) {
|
|
177
|
+
return [
|
|
178
|
+
"info",
|
|
179
|
+
`Media type '${media_type}' is a valid application extension media type. ` +
|
|
180
|
+
"ARD permits extension types without core registration.",
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return [
|
|
185
|
+
"warning",
|
|
186
|
+
`Media type '${media_type}' is not one of standard discovery types: ` +
|
|
187
|
+
`${STANDARD_MEDIA_TYPES_REPR}.`,
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {unknown} value
|
|
193
|
+
* @return {value is Record<string, any>}
|
|
194
|
+
*/
|
|
195
|
+
function is_object(value) {
|
|
196
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Errors reported inside a oneOf/anyOf branch or a `not` are restated by the combinator itself.
|
|
200
|
+
const COMBINATOR_BRANCH_REGEX = /\/(?:oneOf|anyOf)\/\d+(?:\/|$)|\/not(?:\/|$)/;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Formats the most relevant Ajv error as `<message> at path '<path>'`.
|
|
204
|
+
* @param {Array<any>|null|undefined} errors
|
|
205
|
+
* @return {string}
|
|
206
|
+
*/
|
|
207
|
+
function format_schema_error(errors) {
|
|
208
|
+
const e = errors?.find(err => !COMBINATOR_BRANCH_REGEX.test(err.schemaPath)) || errors?.[0];
|
|
209
|
+
const propertyPath = e?.instancePath ? e.instancePath.replace(/^\//, '').replace(/\//g, '.') : 'root';
|
|
210
|
+
let msg = e?.message || 'unknown schema error';
|
|
211
|
+
if (e?.keyword === 'required') {
|
|
212
|
+
msg = `'${e.params.missingProperty}' is a required property`;
|
|
213
|
+
}
|
|
214
|
+
return `${msg} at path '${propertyPath}'`;
|
|
215
|
+
}
|
|
216
|
+
|
|
13
217
|
/**
|
|
14
218
|
* @typedef {{
|
|
15
219
|
* element: string,
|
|
@@ -23,6 +227,8 @@ class ConformanceTester {
|
|
|
23
227
|
this.errors = [];
|
|
24
228
|
/** @type {ValidationError[]} */
|
|
25
229
|
this.warnings = [];
|
|
230
|
+
/** @type {ValidationError[]} */
|
|
231
|
+
this.infos = [];
|
|
26
232
|
}
|
|
27
233
|
|
|
28
234
|
/**
|
|
@@ -43,29 +249,53 @@ class ConformanceTester {
|
|
|
43
249
|
log.verbose('ARD', `Validation Warning [${element}]: ${message}`);
|
|
44
250
|
}
|
|
45
251
|
|
|
46
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* @param {string} message
|
|
254
|
+
* @param {string} [element='Root']
|
|
255
|
+
*/
|
|
256
|
+
add_info(message, element = 'Root') {
|
|
257
|
+
this.infos.push({element, message});
|
|
258
|
+
log.verbose('ARD', `Validation Info [${element}]: ${message}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Validate the manifest as an ArdManifest, then every entry as an ArdEntry.
|
|
263
|
+
* @param {any} manifest_data
|
|
264
|
+
*/
|
|
47
265
|
run_json_schema_validation(manifest_data) {
|
|
266
|
+
let ok = true;
|
|
48
267
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (valid) {
|
|
52
|
-
log.verbose('ARD', 'Strict JSON Schema validation passed.');
|
|
53
|
-
return true;
|
|
268
|
+
if (validateArdManifest(manifest_data)) {
|
|
269
|
+
log.verbose('ARD', 'Manifest validates against ArdManifest.');
|
|
54
270
|
} else {
|
|
55
|
-
const errors = /** @type {any} */ (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let msg = e?.message || 'unknown schema error';
|
|
59
|
-
if (e?.keyword === 'required') {
|
|
60
|
-
msg = `'${e.params.missingProperty}' is a required property`;
|
|
61
|
-
}
|
|
62
|
-
this.add_error(`JSON Schema Validation Failed: ${msg} at path '${propertyPath}'`, 'Root');
|
|
63
|
-
return false;
|
|
271
|
+
const errors = /** @type {any} */ (validateArdManifest).errors;
|
|
272
|
+
this.add_error(`ArdManifest validation failed: ${format_schema_error(errors)}`, 'Root');
|
|
273
|
+
ok = false;
|
|
64
274
|
}
|
|
65
275
|
} catch (e) {
|
|
66
276
|
this.add_warning(`Failed to run JSON Schema validator: ${e}`, 'Root');
|
|
67
277
|
return true;
|
|
68
278
|
}
|
|
279
|
+
|
|
280
|
+
const entries = is_object(manifest_data) ? manifest_data["entries"] : undefined;
|
|
281
|
+
if (Array.isArray(entries)) {
|
|
282
|
+
let bad = 0;
|
|
283
|
+
entries.forEach((entry, idx) => {
|
|
284
|
+
const label = is_object(entry) ?
|
|
285
|
+
String(entry["displayName"] || entry["identifier"] || `Entry #${idx}`) :
|
|
286
|
+
`Entry #${idx}`;
|
|
287
|
+
if (!validateArdEntry(entry)) {
|
|
288
|
+
const errors = /** @type {any} */ (validateArdEntry).errors;
|
|
289
|
+
this.add_error(`ArdEntry validation failed: ${format_schema_error(errors)}`, label);
|
|
290
|
+
bad += 1;
|
|
291
|
+
ok = false;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
if (!bad) {
|
|
295
|
+
log.verbose('ARD', `All ${entries.length} entries validate against ArdEntry.`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return ok;
|
|
69
299
|
}
|
|
70
300
|
|
|
71
301
|
/**
|
|
@@ -91,14 +321,18 @@ class ConformanceTester {
|
|
|
91
321
|
// 3. Custom Semantic and Protocol-Specific Validation
|
|
92
322
|
log.verbose('ARD', 'Running custom semantic checks...');
|
|
93
323
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
324
|
+
// Upstream assumes a JSON object here; treat anything else as an empty root so the
|
|
325
|
+
// schema error above is accompanied by the missing 'entries' error instead of a crash.
|
|
326
|
+
const root = is_object(data) ? data : {};
|
|
327
|
+
|
|
328
|
+
// ARD requires only 'entries' at the top level (§5.1). Other members are
|
|
329
|
+
// transport-defined and ignored, so their presence is noted, never an error.
|
|
330
|
+
const extra_roots = Object.keys(root).filter(k => k !== "entries");
|
|
331
|
+
if (extra_roots.length) {
|
|
332
|
+
log.verbose('ARD', `Top-level members ignored by ARD (transport-defined): ${extra_roots.sort().join(', ')}.`);
|
|
99
333
|
}
|
|
100
334
|
|
|
101
|
-
const entries =
|
|
335
|
+
const entries = root["entries"];
|
|
102
336
|
if (entries === undefined || entries === null) {
|
|
103
337
|
this.add_error("Missing required 'entries' array.", 'Root');
|
|
104
338
|
return false;
|
|
@@ -108,8 +342,10 @@ class ConformanceTester {
|
|
|
108
342
|
}
|
|
109
343
|
|
|
110
344
|
log.verbose('ARD', `Found ${entries.length} entries to validate.`);
|
|
111
|
-
entries.forEach((
|
|
112
|
-
|
|
345
|
+
entries.forEach((raw_entry, idx) => {
|
|
346
|
+
// Upstream assumes each entry is a JSON object; treat anything else as empty.
|
|
347
|
+
const entry = is_object(raw_entry) ? raw_entry : {};
|
|
348
|
+
const label = String(entry["displayName"] || entry["identifier"] || `Entry #${idx}`);
|
|
113
349
|
|
|
114
350
|
// Required properties
|
|
115
351
|
const ident = entry["identifier"];
|
|
@@ -136,35 +372,30 @@ class ConformanceTester {
|
|
|
136
372
|
if (!media_type) {
|
|
137
373
|
this.add_error("Missing required 'type' (mediaType).", label);
|
|
138
374
|
} else {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"application/ai-registry",
|
|
148
|
-
"application/ai-registry+json"
|
|
149
|
-
];
|
|
150
|
-
if (!valid_types.includes(media_type)) {
|
|
151
|
-
this.add_warning(`Media type '${media_type}' is not one of standard discovery types: ${valid_types}.`, label);
|
|
375
|
+
const diagnostic = classify_media_type(media_type);
|
|
376
|
+
if (diagnostic !== null) {
|
|
377
|
+
const [severity, message] = diagnostic;
|
|
378
|
+
if (severity === "info") {
|
|
379
|
+
this.add_info(message, label);
|
|
380
|
+
} else {
|
|
381
|
+
this.add_warning(message, label);
|
|
382
|
+
}
|
|
152
383
|
}
|
|
153
384
|
}
|
|
154
385
|
|
|
155
386
|
// Strict Value-or-Reference checks
|
|
156
|
-
const has_url =
|
|
157
|
-
const has_data =
|
|
387
|
+
const has_url = "url" in entry;
|
|
388
|
+
const has_data = "data" in entry;
|
|
158
389
|
if (has_url && has_data) {
|
|
159
390
|
this.add_error("Constraint violation: both 'url' and 'data' are provided. MUST provide exactly one.", label);
|
|
160
391
|
} else if (!has_url && !has_data) {
|
|
161
392
|
this.add_error("Constraint violation: neither 'url' nor 'data' is provided. MUST provide exactly one.", label);
|
|
162
393
|
}
|
|
163
394
|
|
|
164
|
-
// Custom constraints for representativeQueries
|
|
395
|
+
// Custom constraints for representativeQueries (§4.2, §D.2)
|
|
165
396
|
const queries = entry["representativeQueries"];
|
|
166
397
|
if (queries === undefined || queries === null) {
|
|
167
|
-
this.add_warning("
|
|
398
|
+
this.add_warning("No 'representativeQueries'. The semantic index is built from this term, so the entry will not be found by search — it is a valid catalog entry but not a discoverable ARD entry.", label);
|
|
168
399
|
} else if (!Array.isArray(queries)) {
|
|
169
400
|
this.add_error("'representativeQueries' must be an array of strings.", label);
|
|
170
401
|
} else {
|
|
@@ -181,7 +412,7 @@ class ConformanceTester {
|
|
|
181
412
|
// Progressive trust checks
|
|
182
413
|
const trust = entry["trustManifest"];
|
|
183
414
|
if (trust !== undefined && trust !== null) {
|
|
184
|
-
if (
|
|
415
|
+
if (!is_object(trust)) {
|
|
185
416
|
this.add_error("'trustManifest' must be a JSON object.", label);
|
|
186
417
|
} else {
|
|
187
418
|
const trust_id = trust["identity"];
|
|
@@ -193,12 +424,12 @@ class ConformanceTester {
|
|
|
193
424
|
});
|
|
194
425
|
|
|
195
426
|
// Top-level deprecated property check
|
|
196
|
-
if ("collections" in
|
|
197
|
-
this.
|
|
427
|
+
if ("collections" in root) {
|
|
428
|
+
this.add_warning("Found 'collections' at root. Top-level collections were removed in ADR-0003; ARD ignores unrecognized top-level members, so this does not invalidate the manifest, but hierarchies should be modeled inside 'entries'.", 'Root');
|
|
198
429
|
}
|
|
199
430
|
|
|
200
431
|
return this.errors.length === 0;
|
|
201
432
|
}
|
|
202
433
|
}
|
|
203
434
|
|
|
204
|
-
export { ConformanceTester };
|
|
435
|
+
export { ConformanceTester, classify_media_type };
|
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function ArdManifest(data: any, { instancePath, parentData, parentDataProperty, rootData, dynamicAnchors }?: {
|
|
2
2
|
instancePath?: string | undefined;
|
|
3
3
|
rootData?: any;
|
|
4
4
|
dynamicAnchors?: {} | undefined;
|
|
5
5
|
}): boolean;
|
|
6
|
-
export namespace
|
|
6
|
+
export namespace ArdManifest {
|
|
7
7
|
namespace evaluated {
|
|
8
8
|
let props: boolean;
|
|
9
9
|
let dynamicProps: boolean;
|
|
10
10
|
let dynamicItems: boolean;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
export
|
|
14
|
-
declare function validate20(data: any, { instancePath, parentData, parentDataProperty, rootData, dynamicAnchors }?: {
|
|
13
|
+
export function ArdEntry(data: any, { instancePath, parentData, parentDataProperty, rootData, dynamicAnchors }?: {
|
|
15
14
|
instancePath?: string | undefined;
|
|
16
15
|
rootData?: any;
|
|
17
16
|
dynamicAnchors?: {} | undefined;
|
|
18
17
|
}): boolean;
|
|
19
|
-
|
|
18
|
+
export namespace ArdEntry {
|
|
19
|
+
export namespace evaluated_1 {
|
|
20
|
+
let props_1: boolean;
|
|
21
|
+
export { props_1 as props };
|
|
22
|
+
let dynamicProps_1: boolean;
|
|
23
|
+
export { dynamicProps_1 as dynamicProps };
|
|
24
|
+
let dynamicItems_1: boolean;
|
|
25
|
+
export { dynamicItems_1 as dynamicItems };
|
|
26
|
+
}
|
|
27
|
+
export { evaluated_1 as evaluated };
|
|
28
|
+
}
|
|
20
29
|
//# sourceMappingURL=schema-validator.d.ts.map
|