Setting the file. One moment. Contract · Vercel Optimize · vercel-labs/agent-skills · Skills Docslib/gates/contract.mjs
JavaScript·79 lines·3 KB
}`
).
join
(
'
\n
'
)
}`
);
6 this.name = 'CandidateContractError';
7 this.errors = errors;
8 }
9}
10
11export function validateCandidates(candidates, ctx = {}) {
12 if (!Array.isArray(candidates)) {
13 throw new CandidateContractError([`${ctx.source ?? 'gate'}: expected candidate array`]);
14 }
15 const errors = [];
16 for (let i = 0; i < candidates.length; i++) {
17 errors.push(...validateCandidate(candidates[i], { ...ctx, index: i }).errors);
18 }
19 if (errors.length > 0) throw new CandidateContractError(errors);
20 return candidates;
21}
22
23export function validateCandidate(candidate, ctx = {}) {
24 const label = candidateLabel(candidate, ctx);
25 const errors = [];
26 if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
27 return { ok: false, errors: [`${label}: candidate must be an object`] };
28 }
29
30 if (!nonEmptyString(candidate.kind)) errors.push(`${label}: kind must be a non-empty string`);
31 if (!VALID_SCOPES.has(candidate.scope)) {
32 errors.push(`${label}: scope must be one of route, file, account`);
33 }
34 if (!Number.isFinite(candidate.priority)) errors.push(`${label}: priority must be a finite number`);
35 if (!Number.isFinite(candidate.confidence)) errors.push(`${label}: confidence must be a finite number`);
36 if (Array.isArray(candidate.files)) {
37 if (!candidate.files.every((f) => typeof f === 'string' && f.length > 0)) {
38 errors.push(`${label}: files must contain only non-empty strings`);
39 }
40 } else {
41 errors.push(`${label}: files must be an array`);
42 }
43 if (!nonEmptyString(candidate.reason)) errors.push(`${label}: reason must be a non-empty string`);
44 if (!nonEmptyString(candidate.question)) errors.push(`${label}: question must be a non-empty string`);
45
46 if (candidate.scope === 'route') {
47 const hasRoute = nonEmptyString(candidate.route);
48 const hasHostname = nonEmptyString(candidate.hostname);
49 if (!hasRoute && !hasHostname) {
50 errors.push(`${label}: route-scoped candidates must set route or hostname`);
51 }
52 }
53 if (candidate.scope === 'file') {
54 if (candidate.route != null || candidate.hostname != null) {
55 errors.push(`${label}: file-scoped candidates must not set route or hostname`);
56 }
57 if (!Array.isArray(candidate.files) || candidate.files.length === 0) {
58 errors.push(`${label}: file-scoped candidates must include at least one file`);
59 }
60 }
61 if (candidate.scope === 'account') {
62 if (candidate.route != null || candidate.hostname != null) {
63 errors.push(`${label}: account-scoped candidates must not set route or hostname`);
64 }
65 }
66
67 return { ok: errors.length === 0, errors };
68}
69
70function candidateLabel(candidate, ctx) {
71 const source = ctx.source ?? 'gate';
72 const index = ctx.index == null ? '?' : ctx.index;
73 const kind = candidate?.kind ?? '?';
74 return `${source}[${index}] ${kind}`;
75}
76
77function nonEmptyString(value) {
78 return typeof value === 'string' && value.trim().length > 0;
79}