chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 11:58:09 +08:00
commit c48e26cdd0
2909 changed files with 1591131 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import prettier from 'prettier';
export async function formatWithPrettier(content: string, filePath: string) {
const options = await prettier.resolveConfig(filePath);
return prettier.format(content, {
...options,
filepath: filePath,
});
}
export function normalizeForCompare(content: string): string {
return content.replace(/\r\n/g, '\n').trimEnd();
}
export function escapeBackticks(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
}
export interface FormatDefaultValueOptions {
/**
* When true, string values are JSON-stringified, including surrounding quotes.
* Defaults to false to return raw string content.
*/
quoteStrings?: boolean;
}
export function formatDefaultValue(
value: unknown,
options: FormatDefaultValueOptions = {},
): string {
const { quoteStrings = false } = options;
if (value === undefined) {
return 'undefined';
}
if (value === null) {
return 'null';
}
if (typeof value === 'string') {
return quoteStrings ? JSON.stringify(value) : value;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
if (Array.isArray(value)) {
if (value.length === 0) {
return '[]';
}
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
if (typeof value === 'object') {
try {
const json = JSON.stringify(value, null, 2);
if (json === '{}') {
return '{}';
}
return json;
} catch {
return '[object Object]';
}
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
interface MarkerInsertionOptions {
document: string;
startMarker: string;
endMarker: string;
newContent: string;
paddingBefore?: string;
paddingAfter?: string;
}
/**
* Replaces the content between two markers with `newContent`, preserving the
* original document outside the markers and applying optional padding.
*/
export function injectBetweenMarkers({
document,
startMarker,
endMarker,
newContent,
paddingBefore = '\n',
paddingAfter = '\n',
}: MarkerInsertionOptions): string {
const startIndex = document.indexOf(startMarker);
const endIndex = document.indexOf(endMarker);
if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
throw new Error(
`Could not locate documentation markers (${startMarker}, ${endMarker}).`,
);
}
const before = document.slice(0, startIndex + startMarker.length);
const after = document.slice(endIndex);
return `${before}${paddingBefore}${newContent}${paddingAfter}${after}`;
}
+680
View File
@@ -0,0 +1,680 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import * as ts from 'typescript';
import {
ALL_BUILTIN_TOOL_NAMES,
isValidToolName,
} from '@google/gemini-cli-core';
import { buildToolRegistry } from './tool-registry.js';
export const BASE_EVAL_HELPERS = [
'evalTest',
'appEvalTest',
'componentEvalTest',
] as const;
export type BaseEvalHelper = (typeof BASE_EVAL_HELPERS)[number];
export type EvalHelperName = BaseEvalHelper | string;
export type EvalPolicy =
| 'ALWAYS_PASSES'
| 'USUALLY_PASSES'
| 'USUALLY_FAILS'
| 'unknown';
export interface EvalSourceLocation {
line: number;
column: number;
}
export interface EvalAnalysisDiagnostic {
severity: 'warning';
message: string;
filePath: string;
location: EvalSourceLocation;
}
export interface EvalCaseRecord {
filePath: string;
relativePath: string;
helperName: EvalHelperName;
baseHelperName: BaseEvalHelper | 'unknown';
policy: EvalPolicy;
name: string;
suiteName?: string;
suiteType?: string;
timeout?: number;
hasFiles: boolean;
hasPrompt: boolean;
toolReferences: readonly string[];
location: EvalSourceLocation;
}
export interface EvalFileAnalysis {
filePath: string;
relativePath: string;
helpers: Record<string, BaseEvalHelper | 'unknown'>;
cases: readonly EvalCaseRecord[];
toolReferences: readonly string[];
diagnostics: readonly EvalAnalysisDiagnostic[];
}
export interface AnalyzeEvalSourceOptions {
filePath?: string;
repoRoot?: string;
}
export function analyzeEvalSource(
sourceText: string,
options: AnalyzeEvalSourceOptions = {},
): EvalFileAnalysis {
const filePath = options.filePath ?? '<inline>';
const relativePath = getRelativePath(filePath, options.repoRoot);
const sourceFile = ts.createSourceFile(
filePath,
sourceText,
ts.ScriptTarget.Latest,
true,
getScriptKind(filePath),
);
const helpers = collectHelperMappings(sourceFile);
const importedConstants = collectImportedToolNameConstants(sourceFile);
const diagnostics: EvalAnalysisDiagnostic[] = [];
const cases: EvalCaseRecord[] = [];
collectEvalCalls(sourceFile, helpers, (callExpression, helperName) => {
const args = callExpression.arguments;
const policyArg = args[0];
const evalCaseArg = args[1];
const policy = policyArg ? getStringLiteralValue(policyArg) : undefined;
const evalCase =
evalCaseArg && ts.isObjectLiteralExpression(evalCaseArg)
? evalCaseArg
: undefined;
if (!policy || !isEvalPolicy(policy)) {
diagnostics.push({
severity: 'warning',
message: `Could not statically resolve policy for ${helperName} call.`,
filePath,
location: getLocation(sourceFile, policyArg ?? callExpression),
});
}
if (!evalCase) {
diagnostics.push({
severity: 'warning',
message: `Could not statically resolve eval case object for ${helperName} call.`,
filePath,
location: getLocation(sourceFile, evalCaseArg ?? callExpression),
});
return;
}
const name = getStaticStringProperty(evalCase, 'name');
if (!name) {
diagnostics.push({
severity: 'warning',
message: `Could not statically resolve eval case name for ${helperName} call.`,
filePath,
location: getLocation(sourceFile, evalCase),
});
}
const assertProp = getPropertyAssignment(evalCase, 'assert');
const assertBody = assertProp
? getFunctionBody(assertProp.initializer)
: undefined;
const toolRefsInfo = assertBody
? collectToolReferences(assertBody, importedConstants)
: [];
const toolRefs: string[] = [];
const registry = buildToolRegistry();
for (const { name: resolvedName, node } of toolRefsInfo) {
const canonicalName = registry.aliasLookup.get(resolvedName);
if (!canonicalName && !isValidToolName(resolvedName)) {
diagnostics.push({
severity: 'warning',
message: `Unrecognized tool name extracted: "${resolvedName}"`,
filePath,
location: getLocation(sourceFile, node),
});
}
toolRefs.push(canonicalName ?? resolvedName);
}
cases.push({
filePath,
relativePath,
helperName,
baseHelperName: helpers[helperName] ?? 'unknown',
policy: isEvalPolicy(policy) ? policy : 'unknown',
name: name ?? '<unknown>',
suiteName: getStaticStringProperty(evalCase, 'suiteName'),
suiteType: getStaticStringProperty(evalCase, 'suiteType'),
timeout: getStaticNumberProperty(evalCase, 'timeout'),
hasFiles: hasProperty(evalCase, 'files'),
hasPrompt: hasProperty(evalCase, 'prompt'),
toolReferences: Object.freeze([...new Set(toolRefs)].sort()),
location: getLocation(sourceFile, callExpression),
});
});
cases.sort(compareEvalCases);
const fileToolRefs = [
...new Set(cases.flatMap((c) => [...c.toolReferences])),
].sort();
return {
filePath,
relativePath,
helpers,
cases,
toolReferences: Object.freeze(fileToolRefs),
diagnostics: diagnostics.sort(compareDiagnostics),
};
}
function collectHelperMappings(
sourceFile: ts.SourceFile,
): Record<string, BaseEvalHelper | 'unknown'> {
const helpers: Record<string, BaseEvalHelper | 'unknown'> = {};
for (const helper of BASE_EVAL_HELPERS) {
helpers[helper] = helper;
}
for (const alias of collectImportedHelperAliases(sourceFile)) {
helpers[alias.name] = alias.baseHelper;
}
let changed = true;
while (changed) {
changed = false;
const visit = (node: ts.Node) => {
const name = getFunctionLikeBindingName(node);
if (name && !helpers[name]) {
const functionNode = getFunctionLikeNode(node);
if (functionNode) {
const baseHelper = findCalledHelper(functionNode, helpers);
if (
baseHelper &&
helpers[baseHelper] &&
helpers[baseHelper] !== 'unknown'
) {
helpers[name] = helpers[baseHelper];
changed = true;
}
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}
return helpers;
}
function collectImportedHelperAliases(sourceFile: ts.SourceFile) {
const aliases: Array<{ name: string; baseHelper: BaseEvalHelper }> = [];
for (const statement of sourceFile.statements) {
if (
!ts.isImportDeclaration(statement) ||
!statement.importClause?.namedBindings ||
!ts.isNamedImports(statement.importClause.namedBindings)
) {
continue;
}
for (const element of statement.importClause.namedBindings.elements) {
const importedName = element.propertyName?.text ?? element.name.text;
if (isBaseEvalHelper(importedName)) {
aliases.push({
name: element.name.text,
baseHelper: importedName,
});
}
}
}
return aliases;
}
function collectEvalCalls(
sourceFile: ts.SourceFile,
helpers: Record<string, BaseEvalHelper | 'unknown'>,
onCall: (callExpression: ts.CallExpression, helperName: string) => void,
) {
const visit = (node: ts.Node) => {
const wrapperName = getFunctionLikeBindingName(node);
if (wrapperName && helpers[wrapperName] && !isBaseEvalHelper(wrapperName)) {
return;
}
if (ts.isCallExpression(node)) {
const helperName = getCalledIdentifierName(node);
if (helperName && helpers[helperName]) {
onCall(node, helperName);
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}
function findCalledHelper(
functionNode: ts.Node,
helpers: Record<string, BaseEvalHelper | 'unknown'>,
): string | undefined {
let found: string | undefined;
const visit = (candidate: ts.Node) => {
if (found) {
return;
}
if (
candidate !== functionNode &&
(ts.isFunctionDeclaration(candidate) ||
ts.isFunctionExpression(candidate) ||
ts.isArrowFunction(candidate) ||
ts.isMethodDeclaration(candidate))
) {
return;
}
if (ts.isCallExpression(candidate)) {
const helperName = getCalledIdentifierName(candidate);
if (helperName && helpers[helperName]) {
found = helperName;
return;
}
}
ts.forEachChild(candidate, visit);
};
ts.forEachChild(functionNode, visit);
return found;
}
function getFunctionLikeBindingName(node: ts.Node) {
if (ts.isFunctionDeclaration(node) && node.name) {
return node.name.text;
}
if (ts.isVariableDeclaration(node)) {
if (
ts.isIdentifier(node.name) &&
node.initializer &&
(ts.isArrowFunction(node.initializer) ||
ts.isFunctionExpression(node.initializer))
) {
return node.name.text;
}
}
return undefined;
}
function getFunctionLikeNode(node: ts.Node) {
if (ts.isFunctionDeclaration(node)) {
return node;
}
if (
ts.isVariableDeclaration(node) &&
node.initializer &&
(ts.isArrowFunction(node.initializer) ||
ts.isFunctionExpression(node.initializer))
) {
return node.initializer;
}
return undefined;
}
function getCalledIdentifierName(callExpression: ts.CallExpression) {
return ts.isIdentifier(callExpression.expression)
? callExpression.expression.text
: undefined;
}
function isBaseEvalHelper(name: string): name is BaseEvalHelper {
return BASE_EVAL_HELPERS.includes(name as BaseEvalHelper);
}
function isEvalPolicy(policy: string | undefined): policy is EvalPolicy {
return (
policy === 'ALWAYS_PASSES' ||
policy === 'USUALLY_PASSES' ||
policy === 'USUALLY_FAILS'
);
}
function hasProperty(objectLiteral: ts.ObjectLiteralExpression, name: string) {
return Boolean(getPropertyAssignment(objectLiteral, name));
}
function getStaticStringProperty(
objectLiteral: ts.ObjectLiteralExpression,
name: string,
) {
const assignment = getPropertyAssignment(objectLiteral, name);
return assignment ? getStringLiteralValue(assignment.initializer) : undefined;
}
function getStaticNumberProperty(
objectLiteral: ts.ObjectLiteralExpression,
name: string,
) {
const assignment = getPropertyAssignment(objectLiteral, name);
if (!assignment) {
return undefined;
}
const initializer = assignment.initializer;
return ts.isNumericLiteral(initializer)
? Number(initializer.text)
: undefined;
}
function getPropertyAssignment(
objectLiteral: ts.ObjectLiteralExpression,
name: string,
) {
return objectLiteral.properties.find((property) => {
if (!ts.isPropertyAssignment(property)) {
return false;
}
const propertyName = property.name;
return (
(ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName)) &&
propertyName.text === name
);
}) as ts.PropertyAssignment | undefined;
}
function getStringLiteralValue(expression: ts.Expression | undefined) {
if (!expression) {
return undefined;
}
if (
ts.isStringLiteral(expression) ||
ts.isNoSubstitutionTemplateLiteral(expression)
) {
return expression.text;
}
return undefined;
}
function getLocation(
sourceFile: ts.SourceFile,
node: ts.Node,
): EvalSourceLocation {
const location = sourceFile.getLineAndCharacterOfPosition(
node.getStart(sourceFile),
);
return {
line: location.line + 1,
column: location.character + 1,
};
}
function getRelativePath(filePath: string, repoRoot: string | undefined) {
if (filePath === '<inline>') {
return filePath;
}
const relativePath = repoRoot ? path.relative(repoRoot, filePath) : filePath;
return relativePath.replace(/\\/g, '/');
}
function getScriptKind(filePath: string) {
const extension = path.extname(filePath).toLowerCase();
switch (extension) {
case '.tsx':
return ts.ScriptKind.TSX;
case '.jsx':
return ts.ScriptKind.JSX;
case '.js':
case '.mjs':
case '.cjs':
return ts.ScriptKind.JS;
default:
return ts.ScriptKind.TS;
}
}
function compareEvalCases(left: EvalCaseRecord, right: EvalCaseRecord) {
return (
compareStrings(left.relativePath, right.relativePath) ||
left.location.line - right.location.line ||
left.location.column - right.location.column ||
compareStrings(left.name, right.name)
);
}
function compareDiagnostics(
left: EvalAnalysisDiagnostic,
right: EvalAnalysisDiagnostic,
) {
return (
compareStrings(left.filePath, right.filePath) ||
left.location.line - right.location.line ||
left.location.column - right.location.column ||
compareStrings(left.message, right.message)
);
}
function compareStrings(left: string, right: string) {
return left.localeCompare(right, 'en');
}
const TOOL_NAME_TO_CONSTANT: Record<
(typeof ALL_BUILTIN_TOOL_NAMES)[number],
keyof typeof import('@google/gemini-cli-core')
> = {
glob: 'GLOB_TOOL_NAME',
grep_search: 'GREP_TOOL_NAME',
list_directory: 'LS_TOOL_NAME',
read_file: 'READ_FILE_TOOL_NAME',
run_shell_command: 'SHELL_TOOL_NAME',
write_file: 'WRITE_FILE_TOOL_NAME',
replace: 'EDIT_TOOL_NAME',
google_web_search: 'WEB_SEARCH_TOOL_NAME',
write_todos: 'WRITE_TODOS_TOOL_NAME',
web_fetch: 'WEB_FETCH_TOOL_NAME',
read_many_files: 'READ_MANY_FILES_TOOL_NAME',
get_internal_docs: 'GET_INTERNAL_DOCS_TOOL_NAME',
activate_skill: 'ACTIVATE_SKILL_TOOL_NAME',
ask_user: 'ASK_USER_TOOL_NAME',
exit_plan_mode: 'EXIT_PLAN_MODE_TOOL_NAME',
enter_plan_mode: 'ENTER_PLAN_MODE_TOOL_NAME',
update_topic: 'UPDATE_TOPIC_TOOL_NAME',
complete_task: 'COMPLETE_TASK_TOOL_NAME',
read_mcp_resource: 'READ_MCP_RESOURCE_TOOL_NAME',
list_mcp_resources: 'LIST_MCP_RESOURCES_TOOL_NAME',
tracker_create_task: 'TRACKER_CREATE_TASK_TOOL_NAME',
tracker_update_task: 'TRACKER_UPDATE_TASK_TOOL_NAME',
tracker_get_task: 'TRACKER_GET_TASK_TOOL_NAME',
tracker_list_tasks: 'TRACKER_LIST_TASKS_TOOL_NAME',
tracker_add_dependency: 'TRACKER_ADD_DEPENDENCY_TOOL_NAME',
tracker_visualize: 'TRACKER_VISUALIZE_TOOL_NAME',
invoke_agent: 'AGENT_TOOL_NAME',
};
const WELL_KNOWN_TOOL_CONSTANTS: Record<
string,
(typeof ALL_BUILTIN_TOOL_NAMES)[number]
> = Object.fromEntries(
Object.entries(TOOL_NAME_TO_CONSTANT).map(([toolName, constantName]) => [
constantName,
toolName as (typeof ALL_BUILTIN_TOOL_NAMES)[number],
]),
);
function collectImportedToolNameConstants(
sourceFile: ts.SourceFile,
): Map<string, string> {
const constants = new Map<string, string>();
for (const statement of sourceFile.statements) {
if (
!ts.isImportDeclaration(statement) ||
!statement.importClause?.namedBindings ||
!ts.isNamedImports(statement.importClause.namedBindings) ||
!ts.isStringLiteral(statement.moduleSpecifier) ||
statement.moduleSpecifier.text !== '@google/gemini-cli-core'
) {
continue;
}
for (const element of statement.importClause.namedBindings.elements) {
const importedName = element.propertyName?.text ?? element.name.text;
const localName = element.name.text;
const resolvedValue = WELL_KNOWN_TOOL_CONSTANTS[importedName];
if (resolvedValue !== undefined) {
constants.set(localName, resolvedValue);
}
}
}
return constants;
}
function getFunctionBody(
node: ts.Expression,
): ts.ConciseBody | ts.Block | undefined {
if (ts.isArrowFunction(node)) {
return node.body;
}
if (ts.isFunctionExpression(node)) {
return node.body;
}
return undefined;
}
function collectToolReferences(
body: ts.ConciseBody | ts.Block,
importedConstants: Map<string, string>,
): { name: string; node: ts.Node }[] {
const refs: { name: string; node: ts.Node }[] = [];
const visit = (node: ts.Node) => {
if (ts.isCallExpression(node)) {
extractFromWaitForToolCall(node, importedConstants, refs);
extractFromArrayIncludes(node, importedConstants, refs);
} else if (
ts.isBinaryExpression(node) &&
node.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken
) {
extractFromToolRequestNameComparison(node, importedConstants, refs);
}
ts.forEachChild(node, visit);
};
visit(body);
return refs;
}
function extractFromWaitForToolCall(
call: ts.CallExpression,
importedConstants: Map<string, string>,
refs: { name: string; node: ts.Node }[],
) {
const expr = call.expression;
if (
!ts.isPropertyAccessExpression(expr) ||
expr.name.text !== 'waitForToolCall'
) {
return;
}
const firstArg = call.arguments[0];
if (!firstArg) {
return;
}
const resolved = resolveStringValue(firstArg, importedConstants);
if (resolved) {
refs.push({ name: resolved, node: firstArg });
}
}
function isToolRequestName(node: ts.Expression): boolean {
return (
ts.isPropertyAccessExpression(node) &&
node.name.text === 'name' &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'toolRequest'
);
}
function extractFromToolRequestNameComparison(
binary: ts.BinaryExpression,
importedConstants: Map<string, string>,
refs: { name: string; node: ts.Node }[],
) {
let valueNode: ts.Expression | undefined;
if (isToolRequestName(binary.left)) {
valueNode = binary.right;
} else if (isToolRequestName(binary.right)) {
valueNode = binary.left;
}
if (valueNode) {
const resolved = resolveStringValue(valueNode, importedConstants);
if (resolved) {
refs.push({ name: resolved, node: valueNode });
}
}
}
function extractFromArrayIncludes(
call: ts.CallExpression,
importedConstants: Map<string, string>,
refs: { name: string; node: ts.Node }[],
) {
const expr = call.expression;
if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== 'includes') {
return;
}
const firstArg = call.arguments[0];
if (!firstArg || !isToolRequestName(firstArg)) {
return;
}
const arrayExpr = expr.expression;
if (!ts.isArrayLiteralExpression(arrayExpr)) {
return;
}
for (const element of arrayExpr.elements) {
const resolved = resolveStringValue(element, importedConstants);
if (resolved) {
refs.push({ name: resolved, node: element });
}
}
}
function resolveStringValue(
node: ts.Expression,
importedConstants: Map<string, string>,
): string | undefined {
const literal = getStringLiteralValue(node);
if (literal !== undefined) {
return literal;
}
if (ts.isIdentifier(node)) {
return importedConstants.get(node.text);
}
return undefined;
}
+352
View File
@@ -0,0 +1,352 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import path from 'node:path';
import { glob } from 'glob';
import {
analyzeEvalSource,
type EvalCaseRecord,
type EvalFileAnalysis,
type EvalAnalysisDiagnostic,
type EvalPolicy,
} from './eval-analysis.js';
const POLICY_ORDER: EvalPolicy[] = [
'ALWAYS_PASSES',
'USUALLY_PASSES',
'USUALLY_FAILS',
'unknown',
];
export interface InventoryResult {
totalFiles: number;
totalCases: number;
repoRoot: string;
files: EvalFileAnalysis[];
cases: readonly EvalCaseRecord[];
diagnostics: readonly EvalAnalysisDiagnostic[];
}
/**
* Discovers all eval files under the given repo root and runs
* the static analyzer on each, returning the aggregated results.
*/
export async function collectInventory(
repoRoot: string,
): Promise<InventoryResult> {
const evalsDir = path.join(repoRoot, 'evals');
try {
const stat = await fs.promises.stat(evalsDir);
if (!stat.isDirectory()) {
throw new Error(`evals path exists but is not a directory: ${evalsDir}`);
}
} catch (err: unknown) {
if (isNodeError(err) && err.code === 'ENOENT') {
throw new Error(
`evals directory not found under repo root: ${evalsDir}\n` +
`Make sure --root points to the repository root.`,
);
}
throw err;
}
const pattern = '**/*.eval.{ts,tsx}';
const evalFiles = await glob(pattern, {
cwd: evalsDir,
absolute: true,
nodir: true,
});
evalFiles.sort();
const files: EvalFileAnalysis[] = [];
const allCases: EvalCaseRecord[] = [];
const allDiagnostics: EvalAnalysisDiagnostic[] = [];
for (const filePath of evalFiles) {
const sourceText = await fs.promises.readFile(filePath, 'utf-8');
const analysis = analyzeEvalSource(sourceText, { filePath, repoRoot });
files.push(analysis);
allCases.push(...analysis.cases);
allDiagnostics.push(...analysis.diagnostics);
}
return {
totalFiles: files.length,
totalCases: allCases.length,
repoRoot,
files,
cases: allCases,
diagnostics: allDiagnostics,
};
}
/**
* Formats an InventoryResult into a human-readable report string.
*/
export function formatInventoryReport(result: InventoryResult): string {
const lines: string[] = [];
lines.push('Eval Inventory');
lines.push('══════════════');
lines.push('');
lines.push(
`${result.totalFiles} files · ${result.totalCases} cases · ${result.diagnostics.length} diagnostics`,
);
lines.push('');
// --- By Policy ---
lines.push('By Policy');
lines.push('─────────');
const byPolicyMap = groupBy(result.cases, (c) => c.policy);
const renderedPolicies = new Set<string>();
for (const policy of POLICY_ORDER) {
const cases = byPolicyMap.get(policy);
if (!cases || cases.length === 0) {
continue;
}
renderedPolicies.add(policy);
lines.push(`${policy} (${cases.length} cases)`);
const byFile = groupBy(cases, (c) => c.relativePath);
for (const [filePath, fileCases] of byFile) {
lines.push(` ${filePath}`);
for (const evalCase of fileCases) {
lines.push(`${evalCase.name} [${evalCase.helperName}]`);
}
}
lines.push('');
}
for (const [policy, cases] of byPolicyMap) {
if (renderedPolicies.has(policy) || !cases || cases.length === 0) {
continue;
}
lines.push(`${policy} (${cases.length} cases)`);
const byFile = groupBy(cases, (c) => c.relativePath);
for (const [filePath, fileCases] of byFile) {
lines.push(` ${filePath}`);
for (const evalCase of fileCases) {
lines.push(`${evalCase.name} [${evalCase.helperName}]`);
}
}
lines.push('');
}
// --- By Suite ---
lines.push('By Suite');
lines.push('────────');
const bySuite = groupBy(result.cases, (c) => c.suiteName ?? '(no suite)');
const suiteNames = [...bySuite.keys()].sort((a, b) => {
if (a === b) return 0;
if (a === '(no suite)') return 1;
if (b === '(no suite)') return -1;
return a.localeCompare(b, 'en');
});
for (const suite of suiteNames) {
const cases = bySuite.get(suite)!;
lines.push(`${suite} (${cases.length} cases)`);
for (const evalCase of cases) {
lines.push(
`${evalCase.name} [${evalCase.relativePath}] (${evalCase.policy})`,
);
}
lines.push('');
}
// --- Diagnostics ---
if (result.diagnostics.length > 0) {
const filePaths = new Map<string, string>();
for (const f of result.files) {
filePaths.set(f.filePath, f.relativePath);
}
lines.push('Diagnostics');
lines.push('───────────');
for (const diagnostic of result.diagnostics) {
const displayPath = resolveRelativePath(
diagnostic.filePath,
filePaths,
result.repoRoot,
);
lines.push(
`${displayPath}:${diagnostic.location.line}:${diagnostic.location.column}${diagnostic.message}`,
);
}
lines.push('');
}
return lines.join('\n');
}
export interface InventoryJsonOutput {
version: 1;
generated: string;
summary: {
totalFiles: number;
totalCases: number;
totalDiagnostics: number;
byPolicy: Record<string, number>;
};
cases: InventoryJsonCase[];
diagnostics: InventoryJsonDiagnostic[];
}
interface InventoryJsonCase {
name: string;
filePath: string;
helperName: string;
baseHelperName: string;
policy: string;
suiteName: string | null;
suiteType: string | null;
timeout: number | null;
hasFiles: boolean;
hasPrompt: boolean;
location: { line: number; column: number };
}
interface InventoryJsonDiagnostic {
severity: string;
message: string;
filePath: string;
location: { line: number; column: number };
}
export function formatInventoryJson(
result: InventoryResult,
now?: Date,
): string {
const filePathLookup = new Map<string, string>();
for (const f of result.files) {
filePathLookup.set(f.filePath, f.relativePath);
}
const policyCounts = new Map<string, number>();
for (const evalCase of result.cases) {
policyCounts.set(
evalCase.policy,
(policyCounts.get(evalCase.policy) ?? 0) + 1,
);
}
const byPolicy: Record<string, number> = {};
for (const policy of POLICY_ORDER) {
const count = policyCounts.get(policy);
if (count !== undefined) {
byPolicy[policy] = count;
}
}
for (const [policy, count] of policyCounts) {
if (!(policy in byPolicy)) {
byPolicy[policy] = count;
}
}
let generatedDate = now;
if (!generatedDate && process.env.SOURCE_DATE_EPOCH) {
const epoch = parseInt(process.env.SOURCE_DATE_EPOCH, 10);
if (!isNaN(epoch)) {
generatedDate = new Date(epoch * 1000);
}
}
if (
!generatedDate &&
(process.env.EVAL_INVENTORY_STABLE_DATE ||
process.env.EVAL_INVENTORY_DETERMINISTIC)
) {
generatedDate = new Date(0);
}
if (!generatedDate) {
generatedDate = new Date();
}
const output: InventoryJsonOutput = {
version: 1,
generated: generatedDate.toISOString(),
summary: {
totalFiles: result.totalFiles,
totalCases: result.totalCases,
totalDiagnostics: result.diagnostics.length,
byPolicy,
},
cases: result.cases.map((c) => ({
name: c.name,
filePath: c.relativePath,
helperName: c.helperName,
baseHelperName: c.baseHelperName,
policy: c.policy,
suiteName: c.suiteName ?? null,
suiteType: c.suiteType ?? null,
timeout: c.timeout ?? null,
hasFiles: c.hasFiles,
hasPrompt: c.hasPrompt,
location: { line: c.location.line, column: c.location.column },
})),
diagnostics: result.diagnostics.map((d) => {
const relativePath = resolveRelativePath(
d.filePath,
filePathLookup,
result.repoRoot,
);
return {
severity: d.severity,
message: d.message,
filePath: relativePath,
location: { line: d.location.line, column: d.location.column },
};
}),
};
return JSON.stringify(output, null, 2);
}
function groupBy<T>(
items: readonly T[],
keyFn: (item: T) => string,
): Map<string, T[]> {
const groups = new Map<string, T[]>();
for (const item of items) {
const key = keyFn(item);
const group = groups.get(key);
if (group) {
group.push(item);
} else {
groups.set(key, [item]);
}
}
return groups;
}
function resolveRelativePath(
filePath: string,
lookup: Map<string, string>,
baseDir: string,
): string {
if (filePath === '<inline>') {
return filePath;
}
const mapped = lookup.get(filePath);
if (mapped !== undefined) {
return mapped;
}
return path.isAbsolute(filePath)
? path.relative(baseDir, filePath).replace(/\\/g, '/')
: filePath;
}
function isNodeError(err: unknown): err is NodeJS.ErrnoException {
return err instanceof Error && 'code' in err;
}
+143
View File
@@ -0,0 +1,143 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
ALL_BUILTIN_TOOL_NAMES,
TOOL_LEGACY_ALIASES,
} from '@google/gemini-cli-core';
export type ToolCategory =
| 'file-system'
| 'shell'
| 'web'
| 'planning'
| 'user-interaction'
| 'skills'
| 'task-tracker'
| 'agent'
| 'mcp';
export interface ToolRegistryEntry {
name: string;
category: ToolCategory;
aliases: readonly string[];
}
export interface ToolRegistry {
tools: ReadonlyMap<string, ToolRegistryEntry>;
totalTools: number;
byCategory: ReadonlyMap<ToolCategory, readonly ToolRegistryEntry[]>;
aliasLookup: ReadonlyMap<string, string>;
}
const TOOL_CATEGORIES: Record<
(typeof ALL_BUILTIN_TOOL_NAMES)[number],
ToolCategory
> = {
glob: 'file-system',
grep_search: 'file-system',
list_directory: 'file-system',
read_file: 'file-system',
read_many_files: 'file-system',
write_file: 'file-system',
replace: 'file-system',
run_shell_command: 'shell',
google_web_search: 'web',
web_fetch: 'web',
enter_plan_mode: 'planning',
exit_plan_mode: 'planning',
write_todos: 'planning',
ask_user: 'user-interaction',
activate_skill: 'skills',
get_internal_docs: 'skills',
tracker_create_task: 'task-tracker',
tracker_update_task: 'task-tracker',
tracker_get_task: 'task-tracker',
tracker_list_tasks: 'task-tracker',
tracker_add_dependency: 'task-tracker',
tracker_visualize: 'task-tracker',
invoke_agent: 'agent',
complete_task: 'agent',
update_topic: 'agent',
read_mcp_resource: 'mcp',
list_mcp_resources: 'mcp',
};
let registryCache: ToolRegistry | undefined;
export function buildToolRegistry(): ToolRegistry {
if (registryCache) {
return registryCache;
}
const tools = new Map<string, ToolRegistryEntry>();
const aliasLookup = new Map<string, string>();
const categoryGroups = new Map<ToolCategory, ToolRegistryEntry[]>();
for (const name of ALL_BUILTIN_TOOL_NAMES) {
const category = TOOL_CATEGORIES[name];
const aliases: string[] = [];
for (const [legacyName, canonicalName] of Object.entries(
TOOL_LEGACY_ALIASES,
)) {
if (canonicalName === name) {
aliases.push(legacyName);
aliasLookup.set(legacyName, name);
}
}
aliasLookup.set(name, name);
const entry: ToolRegistryEntry = {
name,
category,
aliases: Object.freeze(aliases),
};
tools.set(name, entry);
const group = categoryGroups.get(category);
if (group) {
group.push(entry);
} else {
categoryGroups.set(category, [entry]);
}
}
const frozenCategories = new Map<
ToolCategory,
readonly ToolRegistryEntry[]
>();
for (const [cat, entries] of categoryGroups) {
frozenCategories.set(cat, Object.freeze(entries));
}
registryCache = {
tools,
totalTools: tools.size,
byCategory: frozenCategories,
aliasLookup,
};
return registryCache;
}
export function resolveToolName(
registry: ToolRegistry,
name: string,
): string | undefined {
if (!name) {
return undefined;
}
return registry.aliasLookup.get(name);
}
export function getToolsByCategory(
registry: ToolRegistry,
category: ToolCategory,
): readonly ToolRegistryEntry[] {
return registry.byCategory.get(category) ?? [];
}