@farming-labs/docs 0.2.62 → 0.2.63
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/agent-CQTH7NFu.mjs +624 -0
- package/dist/agent-DKKptIgy.mjs +4365 -0
- package/dist/agent-evals-B7MIxuEX.mjs +2144 -0
- package/dist/agent-export-CBgWgPvH.mjs +910 -0
- package/dist/agent-scope-C_U--OZ7.mjs +283 -0
- package/dist/agent-skills-bundle.d.mts +13 -0
- package/dist/agent-skills-bundle.mjs +12 -0
- package/dist/agent-skills-server-CPja6Syt.d.mts +14 -0
- package/dist/agent-skills-server-DraIb6FV.mjs +415 -0
- package/dist/agent-skills-vite.d.mts +31 -0
- package/dist/agent-skills-vite.mjs +70 -0
- package/dist/agents-XWZBub6f.mjs +221 -0
- package/dist/analytics-Bx44lg6d.mjs +177 -0
- package/dist/cli/index.d.mts +15 -0
- package/dist/cli/index.mjs +452 -0
- package/dist/client/react.d.mts +45 -0
- package/dist/client/react.mjs +223 -0
- package/dist/cloud-analytics-CSyFE6SS.mjs +132 -0
- package/dist/cloud-ask-ai-sbpjOR2K.mjs +382 -0
- package/dist/cloud-ask-ai-zpwkdwnF.d.mts +23 -0
- package/dist/cloud-pdNC-tyj.mjs +1615 -0
- package/dist/code-blocks-DnNVNK2M.mjs +871 -0
- package/dist/codeblocks-CFuurVIH.mjs +250 -0
- package/dist/config-Wcdj-D0a.mjs +369 -0
- package/dist/dev-Cmy6DtdF.mjs +1333 -0
- package/dist/docs-cloud-server.d.mts +70 -0
- package/dist/docs-cloud-server.mjs +310 -0
- package/dist/doctor-CJqtzKZT.mjs +2036 -0
- package/dist/downgrade-w7e6Se0L.mjs +184 -0
- package/dist/errors-DbOhkE1h.mjs +20 -0
- package/dist/golden-evaluations-Dsvh0etV.mjs +1503 -0
- package/dist/i18n-CCaFUnAN.mjs +40 -0
- package/dist/index.d.mts +1150 -0
- package/dist/index.mjs +10 -0
- package/dist/init-CQY0Woe3.mjs +1264 -0
- package/dist/mcp-B9dcsivk.mjs +156 -0
- package/dist/mcp.d.mts +298 -0
- package/dist/mcp.mjs +4430 -0
- package/dist/metadata-DWExHQnx.mjs +237 -0
- package/dist/package-version-n5AFur8a.mjs +128 -0
- package/dist/reading-time-CYZ5VvKU.mjs +742 -0
- package/dist/review-Fndl5brR.mjs +673 -0
- package/dist/robots-BIpC4j4P.mjs +201 -0
- package/dist/robots-CUTahhoY.mjs +179 -0
- package/dist/search-B6V6qtiI.mjs +1826 -0
- package/dist/search-CaSyi6H6.d.mts +279 -0
- package/dist/search-DSjCeOk7.mjs +104 -0
- package/dist/server.d.mts +343 -0
- package/dist/server.mjs +14 -0
- package/dist/sitemap-Cykpe3Tz.mjs +249 -0
- package/dist/sitemap-server-C_6Wes83.mjs +1137 -0
- package/dist/standards-discovery-C4HUqMd2.d.mts +227 -0
- package/dist/standards-discovery-jkykaXq1.mjs +519 -0
- package/dist/templates-Bq_P7ctv.mjs +2465 -0
- package/dist/types-lMBIdZg0.d.mts +3315 -0
- package/dist/upgrade-oz-GChgt.mjs +56 -0
- package/dist/utils-DpiIioYb.mjs +225 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2144 @@
|
|
|
1
|
+
import { m as findDocsMarkdownSection, p as resolveSearchRequestConfig, t as buildDocsAskAIContext, u as performDocsSearch } from "./search-B6V6qtiI.mjs";
|
|
2
|
+
import { a as normalizeAgentScopeValues, i as normalizeAgentLocale, n as agentVersionConstraintsOverlap, o as normalizeAgentVersion, r as normalizeAgentFramework, t as agentVersionConstraintMatches } from "./agent-scope-C_U--OZ7.mjs";
|
|
3
|
+
import { buildDocsMcpContext } from "./mcp.mjs";
|
|
4
|
+
import { i as validateCodeBlockPlans, n as planCodeBlockTargets, r as resolveDocsCodeBlocksValidateConfig, t as extractCodeBlocksFromMarkdown } from "./code-blocks-DnNVNK2M.mjs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
7
|
+
import { createJiti } from "jiti";
|
|
8
|
+
|
|
9
|
+
//#region src/review.ts
|
|
10
|
+
const DEFAULT_DOCS_REVIEW_WORKFLOW_PATH = ".github/workflows/docs-review.yml";
|
|
11
|
+
const DEFAULT_DOCS_REVIEW_REUSABLE_WORKFLOW_PATH = ".github/workflows/docs-review-reusable.yml";
|
|
12
|
+
const DEFAULT_DOCS_REVIEW_REUSABLE_WORKFLOW = "farming-labs/docs/.github/workflows/docs-review-reusable.yml@main";
|
|
13
|
+
const LOCAL_DOCS_REVIEW_REUSABLE_WORKFLOW = "./.github/workflows/docs-review-reusable.yml";
|
|
14
|
+
const DEFAULT_DOCS_REVIEW_PNPM_VERSION = "10";
|
|
15
|
+
const DEFAULT_DOCS_REVIEW_SCORE_THRESHOLD = 80;
|
|
16
|
+
const DEFAULT_DOCS_REVIEW_CI_NAME = "docs-review";
|
|
17
|
+
const DEFAULT_REVIEW_RULES = {
|
|
18
|
+
brokenLinks: "error",
|
|
19
|
+
frontmatter: "error",
|
|
20
|
+
duplicateSlugs: "error",
|
|
21
|
+
invalidMdx: "error",
|
|
22
|
+
configExamples: "warn",
|
|
23
|
+
codeFenceMetadata: "warn",
|
|
24
|
+
runnableMetadata: "warn",
|
|
25
|
+
agentContext: "warn",
|
|
26
|
+
commandHealth: "warn",
|
|
27
|
+
relatedCoverage: "suggestion",
|
|
28
|
+
configConfidence: "warn",
|
|
29
|
+
agentSurfaceDrift: "error",
|
|
30
|
+
goldenTasks: "warn"
|
|
31
|
+
};
|
|
32
|
+
const DEFAULT_REVIEW_WEIGHTS = {
|
|
33
|
+
error: 20,
|
|
34
|
+
warn: 8,
|
|
35
|
+
suggestion: 2
|
|
36
|
+
};
|
|
37
|
+
const REVIEW_SEVERITIES = new Set([
|
|
38
|
+
"off",
|
|
39
|
+
"suggestion",
|
|
40
|
+
"warn",
|
|
41
|
+
"error"
|
|
42
|
+
]);
|
|
43
|
+
const REVIEW_CI_MODES = new Set([
|
|
44
|
+
"off",
|
|
45
|
+
"warn",
|
|
46
|
+
"block"
|
|
47
|
+
]);
|
|
48
|
+
const FILE_EXTS = [
|
|
49
|
+
"tsx",
|
|
50
|
+
"ts",
|
|
51
|
+
"jsx",
|
|
52
|
+
"js"
|
|
53
|
+
];
|
|
54
|
+
function resolveDocsReviewConfig(review) {
|
|
55
|
+
if (review === false) return {
|
|
56
|
+
enabled: false,
|
|
57
|
+
ci: {
|
|
58
|
+
enabled: false,
|
|
59
|
+
name: DEFAULT_DOCS_REVIEW_CI_NAME,
|
|
60
|
+
mode: "off",
|
|
61
|
+
annotations: false,
|
|
62
|
+
comment: false
|
|
63
|
+
},
|
|
64
|
+
score: {
|
|
65
|
+
threshold: DEFAULT_DOCS_REVIEW_SCORE_THRESHOLD,
|
|
66
|
+
weights: DEFAULT_REVIEW_WEIGHTS
|
|
67
|
+
},
|
|
68
|
+
rules: DEFAULT_REVIEW_RULES
|
|
69
|
+
};
|
|
70
|
+
const objectConfig = review && typeof review === "object" ? review : {};
|
|
71
|
+
const ciConfig = objectConfig.ci;
|
|
72
|
+
const ciObject = ciConfig && typeof ciConfig === "object" ? ciConfig : {};
|
|
73
|
+
const configuredMode = ciObject.mode;
|
|
74
|
+
const mode = configuredMode && REVIEW_CI_MODES.has(configuredMode) ? configuredMode : "warn";
|
|
75
|
+
const enabled = objectConfig.enabled !== false;
|
|
76
|
+
const ciEnabled = enabled && ciConfig !== false && ciObject.enabled !== false && mode !== "off";
|
|
77
|
+
return {
|
|
78
|
+
enabled,
|
|
79
|
+
ci: {
|
|
80
|
+
enabled: ciEnabled,
|
|
81
|
+
name: normalizeCiName(ciObject.name),
|
|
82
|
+
mode: ciEnabled ? mode : "off",
|
|
83
|
+
annotations: ciObject.annotations !== false,
|
|
84
|
+
comment: ciObject.comment !== false
|
|
85
|
+
},
|
|
86
|
+
score: {
|
|
87
|
+
threshold: clampScoreThreshold(objectConfig.score?.threshold),
|
|
88
|
+
weights: {
|
|
89
|
+
error: normalizeWeight(objectConfig.score?.weights?.error, DEFAULT_REVIEW_WEIGHTS.error),
|
|
90
|
+
warn: normalizeWeight(objectConfig.score?.weights?.warn, DEFAULT_REVIEW_WEIGHTS.warn),
|
|
91
|
+
suggestion: normalizeWeight(objectConfig.score?.weights?.suggestion, DEFAULT_REVIEW_WEIGHTS.suggestion)
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
rules: {
|
|
95
|
+
...DEFAULT_REVIEW_RULES,
|
|
96
|
+
...normalizeRules(objectConfig.rules)
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function buildDocsReviewWorkflow(options = {}) {
|
|
101
|
+
const packageManager = options.packageManager ?? "npm";
|
|
102
|
+
const ciName = normalizeCiName(options.ciName);
|
|
103
|
+
const projectDir = normalizeProjectDir(options.projectDir);
|
|
104
|
+
const configPath = options.configPath ?? "docs.config.ts";
|
|
105
|
+
const reusableWorkflow = options.reusableWorkflow ?? DEFAULT_DOCS_REVIEW_REUSABLE_WORKFLOW;
|
|
106
|
+
const filters = normalizePathFilters(options.pathFilters);
|
|
107
|
+
const workflowInputs = [
|
|
108
|
+
["check-name", ciName],
|
|
109
|
+
["config", configPath],
|
|
110
|
+
["working-directory", projectDir],
|
|
111
|
+
["package-manager", packageManager],
|
|
112
|
+
options.pnpmVersion ? ["pnpm-version", options.pnpmVersion] : void 0,
|
|
113
|
+
options.buildCommand ? ["build-command", options.buildCommand] : void 0,
|
|
114
|
+
options.reviewCommand ? ["review-command", options.reviewCommand] : void 0
|
|
115
|
+
].filter((input) => Boolean(input)).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`).join("\n");
|
|
116
|
+
return `# Generated by @farming-labs/docs. You can edit this file.
|
|
117
|
+
name: Docs Review
|
|
118
|
+
|
|
119
|
+
on:
|
|
120
|
+
pull_request:
|
|
121
|
+
paths:
|
|
122
|
+
${filters.map((filter) => ` - ${JSON.stringify(filter)}`).join("\n")}
|
|
123
|
+
|
|
124
|
+
permissions:
|
|
125
|
+
contents: read
|
|
126
|
+
checks: write
|
|
127
|
+
pull-requests: write
|
|
128
|
+
|
|
129
|
+
jobs:
|
|
130
|
+
docs-review:
|
|
131
|
+
name: ${JSON.stringify(ciName)}
|
|
132
|
+
uses: ${reusableWorkflow}
|
|
133
|
+
with:
|
|
134
|
+
${workflowInputs}
|
|
135
|
+
permissions:
|
|
136
|
+
contents: read
|
|
137
|
+
checks: write
|
|
138
|
+
pull-requests: write
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
function ensureDocsReviewWorkflow(options) {
|
|
142
|
+
const rootDir = options.rootDir;
|
|
143
|
+
const repoRoot = findGitRoot(rootDir) ?? rootDir;
|
|
144
|
+
const workflowRelativePath = options.workflowPath ?? DEFAULT_DOCS_REVIEW_WORKFLOW_PATH;
|
|
145
|
+
const workflowPath = isAbsolute(workflowRelativePath) ? workflowRelativePath : join(repoRoot, workflowRelativePath);
|
|
146
|
+
const resultRelativePath = toPosixPath(relative(repoRoot, workflowPath));
|
|
147
|
+
const review = resolveDocsReviewConfig(options.config?.review ?? readDocsReviewConfigFromSource(options.configContent ?? readConfig(rootDir)));
|
|
148
|
+
if (!review.enabled || !review.ci.enabled) return {
|
|
149
|
+
status: "disabled",
|
|
150
|
+
path: workflowPath,
|
|
151
|
+
relativePath: resultRelativePath
|
|
152
|
+
};
|
|
153
|
+
if (existsSync(workflowPath)) return {
|
|
154
|
+
status: "exists",
|
|
155
|
+
path: workflowPath,
|
|
156
|
+
relativePath: resultRelativePath
|
|
157
|
+
};
|
|
158
|
+
const configPath = options.configPath ?? findDocsConfigPath(rootDir);
|
|
159
|
+
const configContent = options.configContent ?? readConfig(rootDir, configPath);
|
|
160
|
+
const projectDir = toPosixPath(relative(repoRoot, rootDir)) || ".";
|
|
161
|
+
const packageManager = detectPackageManager(repoRoot);
|
|
162
|
+
const workflow = buildDocsReviewWorkflow({
|
|
163
|
+
packageManager,
|
|
164
|
+
ciName: review.ci.name,
|
|
165
|
+
reusableWorkflow: hasLocalDocsWorkspacePackage(repoRoot) ? LOCAL_DOCS_REVIEW_REUSABLE_WORKFLOW : DEFAULT_DOCS_REVIEW_REUSABLE_WORKFLOW,
|
|
166
|
+
pnpmVersion: detectPnpmVersionFallback(repoRoot, packageManager),
|
|
167
|
+
projectDir,
|
|
168
|
+
configPath,
|
|
169
|
+
buildCommand: detectLocalDocsCliBuildCommand(repoRoot, packageManager),
|
|
170
|
+
reviewCommand: detectLocalDocsCliReviewCommand(repoRoot, rootDir),
|
|
171
|
+
pathFilters: buildDocsReviewWorkflowPathFilters({
|
|
172
|
+
rootDir,
|
|
173
|
+
repoRoot,
|
|
174
|
+
config: options.config,
|
|
175
|
+
configPath,
|
|
176
|
+
configContent
|
|
177
|
+
})
|
|
178
|
+
});
|
|
179
|
+
mkdirSync(dirname(workflowPath), { recursive: true });
|
|
180
|
+
writeFileSync(workflowPath, workflow, "utf-8");
|
|
181
|
+
options.log?.(`[docs] Created ${resultRelativePath} for Docs Review CI.`);
|
|
182
|
+
return {
|
|
183
|
+
status: "created",
|
|
184
|
+
path: workflowPath,
|
|
185
|
+
relativePath: resultRelativePath
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function readDocsReviewConfigFromSource(content) {
|
|
189
|
+
if (!content) return void 0;
|
|
190
|
+
const rootObject = extractRootObjectLiteral(content) ?? content;
|
|
191
|
+
const reviewCursor = findTopLevelPropertyValueIndex(rootObject, "review");
|
|
192
|
+
if (reviewCursor === void 0) return void 0;
|
|
193
|
+
if (rootObject.startsWith("true", reviewCursor)) return true;
|
|
194
|
+
if (rootObject.startsWith("false", reviewCursor)) return false;
|
|
195
|
+
if (rootObject[reviewCursor] !== "{") return void 0;
|
|
196
|
+
const reviewEnd = findMatchingObjectEnd(rootObject, reviewCursor);
|
|
197
|
+
if (reviewEnd === void 0) return void 0;
|
|
198
|
+
const reviewBlock = rootObject.slice(reviewCursor + 1, reviewEnd);
|
|
199
|
+
const scoreBlock = extractTopLevelObjectLiteral(reviewBlock, "score");
|
|
200
|
+
const ciCursor = findTopLevelPropertyValueIndex(reviewBlock, "ci");
|
|
201
|
+
const rulesBlock = extractTopLevelObjectLiteral(reviewBlock, "rules");
|
|
202
|
+
const ci = ciCursor === void 0 ? void 0 : reviewBlock.startsWith("true", ciCursor) ? true : reviewBlock.startsWith("false", ciCursor) ? false : reviewBlock[ciCursor] === "{" ? readReviewCiObject(reviewBlock, ciCursor) : void 0;
|
|
203
|
+
return {
|
|
204
|
+
enabled: readTopLevelBoolean(reviewBlock, "enabled"),
|
|
205
|
+
score: scoreBlock ? {
|
|
206
|
+
threshold: readTopLevelNumber(scoreBlock, "threshold"),
|
|
207
|
+
weights: readReviewWeights(scoreBlock)
|
|
208
|
+
} : void 0,
|
|
209
|
+
ci,
|
|
210
|
+
rules: rulesBlock ? readReviewRules(rulesBlock) : void 0
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function buildDocsReviewWorkflowPathFilters(options) {
|
|
214
|
+
const repoRoot = options.repoRoot ?? findGitRoot(options.rootDir) ?? options.rootDir;
|
|
215
|
+
const configPath = options.configPath ?? findDocsConfigPath(options.rootDir);
|
|
216
|
+
const configContent = options.configContent ?? readConfig(options.rootDir, configPath);
|
|
217
|
+
const entry = options.config?.entry ?? readTopLevelString(configContent, "entry") ?? "docs";
|
|
218
|
+
const contentDir = options.config?.contentDir ?? readTopLevelString(configContent, "contentDir");
|
|
219
|
+
const projectDir = toPosixPath(relative(repoRoot, options.rootDir));
|
|
220
|
+
const projectPrefix = projectDir && projectDir !== "." ? `${projectDir}/` : "";
|
|
221
|
+
const candidates = [
|
|
222
|
+
prefixPath(projectPrefix, configPath),
|
|
223
|
+
prefixPath(projectPrefix, `${entry}/**`),
|
|
224
|
+
prefixPath(projectPrefix, `app/${entry}/**`),
|
|
225
|
+
prefixPath(projectPrefix, `src/app/${entry}/**`),
|
|
226
|
+
contentDir ? prefixPath(projectPrefix, `${trimSlashes(contentDir)}/**`) : void 0,
|
|
227
|
+
prefixPath(projectPrefix, "content/docs/**"),
|
|
228
|
+
prefixPath(projectPrefix, "src/content/docs/**"),
|
|
229
|
+
prefixPath(projectPrefix, "src/lib/docs.config.*"),
|
|
230
|
+
DEFAULT_DOCS_REVIEW_WORKFLOW_PATH,
|
|
231
|
+
DEFAULT_DOCS_REVIEW_REUSABLE_WORKFLOW_PATH
|
|
232
|
+
];
|
|
233
|
+
return Array.from(new Set(candidates.filter((candidate) => Boolean(candidate))));
|
|
234
|
+
}
|
|
235
|
+
function normalizeRules(rules) {
|
|
236
|
+
if (!rules) return {};
|
|
237
|
+
const knownRules = new Set(Object.keys(DEFAULT_REVIEW_RULES));
|
|
238
|
+
return Object.fromEntries(Object.entries(rules).filter((entry) => knownRules.has(entry[0]) && REVIEW_SEVERITIES.has(entry[1])));
|
|
239
|
+
}
|
|
240
|
+
function normalizeWeight(value, fallback) {
|
|
241
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
242
|
+
}
|
|
243
|
+
function normalizeCiName(value) {
|
|
244
|
+
return value?.trim() || DEFAULT_DOCS_REVIEW_CI_NAME;
|
|
245
|
+
}
|
|
246
|
+
function clampScoreThreshold(value) {
|
|
247
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_DOCS_REVIEW_SCORE_THRESHOLD;
|
|
248
|
+
return Math.min(100, Math.max(0, Math.round(value)));
|
|
249
|
+
}
|
|
250
|
+
function readReviewCiObject(source, cursor) {
|
|
251
|
+
const end = findMatchingObjectEnd(source, cursor);
|
|
252
|
+
if (end === void 0) return void 0;
|
|
253
|
+
const block = source.slice(cursor + 1, end);
|
|
254
|
+
const mode = readTopLevelString(block, "mode");
|
|
255
|
+
return {
|
|
256
|
+
enabled: readTopLevelBoolean(block, "enabled"),
|
|
257
|
+
name: readTopLevelString(block, "name"),
|
|
258
|
+
mode: mode && REVIEW_CI_MODES.has(mode) ? mode : void 0,
|
|
259
|
+
annotations: readTopLevelBoolean(block, "annotations"),
|
|
260
|
+
comment: readTopLevelBoolean(block, "comment")
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function readReviewRules(block) {
|
|
264
|
+
const rules = {};
|
|
265
|
+
for (const key of Object.keys(DEFAULT_REVIEW_RULES)) {
|
|
266
|
+
const value = readTopLevelString(block, key);
|
|
267
|
+
if (value && REVIEW_SEVERITIES.has(value)) rules[key] = value;
|
|
268
|
+
}
|
|
269
|
+
return rules;
|
|
270
|
+
}
|
|
271
|
+
function readReviewWeights(block) {
|
|
272
|
+
const weightsBlock = extractTopLevelObjectLiteral(block, "weights");
|
|
273
|
+
if (!weightsBlock) return void 0;
|
|
274
|
+
return {
|
|
275
|
+
error: readTopLevelNumber(weightsBlock, "error"),
|
|
276
|
+
warn: readTopLevelNumber(weightsBlock, "warn"),
|
|
277
|
+
suggestion: readTopLevelNumber(weightsBlock, "suggestion")
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function detectPackageManager(rootDir) {
|
|
281
|
+
if (existsSync(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
282
|
+
if (existsSync(join(rootDir, "yarn.lock"))) return "yarn";
|
|
283
|
+
if (existsSync(join(rootDir, "bun.lock")) || existsSync(join(rootDir, "bun.lockb"))) return "bun";
|
|
284
|
+
return "npm";
|
|
285
|
+
}
|
|
286
|
+
function detectLocalDocsCliBuildCommand(repoRoot, packageManager) {
|
|
287
|
+
if (!hasLocalDocsWorkspacePackage(repoRoot)) return void 0;
|
|
288
|
+
if (packageManager === "pnpm") return "pnpm --filter @farming-labs/docs run build";
|
|
289
|
+
if (packageManager === "yarn") return "yarn workspace @farming-labs/docs build";
|
|
290
|
+
if (packageManager === "bun") return "bun run --filter @farming-labs/docs build";
|
|
291
|
+
return "npm run build --workspace=@farming-labs/docs";
|
|
292
|
+
}
|
|
293
|
+
function detectPnpmVersionFallback(repoRoot, packageManager) {
|
|
294
|
+
if (packageManager !== "pnpm") return void 0;
|
|
295
|
+
try {
|
|
296
|
+
if (JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf-8")).packageManager?.startsWith("pnpm@")) return void 0;
|
|
297
|
+
} catch {}
|
|
298
|
+
return DEFAULT_DOCS_REVIEW_PNPM_VERSION;
|
|
299
|
+
}
|
|
300
|
+
function detectLocalDocsCliReviewCommand(repoRoot, rootDir) {
|
|
301
|
+
if (!hasLocalDocsWorkspacePackage(repoRoot)) return void 0;
|
|
302
|
+
return `node ${shellQuote(toPosixPath(relative(rootDir, join(repoRoot, "packages", "docs", "dist", "cli", "index.mjs"))) || "./packages/docs/dist/cli/index.mjs")}`;
|
|
303
|
+
}
|
|
304
|
+
function hasLocalDocsWorkspacePackage(repoRoot) {
|
|
305
|
+
const packageJsonPath = join(repoRoot, "packages", "docs", "package.json");
|
|
306
|
+
if (!existsSync(packageJsonPath)) return false;
|
|
307
|
+
try {
|
|
308
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
309
|
+
return packageJson.name === "@farming-labs/docs" && Boolean(packageJson.scripts?.build);
|
|
310
|
+
} catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function shellQuote(value) {
|
|
315
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(value)) return value;
|
|
316
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
317
|
+
}
|
|
318
|
+
function normalizeProjectDir(value) {
|
|
319
|
+
if (!value || value === "") return ".";
|
|
320
|
+
return toPosixPath(value).replace(/\/+$/, "") || ".";
|
|
321
|
+
}
|
|
322
|
+
function normalizePathFilters(filters) {
|
|
323
|
+
const normalized = (filters && filters.length > 0 ? filters : ["docs.config.*", "app/docs/**"]).map((filter) => toPosixPath(filter).replace(/^\.\/+/, "")).filter(Boolean);
|
|
324
|
+
return Array.from(new Set(normalized));
|
|
325
|
+
}
|
|
326
|
+
function findGitRoot(start) {
|
|
327
|
+
let current = start;
|
|
328
|
+
while (true) {
|
|
329
|
+
if (existsSync(join(current, ".git"))) return current;
|
|
330
|
+
const parent = dirname(current);
|
|
331
|
+
if (parent === current) return void 0;
|
|
332
|
+
current = parent;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function findDocsConfigPath(rootDir) {
|
|
336
|
+
for (const ext of FILE_EXTS) {
|
|
337
|
+
const path = `docs.config.${ext}`;
|
|
338
|
+
if (existsSync(join(rootDir, path))) return path;
|
|
339
|
+
}
|
|
340
|
+
return "docs.config.ts";
|
|
341
|
+
}
|
|
342
|
+
function readConfig(rootDir, configPath = findDocsConfigPath(rootDir)) {
|
|
343
|
+
const fullPath = join(rootDir, configPath);
|
|
344
|
+
if (!existsSync(fullPath)) return void 0;
|
|
345
|
+
try {
|
|
346
|
+
return readFileSync(fullPath, "utf-8");
|
|
347
|
+
} catch {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function prefixPath(prefix, value) {
|
|
352
|
+
return `${prefix}${toPosixPath(value).replace(/^\.\/+/, "")}`;
|
|
353
|
+
}
|
|
354
|
+
function trimSlashes(value) {
|
|
355
|
+
return toPosixPath(value).replace(/^\/+|\/+$/g, "");
|
|
356
|
+
}
|
|
357
|
+
function toPosixPath(value) {
|
|
358
|
+
return value.replaceAll("\\", "/");
|
|
359
|
+
}
|
|
360
|
+
function extractRootObjectLiteral(content) {
|
|
361
|
+
const candidateIndexes = [content.search(/\bdefineDocs\s*\(/), content.search(/\bexport\s+default\b/)].filter((value) => value !== -1);
|
|
362
|
+
for (const startIndex of candidateIndexes) {
|
|
363
|
+
const braceStart = content.indexOf("{", startIndex);
|
|
364
|
+
if (braceStart === -1) continue;
|
|
365
|
+
const braceEnd = findMatchingObjectEnd(content, braceStart);
|
|
366
|
+
if (braceEnd !== void 0) return content.slice(braceStart + 1, braceEnd);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function extractTopLevelObjectLiteral(content, key) {
|
|
370
|
+
const cursor = findTopLevelPropertyValueIndex(content, key);
|
|
371
|
+
if (cursor === void 0 || content[cursor] !== "{") return void 0;
|
|
372
|
+
const end = findMatchingObjectEnd(content, cursor);
|
|
373
|
+
return end === void 0 ? void 0 : content.slice(cursor + 1, end);
|
|
374
|
+
}
|
|
375
|
+
function readTopLevelBoolean(content, key) {
|
|
376
|
+
const cursor = findTopLevelPropertyValueIndex(content, key);
|
|
377
|
+
if (cursor === void 0) return void 0;
|
|
378
|
+
if (content.startsWith("true", cursor)) return true;
|
|
379
|
+
if (content.startsWith("false", cursor)) return false;
|
|
380
|
+
}
|
|
381
|
+
function readTopLevelNumber(content, key) {
|
|
382
|
+
const cursor = findTopLevelPropertyValueIndex(content, key);
|
|
383
|
+
if (cursor === void 0) return void 0;
|
|
384
|
+
const match = content.slice(cursor).match(/^-?\d+(?:\.\d+)?/);
|
|
385
|
+
if (!match) return void 0;
|
|
386
|
+
const value = Number(match[0]);
|
|
387
|
+
return Number.isFinite(value) ? value : void 0;
|
|
388
|
+
}
|
|
389
|
+
function readTopLevelString(content, key) {
|
|
390
|
+
if (!content) return void 0;
|
|
391
|
+
const cursor = findTopLevelPropertyValueIndex(content, key);
|
|
392
|
+
if (cursor === void 0) return void 0;
|
|
393
|
+
const quote = content[cursor];
|
|
394
|
+
if (quote !== "\"" && quote !== "'") return void 0;
|
|
395
|
+
let value = "";
|
|
396
|
+
for (let index = cursor + 1; index < content.length; index += 1) {
|
|
397
|
+
const char = content[index];
|
|
398
|
+
if (char === "\\") {
|
|
399
|
+
const escaped = content[index + 1];
|
|
400
|
+
if (escaped) {
|
|
401
|
+
value += escaped;
|
|
402
|
+
index += 1;
|
|
403
|
+
}
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (char === quote) return value;
|
|
407
|
+
value += char;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function findTopLevelPropertyValueIndex(block, key) {
|
|
411
|
+
let objectDepth = 0;
|
|
412
|
+
let arrayDepth = 0;
|
|
413
|
+
let parenDepth = 0;
|
|
414
|
+
let inString = null;
|
|
415
|
+
let inLineComment = false;
|
|
416
|
+
let inBlockComment = false;
|
|
417
|
+
let escaped = false;
|
|
418
|
+
for (let index = 0; index < block.length; index += 1) {
|
|
419
|
+
const char = block[index];
|
|
420
|
+
const next = block[index + 1];
|
|
421
|
+
if (inLineComment) {
|
|
422
|
+
if (char === "\n") inLineComment = false;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (inBlockComment) {
|
|
426
|
+
if (char === "*" && next === "/") {
|
|
427
|
+
inBlockComment = false;
|
|
428
|
+
index += 1;
|
|
429
|
+
}
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (inString) {
|
|
433
|
+
if (escaped) {
|
|
434
|
+
escaped = false;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (char === "\\") {
|
|
438
|
+
escaped = true;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (char === inString) inString = null;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (char === "/" && next === "/") {
|
|
445
|
+
inLineComment = true;
|
|
446
|
+
index += 1;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (char === "/" && next === "*") {
|
|
450
|
+
inBlockComment = true;
|
|
451
|
+
index += 1;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
455
|
+
inString = char;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (char === "{") {
|
|
459
|
+
objectDepth += 1;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (char === "}") {
|
|
463
|
+
objectDepth = Math.max(0, objectDepth - 1);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (char === "[") {
|
|
467
|
+
arrayDepth += 1;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (char === "]") {
|
|
471
|
+
arrayDepth = Math.max(0, arrayDepth - 1);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (char === "(") {
|
|
475
|
+
parenDepth += 1;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (char === ")") {
|
|
479
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (objectDepth !== 0 || arrayDepth !== 0 || parenDepth !== 0) continue;
|
|
483
|
+
if (!block.startsWith(key, index)) continue;
|
|
484
|
+
const before = block[index - 1] ?? "";
|
|
485
|
+
const after = block[index + key.length] ?? "";
|
|
486
|
+
if (/[A-Za-z0-9_$]/.test(before) || /[A-Za-z0-9_$]/.test(after)) continue;
|
|
487
|
+
let cursor = index + key.length;
|
|
488
|
+
while (/\s/.test(block[cursor] ?? "")) cursor += 1;
|
|
489
|
+
if (block[cursor] !== ":") continue;
|
|
490
|
+
cursor += 1;
|
|
491
|
+
while (/\s/.test(block[cursor] ?? "")) cursor += 1;
|
|
492
|
+
return cursor;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function findMatchingObjectEnd(block, start) {
|
|
496
|
+
let depth = 0;
|
|
497
|
+
let inString = null;
|
|
498
|
+
let inLineComment = false;
|
|
499
|
+
let inBlockComment = false;
|
|
500
|
+
let escaped = false;
|
|
501
|
+
for (let index = start; index < block.length; index += 1) {
|
|
502
|
+
const char = block[index];
|
|
503
|
+
const next = block[index + 1];
|
|
504
|
+
if (inLineComment) {
|
|
505
|
+
if (char === "\n") inLineComment = false;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (inBlockComment) {
|
|
509
|
+
if (char === "*" && next === "/") {
|
|
510
|
+
inBlockComment = false;
|
|
511
|
+
index += 1;
|
|
512
|
+
}
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (inString) {
|
|
516
|
+
if (escaped) {
|
|
517
|
+
escaped = false;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (char === "\\") {
|
|
521
|
+
escaped = true;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (char === inString) inString = null;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (char === "/" && next === "/") {
|
|
528
|
+
inLineComment = true;
|
|
529
|
+
index += 1;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (char === "/" && next === "*") {
|
|
533
|
+
inBlockComment = true;
|
|
534
|
+
index += 1;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
538
|
+
inString = char;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (char === "{") {
|
|
542
|
+
depth += 1;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (char !== "}") continue;
|
|
546
|
+
depth -= 1;
|
|
547
|
+
if (depth === 0) return index;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/agent-evals.ts
|
|
553
|
+
const DEFAULT_TOP_K = 5;
|
|
554
|
+
const DEFAULT_TOKEN_BUDGET = 4e3;
|
|
555
|
+
const CONTEXT_SEPARATOR = "\n\n---\n\n";
|
|
556
|
+
const STATIC_CODE_PLAN_CONFIG = resolveDocsCodeBlocksValidateConfig(true);
|
|
557
|
+
const STATIC_JITI = createJiti(import.meta.url, {
|
|
558
|
+
fsCache: false,
|
|
559
|
+
moduleCache: false
|
|
560
|
+
});
|
|
561
|
+
const EXECUTABLE_LANGUAGES = new Set([
|
|
562
|
+
"bash",
|
|
563
|
+
"curl",
|
|
564
|
+
"javascript",
|
|
565
|
+
"js",
|
|
566
|
+
"jsx",
|
|
567
|
+
"json",
|
|
568
|
+
"node",
|
|
569
|
+
"py",
|
|
570
|
+
"python",
|
|
571
|
+
"rb",
|
|
572
|
+
"ruby",
|
|
573
|
+
"sh",
|
|
574
|
+
"shell",
|
|
575
|
+
"ts",
|
|
576
|
+
"tsx",
|
|
577
|
+
"typescript",
|
|
578
|
+
"zsh"
|
|
579
|
+
]);
|
|
580
|
+
function isRecord(value) {
|
|
581
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
582
|
+
}
|
|
583
|
+
function normalizeRuntimeString(value, path, issues, required = false) {
|
|
584
|
+
if (value === void 0) {
|
|
585
|
+
if (required) issues.push(`${path} must be a non-empty string.`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
589
|
+
issues.push(`${path} must be a non-empty string.`);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
return value.trim();
|
|
593
|
+
}
|
|
594
|
+
function normalizeRuntimeStringList(value, path, issues, options = {}) {
|
|
595
|
+
if (value === void 0) {
|
|
596
|
+
if (options.required) issues.push(`${path} must be a non-empty string array.`);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (!Array.isArray(value)) {
|
|
600
|
+
issues.push(`${path} must be a string array.`);
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
const normalized = [];
|
|
604
|
+
let hasInvalidItem = false;
|
|
605
|
+
for (const item of value) {
|
|
606
|
+
if (typeof item !== "string" || !item.trim()) {
|
|
607
|
+
hasInvalidItem = true;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (!normalized.includes(item.trim())) normalized.push(item.trim());
|
|
611
|
+
}
|
|
612
|
+
if (hasInvalidItem) issues.push(`${path} must contain only non-empty strings.`);
|
|
613
|
+
if (options.required && normalized.length === 0) issues.push(`${path} must contain at least one source.`);
|
|
614
|
+
return normalized;
|
|
615
|
+
}
|
|
616
|
+
function normalizeRuntimeNumber(value, path, issues, options = {}) {
|
|
617
|
+
if (value === void 0) return void 0;
|
|
618
|
+
if (typeof value !== "number" || !Number.isFinite(value) || options.integer && !Number.isInteger(value) || options.minimum !== void 0 && value < options.minimum || options.maximum !== void 0 && value > options.maximum) {
|
|
619
|
+
const range = options.minimum !== void 0 || options.maximum !== void 0 ? ` between ${options.minimum ?? "-Infinity"} and ${options.maximum ?? "Infinity"}` : "";
|
|
620
|
+
issues.push(`${path} must be a finite${options.integer ? " integer" : " number"}${range}.`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
return value;
|
|
624
|
+
}
|
|
625
|
+
function normalizeRuntimeFilters(value, path, issues) {
|
|
626
|
+
if (value === void 0) return void 0;
|
|
627
|
+
if (!isRecord(value)) {
|
|
628
|
+
issues.push(`${path} must be an object.`);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const framework = normalizeRuntimeString(value.framework, `${path}.framework`, issues);
|
|
632
|
+
const version = normalizeRuntimeString(value.version, `${path}.version`, issues);
|
|
633
|
+
const locale = normalizeRuntimeString(value.locale, `${path}.locale`, issues);
|
|
634
|
+
return framework || version || locale ? {
|
|
635
|
+
framework,
|
|
636
|
+
version,
|
|
637
|
+
locale
|
|
638
|
+
} : void 0;
|
|
639
|
+
}
|
|
640
|
+
function normalizeRuntimeEnum(value, path, issues, allowed) {
|
|
641
|
+
if (value === void 0) return void 0;
|
|
642
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
643
|
+
issues.push(`${path} must be one of ${allowed.map((item) => JSON.stringify(item)).join(", ")}.`);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
return value;
|
|
647
|
+
}
|
|
648
|
+
function normalizeRuntimeAnswerExpectation(value, path, issues) {
|
|
649
|
+
if (value === void 0) return void 0;
|
|
650
|
+
if (!isRecord(value)) {
|
|
651
|
+
issues.push(`${path} must be an object.`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
includes: normalizeRuntimeStringList(value.includes, `${path}.includes`, issues),
|
|
656
|
+
excludes: normalizeRuntimeStringList(value.excludes, `${path}.excludes`, issues),
|
|
657
|
+
requiredCitations: normalizeRuntimeStringList(value.requiredCitations, `${path}.requiredCitations`, issues),
|
|
658
|
+
allowedCitations: normalizeRuntimeStringList(value.allowedCitations, `${path}.allowedCitations`, issues),
|
|
659
|
+
forbiddenCitations: normalizeRuntimeStringList(value.forbiddenCitations, `${path}.forbiddenCitations`, issues)
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function normalizeRuntimeExamples(value, path, issues) {
|
|
663
|
+
if (value === void 0) return void 0;
|
|
664
|
+
if (!Array.isArray(value)) {
|
|
665
|
+
issues.push(`${path} must be an array.`);
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
return value.flatMap((item, index) => {
|
|
669
|
+
const itemPath = `${path}[${index}]`;
|
|
670
|
+
if (!isRecord(item)) {
|
|
671
|
+
issues.push(`${itemPath} must be an object.`);
|
|
672
|
+
return [];
|
|
673
|
+
}
|
|
674
|
+
const source = normalizeRuntimeString(item.source, `${itemPath}.source`, issues);
|
|
675
|
+
const language = normalizeRuntimeString(item.language, `${itemPath}.language`, issues);
|
|
676
|
+
const framework = normalizeRuntimeString(item.framework, `${itemPath}.framework`, issues);
|
|
677
|
+
const packageManager = normalizeRuntimeString(item.packageManager, `${itemPath}.packageManager`, issues);
|
|
678
|
+
const title = normalizeRuntimeString(item.title, `${itemPath}.title`, issues);
|
|
679
|
+
const includes = normalizeRuntimeStringList(item.includes, `${itemPath}.includes`, issues);
|
|
680
|
+
let runnable;
|
|
681
|
+
if (item.runnable !== void 0) if (typeof item.runnable === "boolean") runnable = item.runnable;
|
|
682
|
+
else issues.push(`${itemPath}.runnable must be a boolean.`);
|
|
683
|
+
const verification = normalizeRuntimeEnum(item.verification, `${itemPath}.verification`, issues, [
|
|
684
|
+
"present",
|
|
685
|
+
"syntax",
|
|
686
|
+
"execute"
|
|
687
|
+
]);
|
|
688
|
+
return [{
|
|
689
|
+
source,
|
|
690
|
+
language,
|
|
691
|
+
framework,
|
|
692
|
+
packageManager,
|
|
693
|
+
title,
|
|
694
|
+
runnable,
|
|
695
|
+
includes,
|
|
696
|
+
verification
|
|
697
|
+
}];
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
function normalizeGoldenTaskInput(value, index, initialIssues = []) {
|
|
701
|
+
const issues = [...initialIssues];
|
|
702
|
+
const path = `tasks[${index}]`;
|
|
703
|
+
const input = isRecord(value) ? value : {};
|
|
704
|
+
if (!isRecord(value)) issues.push(`${path} must be an object.`);
|
|
705
|
+
const id = normalizeRuntimeString(input.id, `${path}.id`, issues, true) ?? `invalid-task-${index + 1}`;
|
|
706
|
+
const query = normalizeRuntimeString(input.query, `${path}.query`, issues, true) ?? "";
|
|
707
|
+
const filters = normalizeRuntimeFilters(input.filters, `${path}.filters`, issues);
|
|
708
|
+
const tokenBudget = normalizeRuntimeNumber(input.tokenBudget, `${path}.tokenBudget`, issues, {
|
|
709
|
+
minimum: 1,
|
|
710
|
+
integer: true
|
|
711
|
+
});
|
|
712
|
+
const topK = normalizeRuntimeNumber(input.topK, `${path}.topK`, issues, {
|
|
713
|
+
minimum: 1,
|
|
714
|
+
integer: true
|
|
715
|
+
});
|
|
716
|
+
const surface = normalizeRuntimeEnum(input.surface, `${path}.surface`, issues, [
|
|
717
|
+
"mcp-context",
|
|
718
|
+
"configured-search",
|
|
719
|
+
"ask-ai-context"
|
|
720
|
+
]);
|
|
721
|
+
const expectPath = `${path}.expect`;
|
|
722
|
+
const expectation = isRecord(input.expect) ? input.expect : {};
|
|
723
|
+
if (!isRecord(input.expect)) issues.push(`${expectPath} must be an object.`);
|
|
724
|
+
const relevantSources = normalizeRuntimeStringList(expectation.relevantSources, `${expectPath}.relevantSources`, issues, { required: true }) ?? [];
|
|
725
|
+
const allowedSources = normalizeRuntimeStringList(expectation.allowedSources, `${expectPath}.allowedSources`, issues);
|
|
726
|
+
const forbiddenSources = normalizeRuntimeStringList(expectation.forbiddenSources, `${expectPath}.forbiddenSources`, issues);
|
|
727
|
+
const requiredCitations = normalizeRuntimeStringList(expectation.requiredCitations, `${expectPath}.requiredCitations`, issues);
|
|
728
|
+
const minRecallAtK = normalizeRuntimeNumber(expectation.minRecallAtK, `${expectPath}.minRecallAtK`, issues, {
|
|
729
|
+
minimum: 0,
|
|
730
|
+
maximum: 1
|
|
731
|
+
});
|
|
732
|
+
const maxFirstRelevantRank = normalizeRuntimeNumber(expectation.maxFirstRelevantRank, `${expectPath}.maxFirstRelevantRank`, issues, {
|
|
733
|
+
minimum: 1,
|
|
734
|
+
integer: true
|
|
735
|
+
});
|
|
736
|
+
const minUsefulByteRatio = normalizeRuntimeNumber(expectation.minUsefulByteRatio, `${expectPath}.minUsefulByteRatio`, issues, {
|
|
737
|
+
minimum: 0,
|
|
738
|
+
maximum: 1
|
|
739
|
+
});
|
|
740
|
+
const examples = normalizeRuntimeExamples(expectation.examples, `${expectPath}.examples`, issues);
|
|
741
|
+
const scope = normalizeRuntimeFilters(expectation.scope, `${expectPath}.scope`, issues);
|
|
742
|
+
const answer = normalizeRuntimeAnswerExpectation(expectation.answer, `${expectPath}.answer`, issues);
|
|
743
|
+
const forbidden = forbiddenSources ?? [];
|
|
744
|
+
for (const [name, sources] of [
|
|
745
|
+
["relevantSources", relevantSources],
|
|
746
|
+
["allowedSources", allowedSources ?? []],
|
|
747
|
+
["requiredCitations", requiredCitations ?? []]
|
|
748
|
+
]) {
|
|
749
|
+
const overlaps = sources.filter((source) => forbidden.some((forbiddenSource) => sourceMatches(source, forbiddenSource) || sourceMatches(forbiddenSource, source)));
|
|
750
|
+
if (overlaps.length > 0) issues.push(`${expectPath}.${name} must not overlap ${expectPath}.forbiddenSources (${overlaps.join(", ")}).`);
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
task: {
|
|
754
|
+
id,
|
|
755
|
+
query,
|
|
756
|
+
filters,
|
|
757
|
+
tokenBudget,
|
|
758
|
+
topK,
|
|
759
|
+
surface,
|
|
760
|
+
expect: {
|
|
761
|
+
relevantSources,
|
|
762
|
+
allowedSources,
|
|
763
|
+
forbiddenSources,
|
|
764
|
+
requiredCitations,
|
|
765
|
+
minRecallAtK,
|
|
766
|
+
maxFirstRelevantRank,
|
|
767
|
+
minUsefulByteRatio,
|
|
768
|
+
examples,
|
|
769
|
+
scope,
|
|
770
|
+
answer
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
issues
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function round(value) {
|
|
777
|
+
return Math.round((value + Number.EPSILON) * 1e4) / 1e4;
|
|
778
|
+
}
|
|
779
|
+
function clamp(value, minimum, maximum) {
|
|
780
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
781
|
+
}
|
|
782
|
+
function normalizePositiveInteger(value, fallback, maximum) {
|
|
783
|
+
if (!Number.isFinite(value)) return fallback;
|
|
784
|
+
return clamp(Math.floor(value ?? fallback), 1, maximum);
|
|
785
|
+
}
|
|
786
|
+
function safeDecode(value) {
|
|
787
|
+
try {
|
|
788
|
+
return decodeURIComponent(value);
|
|
789
|
+
} catch {
|
|
790
|
+
return value;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function canonicalizeSource(value, baseUrl) {
|
|
794
|
+
const rawValue = value.trim();
|
|
795
|
+
let pathname;
|
|
796
|
+
let hash;
|
|
797
|
+
let origin = "";
|
|
798
|
+
try {
|
|
799
|
+
let configuredBase;
|
|
800
|
+
if (baseUrl) try {
|
|
801
|
+
const candidate = new URL(baseUrl);
|
|
802
|
+
if (candidate.protocol === "http:" || candidate.protocol === "https:") configuredBase = candidate;
|
|
803
|
+
} catch {}
|
|
804
|
+
const base = configuredBase ?? new URL("https://docs.local");
|
|
805
|
+
const parsed = new URL(rawValue, base);
|
|
806
|
+
pathname = parsed.pathname;
|
|
807
|
+
hash = parsed.hash;
|
|
808
|
+
const explicitlySchemed = /^[a-z][a-z\d+.-]*:/iu.test(rawValue);
|
|
809
|
+
if ((explicitlySchemed && (parsed.protocol === "http:" || parsed.protocol === "https:") || /^[\\/]{2}/u.test(rawValue)) && (!configuredBase || parsed.origin !== configuredBase.origin)) origin = parsed.origin;
|
|
810
|
+
if (explicitlySchemed && parsed.protocol !== "http:" && parsed.protocol !== "https:") origin = `${parsed.protocol}//${parsed.host}`;
|
|
811
|
+
} catch {
|
|
812
|
+
const [pathAndQuery = "/", fragment = ""] = rawValue.split("#", 2);
|
|
813
|
+
pathname = pathAndQuery.split("?", 1)[0] || "/";
|
|
814
|
+
hash = fragment ? `#${fragment}` : "";
|
|
815
|
+
}
|
|
816
|
+
pathname = pathname.replace(/\/{2,}/gu, "/").replace(/\.md$/iu, "");
|
|
817
|
+
if (pathname !== "/") pathname = pathname.replace(/\/+$/gu, "");
|
|
818
|
+
if (!pathname.startsWith("/")) pathname = `/${pathname}`;
|
|
819
|
+
const normalizedHash = hash ? `#${safeDecode(hash.slice(1)).trim().replace(/^#+/u, "").toLowerCase()}` : "";
|
|
820
|
+
return `${origin}${pathname || "/"}${normalizedHash === "#" ? "" : normalizedHash}`;
|
|
821
|
+
}
|
|
822
|
+
function sourcePage(value) {
|
|
823
|
+
return canonicalizeSource(value).split("#", 1)[0] ?? "/";
|
|
824
|
+
}
|
|
825
|
+
function sourceMatches(actual, expected, baseUrl) {
|
|
826
|
+
const canonicalActual = canonicalizeSource(actual, baseUrl);
|
|
827
|
+
const canonicalExpected = canonicalizeSource(expected, baseUrl);
|
|
828
|
+
return canonicalExpected.includes("#") ? canonicalActual === canonicalExpected : sourcePage(canonicalActual) === sourcePage(canonicalExpected);
|
|
829
|
+
}
|
|
830
|
+
function uniqueCanonicalSources(values, baseUrl) {
|
|
831
|
+
return Array.from(new Set(values.map((value) => canonicalizeSource(value, baseUrl))));
|
|
832
|
+
}
|
|
833
|
+
function valuesOverlap(left, right, matches) {
|
|
834
|
+
return left.some((leftValue) => right.some((rightValue) => matches(leftValue, rightValue)));
|
|
835
|
+
}
|
|
836
|
+
function getPageScope(page) {
|
|
837
|
+
const topFramework = normalizeAgentScopeValues(page.framework).map(normalizeAgentFramework);
|
|
838
|
+
const contractFramework = normalizeAgentScopeValues(page.agent?.appliesTo?.framework).map(normalizeAgentFramework);
|
|
839
|
+
const topVersion = normalizeAgentScopeValues(page.version).map(normalizeAgentVersion);
|
|
840
|
+
const contractVersion = normalizeAgentScopeValues(page.agent?.appliesTo?.version).map(normalizeAgentVersion);
|
|
841
|
+
const locale = normalizeAgentScopeValues(page.locale).map(normalizeAgentLocale);
|
|
842
|
+
return {
|
|
843
|
+
framework: Array.from(new Set([...topFramework, ...contractFramework])),
|
|
844
|
+
version: Array.from(new Set([...topVersion, ...contractVersion])),
|
|
845
|
+
locale,
|
|
846
|
+
frameworkAmbiguous: topFramework.length > 0 && contractFramework.length > 0 && !valuesOverlap(topFramework, contractFramework, (left, right) => left === right),
|
|
847
|
+
versionAmbiguous: topVersion.length > 0 && contractVersion.length > 0 && !valuesOverlap(topVersion, contractVersion, agentVersionConstraintsOverlap)
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function compareCodePoints(left, right) {
|
|
851
|
+
if (left === right) return 0;
|
|
852
|
+
return left < right ? -1 : 1;
|
|
853
|
+
}
|
|
854
|
+
function toGoldenSources(sources, relevantSources, baseUrl) {
|
|
855
|
+
return sources.map((source, index) => ({
|
|
856
|
+
rank: index + 1,
|
|
857
|
+
url: canonicalizeSource(source.url, baseUrl),
|
|
858
|
+
title: source.section ?? source.title,
|
|
859
|
+
framework: source.framework,
|
|
860
|
+
version: source.version,
|
|
861
|
+
utf8Bytes: source.utf8Bytes,
|
|
862
|
+
relevant: relevantSources.some((expected) => sourceMatches(source.url, expected, baseUrl)),
|
|
863
|
+
truncated: source.truncated
|
|
864
|
+
}));
|
|
865
|
+
}
|
|
866
|
+
function toContextCandidates(sources, pagesByUrl) {
|
|
867
|
+
return sources.flatMap((source) => {
|
|
868
|
+
const page = pagesByUrl.get(sourcePage(source.pageUrl));
|
|
869
|
+
if (!page) return [];
|
|
870
|
+
return [{
|
|
871
|
+
page,
|
|
872
|
+
source: canonicalizeSource(source.url),
|
|
873
|
+
content: source.content,
|
|
874
|
+
scope: getPageScope(page)
|
|
875
|
+
}];
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
function extractRenderedCitations(context, sources) {
|
|
879
|
+
const actual = [];
|
|
880
|
+
const blockUtf8Bytes = [];
|
|
881
|
+
let cursor = 0;
|
|
882
|
+
let layoutIntegrity = true;
|
|
883
|
+
for (let index = 0; index < sources.length; index += 1) {
|
|
884
|
+
const attributableStart = cursor;
|
|
885
|
+
if (index > 0) {
|
|
886
|
+
if (!context.startsWith(CONTEXT_SEPARATOR, cursor)) {
|
|
887
|
+
layoutIntegrity = false;
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
cursor += 7;
|
|
891
|
+
}
|
|
892
|
+
const contentMarker = `\n\n${sources[index].content}`;
|
|
893
|
+
const contentStart = context.indexOf(contentMarker, cursor);
|
|
894
|
+
if (contentStart < cursor) {
|
|
895
|
+
layoutIntegrity = false;
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
const sourceLines = context.slice(cursor, contentStart).split(/\r?\n/u).map((line) => line.match(/^Source:\s+(.+)$/u)?.[1]?.trim()).filter((source) => Boolean(source));
|
|
899
|
+
if (sourceLines.length !== 1) layoutIntegrity = false;
|
|
900
|
+
actual.push(...sourceLines.map((source) => canonicalizeSource(source)));
|
|
901
|
+
cursor = contentStart + contentMarker.length;
|
|
902
|
+
blockUtf8Bytes.push(Buffer.byteLength(context.slice(attributableStart, cursor), "utf8"));
|
|
903
|
+
}
|
|
904
|
+
if (cursor !== context.length) layoutIntegrity = false;
|
|
905
|
+
return {
|
|
906
|
+
actual,
|
|
907
|
+
layoutIntegrity,
|
|
908
|
+
blockUtf8Bytes
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function getPageAgentMarkdown(page) {
|
|
912
|
+
return page.agentRawContent ?? page.agentFallbackRawContent ?? page.agentContent ?? page.agentFallbackContent ?? page.rawContent ?? page.content;
|
|
913
|
+
}
|
|
914
|
+
function findPageForSource(pagesByUrl, value) {
|
|
915
|
+
return pagesByUrl.get(sourcePage(value));
|
|
916
|
+
}
|
|
917
|
+
function getResultAnchor(value) {
|
|
918
|
+
return canonicalizeSource(value).split("#", 2)[1] || void 0;
|
|
919
|
+
}
|
|
920
|
+
function hydrateSearchSource(result, index, pagesByUrl, contentMode, baseUrl) {
|
|
921
|
+
const normalizedResultUrl = canonicalizeSource(result.url, baseUrl);
|
|
922
|
+
const page = findPageForSource(pagesByUrl, normalizedResultUrl);
|
|
923
|
+
const anchor = getResultAnchor(result.url);
|
|
924
|
+
const section = anchor ?? result.section;
|
|
925
|
+
const pageSection = page && section ? findDocsMarkdownSection(getPageAgentMarkdown(page), section) : void 0;
|
|
926
|
+
const content = contentMode === "page-section" && page ? (pageSection?.content ?? getPageAgentMarkdown(page)).trim() : [result.content, result.description].filter(Boolean).join("\n\n").trim();
|
|
927
|
+
const scope = page ? getPageScope(page) : void 0;
|
|
928
|
+
const url = normalizedResultUrl;
|
|
929
|
+
return {
|
|
930
|
+
id: result.id || `evaluation-result-${index + 1}`,
|
|
931
|
+
title: page?.title ?? result.content.split("—", 1)[0]?.trim() ?? url,
|
|
932
|
+
pageUrl: sourcePage(url),
|
|
933
|
+
url,
|
|
934
|
+
section: pageSection?.title ?? result.section,
|
|
935
|
+
anchor: pageSection?.anchor ?? anchor,
|
|
936
|
+
sourcePath: page?.sourcePath,
|
|
937
|
+
lastModified: page?.lastModified,
|
|
938
|
+
locale: scope?.locale[0],
|
|
939
|
+
framework: scope?.framework[0],
|
|
940
|
+
version: scope?.version[0],
|
|
941
|
+
tags: page?.tags ? [...page.tags] : void 0,
|
|
942
|
+
score: result.score,
|
|
943
|
+
content,
|
|
944
|
+
chars: content.length,
|
|
945
|
+
utf8Bytes: Buffer.byteLength(content, "utf8"),
|
|
946
|
+
truncated: false
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function buildContextFromSources(options) {
|
|
950
|
+
const blocks = [];
|
|
951
|
+
const included = [];
|
|
952
|
+
let usedUtf8Bytes = 0;
|
|
953
|
+
for (const source of options.sources) {
|
|
954
|
+
const header = [`## ${source.title}`, `Source: ${source.url}`].join("\n");
|
|
955
|
+
const separator = blocks.length === 0 ? "" : CONTEXT_SEPARATOR;
|
|
956
|
+
const available = options.tokenBudget - usedUtf8Bytes - Buffer.byteLength(separator + header + "\n\n");
|
|
957
|
+
if (available <= 0) break;
|
|
958
|
+
const contentBuffer = Buffer.from(source.content, "utf8");
|
|
959
|
+
let content = source.content;
|
|
960
|
+
let truncated = false;
|
|
961
|
+
if (contentBuffer.byteLength > available) {
|
|
962
|
+
truncated = true;
|
|
963
|
+
content = contentBuffer.subarray(0, available).toString("utf8").replace(/\uFFFD$/u, "");
|
|
964
|
+
while (Buffer.byteLength(content, "utf8") > available) content = content.slice(0, -1);
|
|
965
|
+
}
|
|
966
|
+
if (!content) break;
|
|
967
|
+
const block = `${header}\n\n${content}`;
|
|
968
|
+
blocks.push(block);
|
|
969
|
+
usedUtf8Bytes += Buffer.byteLength(separator + block, "utf8");
|
|
970
|
+
included.push({
|
|
971
|
+
...source,
|
|
972
|
+
content,
|
|
973
|
+
chars: content.length,
|
|
974
|
+
utf8Bytes: Buffer.byteLength(content, "utf8"),
|
|
975
|
+
truncated
|
|
976
|
+
});
|
|
977
|
+
if (truncated) break;
|
|
978
|
+
}
|
|
979
|
+
const context = blocks.join(CONTEXT_SEPARATOR);
|
|
980
|
+
usedUtf8Bytes = Buffer.byteLength(context, "utf8");
|
|
981
|
+
return {
|
|
982
|
+
query: options.query,
|
|
983
|
+
filters: {
|
|
984
|
+
framework: options.filters?.framework,
|
|
985
|
+
version: options.filters?.version,
|
|
986
|
+
locale: options.filters?.locale
|
|
987
|
+
},
|
|
988
|
+
budget: {
|
|
989
|
+
requestedTokens: options.tokenBudget,
|
|
990
|
+
strategy: "utf8-bytes",
|
|
991
|
+
maxUtf8Bytes: options.tokenBudget,
|
|
992
|
+
usedUtf8Bytes,
|
|
993
|
+
conservativeTokenUpperBound: usedUtf8Bytes,
|
|
994
|
+
remainingUtf8Bytes: Math.max(0, options.tokenBudget - usedUtf8Bytes),
|
|
995
|
+
truncated: included.some((source) => source.truncated) || included.length < options.sources.length
|
|
996
|
+
},
|
|
997
|
+
resultCount: included.length,
|
|
998
|
+
candidateCount: options.sources.length,
|
|
999
|
+
context,
|
|
1000
|
+
sources: included
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function getConfiguredSearchProvider(search) {
|
|
1004
|
+
if (search === false) return "disabled";
|
|
1005
|
+
if (!search || search === true) return "simple";
|
|
1006
|
+
if (search.enabled === false) return "disabled";
|
|
1007
|
+
return search.provider ?? "simple";
|
|
1008
|
+
}
|
|
1009
|
+
function isConfiguredSearchDisabled(search) {
|
|
1010
|
+
return search === false || Boolean(search && typeof search === "object" && search.enabled === false);
|
|
1011
|
+
}
|
|
1012
|
+
function assertEvaluationSearchConfig(search) {
|
|
1013
|
+
if (search === void 0 || typeof search === "boolean") return;
|
|
1014
|
+
if (!isRecord(search)) throw new Error("The configured search value must be a boolean or provider object.");
|
|
1015
|
+
const runtime = search;
|
|
1016
|
+
if (runtime.enabled !== void 0 && typeof runtime.enabled !== "boolean") throw new Error("The configured search enabled field must be boolean.");
|
|
1017
|
+
const provider = runtime.provider ?? "simple";
|
|
1018
|
+
if (typeof provider !== "string" || ![
|
|
1019
|
+
"simple",
|
|
1020
|
+
"custom",
|
|
1021
|
+
"typesense",
|
|
1022
|
+
"mcp",
|
|
1023
|
+
"algolia"
|
|
1024
|
+
].includes(provider)) throw new Error(`Unsupported configured search provider: ${String(provider)}.`);
|
|
1025
|
+
const requireStrings = (fields) => {
|
|
1026
|
+
const missing = fields.filter((field) => typeof runtime[field] !== "string" || !runtime[field].trim());
|
|
1027
|
+
if (missing.length > 0) throw new Error(`The ${provider} search provider requires non-empty ${missing.join(", ")}.`);
|
|
1028
|
+
};
|
|
1029
|
+
if (provider === "mcp") {
|
|
1030
|
+
requireStrings(["endpoint"]);
|
|
1031
|
+
if (runtime.headers !== void 0 && (!isRecord(runtime.headers) || Object.values(runtime.headers).some((value) => typeof value !== "string"))) throw new Error("The MCP search provider headers must contain only string values.");
|
|
1032
|
+
}
|
|
1033
|
+
if (provider === "algolia") requireStrings([
|
|
1034
|
+
"appId",
|
|
1035
|
+
"indexName",
|
|
1036
|
+
"searchApiKey"
|
|
1037
|
+
]);
|
|
1038
|
+
if (provider === "typesense") requireStrings([
|
|
1039
|
+
"baseUrl",
|
|
1040
|
+
"collection",
|
|
1041
|
+
"apiKey"
|
|
1042
|
+
]);
|
|
1043
|
+
if (provider === "custom") {
|
|
1044
|
+
const adapter = runtime.adapter;
|
|
1045
|
+
if (typeof adapter !== "function" && (!isRecord(adapter) || typeof adapter.search !== "function")) throw new Error("The custom search provider requires an adapter or adapter factory with a search function.");
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function assertEvaluationSearchAllowed(search, allowNetwork, surface, baseUrl) {
|
|
1049
|
+
const provider = getConfiguredSearchProvider(search);
|
|
1050
|
+
if (surface === "configured-search" && provider === "disabled") throw new Error("Configured search is disabled; no search surface can be evaluated.");
|
|
1051
|
+
if (provider !== "simple" && provider !== "disabled" && !allowNetwork) throw new Error(`The ${provider} search provider requires agent.evaluations.allowNetwork: true for evaluation.`);
|
|
1052
|
+
if (search && typeof search === "object" && search.provider === "mcp" && !/^https?:\/\//iu.test(search.endpoint) && !baseUrl) throw new Error("A relative MCP evaluation endpoint requires ai.docsUrl or another canonical docs baseUrl.");
|
|
1053
|
+
}
|
|
1054
|
+
async function runEvaluationSearchWithTimeout(options) {
|
|
1055
|
+
if (options.timeoutMs !== void 0 && (typeof options.timeoutMs !== "number" || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) throw new Error("The agent evaluation searchTimeoutMs must be a positive finite number.");
|
|
1056
|
+
const timeoutMs = normalizePositiveInteger(options.timeoutMs, 3e4, 3e5);
|
|
1057
|
+
const controller = new AbortController();
|
|
1058
|
+
let timeout;
|
|
1059
|
+
let timeoutError;
|
|
1060
|
+
const timedOut = new Promise((_, reject) => {
|
|
1061
|
+
timeout = setTimeout(() => {
|
|
1062
|
+
timeoutError = /* @__PURE__ */ new Error(`The ${options.provider} search provider timed out after ${timeoutMs}ms.`);
|
|
1063
|
+
controller.abort();
|
|
1064
|
+
reject(timeoutError);
|
|
1065
|
+
}, timeoutMs);
|
|
1066
|
+
});
|
|
1067
|
+
try {
|
|
1068
|
+
return await Promise.race([Promise.resolve().then(() => options.run(controller.signal)), timedOut]).catch((error) => {
|
|
1069
|
+
if (timeoutError) throw timeoutError;
|
|
1070
|
+
throw error;
|
|
1071
|
+
});
|
|
1072
|
+
} finally {
|
|
1073
|
+
if (timeout) clearTimeout(timeout);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
function createContextShell(options) {
|
|
1077
|
+
const context = options.context ?? "";
|
|
1078
|
+
const sources = options.sources ?? [];
|
|
1079
|
+
const usedUtf8Bytes = Buffer.byteLength(context, "utf8");
|
|
1080
|
+
return {
|
|
1081
|
+
query: options.query,
|
|
1082
|
+
filters: {
|
|
1083
|
+
framework: options.filters?.framework,
|
|
1084
|
+
version: options.filters?.version,
|
|
1085
|
+
locale: options.filters?.locale
|
|
1086
|
+
},
|
|
1087
|
+
budget: {
|
|
1088
|
+
requestedTokens: options.tokenBudget,
|
|
1089
|
+
strategy: "utf8-bytes",
|
|
1090
|
+
maxUtf8Bytes: options.tokenBudget,
|
|
1091
|
+
usedUtf8Bytes,
|
|
1092
|
+
conservativeTokenUpperBound: usedUtf8Bytes,
|
|
1093
|
+
remainingUtf8Bytes: Math.max(0, options.tokenBudget - usedUtf8Bytes),
|
|
1094
|
+
truncated: options.truncated ?? usedUtf8Bytes > options.tokenBudget
|
|
1095
|
+
},
|
|
1096
|
+
resultCount: sources.length,
|
|
1097
|
+
candidateCount: options.candidateCount ?? sources.length,
|
|
1098
|
+
context,
|
|
1099
|
+
sources
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function parseAskAIContextSources(options) {
|
|
1103
|
+
if (!options.context) return {
|
|
1104
|
+
sources: [],
|
|
1105
|
+
citations: [],
|
|
1106
|
+
integrity: options.results.length === 0
|
|
1107
|
+
};
|
|
1108
|
+
const blocks = options.blocks;
|
|
1109
|
+
const sources = [];
|
|
1110
|
+
const citations = [];
|
|
1111
|
+
let integrity = blocks.length === options.results.length && blocks.join(CONTEXT_SEPARATOR) === options.context;
|
|
1112
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
1113
|
+
const block = blocks[index] ?? "";
|
|
1114
|
+
const contentStart = block.indexOf("\n\n");
|
|
1115
|
+
if (contentStart < 0) {
|
|
1116
|
+
integrity = false;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
const urlLines = block.slice(0, contentStart).split(/\r?\n/u).map((line) => line.match(/^URL:\s+(.+)$/u)?.[1]?.trim()).filter((value) => Boolean(value));
|
|
1120
|
+
if (urlLines.length !== 1) integrity = false;
|
|
1121
|
+
const url = canonicalizeSource(urlLines[0] ?? "/", options.baseUrl);
|
|
1122
|
+
const expected = options.results[index];
|
|
1123
|
+
if (!expected || !sourceMatches(url, expected.url) || canonicalizeSource(expected.url) !== url) integrity = false;
|
|
1124
|
+
const content = block.slice(contentStart + 2);
|
|
1125
|
+
const base = expected ?? {
|
|
1126
|
+
id: `ask-ai-result-${index + 1}`,
|
|
1127
|
+
title: url,
|
|
1128
|
+
pageUrl: sourcePage(url),
|
|
1129
|
+
url,
|
|
1130
|
+
content: "",
|
|
1131
|
+
chars: 0,
|
|
1132
|
+
utf8Bytes: 0,
|
|
1133
|
+
truncated: false
|
|
1134
|
+
};
|
|
1135
|
+
citations.push(url);
|
|
1136
|
+
sources.push({
|
|
1137
|
+
...base,
|
|
1138
|
+
url,
|
|
1139
|
+
pageUrl: sourcePage(url),
|
|
1140
|
+
content,
|
|
1141
|
+
chars: content.length,
|
|
1142
|
+
utf8Bytes: Buffer.byteLength(content, "utf8"),
|
|
1143
|
+
truncated: content !== base.content
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
return {
|
|
1147
|
+
sources,
|
|
1148
|
+
citations,
|
|
1149
|
+
integrity
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
async function buildGoldenSurface(options) {
|
|
1153
|
+
const surface = options.task.surface ?? options.runOptions.surface ?? "mcp-context";
|
|
1154
|
+
const orderedPages = options.pages.slice().sort((left, right) => compareCodePoints(canonicalizeSource(left.url), canonicalizeSource(right.url)));
|
|
1155
|
+
const pagesByUrl = new Map(orderedPages.map((page) => [sourcePage(page.url), page]));
|
|
1156
|
+
if (surface === "mcp-context") {
|
|
1157
|
+
const contextOptions = {
|
|
1158
|
+
pages: orderedPages,
|
|
1159
|
+
query: options.task.query,
|
|
1160
|
+
framework: options.task.filters?.framework,
|
|
1161
|
+
version: options.task.filters?.version,
|
|
1162
|
+
locale: options.task.filters?.locale,
|
|
1163
|
+
maxResults: options.topK
|
|
1164
|
+
};
|
|
1165
|
+
const [rankedContext, budgetedContext] = await Promise.all([buildDocsMcpContext({
|
|
1166
|
+
...contextOptions,
|
|
1167
|
+
tokenBudget: Number.MAX_SAFE_INTEGER
|
|
1168
|
+
}), buildDocsMcpContext({
|
|
1169
|
+
...contextOptions,
|
|
1170
|
+
tokenBudget: options.tokenBudget
|
|
1171
|
+
})]);
|
|
1172
|
+
const extracted = extractRenderedCitations(budgetedContext.context, budgetedContext.sources);
|
|
1173
|
+
return {
|
|
1174
|
+
surface,
|
|
1175
|
+
provider: "simple",
|
|
1176
|
+
rankedContext,
|
|
1177
|
+
budgetedContext,
|
|
1178
|
+
citationEvidence: {
|
|
1179
|
+
mode: "context",
|
|
1180
|
+
actual: extracted.actual,
|
|
1181
|
+
integrity: extracted.layoutIntegrity
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
const configuredSurfaceSearch = surface === "ask-ai-context" ? options.runOptions.askAISearch ?? options.runOptions.search : options.runOptions.search;
|
|
1186
|
+
const effectiveSurfaceSearch = surface === "ask-ai-context" && isConfiguredSearchDisabled(configuredSurfaceSearch) ? true : configuredSurfaceSearch;
|
|
1187
|
+
assertEvaluationSearchConfig(effectiveSurfaceSearch);
|
|
1188
|
+
const surfaceSearch = resolveSearchRequestConfig(effectiveSurfaceSearch, options.runOptions.baseUrl);
|
|
1189
|
+
assertEvaluationSearchAllowed(surfaceSearch, options.runOptions.allowNetwork === true, surface, options.runOptions.baseUrl);
|
|
1190
|
+
const provider = getConfiguredSearchProvider(surfaceSearch);
|
|
1191
|
+
if (surface === "configured-search") {
|
|
1192
|
+
const results = await runEvaluationSearchWithTimeout({
|
|
1193
|
+
provider,
|
|
1194
|
+
timeoutMs: options.runOptions.searchTimeoutMs,
|
|
1195
|
+
run: (signal) => performDocsSearch({
|
|
1196
|
+
pages: orderedPages,
|
|
1197
|
+
query: options.task.query,
|
|
1198
|
+
search: surfaceSearch,
|
|
1199
|
+
audience: "agent",
|
|
1200
|
+
locale: options.task.filters?.locale,
|
|
1201
|
+
siteTitle: options.runOptions.siteTitle,
|
|
1202
|
+
baseUrl: options.runOptions.baseUrl,
|
|
1203
|
+
limit: options.topK,
|
|
1204
|
+
failureMode: "throw",
|
|
1205
|
+
supplementExternalResults: false,
|
|
1206
|
+
strictExternalOrigins: true,
|
|
1207
|
+
signal
|
|
1208
|
+
})
|
|
1209
|
+
});
|
|
1210
|
+
const rankedSources = results.slice(0, options.topK).map((result, index) => hydrateSearchSource(result, index, pagesByUrl, "result", options.runOptions.baseUrl));
|
|
1211
|
+
return {
|
|
1212
|
+
surface,
|
|
1213
|
+
provider,
|
|
1214
|
+
rankedContext: buildContextFromSources({
|
|
1215
|
+
query: options.task.query,
|
|
1216
|
+
filters: options.task.filters,
|
|
1217
|
+
sources: rankedSources,
|
|
1218
|
+
tokenBudget: Number.MAX_SAFE_INTEGER
|
|
1219
|
+
}),
|
|
1220
|
+
budgetedContext: buildContextFromSources({
|
|
1221
|
+
query: options.task.query,
|
|
1222
|
+
filters: options.task.filters,
|
|
1223
|
+
sources: rankedSources,
|
|
1224
|
+
tokenBudget: options.tokenBudget
|
|
1225
|
+
}),
|
|
1226
|
+
citationEvidence: {
|
|
1227
|
+
mode: "results",
|
|
1228
|
+
actual: rankedSources.map((source) => canonicalizeSource(source.url)),
|
|
1229
|
+
integrity: rankedSources.every((source, index) => source.url === canonicalizeSource(results[index]?.url ?? "", options.runOptions.baseUrl))
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
const askAI = await runEvaluationSearchWithTimeout({
|
|
1234
|
+
provider,
|
|
1235
|
+
timeoutMs: options.runOptions.searchTimeoutMs,
|
|
1236
|
+
run: (signal) => buildDocsAskAIContext({
|
|
1237
|
+
pages: orderedPages,
|
|
1238
|
+
query: options.task.query,
|
|
1239
|
+
search: surfaceSearch,
|
|
1240
|
+
locale: options.task.filters?.locale,
|
|
1241
|
+
siteTitle: options.runOptions.siteTitle,
|
|
1242
|
+
baseUrl: options.runOptions.baseUrl,
|
|
1243
|
+
limit: options.topK,
|
|
1244
|
+
maxContextChars: options.tokenBudget,
|
|
1245
|
+
searchFailureMode: "throw",
|
|
1246
|
+
strictExternalOrigins: true,
|
|
1247
|
+
signal
|
|
1248
|
+
})
|
|
1249
|
+
});
|
|
1250
|
+
const rankedSources = askAI.searchResults.slice(0, options.topK).map((result, index) => hydrateSearchSource(result, index, pagesByUrl, "result", options.runOptions.baseUrl));
|
|
1251
|
+
const resultSources = askAI.results.map((result, index) => hydrateSearchSource(result, index, pagesByUrl, "page-section", options.runOptions.baseUrl));
|
|
1252
|
+
const parsed = parseAskAIContextSources({
|
|
1253
|
+
context: askAI.context,
|
|
1254
|
+
blocks: askAI.blocks.map((block) => block.text),
|
|
1255
|
+
results: resultSources,
|
|
1256
|
+
baseUrl: options.runOptions.baseUrl
|
|
1257
|
+
});
|
|
1258
|
+
return {
|
|
1259
|
+
surface,
|
|
1260
|
+
provider,
|
|
1261
|
+
rankedContext: createContextShell({
|
|
1262
|
+
query: options.task.query,
|
|
1263
|
+
filters: options.task.filters,
|
|
1264
|
+
tokenBudget: Number.MAX_SAFE_INTEGER,
|
|
1265
|
+
sources: rankedSources
|
|
1266
|
+
}),
|
|
1267
|
+
budgetedContext: createContextShell({
|
|
1268
|
+
query: options.task.query,
|
|
1269
|
+
filters: options.task.filters,
|
|
1270
|
+
tokenBudget: options.tokenBudget,
|
|
1271
|
+
context: askAI.context,
|
|
1272
|
+
sources: parsed.sources,
|
|
1273
|
+
candidateCount: askAI.results.length,
|
|
1274
|
+
truncated: parsed.sources.some((source) => source.truncated) || Buffer.byteLength(askAI.context, "utf8") > options.tokenBudget
|
|
1275
|
+
}),
|
|
1276
|
+
citationEvidence: {
|
|
1277
|
+
mode: "context",
|
|
1278
|
+
actual: parsed.citations,
|
|
1279
|
+
integrity: parsed.integrity
|
|
1280
|
+
},
|
|
1281
|
+
attributionUtf8Bytes: askAI.blocks.map((block, index) => Buffer.byteLength(`${index === 0 ? "" : CONTEXT_SEPARATOR}${block.text}`, "utf8"))
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
function validateStaticSyntax(target) {
|
|
1285
|
+
const language = target.language?.toLowerCase();
|
|
1286
|
+
if (language === "json") try {
|
|
1287
|
+
JSON.parse(target.code);
|
|
1288
|
+
return {
|
|
1289
|
+
supported: true,
|
|
1290
|
+
valid: true
|
|
1291
|
+
};
|
|
1292
|
+
} catch {
|
|
1293
|
+
return {
|
|
1294
|
+
supported: true,
|
|
1295
|
+
valid: false
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
if (language === "javascript" || language === "js" || language === "jsx" || language === "node" || language === "typescript" || language === "ts" || language === "tsx") try {
|
|
1299
|
+
const typescript = language === "typescript" || language === "ts" || language === "tsx";
|
|
1300
|
+
const jsx = language === "jsx" || language === "tsx";
|
|
1301
|
+
const transformed = STATIC_JITI.transform({
|
|
1302
|
+
source: target.code,
|
|
1303
|
+
filename: `golden-example.${language === "typescript" ? "ts" : language}`,
|
|
1304
|
+
ts: typescript,
|
|
1305
|
+
jsx,
|
|
1306
|
+
async: true
|
|
1307
|
+
});
|
|
1308
|
+
return {
|
|
1309
|
+
supported: true,
|
|
1310
|
+
valid: !/\bexports\.__JITI_ERROR__\s*=/u.test(transformed)
|
|
1311
|
+
};
|
|
1312
|
+
} catch {
|
|
1313
|
+
return {
|
|
1314
|
+
supported: true,
|
|
1315
|
+
valid: false
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
supported: false,
|
|
1320
|
+
valid: false
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
async function collectRetrievedExamples(candidates) {
|
|
1324
|
+
const examples = [];
|
|
1325
|
+
for (const candidate of candidates) {
|
|
1326
|
+
const targets = extractCodeBlocksFromMarkdown({
|
|
1327
|
+
source: candidate.content,
|
|
1328
|
+
filePath: candidate.page.sourcePath ?? candidate.page.slug,
|
|
1329
|
+
relativePath: candidate.page.slug
|
|
1330
|
+
});
|
|
1331
|
+
for (const target of targets) examples.push({
|
|
1332
|
+
source: candidate.source,
|
|
1333
|
+
originalId: target.id,
|
|
1334
|
+
target: {
|
|
1335
|
+
...target,
|
|
1336
|
+
id: `${canonicalizeSource(candidate.source)}::${target.id}`
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
const plans = await planCodeBlockTargets(examples.map((example) => example.target), STATIC_CODE_PLAN_CONFIG);
|
|
1341
|
+
return examples.map((example, index) => ({
|
|
1342
|
+
...example,
|
|
1343
|
+
plan: plans[index],
|
|
1344
|
+
...(() => {
|
|
1345
|
+
const syntax = validateStaticSyntax(example.target);
|
|
1346
|
+
const structurallyComplete = Boolean(example.target.code.trim()) && !example.target.code.split("\n").some((line) => /^\s*(?:(?:\/\/|#)\s*)?\.\.\.\s*$/u.test(line));
|
|
1347
|
+
return {
|
|
1348
|
+
syntaxSupported: syntax.supported,
|
|
1349
|
+
syntaxValid: syntax.valid && structurallyComplete,
|
|
1350
|
+
staticExecutable: example.target.runnable && Boolean(example.target.language) && EXECUTABLE_LANGUAGES.has(example.target.language.toLowerCase()) && plans[index]?.action !== "skip" && structurallyComplete && (!syntax.supported || syntax.valid)
|
|
1351
|
+
};
|
|
1352
|
+
})()
|
|
1353
|
+
}));
|
|
1354
|
+
}
|
|
1355
|
+
function normalizedEqual(left, right) {
|
|
1356
|
+
return (left ?? "").trim().toLowerCase() === (right ?? "").trim().toLowerCase();
|
|
1357
|
+
}
|
|
1358
|
+
function exampleMetadataMatches(expected, actual, baseUrl) {
|
|
1359
|
+
if (expected.source && !sourceMatches(actual.source, expected.source, baseUrl)) return false;
|
|
1360
|
+
if (expected.language && !normalizedEqual(actual.target.language, expected.language)) return false;
|
|
1361
|
+
if (expected.framework && !normalizedEqual(actual.target.framework, expected.framework)) return false;
|
|
1362
|
+
if (expected.packageManager && !normalizedEqual(actual.target.packageManager, expected.packageManager)) return false;
|
|
1363
|
+
if (expected.title && !normalizedEqual(actual.target.title, expected.title)) return false;
|
|
1364
|
+
if (expected.includes?.some((fragment) => !actual.target.code.includes(fragment))) return false;
|
|
1365
|
+
if (expected.runnable === false) return !actual.target.runnable;
|
|
1366
|
+
return actual.target.runnable;
|
|
1367
|
+
}
|
|
1368
|
+
function resolveExampleVerification(expected) {
|
|
1369
|
+
return expected.verification ?? (expected.runnable === false ? "present" : "syntax");
|
|
1370
|
+
}
|
|
1371
|
+
function failedExampleResult(expected, verification, reason, status = "failed") {
|
|
1372
|
+
return {
|
|
1373
|
+
expected,
|
|
1374
|
+
verification,
|
|
1375
|
+
status,
|
|
1376
|
+
reason,
|
|
1377
|
+
syntaxValid: false,
|
|
1378
|
+
executed: false,
|
|
1379
|
+
executable: false,
|
|
1380
|
+
passed: false
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
async function evaluateExamples(expectedExamples, candidates, options) {
|
|
1384
|
+
const actualExamples = await collectRetrievedExamples(candidates);
|
|
1385
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1386
|
+
const matches = expectedExamples.map((expected) => {
|
|
1387
|
+
const index = actualExamples.findIndex((example, candidateIndex) => !claimed.has(candidateIndex) && exampleMetadataMatches(expected, example, options.baseUrl));
|
|
1388
|
+
const verification = resolveExampleVerification(expected);
|
|
1389
|
+
if (index < 0) return {
|
|
1390
|
+
expected,
|
|
1391
|
+
verification,
|
|
1392
|
+
result: failedExampleResult(expected, verification, "No matching code block was retrieved.")
|
|
1393
|
+
};
|
|
1394
|
+
claimed.add(index);
|
|
1395
|
+
return {
|
|
1396
|
+
expected,
|
|
1397
|
+
verification,
|
|
1398
|
+
matched: actualExamples[index]
|
|
1399
|
+
};
|
|
1400
|
+
});
|
|
1401
|
+
const executeMatches = matches.filter((match) => match.verification === "execute" && Boolean(match.matched));
|
|
1402
|
+
const executionByTarget = /* @__PURE__ */ new Map();
|
|
1403
|
+
let executionFailure;
|
|
1404
|
+
if (executeMatches.length > 0) {
|
|
1405
|
+
const resolved = resolveDocsCodeBlocksValidateConfig(options.codeBlocksValidate);
|
|
1406
|
+
if (options.allowNetwork !== true) executionFailure = "Execution verification requires agent.evaluations.allowNetwork: true because arbitrary examples and validators may access the network.";
|
|
1407
|
+
else if (!resolved.enabled) executionFailure = "Execution verification requires codeBlocks.validate to be explicitly enabled.";
|
|
1408
|
+
else if (resolved.mode !== "report") executionFailure = "Execution verification requires codeBlocks.validate.mode to be report.";
|
|
1409
|
+
else if (!options.rootDir) executionFailure = "Execution verification requires a project rootDir.";
|
|
1410
|
+
else try {
|
|
1411
|
+
const targets = executeMatches.map((match) => match.matched.target);
|
|
1412
|
+
const plans = await planCodeBlockTargets(targets, resolved);
|
|
1413
|
+
const validation = await validateCodeBlockPlans({
|
|
1414
|
+
plans,
|
|
1415
|
+
rootDir: options.rootDir,
|
|
1416
|
+
config: resolved
|
|
1417
|
+
});
|
|
1418
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
1419
|
+
const plan = plans[index];
|
|
1420
|
+
const result = validation.find((candidate) => candidate.plan === plan);
|
|
1421
|
+
if (result) executionByTarget.set(targets[index], result);
|
|
1422
|
+
}
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
executionFailure = `Execution verification failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
const results = matches.map(({ expected, verification, matched }) => {
|
|
1428
|
+
if (!matched) return failedExampleResult(expected, verification, "No matching code block was retrieved.");
|
|
1429
|
+
const base = {
|
|
1430
|
+
expected,
|
|
1431
|
+
matchedId: matched.originalId,
|
|
1432
|
+
source: matched.source,
|
|
1433
|
+
verification,
|
|
1434
|
+
syntaxValid: matched.syntaxValid,
|
|
1435
|
+
executed: false,
|
|
1436
|
+
executable: matched.staticExecutable
|
|
1437
|
+
};
|
|
1438
|
+
if (verification === "present") return {
|
|
1439
|
+
...base,
|
|
1440
|
+
status: "passed",
|
|
1441
|
+
passed: true
|
|
1442
|
+
};
|
|
1443
|
+
if (verification === "syntax") {
|
|
1444
|
+
if (!matched.syntaxSupported) return {
|
|
1445
|
+
...base,
|
|
1446
|
+
status: "skipped",
|
|
1447
|
+
reason: `Static syntax verification is unsupported for ${matched.target.language ?? "an untyped fence"}.`,
|
|
1448
|
+
passed: false
|
|
1449
|
+
};
|
|
1450
|
+
return {
|
|
1451
|
+
...base,
|
|
1452
|
+
status: matched.syntaxValid ? "passed" : "failed",
|
|
1453
|
+
reason: matched.syntaxValid ? void 0 : "The retrieved code block has invalid syntax.",
|
|
1454
|
+
passed: matched.syntaxValid
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
if (executionFailure) return {
|
|
1458
|
+
...base,
|
|
1459
|
+
status: "skipped",
|
|
1460
|
+
reason: executionFailure,
|
|
1461
|
+
passed: false
|
|
1462
|
+
};
|
|
1463
|
+
const validation = executionByTarget.get(matched.target);
|
|
1464
|
+
if (!validation) return {
|
|
1465
|
+
...base,
|
|
1466
|
+
status: "skipped",
|
|
1467
|
+
reason: "The configured validator did not return an execution result.",
|
|
1468
|
+
passed: false
|
|
1469
|
+
};
|
|
1470
|
+
const executed = validation.plan.action === "execute" && validation.status === "PASS";
|
|
1471
|
+
return {
|
|
1472
|
+
...base,
|
|
1473
|
+
status: executed ? "passed" : validation.status === "SKIP" ? "skipped" : "failed",
|
|
1474
|
+
reason: executed ? void 0 : validation.reason ?? (validation.plan.action !== "execute" ? "The validator did not plan runtime execution." : `Execution returned ${validation.status}.`),
|
|
1475
|
+
executed,
|
|
1476
|
+
executable: matched.staticExecutable,
|
|
1477
|
+
executionStatus: validation.status,
|
|
1478
|
+
passed: executed
|
|
1479
|
+
};
|
|
1480
|
+
});
|
|
1481
|
+
const matched = results.filter((result) => result.passed).length;
|
|
1482
|
+
return {
|
|
1483
|
+
expected: expectedExamples.length,
|
|
1484
|
+
matched,
|
|
1485
|
+
syntaxValid: results.filter((result) => result.syntaxValid).length,
|
|
1486
|
+
executed: results.filter((result) => result.executed).length,
|
|
1487
|
+
executable: results.filter((result) => result.executable).length,
|
|
1488
|
+
results,
|
|
1489
|
+
passed: matched === expectedExamples.length
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
function buildRetrievalMetrics(task, sources, topK, baseUrl) {
|
|
1493
|
+
const expected = uniqueCanonicalSources(task.expect.relevantSources, baseUrl);
|
|
1494
|
+
const forbidden = uniqueCanonicalSources(task.expect.forbiddenSources ?? [], baseUrl);
|
|
1495
|
+
const retrievedRelevant = expected.filter((source) => sources.some((actual) => sourceMatches(actual.url, source, baseUrl))).length;
|
|
1496
|
+
const recallAtK = expected.length > 0 ? retrievedRelevant / expected.length : 0;
|
|
1497
|
+
const firstRelevant = sources.find((source) => source.relevant)?.rank ?? null;
|
|
1498
|
+
const forbiddenSources = sources.filter((source) => forbidden.some((forbiddenSource) => sourceMatches(source.url, forbiddenSource, baseUrl))).map((source) => source.url);
|
|
1499
|
+
const minimumRecall = clamp(task.expect.minRecallAtK ?? 1, 0, 1);
|
|
1500
|
+
const maximumRank = normalizePositiveInteger(task.expect.maxFirstRelevantRank, topK, topK);
|
|
1501
|
+
return {
|
|
1502
|
+
expectedRelevant: expected.length,
|
|
1503
|
+
retrievedRelevant,
|
|
1504
|
+
recallAtK: round(recallAtK),
|
|
1505
|
+
firstRelevantRank: firstRelevant,
|
|
1506
|
+
reciprocalRank: firstRelevant ? round(1 / firstRelevant) : 0,
|
|
1507
|
+
forbiddenSources,
|
|
1508
|
+
passed: expected.length > 0 && recallAtK >= minimumRecall && firstRelevant !== null && firstRelevant <= maximumRank && forbiddenSources.length === 0
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function buildCitationMetrics(task, context, sources, evidence, baseUrl) {
|
|
1512
|
+
const expected = uniqueCanonicalSources(task.expect.requiredCitations ?? task.expect.relevantSources, baseUrl);
|
|
1513
|
+
const allowed = uniqueCanonicalSources([
|
|
1514
|
+
...task.expect.relevantSources,
|
|
1515
|
+
...task.expect.requiredCitations ?? [],
|
|
1516
|
+
...task.expect.allowedSources ?? []
|
|
1517
|
+
], baseUrl);
|
|
1518
|
+
const extracted = evidence ? {
|
|
1519
|
+
actual: evidence.actual,
|
|
1520
|
+
layoutIntegrity: evidence.integrity
|
|
1521
|
+
} : extractRenderedCitations(context, sources);
|
|
1522
|
+
const actual = extracted.actual;
|
|
1523
|
+
const sourceRecords = sources.map((source) => canonicalizeSource(source.url));
|
|
1524
|
+
const missing = expected.filter((expectedSource) => !actual.some((actualSource) => sourceMatches(actualSource, expectedSource, baseUrl)));
|
|
1525
|
+
const unexpected = actual.filter((actualSource) => !allowed.some((allowedSource) => sourceMatches(actualSource, allowedSource, baseUrl)));
|
|
1526
|
+
const correct = actual.filter((actualSource) => allowed.some((allowedSource) => sourceMatches(actualSource, allowedSource, baseUrl))).length;
|
|
1527
|
+
const matchedExpected = expected.filter((expectedSource) => actual.some((actualSource) => sourceMatches(actualSource, expectedSource, baseUrl))).length;
|
|
1528
|
+
const precision = actual.length > 0 ? correct / actual.length : expected.length === 0 ? 1 : 0;
|
|
1529
|
+
const recall = expected.length > 0 ? matchedExpected / expected.length : 1;
|
|
1530
|
+
const integrity = extracted.layoutIntegrity && actual.length === sourceRecords.length && actual.every((source, index) => source === sourceRecords[index]);
|
|
1531
|
+
return {
|
|
1532
|
+
evidence: evidence?.mode ?? "context",
|
|
1533
|
+
expected,
|
|
1534
|
+
actual,
|
|
1535
|
+
missing,
|
|
1536
|
+
unexpected,
|
|
1537
|
+
precision: round(precision),
|
|
1538
|
+
recall: round(recall),
|
|
1539
|
+
integrity,
|
|
1540
|
+
passed: missing.length === 0 && unexpected.length === 0 && integrity
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
function canonicalizeAnswerCitation(value, baseUrl) {
|
|
1544
|
+
return canonicalizeSource(value.trim(), baseUrl);
|
|
1545
|
+
}
|
|
1546
|
+
function uniqueAnswerCitations(values, baseUrl) {
|
|
1547
|
+
return Array.from(new Set(values.map((value) => canonicalizeAnswerCitation(value, baseUrl))));
|
|
1548
|
+
}
|
|
1549
|
+
function answerCitationMatches(actual, expected) {
|
|
1550
|
+
return expected.includes("#") ? actual === expected : actual.split("#", 1)[0] === expected.split("#", 1)[0];
|
|
1551
|
+
}
|
|
1552
|
+
function maskMarkdownCitationInertRegions(text) {
|
|
1553
|
+
const masked = new Uint8Array(text.length);
|
|
1554
|
+
const maskRange = (start, end) => {
|
|
1555
|
+
for (let index = start; index < end; index += 1) masked[index] = 1;
|
|
1556
|
+
};
|
|
1557
|
+
for (let start = text.indexOf("<!--"); start >= 0; start = text.indexOf("<!--", start + 4)) {
|
|
1558
|
+
const closing = text.indexOf("-->", start + 4);
|
|
1559
|
+
const end = closing < 0 ? text.length : closing + 3;
|
|
1560
|
+
maskRange(start, end);
|
|
1561
|
+
if (closing < 0) break;
|
|
1562
|
+
start = closing;
|
|
1563
|
+
}
|
|
1564
|
+
let openFence;
|
|
1565
|
+
let lineStart = 0;
|
|
1566
|
+
while (lineStart <= text.length) {
|
|
1567
|
+
const newline = text.indexOf("\n", lineStart);
|
|
1568
|
+
const lineEnd = newline < 0 ? text.length : newline + 1;
|
|
1569
|
+
const line = text.slice(lineStart, newline < 0 ? text.length : newline);
|
|
1570
|
+
if (!masked[lineStart]) {
|
|
1571
|
+
if (!openFence) {
|
|
1572
|
+
const opening = line.match(/^[ \t]{0,3}(`{3,}|~{3,})/u)?.[1];
|
|
1573
|
+
if (opening) openFence = {
|
|
1574
|
+
character: opening[0],
|
|
1575
|
+
length: opening.length,
|
|
1576
|
+
start: lineStart
|
|
1577
|
+
};
|
|
1578
|
+
} else if (new RegExp(`^[ \\t]{0,3}${openFence.character}{${openFence.length},}[ \\t]*$`, "u").test(line)) {
|
|
1579
|
+
maskRange(openFence.start, lineEnd);
|
|
1580
|
+
openFence = void 0;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
if (newline < 0) break;
|
|
1584
|
+
lineStart = lineEnd;
|
|
1585
|
+
}
|
|
1586
|
+
if (openFence) maskRange(openFence.start, text.length);
|
|
1587
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1588
|
+
if (masked[index] || text[index] !== "`") continue;
|
|
1589
|
+
let runEnd = index + 1;
|
|
1590
|
+
while (!masked[runEnd] && text[runEnd] === "`") runEnd += 1;
|
|
1591
|
+
const runLength = runEnd - index;
|
|
1592
|
+
let cursor = runEnd;
|
|
1593
|
+
let closingEnd = -1;
|
|
1594
|
+
while (cursor < text.length) {
|
|
1595
|
+
if (masked[cursor] || text[cursor] !== "`") {
|
|
1596
|
+
cursor += 1;
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
let candidateEnd = cursor + 1;
|
|
1600
|
+
while (!masked[candidateEnd] && text[candidateEnd] === "`") candidateEnd += 1;
|
|
1601
|
+
if (candidateEnd - cursor === runLength) {
|
|
1602
|
+
closingEnd = candidateEnd;
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1605
|
+
cursor = candidateEnd;
|
|
1606
|
+
}
|
|
1607
|
+
if (closingEnd > 0) {
|
|
1608
|
+
maskRange(index, closingEnd);
|
|
1609
|
+
index = closingEnd - 1;
|
|
1610
|
+
} else index = runEnd - 1;
|
|
1611
|
+
}
|
|
1612
|
+
return text.split("").map((character, index) => masked[index] && character !== "\n" && character !== "\r" ? " " : character).join("");
|
|
1613
|
+
}
|
|
1614
|
+
function extractInlineMarkdownLinkDestinations(text) {
|
|
1615
|
+
text = maskMarkdownCitationInertRegions(text);
|
|
1616
|
+
const destinations = [];
|
|
1617
|
+
const escapedAt = (index) => {
|
|
1618
|
+
let backslashes = 0;
|
|
1619
|
+
for (let cursor = index - 1; cursor >= 0 && text[cursor] === "\\"; cursor -= 1) backslashes += 1;
|
|
1620
|
+
return backslashes % 2 === 1;
|
|
1621
|
+
};
|
|
1622
|
+
for (let index = 0; index < text.length - 1; index += 1) {
|
|
1623
|
+
if (text[index] !== "]" || text[index + 1] !== "(" || escapedAt(index)) continue;
|
|
1624
|
+
let bracketDepth = 1;
|
|
1625
|
+
let openingBracket = -1;
|
|
1626
|
+
for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
|
|
1627
|
+
if (escapedAt(cursor)) continue;
|
|
1628
|
+
if (text[cursor] === "]") bracketDepth += 1;
|
|
1629
|
+
if (text[cursor] === "[") {
|
|
1630
|
+
bracketDepth -= 1;
|
|
1631
|
+
if (bracketDepth === 0) {
|
|
1632
|
+
openingBracket = cursor;
|
|
1633
|
+
break;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
if (openingBracket < 0 || text[openingBracket - 1] === "!" && !escapedAt(openingBracket - 1)) continue;
|
|
1638
|
+
let cursor = index + 2;
|
|
1639
|
+
while (/\s/u.test(text[cursor] ?? "")) cursor += 1;
|
|
1640
|
+
if (text[cursor] === "<") {
|
|
1641
|
+
const end = text.indexOf(">", cursor + 1);
|
|
1642
|
+
if (end <= cursor + 1) continue;
|
|
1643
|
+
let closingCursor = end + 1;
|
|
1644
|
+
while (/\s/u.test(text[closingCursor] ?? "")) closingCursor += 1;
|
|
1645
|
+
const titleOpener = text[closingCursor];
|
|
1646
|
+
if (titleOpener === "\"" || titleOpener === "'" || titleOpener === "(") {
|
|
1647
|
+
const titleCloser = titleOpener === "(" ? ")" : titleOpener;
|
|
1648
|
+
closingCursor += 1;
|
|
1649
|
+
let escaped = false;
|
|
1650
|
+
while (closingCursor < text.length) {
|
|
1651
|
+
const character = text[closingCursor] ?? "";
|
|
1652
|
+
if (escaped) escaped = false;
|
|
1653
|
+
else if (character === "\\") escaped = true;
|
|
1654
|
+
else if (character === titleCloser) {
|
|
1655
|
+
closingCursor += 1;
|
|
1656
|
+
break;
|
|
1657
|
+
}
|
|
1658
|
+
closingCursor += 1;
|
|
1659
|
+
}
|
|
1660
|
+
while (/\s/u.test(text[closingCursor] ?? "")) closingCursor += 1;
|
|
1661
|
+
}
|
|
1662
|
+
if (text[closingCursor] === ")") destinations.push(text.slice(cursor + 1, end));
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
const start = cursor;
|
|
1666
|
+
let depth = 1;
|
|
1667
|
+
let escaped = false;
|
|
1668
|
+
let destinationEnd = -1;
|
|
1669
|
+
for (; cursor < text.length; cursor += 1) {
|
|
1670
|
+
const character = text[cursor] ?? "";
|
|
1671
|
+
if (escaped) {
|
|
1672
|
+
escaped = false;
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
if (character === "\\") {
|
|
1676
|
+
escaped = true;
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
if (character === "(") depth += 1;
|
|
1680
|
+
if (character === ")") {
|
|
1681
|
+
depth -= 1;
|
|
1682
|
+
if (depth === 0) {
|
|
1683
|
+
destinationEnd = cursor;
|
|
1684
|
+
break;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (destinationEnd <= start) continue;
|
|
1689
|
+
const destination = text.slice(start, destinationEnd).trim().match(/^(\S+)/u)?.[1];
|
|
1690
|
+
if (destination) destinations.push(destination.replace(/\\([()])/gu, "$1"));
|
|
1691
|
+
}
|
|
1692
|
+
return destinations;
|
|
1693
|
+
}
|
|
1694
|
+
function extractAnswerCitations(answer, baseUrl) {
|
|
1695
|
+
const values = [...answer.citations ?? []];
|
|
1696
|
+
for (const destination of extractInlineMarkdownLinkDestinations(answer.text)) {
|
|
1697
|
+
const value = destination.trim();
|
|
1698
|
+
const explicitlyReferenced = /^[a-z][a-z\d+.-]*:/iu.test(value) || /^[\\/]{2}/u.test(value);
|
|
1699
|
+
let isHttpReference = false;
|
|
1700
|
+
if (explicitlyReferenced) try {
|
|
1701
|
+
let parsedBase = new URL("https://docs.local");
|
|
1702
|
+
if (baseUrl) try {
|
|
1703
|
+
const candidate = new URL(baseUrl);
|
|
1704
|
+
if (candidate.protocol === "http:" || candidate.protocol === "https:") parsedBase = candidate;
|
|
1705
|
+
} catch {}
|
|
1706
|
+
const parsed = new URL(value, parsedBase);
|
|
1707
|
+
isHttpReference = parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
1708
|
+
} catch {}
|
|
1709
|
+
if (value && (value.startsWith("/") || isHttpReference)) values.push(value);
|
|
1710
|
+
}
|
|
1711
|
+
return uniqueAnswerCitations(values, baseUrl);
|
|
1712
|
+
}
|
|
1713
|
+
function buildAnswerMetrics(task, answer, baseUrl) {
|
|
1714
|
+
const expectation = task.expect.answer;
|
|
1715
|
+
if (!expectation) return {
|
|
1716
|
+
evidence: "answer",
|
|
1717
|
+
expected: false,
|
|
1718
|
+
provided: Boolean(answer),
|
|
1719
|
+
textIncludesMissing: [],
|
|
1720
|
+
textExcludesPresent: [],
|
|
1721
|
+
citations: answer ? extractAnswerCitations(answer, baseUrl) : [],
|
|
1722
|
+
requiredCitations: [],
|
|
1723
|
+
missingCitations: [],
|
|
1724
|
+
unexpectedCitations: [],
|
|
1725
|
+
forbiddenCitations: [],
|
|
1726
|
+
passed: true
|
|
1727
|
+
};
|
|
1728
|
+
const text = answer?.text ?? "";
|
|
1729
|
+
const citations = answer ? extractAnswerCitations(answer, baseUrl) : [];
|
|
1730
|
+
const requiredCitations = uniqueAnswerCitations(expectation.requiredCitations ?? task.expect.requiredCitations ?? task.expect.relevantSources, baseUrl);
|
|
1731
|
+
const allowed = uniqueAnswerCitations([
|
|
1732
|
+
...task.expect.relevantSources,
|
|
1733
|
+
...task.expect.requiredCitations ?? [],
|
|
1734
|
+
...task.expect.allowedSources ?? [],
|
|
1735
|
+
...requiredCitations,
|
|
1736
|
+
...expectation.allowedCitations ?? []
|
|
1737
|
+
], baseUrl);
|
|
1738
|
+
const forbiddenExpected = uniqueAnswerCitations([...task.expect.forbiddenSources ?? [], ...expectation.forbiddenCitations ?? []], baseUrl);
|
|
1739
|
+
const textIncludesMissing = (expectation.includes ?? []).filter((fragment) => !text.includes(fragment));
|
|
1740
|
+
const textExcludesPresent = (expectation.excludes ?? []).filter((fragment) => text.includes(fragment));
|
|
1741
|
+
const missingCitations = requiredCitations.filter((expected) => !citations.some((citation) => answerCitationMatches(citation, expected)));
|
|
1742
|
+
const unexpectedCitations = citations.filter((citation) => !allowed.some((allowedSource) => answerCitationMatches(citation, allowedSource)));
|
|
1743
|
+
const forbiddenCitations = citations.filter((citation) => forbiddenExpected.some((forbidden) => answerCitationMatches(citation, forbidden)));
|
|
1744
|
+
return {
|
|
1745
|
+
evidence: "answer",
|
|
1746
|
+
expected: true,
|
|
1747
|
+
provided: Boolean(answer),
|
|
1748
|
+
textIncludesMissing,
|
|
1749
|
+
textExcludesPresent,
|
|
1750
|
+
citations,
|
|
1751
|
+
requiredCitations,
|
|
1752
|
+
missingCitations,
|
|
1753
|
+
unexpectedCitations,
|
|
1754
|
+
forbiddenCitations,
|
|
1755
|
+
passed: Boolean(answer) && textIncludesMissing.length === 0 && textExcludesPresent.length === 0 && missingCitations.length === 0 && unexpectedCitations.length === 0 && forbiddenCitations.length === 0
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
function normalizeAnswerResult(value) {
|
|
1759
|
+
if (!isRecord(value) || typeof value.text !== "string" || !value.text.trim()) throw new Error("The answer runner must return { text: string, citations?: string[] }.");
|
|
1760
|
+
if (value.citations !== void 0 && (!Array.isArray(value.citations) || value.citations.some((citation) => typeof citation !== "string" || !citation.trim()))) throw new Error("The answer runner citations field must contain only non-empty strings.");
|
|
1761
|
+
return {
|
|
1762
|
+
text: value.text,
|
|
1763
|
+
citations: Array.isArray(value.citations) ? value.citations : void 0
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
async function readResponseTextWithLimit(response, maxBytes, controller) {
|
|
1767
|
+
if (!response.body) return "";
|
|
1768
|
+
const reader = response.body.getReader();
|
|
1769
|
+
const decoder = new TextDecoder();
|
|
1770
|
+
const chunks = [];
|
|
1771
|
+
let bytes = 0;
|
|
1772
|
+
try {
|
|
1773
|
+
while (true) {
|
|
1774
|
+
const { done, value } = await reader.read();
|
|
1775
|
+
if (done) break;
|
|
1776
|
+
bytes += value.byteLength;
|
|
1777
|
+
if (bytes > maxBytes) {
|
|
1778
|
+
controller.abort();
|
|
1779
|
+
await reader.cancel().catch(() => void 0);
|
|
1780
|
+
throw new Error("The HTTP answer provider response exceeds the 1 MB limit.");
|
|
1781
|
+
}
|
|
1782
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
1783
|
+
}
|
|
1784
|
+
chunks.push(decoder.decode());
|
|
1785
|
+
return chunks.join("");
|
|
1786
|
+
} finally {
|
|
1787
|
+
reader.releaseLock();
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
async function runAnswerProvider(options) {
|
|
1791
|
+
if (!isRecord(options.provider)) throw new Error("The answer provider must be a callback or HTTP provider object.");
|
|
1792
|
+
const provider = options.provider.provider;
|
|
1793
|
+
if (provider !== "callback" && provider !== "http") throw new Error("The answer provider must set provider to \"callback\" or \"http\".");
|
|
1794
|
+
const timeoutValue = options.provider.timeoutMs;
|
|
1795
|
+
if (timeoutValue !== void 0 && (typeof timeoutValue !== "number" || !Number.isFinite(timeoutValue) || timeoutValue <= 0)) throw new Error("The answer provider timeoutMs must be a positive finite number.");
|
|
1796
|
+
const timeoutMs = normalizePositiveInteger(typeof timeoutValue === "number" ? timeoutValue : void 0, 3e4, 3e5);
|
|
1797
|
+
const controller = new AbortController();
|
|
1798
|
+
let timeout;
|
|
1799
|
+
let timeoutError;
|
|
1800
|
+
const timedOut = new Promise((_, reject) => {
|
|
1801
|
+
timeout = setTimeout(() => {
|
|
1802
|
+
timeoutError = /* @__PURE__ */ new Error(`The ${provider} answer provider timed out after ${timeoutMs}ms.`);
|
|
1803
|
+
controller.abort();
|
|
1804
|
+
reject(timeoutError);
|
|
1805
|
+
}, timeoutMs);
|
|
1806
|
+
});
|
|
1807
|
+
if (provider === "callback") {
|
|
1808
|
+
if (typeof options.provider.run !== "function") {
|
|
1809
|
+
if (timeout) clearTimeout(timeout);
|
|
1810
|
+
throw new Error("The callback answer provider requires a callable run function.");
|
|
1811
|
+
}
|
|
1812
|
+
try {
|
|
1813
|
+
return normalizeAnswerResult(await Promise.race([Promise.resolve(options.provider.run({
|
|
1814
|
+
...options.input,
|
|
1815
|
+
signal: controller.signal
|
|
1816
|
+
})), timedOut]).catch((error) => {
|
|
1817
|
+
if (timeoutError) throw timeoutError;
|
|
1818
|
+
throw error;
|
|
1819
|
+
}));
|
|
1820
|
+
} finally {
|
|
1821
|
+
if (timeout) clearTimeout(timeout);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (!options.allowNetwork) {
|
|
1825
|
+
if (timeout) clearTimeout(timeout);
|
|
1826
|
+
throw new Error("The HTTP answer provider requires agent.evaluations.allowNetwork: true for evaluation.");
|
|
1827
|
+
}
|
|
1828
|
+
if (typeof options.provider.endpoint !== "string" || !options.provider.endpoint.trim()) {
|
|
1829
|
+
if (timeout) clearTimeout(timeout);
|
|
1830
|
+
throw new Error("The HTTP answer provider requires a non-empty endpoint.");
|
|
1831
|
+
}
|
|
1832
|
+
let endpoint;
|
|
1833
|
+
try {
|
|
1834
|
+
endpoint = new URL(options.provider.endpoint);
|
|
1835
|
+
} catch {
|
|
1836
|
+
if (timeout) clearTimeout(timeout);
|
|
1837
|
+
throw new Error("The HTTP answer provider endpoint must be an absolute HTTP(S) URL.");
|
|
1838
|
+
}
|
|
1839
|
+
if (endpoint.protocol !== "http:" && endpoint.protocol !== "https:") {
|
|
1840
|
+
if (timeout) clearTimeout(timeout);
|
|
1841
|
+
throw new Error("The HTTP answer provider endpoint must use HTTP or HTTPS.");
|
|
1842
|
+
}
|
|
1843
|
+
const rawHeaders = options.provider.headers;
|
|
1844
|
+
if (rawHeaders !== void 0 && (!isRecord(rawHeaders) || Object.values(rawHeaders).some((value) => typeof value !== "string"))) {
|
|
1845
|
+
if (timeout) clearTimeout(timeout);
|
|
1846
|
+
throw new Error("The HTTP answer provider headers must contain only string values.");
|
|
1847
|
+
}
|
|
1848
|
+
try {
|
|
1849
|
+
const headers = new Headers(rawHeaders);
|
|
1850
|
+
headers.set("content-type", "application/json");
|
|
1851
|
+
return await Promise.race([(async () => {
|
|
1852
|
+
const response = await fetch(endpoint, {
|
|
1853
|
+
method: "POST",
|
|
1854
|
+
headers,
|
|
1855
|
+
body: JSON.stringify(options.input),
|
|
1856
|
+
signal: controller.signal
|
|
1857
|
+
});
|
|
1858
|
+
if (!response.ok) throw new Error(`The HTTP answer provider returned ${response.status}.`);
|
|
1859
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
1860
|
+
const maxResponseBytes = 1e6;
|
|
1861
|
+
if (Number.isFinite(contentLength) && contentLength > maxResponseBytes) throw new Error("The HTTP answer provider response exceeds the 1 MB limit.");
|
|
1862
|
+
const responseText = await readResponseTextWithLimit(response, maxResponseBytes, controller);
|
|
1863
|
+
let payload;
|
|
1864
|
+
try {
|
|
1865
|
+
payload = JSON.parse(responseText);
|
|
1866
|
+
} catch {
|
|
1867
|
+
throw new Error("The HTTP answer provider must return valid JSON.");
|
|
1868
|
+
}
|
|
1869
|
+
return normalizeAnswerResult(payload);
|
|
1870
|
+
})(), timedOut]).catch((error) => {
|
|
1871
|
+
if (timeoutError) throw timeoutError;
|
|
1872
|
+
throw error;
|
|
1873
|
+
});
|
|
1874
|
+
} finally {
|
|
1875
|
+
if (timeout) clearTimeout(timeout);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
function buildSelectionMetrics(task, sources, pagesByUrl) {
|
|
1879
|
+
const requestedFramework = task.filters?.framework;
|
|
1880
|
+
const requestedVersion = task.filters?.version;
|
|
1881
|
+
const requestedLocale = task.filters?.locale;
|
|
1882
|
+
const expectedFramework = task.expect.scope?.framework ?? requestedFramework;
|
|
1883
|
+
const expectedVersion = task.expect.scope?.version ?? requestedVersion;
|
|
1884
|
+
const expectedLocale = task.expect.scope?.locale ?? requestedLocale;
|
|
1885
|
+
const framework = expectedFramework ? normalizeAgentFramework(expectedFramework) : void 0;
|
|
1886
|
+
const locale = expectedLocale ? normalizeAgentLocale(expectedLocale) : void 0;
|
|
1887
|
+
const candidates = sources.map((source) => {
|
|
1888
|
+
const page = findPageForSource(pagesByUrl, source.pageUrl || source.url);
|
|
1889
|
+
const scope = page ? getPageScope(page) : {
|
|
1890
|
+
framework: normalizeAgentScopeValues(source.framework).map(normalizeAgentFramework),
|
|
1891
|
+
version: normalizeAgentScopeValues(source.version).map(normalizeAgentVersion),
|
|
1892
|
+
locale: normalizeAgentScopeValues(source.locale).map(normalizeAgentLocale),
|
|
1893
|
+
frameworkAmbiguous: false,
|
|
1894
|
+
versionAmbiguous: false
|
|
1895
|
+
};
|
|
1896
|
+
return {
|
|
1897
|
+
source: canonicalizeSource(source.url),
|
|
1898
|
+
scope
|
|
1899
|
+
};
|
|
1900
|
+
});
|
|
1901
|
+
const firstFrameworkMatchRank = framework ? candidates.findIndex((candidate) => candidate.scope.framework.includes(framework)) + 1 || null : null;
|
|
1902
|
+
const firstVersionMatchRank = expectedVersion ? candidates.findIndex((candidate) => candidate.scope.version.some((version) => agentVersionConstraintMatches(expectedVersion, version))) + 1 || null : null;
|
|
1903
|
+
const firstLocaleMatchRank = locale ? candidates.findIndex((candidate) => candidate.scope.locale.includes(locale)) + 1 || null : null;
|
|
1904
|
+
const conflictingSources = candidates.filter((candidate) => framework && candidate.scope.framework.length > 0 && !candidate.scope.framework.includes(framework) || expectedVersion && candidate.scope.version.length > 0 && !candidate.scope.version.some((version) => agentVersionConstraintMatches(expectedVersion, version)) || locale && candidate.scope.locale.length > 0 && !candidate.scope.locale.includes(locale)).map((candidate) => candidate.source);
|
|
1905
|
+
const ambiguousSources = candidates.filter((candidate) => candidate.scope.frameworkAmbiguous || candidate.scope.versionAmbiguous || Boolean(framework && candidate.scope.framework.length === 0) || Boolean(expectedVersion && candidate.scope.version.length === 0) || Boolean(locale && candidate.scope.locale.length === 0)).map((candidate) => candidate.source);
|
|
1906
|
+
return {
|
|
1907
|
+
requestedFramework,
|
|
1908
|
+
requestedVersion,
|
|
1909
|
+
requestedLocale,
|
|
1910
|
+
expectedFramework,
|
|
1911
|
+
expectedVersion,
|
|
1912
|
+
expectedLocale,
|
|
1913
|
+
firstFrameworkMatchRank,
|
|
1914
|
+
firstVersionMatchRank,
|
|
1915
|
+
firstLocaleMatchRank,
|
|
1916
|
+
conflictingSources,
|
|
1917
|
+
ambiguousSources,
|
|
1918
|
+
passed: (!framework || firstFrameworkMatchRank !== null) && (!expectedVersion || firstVersionMatchRank !== null) && (!locale || firstLocaleMatchRank !== null) && conflictingSources.length === 0 && ambiguousSources.length === 0
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
function buildUsageMetrics(task, tokenBudget, context, relevantSources, attributionUtf8Bytes, baseUrl) {
|
|
1922
|
+
const usedUtf8Bytes = context.budget.usedUtf8Bytes;
|
|
1923
|
+
const rendered = extractRenderedCitations(context.context, context.sources);
|
|
1924
|
+
const blockUtf8Bytes = attributionUtf8Bytes ?? (rendered.layoutIntegrity ? rendered.blockUtf8Bytes : []);
|
|
1925
|
+
const usefulUtf8Bytes = context.sources.reduce((total, source, index) => relevantSources.some((expected) => sourceMatches(source.url, expected, baseUrl)) ? total + (blockUtf8Bytes[index] ?? 0) : total, 0);
|
|
1926
|
+
const usefulByteRatio = usedUtf8Bytes > 0 ? usefulUtf8Bytes / usedUtf8Bytes : 0;
|
|
1927
|
+
const minimumUsefulRatio = clamp(task.expect.minUsefulByteRatio ?? 0, 0, 1);
|
|
1928
|
+
const withinBudget = usedUtf8Bytes <= tokenBudget;
|
|
1929
|
+
return {
|
|
1930
|
+
budgetUnit: "utf8-bytes",
|
|
1931
|
+
tokenBudget,
|
|
1932
|
+
usedUtf8Bytes,
|
|
1933
|
+
remainingUtf8Bytes: context.budget.remainingUtf8Bytes,
|
|
1934
|
+
estimatedTokens: Math.ceil(usedUtf8Bytes / 4),
|
|
1935
|
+
conservativeTokenUpperBound: usedUtf8Bytes,
|
|
1936
|
+
usefulUtf8Bytes,
|
|
1937
|
+
usefulByteRatio: round(usefulByteRatio),
|
|
1938
|
+
truncated: context.budget.truncated,
|
|
1939
|
+
withinBudget,
|
|
1940
|
+
passed: withinBudget && usefulByteRatio >= minimumUsefulRatio
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
function calculateTaskScore(report) {
|
|
1944
|
+
const dimensions = [
|
|
1945
|
+
{
|
|
1946
|
+
weight: 30,
|
|
1947
|
+
score: report.retrieval.forbiddenSources.length > 0 ? 0 : (report.retrieval.recallAtK + report.retrieval.reciprocalRank) / 2
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
weight: 25,
|
|
1951
|
+
score: report.citations.integrity ? (report.citations.precision + report.citations.recall) / 2 : 0
|
|
1952
|
+
},
|
|
1953
|
+
...report.hasSelectionExpectation ? [{
|
|
1954
|
+
weight: 20,
|
|
1955
|
+
score: report.selection.passed ? 1 : 0
|
|
1956
|
+
}] : [],
|
|
1957
|
+
...report.hasAnswerExpectation ? [{
|
|
1958
|
+
weight: 25,
|
|
1959
|
+
score: report.answer.passed ? 1 : 0
|
|
1960
|
+
}] : [],
|
|
1961
|
+
...report.hasExampleExpectation ? [{
|
|
1962
|
+
weight: 15,
|
|
1963
|
+
score: report.examples.expected > 0 ? report.examples.matched / report.examples.expected : 1
|
|
1964
|
+
}] : [],
|
|
1965
|
+
{
|
|
1966
|
+
weight: 10,
|
|
1967
|
+
score: report.usage.passed ? 1 : 0
|
|
1968
|
+
}
|
|
1969
|
+
];
|
|
1970
|
+
const totalWeight = dimensions.reduce((sum, dimension) => sum + dimension.weight, 0);
|
|
1971
|
+
return round(dimensions.reduce((sum, dimension) => sum + dimension.weight * dimension.score, 0) / totalWeight * 100);
|
|
1972
|
+
}
|
|
1973
|
+
async function evaluateTask(pages, task, configurationIssues = [], runOptions = {}) {
|
|
1974
|
+
const topK = normalizePositiveInteger(task.topK, DEFAULT_TOP_K, 50);
|
|
1975
|
+
const tokenBudget = normalizePositiveInteger(task.tokenBudget, DEFAULT_TOKEN_BUDGET, 1e6);
|
|
1976
|
+
const relevantSources = uniqueCanonicalSources(task.expect.relevantSources, runOptions.baseUrl);
|
|
1977
|
+
const orderedPages = pages.slice().sort((left, right) => compareCodePoints(canonicalizeSource(left.url), canonicalizeSource(right.url)));
|
|
1978
|
+
const pagesByUrl = new Map(orderedPages.map((page) => [sourcePage(page.url), page]));
|
|
1979
|
+
const requestedSurface = task.surface ?? runOptions.surface ?? "mcp-context";
|
|
1980
|
+
let surfaceResult;
|
|
1981
|
+
let surfaceError;
|
|
1982
|
+
if (configurationIssues.length > 0) {
|
|
1983
|
+
const empty = createContextShell({
|
|
1984
|
+
query: task.query,
|
|
1985
|
+
filters: task.filters,
|
|
1986
|
+
tokenBudget
|
|
1987
|
+
});
|
|
1988
|
+
surfaceResult = {
|
|
1989
|
+
surface: requestedSurface === "configured-search" || requestedSurface === "ask-ai-context" ? requestedSurface : "mcp-context",
|
|
1990
|
+
provider: "unavailable",
|
|
1991
|
+
rankedContext: empty,
|
|
1992
|
+
budgetedContext: empty,
|
|
1993
|
+
citationEvidence: {
|
|
1994
|
+
mode: "context",
|
|
1995
|
+
actual: [],
|
|
1996
|
+
integrity: false
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
} else try {
|
|
2000
|
+
if (requestedSurface !== "mcp-context" && requestedSurface !== "configured-search" && requestedSurface !== "ask-ai-context") throw new Error(`Unknown evaluation surface: ${String(requestedSurface)}.`);
|
|
2001
|
+
surfaceResult = await buildGoldenSurface({
|
|
2002
|
+
pages: orderedPages,
|
|
2003
|
+
task,
|
|
2004
|
+
topK,
|
|
2005
|
+
tokenBudget,
|
|
2006
|
+
runOptions
|
|
2007
|
+
});
|
|
2008
|
+
} catch (error) {
|
|
2009
|
+
surfaceError = error instanceof Error ? error.message : String(error);
|
|
2010
|
+
const empty = createContextShell({
|
|
2011
|
+
query: task.query,
|
|
2012
|
+
filters: task.filters,
|
|
2013
|
+
tokenBudget
|
|
2014
|
+
});
|
|
2015
|
+
surfaceResult = {
|
|
2016
|
+
surface: requestedSurface === "configured-search" || requestedSurface === "ask-ai-context" ? requestedSurface : "mcp-context",
|
|
2017
|
+
provider: "unavailable",
|
|
2018
|
+
rankedContext: empty,
|
|
2019
|
+
budgetedContext: empty,
|
|
2020
|
+
citationEvidence: {
|
|
2021
|
+
mode: "context",
|
|
2022
|
+
actual: [],
|
|
2023
|
+
integrity: false
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
const { rankedContext, budgetedContext } = surfaceResult;
|
|
2028
|
+
const rankedSources = toGoldenSources(rankedContext.sources, relevantSources, runOptions.baseUrl);
|
|
2029
|
+
const budgetedSources = toGoldenSources(budgetedContext.sources, relevantSources, runOptions.baseUrl);
|
|
2030
|
+
const budgetedCandidates = toContextCandidates(budgetedContext.sources, pagesByUrl);
|
|
2031
|
+
const retrieval = buildRetrievalMetrics(task, rankedSources, topK, runOptions.baseUrl);
|
|
2032
|
+
const citationSources = surfaceResult.citationEvidence.mode === "results" ? rankedContext.sources : budgetedContext.sources;
|
|
2033
|
+
const citations = buildCitationMetrics(task, budgetedContext.context, citationSources, surfaceResult.citationEvidence, runOptions.baseUrl);
|
|
2034
|
+
const selection = buildSelectionMetrics(task, rankedContext.sources, pagesByUrl);
|
|
2035
|
+
const examples = await evaluateExamples(task.expect.examples ?? [], budgetedCandidates, runOptions);
|
|
2036
|
+
const usage = buildUsageMetrics(task, tokenBudget, budgetedContext, relevantSources, surfaceResult.attributionUtf8Bytes, runOptions.baseUrl);
|
|
2037
|
+
const issues = configurationIssues.map((issue) => `Invalid golden task configuration: ${issue}`);
|
|
2038
|
+
if (surfaceError) issues.push(`Evaluation surface failed: ${surfaceError}`);
|
|
2039
|
+
let answerResult;
|
|
2040
|
+
let answerError;
|
|
2041
|
+
if (configurationIssues.length === 0 && !surfaceError && task.expect.answer && runOptions.answer) try {
|
|
2042
|
+
answerResult = await runAnswerProvider({
|
|
2043
|
+
provider: runOptions.answer,
|
|
2044
|
+
allowNetwork: runOptions.allowNetwork === true,
|
|
2045
|
+
input: {
|
|
2046
|
+
task: {
|
|
2047
|
+
id: task.id,
|
|
2048
|
+
query: task.query,
|
|
2049
|
+
filters: task.filters
|
|
2050
|
+
},
|
|
2051
|
+
surface: surfaceResult.surface,
|
|
2052
|
+
context: budgetedContext.context,
|
|
2053
|
+
sources: budgetedContext.sources.map((source) => ({
|
|
2054
|
+
url: source.url,
|
|
2055
|
+
title: source.title,
|
|
2056
|
+
framework: source.framework,
|
|
2057
|
+
version: source.version,
|
|
2058
|
+
locale: source.locale
|
|
2059
|
+
}))
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
answerError = error instanceof Error ? error.message : String(error);
|
|
2064
|
+
issues.push(`Answer evaluation failed: ${answerError}`);
|
|
2065
|
+
}
|
|
2066
|
+
const answer = buildAnswerMetrics(task, answerResult, runOptions.baseUrl);
|
|
2067
|
+
if (relevantSources.length === 0) issues.push("No relevantSources are configured for this task.");
|
|
2068
|
+
if (!retrieval.passed) issues.push("Retrieval did not satisfy the expected recall, rank, or exclusion rules.");
|
|
2069
|
+
if (!citations.passed) issues.push(`${citations.evidence === "results" ? "Search-result" : "Context"} citations are missing, unexpected, or do not match source records.`);
|
|
2070
|
+
if (task.expect.answer && !runOptions.answer) issues.push("Answer expectations require agent.evaluations.answer to be configured.");
|
|
2071
|
+
if (!answer.passed) issues.push("The actual answer is missing required text/citations or contains invalid evidence.");
|
|
2072
|
+
if (!selection.passed) issues.push("Retrieved sources are ambiguous or do not explicitly match the expected framework/version/locale.");
|
|
2073
|
+
if (!examples.passed) issues.push("One or more expected examples did not meet the requested verification level.");
|
|
2074
|
+
if (!usage.passed) issues.push("Context exceeded its budget or did not meet the configured useful-byte ratio.");
|
|
2075
|
+
const passed = configurationIssues.length === 0 && !surfaceError && !answerError && relevantSources.length > 0 && retrieval.passed && citations.passed && answer.passed && selection.passed && examples.passed && usage.passed;
|
|
2076
|
+
const calculatedScore = configurationIssues.length > 0 ? 0 : calculateTaskScore({
|
|
2077
|
+
retrieval,
|
|
2078
|
+
citations,
|
|
2079
|
+
answer,
|
|
2080
|
+
selection,
|
|
2081
|
+
examples,
|
|
2082
|
+
usage,
|
|
2083
|
+
hasSelectionExpectation: Boolean(task.expect.scope?.framework || task.expect.scope?.version || task.expect.scope?.locale || task.filters?.framework || task.filters?.version || task.filters?.locale),
|
|
2084
|
+
hasExampleExpectation: (task.expect.examples?.length ?? 0) > 0,
|
|
2085
|
+
hasAnswerExpectation: Boolean(task.expect.answer)
|
|
2086
|
+
});
|
|
2087
|
+
const score = passed ? calculatedScore : Math.min(calculatedScore, 99);
|
|
2088
|
+
return {
|
|
2089
|
+
id: task.id,
|
|
2090
|
+
query: task.query,
|
|
2091
|
+
surface: surfaceResult.surface,
|
|
2092
|
+
provider: surfaceResult.provider,
|
|
2093
|
+
status: passed ? "passed" : "failed",
|
|
2094
|
+
passed,
|
|
2095
|
+
score,
|
|
2096
|
+
context: budgetedContext.context,
|
|
2097
|
+
sources: budgetedSources,
|
|
2098
|
+
retrieval,
|
|
2099
|
+
citations,
|
|
2100
|
+
answer,
|
|
2101
|
+
selection,
|
|
2102
|
+
examples,
|
|
2103
|
+
usage,
|
|
2104
|
+
issues
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Run offline-by-default golden-task evaluations against MCP-ready docs pages.
|
|
2109
|
+
* Configured external retrieval, HTTP answers, and runtime execution require explicit opt-in.
|
|
2110
|
+
* An empty task list is intentionally unmeasured so CI cannot turn absent coverage into a pass.
|
|
2111
|
+
*/
|
|
2112
|
+
async function runDocsGoldenTasks(pages, tasks, options = {}) {
|
|
2113
|
+
const runtimeTasks = tasks;
|
|
2114
|
+
if (runtimeTasks === void 0 || Array.isArray(runtimeTasks) && runtimeTasks.length === 0) return {
|
|
2115
|
+
status: "unmeasured",
|
|
2116
|
+
passed: null,
|
|
2117
|
+
score: null,
|
|
2118
|
+
taskCount: 0,
|
|
2119
|
+
passedTaskCount: 0,
|
|
2120
|
+
failedTaskCount: 0,
|
|
2121
|
+
tasks: []
|
|
2122
|
+
};
|
|
2123
|
+
const normalizedTasks = Array.isArray(runtimeTasks) ? runtimeTasks.map((task, index) => normalizeGoldenTaskInput(task, index)) : [normalizeGoldenTaskInput(runtimeTasks, 0, ["agent.evaluations.tasks must be an array."])];
|
|
2124
|
+
const idCounts = normalizedTasks.reduce((counts, input) => {
|
|
2125
|
+
counts.set(input.task.id, (counts.get(input.task.id) ?? 0) + 1);
|
|
2126
|
+
return counts;
|
|
2127
|
+
}, /* @__PURE__ */ new Map());
|
|
2128
|
+
for (const input of normalizedTasks) if ((idCounts.get(input.task.id) ?? 0) > 1) input.issues.push(`task id ${JSON.stringify(input.task.id)} is duplicated.`);
|
|
2129
|
+
const reports = await Promise.all(normalizedTasks.map(({ task, issues }) => evaluateTask(pages, task, issues, options)));
|
|
2130
|
+
const passedTaskCount = reports.filter((task) => task.passed).length;
|
|
2131
|
+
const failedTaskCount = reports.length - passedTaskCount;
|
|
2132
|
+
return {
|
|
2133
|
+
status: failedTaskCount === 0 ? "passed" : "failed",
|
|
2134
|
+
passed: failedTaskCount === 0,
|
|
2135
|
+
score: round(reports.reduce((sum, task) => sum + task.score, 0) / reports.length),
|
|
2136
|
+
taskCount: reports.length,
|
|
2137
|
+
passedTaskCount,
|
|
2138
|
+
failedTaskCount,
|
|
2139
|
+
tasks: reports
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
//#endregion
|
|
2144
|
+
export { buildDocsReviewWorkflowPathFilters as a, resolveDocsReviewConfig as c, buildDocsReviewWorkflow as i, DEFAULT_DOCS_REVIEW_SCORE_THRESHOLD as n, ensureDocsReviewWorkflow as o, DEFAULT_DOCS_REVIEW_WORKFLOW_PATH as r, readDocsReviewConfigFromSource as s, runDocsGoldenTasks as t };
|