diff --git a/.vscode/launch.json b/.vscode/launch.json index db7fe6e807a..572ce8cd8e8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -52,6 +52,27 @@ "order": 999 } }, + { + "type": "node", + "request": "launch", + "name": "Compile for azure-linter", + "program": "${workspaceFolder}/packages/compiler/entrypoints/cli.js", + "args": [ + "compile", + "C:/t/tspProj/p1/client.tsp", + ], + "smartStep": true, + "sourceMaps": true, + "skipFiles": ["/**/*.js"], + "outFiles": [ + "${workspaceFolder}/packages/*/dist/**/*.js", + "${workspaceFolder}/packages/*/dist-dev/**/*.js" + ], + "cwd": "C:/t/tspProj/p1", + "presentation": { + "order": 2 + } + }, { "type": "node", "request": "launch", @@ -140,12 +161,12 @@ // Set the telemetry key environment variable to use if you dont want to set it in package.json //"TYPESPEC_VSCODE_TELEMETRY_KEY": "{The instrumentation key of your Application Insights}", + + "ENABLE_SERVER_COMPILE_LOGGING": "true", + // "ENABLE_UPDATE_MANAGER_LOGGING": "true", "TYPESPEC_SERVER_NODE_OPTIONS": "--nolazy --inspect-brk=4242", "TYPESPEC_DEVELOPMENT_MODE": "true" - - // "ENABLE_SERVER_COMPILE_LOGGING": "true", - // "ENABLE_UPDATE_MANAGER_LOGGING": "true" }, "presentation": { "hidden": true diff --git a/packages/azure-linter/package.json b/packages/azure-linter/package.json new file mode 100644 index 00000000000..20230835df4 --- /dev/null +++ b/packages/azure-linter/package.json @@ -0,0 +1,44 @@ +{ + "name": "azure-linter", + "version": "0.1.0", + "type": "module", + "main": "dist/src/index.js", + "tspMain": "lib/main.tsp", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "default": "./dist/src/index.js" + }, + "./testing": { + "types": "./dist/src/testing/index.d.ts", + "default": "./dist/src/testing/index.js" + } + }, + "devDependencies": { + "@types/node": "latest", + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", + "@typespec/compiler": "workspace:^", + "@typespec/library-linter": "latest", + "eslint": "^9.15.0", + "prettier": "^3.3.3", + "typescript": "^5.3.3" + }, + "scripts": { + "build": "tsc -p ./tsconfig.json", + "watch": "tsc --watch", + "test": "node --test", + "lint": "eslint src/ test/ --report-unused-disable-directives --max-warnings=0", + "lint:fix": "eslint . --report-unused-disable-directives --fix", + "format": "prettier . --write", + "format:check": "prettier --check ." + }, + "private": true, + "dependencies": { + "@azure/identity": "~4.10.2", + "jsonrepair": "^3.12.0", + "openai": "^5.9.0", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.24.6" + } +} diff --git a/packages/azure-linter/src/index.ts b/packages/azure-linter/src/index.ts new file mode 100644 index 00000000000..04ecc6d7251 --- /dev/null +++ b/packages/azure-linter/src/index.ts @@ -0,0 +1,2 @@ +export { $lib } from "./lib.js"; +export { $linter } from "./linter.js"; diff --git a/packages/azure-linter/src/lib.ts b/packages/azure-linter/src/lib.ts new file mode 100644 index 00000000000..0af38433a7f --- /dev/null +++ b/packages/azure-linter/src/lib.ts @@ -0,0 +1,24 @@ +import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler"; + +export const $lib = createTypeSpecLibrary({ + name: "azure-linter", + // Define diagnostics for the library. This will provide a typed API to report diagnostic as well as a auto doc generation. + diagnostics: { + "banned-alternate-name": { + severity: "error", + messages: { + default: paramMessage`Banned alternate name "${"name"}".`, + }, + }, + }, + // Defined state keys for storing metadata in decorator. + state: { + alternateName: { description: "alternateName" }, + }, +}); + +export const { + reportDiagnostic, + createDiagnostic, + stateKeys: StateKeys, +} = $lib; diff --git a/packages/azure-linter/src/linter.ts b/packages/azure-linter/src/linter.ts new file mode 100644 index 00000000000..d0a7a5afef6 --- /dev/null +++ b/packages/azure-linter/src/linter.ts @@ -0,0 +1,27 @@ +import { defineLinter } from "@typespec/compiler"; +import { tooGenericRule } from "./rules/avoid-too-generic-name.naming.csharp.rule.js"; +import { booleanPropertyStartsWithVerbRule } from "./rules/boolean-property.naming.csharp.rule.js"; +import { durationWithUnitRule } from "./rules/duration-with-unit.naming.csharp.rule.js"; +import { noInterfaceRule } from "./rules/no-interfaces.rule.js"; + +export const $linter = defineLinter({ + rules: [noInterfaceRule, booleanPropertyStartsWithVerbRule, durationWithUnitRule, tooGenericRule], + ruleSets: { + recommended: { + enable: { + [`azure-linter/${noInterfaceRule.name}`]: true, + [`azure-linter/${booleanPropertyStartsWithVerbRule.name}`]: true, + [`azure-linter/${durationWithUnitRule.name}`]: true, + [`azure-linter/${tooGenericRule.name}`]: true, + }, + }, + all: { + enable: { + [`azure-linter/${noInterfaceRule.name}`]: true, + [`azure-linter/${booleanPropertyStartsWithVerbRule.name}`]: true, + [`azure-linter/${durationWithUnitRule.name}`]: true, + [`azure-linter/${tooGenericRule.name}`]: true, + }, + }, + }, +}); diff --git a/packages/azure-linter/src/lm/lm-cache.ts b/packages/azure-linter/src/lm/lm-cache.ts new file mode 100644 index 00000000000..02f94c1049d --- /dev/null +++ b/packages/azure-linter/src/lm/lm-cache.ts @@ -0,0 +1,104 @@ +import { ChatMessage } from "@typespec/compiler/experimental"; +import z from "zod"; +import { logger } from "../log/logger.js"; +import { getRecordValue, setRecordValue, tryReadJsonFile, tryWriteFile } from "../utils.js"; +import { LmResponseContent } from "./types.js"; + +const zLmCacheEntry = z.object({ + msgKey: z.string().describe("The key for the message, joined by role and message content"), + value: z.any().describe("The cached value, should match the expected response type"), +}); + +const zLmCache = z + .record( + z.string().describe("The unique identifier of the caller for the cached response"), + z.array(zLmCacheEntry), + ) + .describe("Map of cached responses keyed by a unique identifier"); + +type LmCacheEntry = z.infer; +type LmCacheMap = z.infer; + +const MAX_LM_CACHE_SIZE_PER_KEY = 3; + +class LmCache { + private _cache: LmCacheMap = {}; + private _cacheFilePath?: string; + + constructor() {} + + async init(cacheFilePath: string) { + if (this._cacheFilePath === undefined) { + this._cacheFilePath = cacheFilePath; + await this.loadFromCacheFile(); + } else { + if (this._cacheFilePath !== cacheFilePath) { + logger.warning( + `Cache file path is already set to ${this._cacheFilePath}, ignore the new path ${cacheFilePath}`, + ); + } + } + + // consider hook on process exit to flush the cache to file + // now we will flush to cache file on every set operation for simplicity + // which should be ignoreable comparing the to actual lm request calls + } + + private generateMessageKey(key: string, messages: ChatMessage[]): string { + const msgPart = messages.map((msg) => `${msg.role}:${msg.message}`).join("|"); + return `${key}->${msgPart}`; + } + + async getForMsg( + callerKey: string, + msg: ChatMessage[], + ): Promise { + const callerCache = getRecordValue(this._cache, callerKey); + if (!callerCache) { + return undefined; + } else { + const msgKey = this.generateMessageKey(callerKey, msg); + const entry = callerCache.find((e) => e.msgKey === msgKey); + if (entry) { + return entry.value as T; + } + return undefined; + } + } + + async setForMsg(callerKey: string, msg: ChatMessage[], value: T) { + const msgKey = this.generateMessageKey(callerKey, msg); + const entry: LmCacheEntry = { msgKey, value }; + + const foundCallerEntry = getRecordValue(this._cache, callerKey); + if (!foundCallerEntry) { + setRecordValue(this._cache, callerKey, [entry]); + } else { + const found = foundCallerEntry.findIndex((e) => e.msgKey === msgKey); + if (found >= 0) { + foundCallerEntry.splice(found, 1); + } + foundCallerEntry.push(entry); + if (foundCallerEntry.length > MAX_LM_CACHE_SIZE_PER_KEY) { + foundCallerEntry.shift(); + } + } + await this.flushToFile(); + } + + private async flushToFile() { + if (!this._cacheFilePath) return; + return tryWriteFile(this._cacheFilePath, JSON.stringify(this._cache, null, 2)); + } + + private async loadFromCacheFile() { + if (!this._cacheFilePath) { + this._cache = {}; + return; + } + const parsed = await tryReadJsonFile(this._cacheFilePath, zLmCache); + this._cache = parsed ?? {}; + } +} + +export const lmCache = new LmCache(); diff --git a/packages/azure-linter/src/lm/lm-rule-checker.ts b/packages/azure-linter/src/lm/lm-rule-checker.ts new file mode 100644 index 00000000000..1a76ad3d6dd --- /dev/null +++ b/packages/azure-linter/src/lm/lm-rule-checker.ts @@ -0,0 +1,253 @@ +import { DiagnosticMessages, LinterRuleContext } from "@typespec/compiler"; +import { ChatCompleteOptions, ChatMessage } from "@typespec/compiler/experimental"; +import z, { AnyZodObject } from "zod"; +import { md5 } from "../utils.js"; +import { askLanguageModeWithRetry } from "./lm-utils.js"; +import { LmResponseError, zLmResponseContent } from "./types.js"; + +const zKeyed = z.object({ + key: z + .string() + .describe( + "Key to identify the data and used in the cache, should be unique globally in the topic", + ), +}); +type Keyed = z.infer; + +/** + * LmChecker is used to help checking data with language model aggregately. + */ +export class LmRuleChecker< + dataT extends object, + resultSchemaT extends AnyZodObject, + P extends DiagnosticMessages, +> { + private cache: LmRuleCheckerCache & Keyed>; + private dataToCheck: (dataT & Keyed)[] = []; + private deferredPromises: Map & Keyed>> = + new Map(); + + /** + * + * @param name for logging purpose only + * @param messages + * @param lmOptions + * @param resultZodType + * @param checkBatchSize + */ + constructor( + private name: string, + public messages: ChatMessage[], + private lmOptions: ChatCompleteOptions, + private resultZodType: resultSchemaT, + private checkBatchSize: number = 30, + ) { + this.cache = new LmRuleCheckerCache & Keyed>(); + } + + private getDataWithKey(data: dataT): dataT & Keyed { + const json = JSON.stringify(data); + // hash the string so that AI won't do strange things to the key field + const hash = md5(json); + return { + ...data, + key: hash, + }; + } + + /** + * Queues data for batch checking. + * - The actual check will be performed when checkAllData is called. + * - createRuleWithLm will help to call checkAllData if you are using it to create the linter rule. + * + * @param data The data to queue for checking + * @returns A promise that resolves with the check result when batch processing completes + */ + async queueDataToCheck(data: dataT): Promise> { + const keyedData = this.getDataWithKey(data); + const found = this.deferredPromises.get(keyedData.key); + if (found) { + return found.getPromise(); + } else { + const deferred = new DeferredPromise(); + this.deferredPromises.set(keyedData.key, deferred); + this.dataToCheck.push(keyedData); + return deferred.getPromise(); + } + } + + async checkAllData(context: LinterRuleContext

) { + const data = this.dataToCheck; + this.dataToCheck = []; + + const dataToLm = []; + + for (const d of data) { + const cached = this.cache.get(d.key); + if (cached) { + this.resolveCheckData(d, cached); + } else { + dataToLm.push(d); + } + } + console.log( + `[ChatComplete(${this.name})] ${dataToLm.length} out of ${data.length} items need to be checked by LM.`, + ); + if (dataToLm.length === 0) { + return; + } + + const s = Date.now(); + const promises = []; + for (let i = 0; i < dataToLm.length; i += this.checkBatchSize) { + const batch = dataToLm.slice(i, i + this.checkBatchSize); + console.log( + `[ChatComplete(${this.name})] Start processing batch ${i} to ${i + batch.length}`, + ); + const p = this.checkDataInternal(batch, context, this.lmOptions, 3); + const pp = p.then(() => { + console.log( + `[ChatComplete(${this.name})] Finished processing batch ${i} to ${i + batch.length}`, + ); + }); + promises.push(pp); + // small delay to avoid sending too many requests to LM in a very short time. + // TODO: more logic may be needed here to handle service throttling + await new Promise((resolve) => setTimeout(resolve, 25)); + } + await Promise.all(promises); + const e = Date.now(); + console.log( + `[ChatComplete(${this.name})] Finished processing all ${data.length} items in ${e - s} ms, cache size = ${this.cache.size()}.`, + ); + } + + private async checkDataInternal( + data: (dataT & Keyed)[], + context: LinterRuleContext

, + options: ChatCompleteOptions, + retryCount: number, + ) { + if (data.length === 0) { + return; + } + if (retryCount < 0) { + // report error for the rest data + for (const d of data) { + this.rejectCheckData(d, { + type: "error", + error: `Failed to get response from LM. Please retry again later.`, + }); + } + return; + } + const keyedResultZodType = z.intersection(zKeyed, this.resultZodType); + const zResponse = zLmResponseContent.extend( + z.object({ + data: z.array(keyedResultZodType).describe("Array of the check result"), + }).shape, + ); + const messages: ChatMessage[] = [ + ...this.messages, + { + role: "user", + message: `Following is the data to check as required, please respond data in json format that strictly matches the required response schema. *IMPORTANT* MAKE SURE the keys in the response match the keys in the request data EXACTLY so that the response can be associated with the request by comparing whether the key is strictly equal. Here is the data to check: ${JSON.stringify(data)}`, + }, + ]; + console.debug(`[ChatComplete] ask LM to check ${data.length} items...`); + const response = await askLanguageModeWithRetry(context, "", messages, options, zResponse); + if (response.type === "error") { + for (const d of data) { + this.rejectCheckData(d, response); + } + return; + } + const responseData = response.data; + for (const d of responseData) { + this.cache.set(d.key, d); + } + const left: (dataT & Keyed)[] = []; + for (const d of data) { + const resolved = this.cache.get(d.key); + if (resolved) { + this.resolveCheckData(d, resolved); + } else { + left.push(d); + } + } + if (left.length > 0) { + console.debug( + `[ChatComplete(${this.name})] ${left.length} out of ${data.length} items not resolved, retrying...`, + ); + // some data are not resolved, maybe missed by AI try again + await this.checkDataInternal(left, context, options, retryCount - 1); + } + } + + private resolveCheckData(data: dataT & Keyed, result: z.infer & Keyed) { + const deferred = this.deferredPromises.get(data.key); + if (deferred) { + deferred.resolvePromise(result); + this.deferredPromises.delete(data.key); + } + } + + private rejectCheckData(data: dataT & Keyed, error: LmResponseError) { + const deferred = this.deferredPromises.get(data.key); + if (deferred) { + deferred.rejectPromise(error); + this.deferredPromises.delete(data.key); + } + } +} + +class DeferredPromise { + private promise: Promise; + private resolve!: (value: T) => void; + private reject!: (reason?: any) => void; + + constructor() { + this.promise = new Promise((res, rej) => { + this.resolve = res; + this.reject = rej; + }); + } + + getPromise(): Promise { + return this.promise; + } + + resolvePromise(value: T) { + this.resolve(value); + } + + rejectPromise(reason?: any) { + this.reject(reason); + } +} + +class LmRuleCheckerCache { + private cache = new Map(); + + constructor() {} + + get(key: string): resultT | undefined { + return this.cache.get(key); + } + + set(key: string, value: resultT) { + this.cache.set(key, value); + } + + has(key: string): boolean { + return this.cache.has(key); + } + + clear() { + this.cache.clear(); + } + + size(): number { + return this.cache.size; + } +} diff --git a/packages/azure-linter/src/lm/lm-utils.ts b/packages/azure-linter/src/lm/lm-utils.ts new file mode 100644 index 00000000000..4abb95e5e53 --- /dev/null +++ b/packages/azure-linter/src/lm/lm-utils.ts @@ -0,0 +1,235 @@ +import { DiagnosticMessages, DiagnosticTarget, LinterRuleContext } from "@typespec/compiler"; +import { ChatCompleteOptions, ChatMessage } from "@typespec/compiler/experimental"; +import { join } from "path"; +import z, { ZodObject } from "zod"; +import { logger } from "../log/logger.js"; +import { toJsonSchemaString, tryRepairAndParseJson } from "../utils.js"; +import { lmCache } from "./lm-cache.js"; +import { getLmProvider } from "./providers/lm-provider.js"; +import { + LmDiagnosticMessages, + LmErrorMessages, + LmResponseContent, + LmResponseError, + zLmResponseError, +} from "./types.js"; + +const skipReportingDiagnosticsWhenLmProviderNotAvailable = true; + +export function reportLmErrors( + result: LmResponseError, + target: DiagnosticTarget, + context: LinterRuleContext, + onOtherErrors: (r: LmResponseError, context: LinterRuleContext) => void, +) { + if (result.type !== "error") { + return; + } + switch (result.error) { + case LmErrorMessages.LanguageModelProviderNotAvailable: + // the template constraint should have already ensured the 'lmProviderNotAvailable' messageId is available + // but the tsc compiler still complains about it. so convert as any here to avoid the error. Same for following cases + // TODO: further investigation needed to figure out a way to let compiler know the messageId is available + if (!skipReportingDiagnosticsWhenLmProviderNotAvailable) { + context.reportDiagnostic({ + target, + messageId: "lmProviderNotAvailable", + } as any); + } + break; + case LmErrorMessages.EmptyLmResponse: + context.reportDiagnostic({ + target, + messageId: "emptyLmResponse", + } as any); + break; + case LmErrorMessages.FailedToParseMappingResult: + context.reportDiagnostic({ + target, + messageId: "failedToParseMappingResult", + } as any); + break; + default: + onOtherErrors(result, context); + } +} + +export async function askLanguageModeWithRetry< + T extends ZodObject, + P extends DiagnosticMessages, +>( + context: LinterRuleContext

, + callerKey: string, + messages: ChatMessage[], + options: ChatCompleteOptions, + responseZod: T, + retryCount = 3, +): Promise | LmResponseError> { + let result; + for (let attempt = 0; attempt < retryCount; attempt++) { + result = await askLanguageModel(context, callerKey, messages, options, responseZod); + if ( + result.type === "error" && + (result.error === LmErrorMessages.LanguageModelProviderNotAvailable || + result.error === LmErrorMessages.EmptyLmResponse || + result.error === LmErrorMessages.FailedToParseMappingResult) + ) { + logger.error( + `Error while asking language model (attempt ${attempt + 1}/${retryCount}): ${result.error}`, + ); + await new Promise((resolve) => setTimeout(resolve, 200)); + } + return result; + // sleep for a short time before retrying + } + logger.error("All attempts to ask the language model failed"); + return result!; +} + +export async function askLanguageModel< + T extends ZodObject, + P extends DiagnosticMessages, +>( + context: LinterRuleContext

, + callerKey: string, + messages: ChatMessage[], + options: ChatCompleteOptions, + responseZod: T, +): Promise | LmResponseError> { + const cachePath = join(context.program.projectRoot, "azure-linter-lm.cache"); + await lmCache.init(cachePath); + if (callerKey) { + const fromCache = await lmCache.getForMsg>(callerKey, messages); + if (fromCache) { + logger.debug("Using cached result for messages: " + JSON.stringify(messages)); + return fromCache; + } + } + // check the provider here to make sure cache is checked first which should work even when the provider is not available + const provider = getLmProvider(); + if (!provider) { + logger.error("Language model provider is not available"); + return createLmErrorResponse(LmErrorMessages.LanguageModelProviderNotAvailable); + } + + const responseSchemaMessage = ` + ** Important: You response MUST follow the RULEs below ** + - If there is error occures, you MUST reponse a valid JSON object that matches the schema: + \`\`\`json schema + ${toJsonSchemaString(zLmResponseError)} + \`\`\` + - If there is no error occurs, you MUST response a valid JSON object or array that matches the schema: + \`\`\`json schema + ${toJsonSchemaString(responseZod)} + \`\`\` + - There MUST NOT be any other text in the response, only the JSON object or array. + - The JSON object or array MUST NOT be wrapped in triple backticks or any other formatting. + - DOUBLE CHECK the JSON object or array before sending it back to ensure it is valid, follows the schema and all the required fields are filled properly. +`; + const msgToLm: ChatMessage[] = [ + ...messages, + { + role: "user", + message: responseSchemaMessage, + }, + ]; + + try { + const result = await provider.chatComplete(msgToLm, options); + if (!result) { + logger.error("No result returned from language model."); + return createLmErrorResponse(LmErrorMessages.EmptyLmResponse); + } + const parsedResult = tryParseLanguageModelResult(result, responseZod); + if (parsedResult === undefined) { + logger.error("Failed to parse mapping result from LLM: " + result); + return createLmErrorResponse(LmErrorMessages.FailedToParseMappingResult); + } + + if (callerKey && parsedResult.type !== "error") { + // Cache the result if it is a valid response + void lmCache.setForMsg(callerKey, messages, parsedResult); + } + + return parsedResult; + } catch (error) { + logger.error(`Error while asking language model: ${error}`); + return createLmErrorResponse(`Error while asking language model: ${error}`); + } +} + +export function createLmErrorResponse(errorMessage: string): LmResponseError { + return { + type: "error", + error: errorMessage, + }; +} + +export function tryParseLanguageModelResult( + text: string | undefined, + responseZod: ZodObject, +): T | LmResponseError | undefined { + if (!text) { + logger.error("No text provided for parsing result"); + return undefined; + } + + const jsonString = getJsonPart(text); + + const jsonObj = tryRepairAndParseJson(jsonString) as T | LmResponseError | undefined; + if (!jsonObj || !jsonObj.type) { + logger.error(`Invalid response from LM which is not a valid LmResponseBasic: ${text}`); + return undefined; + } + if (jsonObj.type === "error") { + const result = zLmResponseError.safeParse(jsonObj); + if (result.success) { + return result.data; + } else { + logger.error(`Invalid error response from LM: ${text}`); + return undefined; + } + } else if (jsonObj.type === "content") { + const result = responseZod.safeParse(jsonObj); + if (result.success) { + return result.data; + } else { + logger.error(`Invalid content response from LM: ${text}`); + return undefined; + } + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logger.error( + `Invalid response type: ${(jsonObj as any).type}. Expected 'error' or 'content'. Response: ${text}`, + ); + return undefined; + } +} + +/** in case AI wrap the json with some text, try to have some simple handling for that */ +function getJsonPart(text: string): string { + // Find the first opening bracket/brace + const openBracket = text.indexOf("["); + const openBrace = text.indexOf("{"); + const startIndex = + openBracket === -1 + ? openBrace + : openBrace === -1 + ? openBracket + : Math.min(openBracket, openBrace); + + if (startIndex === -1) { + return text; + } + + // Determine if we're looking for array or object based on which came first + const isArray = text[startIndex] === "["; + const closeChar = isArray ? "]" : "}"; + const endIndex = text.lastIndexOf(closeChar); + + if (endIndex === -1) { + return text; + } + + return text.substring(startIndex, endIndex + 1); +} diff --git a/packages/azure-linter/src/lm/providers/ai-foundry-lm-provider.ts b/packages/azure-linter/src/lm/providers/ai-foundry-lm-provider.ts new file mode 100644 index 00000000000..7b8bc93a09a --- /dev/null +++ b/packages/azure-linter/src/lm/providers/ai-foundry-lm-provider.ts @@ -0,0 +1,81 @@ +import { DefaultAzureCredential, getBearerTokenProvider } from "@azure/identity"; +import { ChatCompleteOptions, ChatMessage, LmProvider } from "@typespec/compiler/experimental"; +import { AzureOpenAI } from "openai"; +import { logger } from "../../log/logger.js"; + +type AiFoundryLmProviderConnectionString = { + type: "AiFoundryLmProvider"; + // only support open ai service for now + serviceType: "openai"; + endpoint: string; + apiVersion: string; + // model deployment name + deployment: string; +}; + +function isAiFoundryLmProviderConnectionString( + connectionString: Record, +): connectionString is AiFoundryLmProviderConnectionString { + return ( + connectionString.type === "AiFoundryLmProvider" && + connectionString.serviceType === "openai" && + !!connectionString.endpoint && + !!connectionString.apiVersion && + !!connectionString.deployment + ); +} + +export class AiFoundryLmProvider implements LmProvider { + private constructor(private connectionString: AiFoundryLmProviderConnectionString) {} + + static create(connectionString: Record): AiFoundryLmProvider | undefined { + if (isAiFoundryLmProviderConnectionString(connectionString)) { + return new AiFoundryLmProvider(connectionString); + } else { + return undefined; + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async chatComplete(messages: ChatMessage[], options: ChatCompleteOptions): Promise { + // Initialize the DefaultAzureCredential + const credential = new DefaultAzureCredential(); + const scope = "https://cognitiveservices.azure.com/.default"; + const azureADTokenProvider = getBearerTokenProvider(credential, scope); + + // Initialize the AzureOpenAI client with Entra ID (Azure AD) authentication + const client = new AzureOpenAI({ + endpoint: this.connectionString.endpoint, + azureADTokenProvider, + apiVersion: this.connectionString.apiVersion, + deployment: this.connectionString.deployment, + }); + + const result = await client.chat.completions.create({ + messages: messages.map((m) => { + if (m.role === "user") { + return { role: "user", content: m.message }; + } else if (m.role === "assist") { + return { role: "assistant", content: m.message }; + } else { + logger.error(`Unsupported message role: ${m.role}, default to 'user' role`); + return { role: "user", content: m.message }; + } + }), + // shall we use the options's model? or it's defined by the deployment already? needs to double check + model: "gpt-4o", + }); + + // take the first choice and return the content to keep things simple + if (!result.choices || result.choices.length === 0) { + throw new Error("No choices returned from the chat completion."); + } + if (!result.choices[0].message || !result.choices[0].message.content) { + throw new Error("No content in the first choice's message."); + } + // Log the content of the first choice's message + logger.debug("Chat completion result:", result.choices[0].message.content); + // Return the content as a string + return result.choices[0].message.content; + } +} diff --git a/packages/azure-linter/src/lm/providers/lm-provider.ts b/packages/azure-linter/src/lm/providers/lm-provider.ts new file mode 100644 index 00000000000..9a497c4db32 --- /dev/null +++ b/packages/azure-linter/src/lm/providers/lm-provider.ts @@ -0,0 +1,45 @@ +import { LmProvider } from "@typespec/compiler/experimental"; +import { logger } from "../../log/logger.js"; +import { tryParseConnectionString } from "../../utils.js"; +import { ENV_VAR_LM_PROVIDER_CONNECTION_STRING } from "../types.js"; +import { AiFoundryLmProvider } from "./ai-foundry-lm-provider.js"; +import { TspExLmProvider } from "./tsp-ex-lm-provider.js"; + +let lmProvider: LmProvider | undefined | "not-initialized" = "not-initialized"; + +/** + * + * @param connectionString we will read from environment variable LM_PROVIDER_CONNECTION_STRING if not provided + * @returns + */ +export function getLmProvider(): LmProvider | undefined { + if (lmProvider !== "not-initialized") { + return lmProvider; + } + const connectionString = process.env[ENV_VAR_LM_PROVIDER_CONNECTION_STRING]; + if (!connectionString) { + logger.debug( + `No LM provider connection string found in environment variable ${ENV_VAR_LM_PROVIDER_CONNECTION_STRING}. Try to use default TspExLmProvider.`, + ); + const provider = TspExLmProvider.create({ type: "TspExLmProvider" }); + if (!provider) { + logger.debug("Default TspExLmProvider is not available. Return undefined as lm provider."); + } + lmProvider = provider; + return lmProvider; + } else { + const csObj = tryParseConnectionString(connectionString); + if (!csObj || !csObj.type) { + logger.error("Invalid connection string: missing 'type' property"); + return undefined; + } + + const p = TspExLmProvider.create(csObj) || AiFoundryLmProvider.create(csObj); + if (!p) { + logger.error(`Failed to create LmProvider from connection string: ${connectionString}`); + return undefined; + } + lmProvider = p; + return p; + } +} diff --git a/packages/azure-linter/src/lm/providers/tsp-ex-lm-provider.ts b/packages/azure-linter/src/lm/providers/tsp-ex-lm-provider.ts new file mode 100644 index 00000000000..eac062b4490 --- /dev/null +++ b/packages/azure-linter/src/lm/providers/tsp-ex-lm-provider.ts @@ -0,0 +1,36 @@ +import { ChatCompleteOptions, ChatMessage, LmProvider } from "@typespec/compiler/experimental"; +import { logger } from "../../log/logger.js"; + +type TspExLmProviderConnectionString = { + type: "TspExLmProvider"; +}; + +function isTspExLmProviderConnectionString( + connectionString: Record, +): connectionString is TspExLmProviderConnectionString { + return connectionString.type === "TspExLmProvider"; +} + +export class TspExLmProvider implements LmProvider { + private constructor(private tspExLmProvider: LmProvider) {} + + static create(connectionString: Record): TspExLmProvider | undefined { + if (!isTspExLmProviderConnectionString(connectionString)) { + return undefined; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const r = (globalThis as any).TspExLmProvider; + if (!r || typeof r.chatComplete !== "function") { + logger.debug(`Default TspExLmProvider not found`); + return undefined; + } + return new TspExLmProvider(r); + } + + chatComplete(messages: ChatMessage[], options: ChatCompleteOptions): Promise { + if (!this.tspExLmProvider || typeof this.tspExLmProvider.chatComplete !== "function") { + throw new Error("TspExLmProvider is not available or does not have chatComplete method"); + } + return this.tspExLmProvider.chatComplete(messages, options); + } +} diff --git a/packages/azure-linter/src/lm/types.ts b/packages/azure-linter/src/lm/types.ts new file mode 100644 index 00000000000..364dbda1663 --- /dev/null +++ b/packages/azure-linter/src/lm/types.ts @@ -0,0 +1,48 @@ +import { DiagnosticMessages } from "@typespec/compiler"; +import z from "zod"; + +export const zLmResponseContent = z.object({ + type: z + .literal("content") + .describe("Type of the response, sub types should override this with a literal type"), +}); + +export type LmResponseContent = z.infer; + +export const zLmResponseError = zLmResponseContent.merge( + z.object({ + type: z.literal("error"), + error: z.string().describe("Error message from the language model provider"), + }), +); + +export type LmResponseError = z.infer; + +export const ENV_VAR_LM_PROVIDER_CONNECTION_STRING = "LM_PROVIDER_CONNECTION_STRING"; + +export enum LmErrorMessages { + LanguageModelProviderNotAvailable = `Language Model is not available. If you are in VSCode, please make sure TypeSpec extension has been installed and VSCode LM has been initialized which may take some time if you just start the VSCode. If you are outside of VSCode, please make sure the environment variable ${ENV_VAR_LM_PROVIDER_CONNECTION_STRING} is set properly.`, + EmptyLmResponse = "Empty response got from Language Model, please check whether the language model is available and retry again.", + FailedToParseMappingResult = "Failed to parse mapping result from Language model, please retry", +} + +export interface LmDiagnosticMessages extends DiagnosticMessages { + lmProviderNotAvailable: string; + emptyLmResponse: string; + failedToParseMappingResult: string; +} + +export const LmDiagnosticMessages: LmDiagnosticMessages = { + lmProviderNotAvailable: LmErrorMessages.LanguageModelProviderNotAvailable, + emptyLmResponse: LmErrorMessages.EmptyLmResponse, + failedToParseMappingResult: LmErrorMessages.FailedToParseMappingResult, +}; + +export enum LmFamily { + claudeSonnet4 = "claude-sonnet-4", + gpt4o = "gpt-4o", + o4Mini = "o4-mini", + gpt5mini = "gpt-5-mini", +} + +export const defaultLmFamily = LmFamily.gpt4o; diff --git a/packages/azure-linter/src/log/logger.ts b/packages/azure-linter/src/log/logger.ts new file mode 100644 index 00000000000..bafdd465f73 --- /dev/null +++ b/packages/azure-linter/src/log/logger.ts @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +const logFilter = process.env["AZURE_LINTER_LOG_FILTER"] || "warning"; +const logLevelArray = ["trace", "debug", "info", "warning", "error"]; + +const logFilterIndex = logLevelArray.indexOf(logFilter); + +class Logger { + info(message: string, ...args: any[]): void { + if (logLevelArray.indexOf("info") < logFilterIndex) { + return; + } + console.log(`[INFO] ${message}`, ...args); + } + + warning(message: string, ...args: any[]): void { + if (logLevelArray.indexOf("warning") < logFilterIndex) { + return; + } + console.warn(`[WARNING] ${message}`, ...args); + } + + error(message: string, ...args: any[]): void { + if (logLevelArray.indexOf("error") < logFilterIndex) { + return; + } + console.error(`[ERROR] ${message}`, ...args); + } + + debug(message: string, ...args: any[]): void { + if (logLevelArray.indexOf("debug") < logFilterIndex) { + return; + } + console.debug(`[DEBUG] ${message}`, ...args); + } +} + +export const logger = new Logger(); diff --git a/packages/azure-linter/src/rules/avoid-too-generic-name.naming.csharp.rule.ts b/packages/azure-linter/src/rules/avoid-too-generic-name.naming.csharp.rule.ts new file mode 100644 index 00000000000..8464b94590b --- /dev/null +++ b/packages/azure-linter/src/rules/avoid-too-generic-name.naming.csharp.rule.ts @@ -0,0 +1,130 @@ +import { + getDoc, + getSourceLocation, + getTypeName, + listServices, + paramMessage, +} from "@typespec/compiler"; +import { inspect } from "util"; +import { LmRuleChecker } from "../lm/lm-rule-checker.js"; +import { reportLmErrors } from "../lm/lm-utils.js"; +import { defaultLmFamily, LmDiagnosticMessages, LmResponseError } from "../lm/types.js"; +import { + createRenameCodeFix, + createRuleWithLmRuleChecker, + getClientNameFromDec, + isDirectPropertyOfModel, + isMyCode, + isUnnamedModelProperty, + splitNameByUpperCase, +} from "./rule-utils.js"; +import { RenameData, zRenameCheckResult } from "./types.js"; + +/** + * This linter rule is not completed yet, put it here just to simulate a "bad" linter rule which will need to check + * hundreds of properties in a large project so that we can understand how the linter with AI support will perform in this case. + */ +const ruleName = "csharp.naming.avoid-too-generic-name"; +const aiChecker = new LmRuleChecker( + "too-general", + [ + { + role: "user", + message: `Check the given property names which are in camel or pascal case. If the property name is too generic that may cause confusion or confliction with the properties from other model or services, it should be renamed to be more specific. Suggest a few better name according to the given information in this case.`, + }, + ], + { + modelPreferences: defaultLmFamily, + }, + zRenameCheckResult, +); +export const tooGenericRule = createRuleWithLmRuleChecker(aiChecker, { + name: ruleName, + severity: "warning", + description: + "Avoid too generic name which does not provide enough context or information about the property as well as may cause confliction with other properties from other models or services.", + messages: { + ...LmDiagnosticMessages, + errorOccurs: paramMessage`CSharpNaming: Unexpected error occurs when checking generic naming '${"modelName"}.${"propName"}'. You may check console or VSCode TypeSpec Output logs for more details. Error: ${"error"}`, + tooGeneric: paramMessage`CSharpNaming: Property '${"modelName"}.${"propName"}' is too generic. ${"newNameSuggestions"}`, + }, + create: (context) => { + return { + root: async (program) => { + aiChecker.messages.push({ + role: "user", + message: `As a context, the current project has the following services: [${listServices( + program, + ) + .map((s) => `{namespace: ${getTypeName(s.type)}, title: "${s.title ?? "No title"}"}`) + .join(", ")}]`, + }); + }, + modelProperty: async (property) => { + if ( + !isMyCode(property, context) || + !isDirectPropertyOfModel(property) || + isUnnamedModelProperty(property) + ) { + return; + } + const src = getSourceLocation(property); + // console.debug( + // `Start checking property '${property.model?.name ?? "N/A"}.${property.name}' at ${src?.file.path}:${src?.pos}:${src?.end}`, + // ); + + const docString = getDoc(context.program, property); + const [n, clientNameDec] = getClientNameFromDec(property, "csharp"); + const propName = n ?? property.name; + const modelName = property.model ? getTypeName(property.model) : "NoModel"; + const description = `It is a property name '${propName}' in model '${modelName}'. ${ + docString && docString.length > 0 ? `The property description is: ${docString}` : "" + }`; + const words = splitNameByUpperCase(propName); + if (words.length > 2) { + // only check whether the name is too generic if it has more than 2 words + return; + } + + const data: RenameData = { + originalName: propName, + description, + }; + + try { + const result = await aiChecker.queueDataToCheck(data); + if (result.renameNeeded) { + const suggestedNames = result.suggestedNames; + context.reportDiagnostic({ + target: property, + messageId: "tooGeneric", + format: { + modelName: property.model?.name ?? "unknown", + propName: property.name, + newNameSuggestions: + suggestedNames.length > 0 + ? `New name suggestions: ${suggestedNames.join(", ")}` + : "Please give it a more specific name.", + }, + codefixes: createRenameCodeFix(result, clientNameDec, context, property), + }); + } + } catch (error) { + // TODO: handle other error + reportLmErrors(error as LmResponseError, property, context, (r) => { + context.reportDiagnostic({ + target: property, + messageId: "errorOccurs", + format: { + modelName: property.model?.name ?? "unknown", + propName: property.name, + error: `${inspect(r)}`, + }, + }); + }); + return; + } + }, + }; + }, +}); diff --git a/packages/azure-linter/src/rules/boolean-property.naming.csharp.rule.ts b/packages/azure-linter/src/rules/boolean-property.naming.csharp.rule.ts new file mode 100644 index 00000000000..b525a4366cc --- /dev/null +++ b/packages/azure-linter/src/rules/boolean-property.naming.csharp.rule.ts @@ -0,0 +1,153 @@ +import { getDoc, paramMessage } from "@typespec/compiler"; +import { $ } from "@typespec/compiler/typekit"; +import { inspect } from "util"; +import { LmRuleChecker } from "../lm/lm-rule-checker.js"; +import { reportLmErrors } from "../lm/lm-utils.js"; +import { defaultLmFamily, LmDiagnosticMessages, LmResponseError } from "../lm/types.js"; +import { + createRenameCodeFix, + createRuleWithLmRuleChecker, + getClientNameFromDec, + isDirectPropertyOfModel, + isMyCode, + isUnnamedModelProperty, + splitNameByUpperCase, +} from "./rule-utils.js"; +import { RenameData, zRenameCheckResult } from "./types.js"; + +const ruleName = "csharp.naming.boolean-property-starts-with-verb"; +const aiChecker = new LmRuleChecker( + "bool-starts-with-verb", + [ + { + role: "user", + message: `Check the given boolean property names which are in camel or pascal case, the name MUST start with a proper verb (i.e. 'Is', 'Has', 'Can', 'Use'...), otherwise suggest a few new name that starts with a verb (i.e. 'Is', 'Has', 'Can', 'Use'...) in pascal case.`, + }, + ], + { + modelPreferences: defaultLmFamily, + }, + zRenameCheckResult, +); + +export const booleanPropertyStartsWithVerbRule = createRuleWithLmRuleChecker(aiChecker, { + name: ruleName, + severity: "warning", + description: "CSharp:Make sure boolean property's name starts with verb.", + messages: { + ...LmDiagnosticMessages, + errorOccurs: paramMessage`CSharpNaming: Unexpected error occurs when checking boolean property '${"modelName"}.${"propName"}'. You may check console or VSCode TypeSpec Output logs for more details. Error: ${"error"}`, + verbNeeded: paramMessage`CSharpNaming: Boolean property '${"modelName"}.${"propName"}' should start with a verb. ${"newNameSuggestions"}`, + }, + create: (context) => { + return { + modelProperty: async (property) => { + const tk = $(context.program); + let propName = property.name; + const propType = property.type; + const modelName = property.model?.name ?? "No Model"; + + // TODO: do we need to handle properties in ModelExpression? + if (property.node === undefined || propType !== tk.builtin.boolean) { + return; // Only check boolean properties + } + + if ( + !isMyCode(property, context) || + isUnnamedModelProperty(property) || + !isDirectPropertyOfModel(property) + ) { + return; + } + + const [n, csharpClientNameDec] = getClientNameFromDec(property, "csharp"); + if (n) { + propName = n; + } + + const propNameWords = splitNameByUpperCase(propName); + if (propNameWords.length > 0) { + const firstWord = propNameWords[0].toLowerCase(); + if ( + firstWord === "is" || + firstWord === "can" || + firstWord === "has" || + firstWord === "use" + ) { + // console.debug( + // `Skipping boolean property '${propName}' as it already starts with a common boolean verb: ${firstWord}`, + // ); + return; + } + } + + const docString = getDoc(context.program, property); + const docMsg = `It is a property name '${propName}' in model '${modelName}'. ${docString && docString.length > 0 ? `The property description is: ${docString}` : ""}`; + const description = `Check whether the original name is good and meets the requirements. ${docMsg}`; + const data: RenameData = { + originalName: propName, + description, + }; + + try { + const result = await aiChecker.queueDataToCheck(data); + if (result.renameNeeded) { + const suggestedNames = result.suggestedNames; + context.reportDiagnostic({ + target: csharpClientNameDec?.args[0].node ?? property, + messageId: "verbNeeded", + format: { + modelName, + propName, + newNameSuggestions: + suggestedNames.length > 0 + ? `New name suggestions: ${suggestedNames.join(", ")}` + : "Please add a verb prefix to the property name.", + }, + codefixes: createRenameCodeFix(result, csharpClientNameDec, context, property), + }); + } + } catch (e) { + // TODO: handle other errors + reportLmErrors(e as LmResponseError, property, context, (r) => { + context.reportDiagnostic({ + target: property, + messageId: "errorOccurs", + format: { + modelName, + propName, + error: `${inspect(r)}`, + }, + }); + }); + return; + } + + //const message = `Check boolean property name '${propName}' which is in camel or pascal case, the name MUST start with a proper verb (i.e. 'Is', 'Has', 'Can', 'Use'...), otherwise suggest a few new name that starts with a verb (i.e. 'Is', 'Has', 'Can', 'Use'...) in pascal case.\n${docMsg}`; + + // logger.debug( + // `Start calling askLanguageModeWithRetry for boolean property ${property.model?.name}.${propName} with message: ${message}`, + // ); + + //const startTime = Date.now(); + //logger.warning(`start of ask lm for property: ${modelName}.${propName}\ncallstack: ${new Error().stack}`); + // const result = await askLanguageModeWithRetry( + // context, + // `${ruleName}.${getTypeName(property.model!)}.${propName}`, + // [ + // { + // role: "user", + // message, + // }, + // ], + // { + // modelPreferences: ["gpt-4o"], + // }, + // zRenameRespones, + // 2, // retry count + // ); + //logger.warning("end of ask lm, duration: " + (Date.now() - startTime) + " ms"); + }, + }; + }, +}); diff --git a/packages/azure-linter/src/rules/duration-with-unit.naming.csharp.rule.ts b/packages/azure-linter/src/rules/duration-with-unit.naming.csharp.rule.ts new file mode 100644 index 00000000000..97ddfeea23d --- /dev/null +++ b/packages/azure-linter/src/rules/duration-with-unit.naming.csharp.rule.ts @@ -0,0 +1,104 @@ +import { getDoc, getTypeName, paramMessage } from "@typespec/compiler"; +import { $ } from "@typespec/compiler/typekit"; +import { inspect } from "util"; +import { LmRuleChecker } from "../lm/lm-rule-checker.js"; +import { reportLmErrors } from "../lm/lm-utils.js"; +import { defaultLmFamily, LmDiagnosticMessages, LmResponseError } from "../lm/types.js"; +import { + createRenameCodeFix, + createRuleWithLmRuleChecker, + getClientNameFromDec, + isDirectPropertyOfModel, + isMyCode, + isUnnamedModelProperty, +} from "./rule-utils.js"; +import { RenameData, zRenameCheckResult } from "./types.js"; + +const aiChecker = new LmRuleChecker( + "duration-with-unit", + [ + { + role: "user", + message: `Check the given property names which are in camel or pascal case. If the property is for internvals or durations, it MUST ends with a unit suffix in format '...In' (i.e: should be MonitoringIntervalInSeconds instead of MonitoringInterval, TimeToLiveDurationInMilliseconds instead of TimeToLiveDuration), otherwise suggest a new name with a proper suffix if you can determine the correct unit to use, otherwise DO NOT guess a unit suffix if you are not sure about the correct unit, just DON'T provide suggestions in that case.`, + }, + ], + { + modelPreferences: defaultLmFamily, + }, + zRenameCheckResult, +); + +const ruleName = "csharp.naming.duration-with-unit"; +export const durationWithUnitRule = createRuleWithLmRuleChecker(aiChecker, { + name: ruleName, + severity: "warning", + description: + "DO End property or parameter names of type integer that represent intervals or durations with units, for example: MonitoringInterval -> MonitoringIntervalInSeconds.", + messages: { + ...LmDiagnosticMessages, + errorOccurs: paramMessage`CSharpNaming: Unexpected error occurs when checking internals or durations unit for property '${"modelName"}.${"propName"}'. You may check console or VSCode TypeSpec Output logs for more details. Error: ${"error"}`, + unitNeeded: paramMessage`CSharpNaming: Property '${"modelName"}.${"propName"}' is for intervals or durations, but does not have a unit suffix. ${"newNameSuggestions"}`, + }, + create: (context) => { + return { + modelProperty: async (property) => { + const tk = $(context.program); + if (property.node === undefined || property.type !== tk.builtin.duration) { + return; + } + if ( + !isMyCode(property, context) || + isUnnamedModelProperty(property) || + !isDirectPropertyOfModel(property) + ) { + return; + } + const docString = getDoc(context.program, property); + const [n, clientNameDec] = getClientNameFromDec(property, "csharp"); + const modelname = property.model ? getTypeName(property.model) : "NoModel"; + const propName = n ?? property.name; + const description = `property '${propName}' of model '${modelname}'${ + docString ? `, description: '${docString}'` : "" + }`; + const renameData: RenameData = { + originalName: propName, + description, + }; + + try { + const result = await aiChecker.queueDataToCheck(renameData); + if (result.renameNeeded) { + const suggestedNames = result.suggestedNames; + context.reportDiagnostic({ + target: property, + messageId: "unitNeeded", + format: { + modelName: property.model?.name ?? "unknown", + propName: property.name, + newNameSuggestions: + suggestedNames.length > 0 + ? `New name suggestions: ${suggestedNames.join(", ")}` + : "Please append a unit suffix to the property name.", + }, + codefixes: createRenameCodeFix(result, clientNameDec, context, property), + }); + } + } catch (error) { + // TODO: handle other errors + reportLmErrors(error as LmResponseError, property, context, (r) => { + context.reportDiagnostic({ + target: property, + messageId: "errorOccurs", + format: { + modelName: property.model?.name ?? "unknown", + propName: property.name, + error: `${inspect(r)}`, + }, + }); + }); + return; + } + }, + }; + }, +}); diff --git a/packages/azure-linter/src/rules/no-interfaces.rule.ts b/packages/azure-linter/src/rules/no-interfaces.rule.ts new file mode 100644 index 00000000000..4a80c38d53f --- /dev/null +++ b/packages/azure-linter/src/rules/no-interfaces.rule.ts @@ -0,0 +1,20 @@ +import { createRule } from "@typespec/compiler"; + +export const noInterfaceRule = createRule({ + name: "no-interface", + severity: "warning", + description: "Make sure interface are not used.", + messages: { + default: + "Interface shouldn't be used with this library. Keep operations at the root.", + }, + create: (context) => { + return { + interface: (iface) => { + context.reportDiagnostic({ + target: iface, + }); + }, + }; + }, +}); diff --git a/packages/azure-linter/src/rules/rule-utils.ts b/packages/azure-linter/src/rules/rule-utils.ts new file mode 100644 index 00000000000..d23da1e7d9a --- /dev/null +++ b/packages/azure-linter/src/rules/rule-utils.ts @@ -0,0 +1,260 @@ +import { + CodeFix, + CodeFixContext, + createRule, + createSourceFile, + DecoratorApplication, + DiagnosticMessages, + DiagnosticTarget, + getSourceLocation, + getTypeName, + InsertTextCodeFixEdit, + LinterRuleContext, + LinterRuleDefinition, + ModelProperty, + Namespace, + normalizePath, + Program, + SemanticNodeListener, + Type, +} from "@typespec/compiler"; +import { + IdentifierNode, + MemberExpressionNode, + SyntaxKind, + TypeSpecScriptNode, + UsingStatementNode, +} from "@typespec/compiler/ast"; +import { join } from "path"; +import { AnyZodObject } from "zod"; +import { LmRuleChecker } from "../lm/lm-rule-checker.js"; +import { RenameCheckResult } from "./types.js"; + +export function isMyCode( + target: DiagnosticTarget, + context: LinterRuleContext, +): boolean { + const srcFile = getSourceLocation(target); + const tspFileContext = context.program.getSourceFileLocationContext(srcFile.file); + return tspFileContext.type === "project"; +} + +export function isDirectPropertyOfModel(property: ModelProperty): boolean { + return property.model?.properties.has(property.name) ?? false; +} + +export function isUnnamedModelProperty(property: ModelProperty): boolean { + return property.model?.name === undefined || property.model.name === ""; +} + +export function getDecratorStringArgValue( + dec: DecoratorApplication, + argIndex: number, +): string | undefined { + if (dec.args.length <= argIndex) { + return undefined; + } + const arg = dec.args[argIndex].value; + if (arg.entityKind === "Value" && arg.valueKind === "StringValue") { + return arg.value; + } else { + return undefined; + } +} + +export function getFullNamespace(ns: Namespace | undefined): string { + if (!ns || !ns.name) { + return ""; + } else { + const prefix = getFullNamespace(ns.namespace); + return prefix ? `${prefix}.${ns.name}` : ns.name; + } +} + +export function getFullUsing(using: UsingStatementNode): string { + const getFullUsingName = (member: MemberExpressionNode | IdentifierNode): string => { + if (member.kind === SyntaxKind.Identifier) { + return member.sv; + } else { + if (member.base === undefined) { + return member.id.sv; + } else { + return `${getFullUsingName(member.base)}.${member.id.sv}`; + } + } + }; + return getFullUsingName(using.name); +} + +export function hasUsing(scriptNode: TypeSpecScriptNode, usingName: string) { + if (scriptNode !== undefined) { + for (const u of scriptNode.usings) { + const fullUsingName = getFullUsing(u); + if (fullUsingName === usingName) { + return true; + } + } + } +} + +/** The first found one will be returned */ +export function getClientNameFromDec( + target: Type, + language: string, +): [string | undefined, DecoratorApplication | undefined] { + const decs = getDecorators(target, "Azure.ClientGenerator.Core", "@clientName"); + for (const dec of decs) { + const lang = getDecratorStringArgValue(dec, 1); + const name = getDecratorStringArgValue(dec, 0); + if (lang === language) { + return [name, dec]; + } + } + return [undefined, undefined]; +} + +export function getDecorators(target: T, decNamespace: string, decName: string) { + const foundDecs: DecoratorApplication[] = []; + if ("decorators" in target && target.decorators) { + for (const dec of target.decorators) { + if (!dec.definition) { + continue; + } + const curDecName = dec.definition.name; + const curDecNamespace = getFullNamespace(dec.definition.namespace); + if (curDecName === decName && curDecNamespace === decNamespace) { + foundDecs.push(dec); + } + } + } + return foundDecs; +} + +// export function getDoc(target: T): string[] { +// const docArray: string[] = []; +// for (const doc of target.node?.docs ?? []) { +// docArray.push(...doc.content.map((c) => c.text)); +// } + +// const docDecs = getDecorators(target, "TypeSpec", "@doc"); +// for (const dec of docDecs) { +// const docValue = getDecratorStringArgValue(dec, 0); +// if (docValue) { +// docArray.push(docValue); +// } +// } +// return docArray; +// } + +export function findClientTsp(program: Program): TypeSpecScriptNode | undefined { + const root = program.projectRoot; + const clientTspPath = normalizePath(join(root, "client.tsp")); + for (const [k, v] of program.sourceFiles) { + if (normalizePath(k) === clientTspPath) { + return v; + } + } + return undefined; +} + +export function createRenameCodeFix( + aiResult: RenameCheckResult, + existingClientNameDec: DecoratorApplication | undefined, + context: LinterRuleContext, + target: Type, +): CodeFix[] { + const targetNode = target.node; + if (targetNode === undefined) { + // this is not expected + return []; + } + const fixes = aiResult.suggestedNames.map((newName) => { + return { + id: `rename-from-lm-suggestion-${getTypeName(target)}-${newName}`, + label: `Rename to "${newName}" by adding @@clientName to 'client.tsp' file`, + fix: (fixContext: CodeFixContext) => { + if (!existingClientNameDec) { + let fix: InsertTextCodeFixEdit; + const clientTsp = findClientTsp(context.program); + const clientNameDecName = + clientTsp && hasUsing(clientTsp, "Azure.ClientGenerator.Core") + ? "@clientName" + : "@Azure.ClientGenerator.Core.clientName"; + const targetName = getTypeName(target); + if (clientTsp) { + const p = clientTsp.file.text.length; + fix = { + kind: "insert-text", + text: `\n@${clientNameDecName}(${targetName}, "${newName}", "csharp");`, + pos: p, + file: clientTsp.file, + }; + } else { + const root = context.program.projectRoot; + const clientTspPath = join(root, "client.tsp"); + + const s = createSourceFile("", clientTspPath); + fix = { + kind: "insert-text", + text: `import "@azure-tools/typespec-client-generator-core"; +import "./main.tsp"; + +@${clientNameDecName}(${targetName}, "${newName}", "csharp");`, + pos: 0, + file: s, + }; + } + return fix; + } else { + const location = getSourceLocation(existingClientNameDec.args[0].node!); + return fixContext.replaceText(location, `"${newName}"`); + } + }, + }; + }); + return fixes; +} + +/** a simple function to split name by upper case letters, plase be aware that non char letter like -_0-9 will be ignored */ +export function splitNameByUpperCase(name: string): string[] { + const parts = name.split(/(?=[A-Z])|[^a-zA-Z]+/); + if (parts.length === 0) { + return []; + } + const isUpperCaseLetter = (char: string) => char.length === 1 && char >= "A" && char <= "Z"; + const result = [parts[0]]; + for (let i = 1; i < parts.length; i++) { + if (isUpperCaseLetter(parts[i]) && isUpperCaseLetter(parts[i - 1])) { + result[result.length - 1] += parts[i]; + } else { + result.push(parts[i]); + } + } + return result.filter((part) => part.length > 0); +} + +export function createRuleWithLmRuleChecker< + const N extends string, + const T extends DiagnosticMessages, + const P extends object, + const R extends AnyZodObject, +>(lmChecker: LmRuleChecker, definition: LinterRuleDefinition) { + const createFunc = (context: LinterRuleContext): SemanticNodeListener => { + const listener = definition.create(context); + return { + ...definition.create(context), + exitRoot: async (program) => { + let result = undefined; + if (listener.exitRoot) { + result = await listener.exitRoot(program); + } + await lmChecker.checkAllData(context); + return result; + }, + }; + }; + return createRule({ + ...definition, + create: createFunc, + }); +} diff --git a/packages/azure-linter/src/rules/types.ts b/packages/azure-linter/src/rules/types.ts new file mode 100644 index 00000000000..5eee812798f --- /dev/null +++ b/packages/azure-linter/src/rules/types.ts @@ -0,0 +1,26 @@ +import z from "zod"; + +export const zRenameData = z.object({ + originalName: z + .string() + .describe("The exact original name given by user to check whether it needs to be changed."), + description: z + .string() + .describe( + "Description about the name, including requirements about the name and other related information that can help to decide whether the name is good or not.", + ), +}); +export type RenameData = z.infer; + +export const zRenameCheckResult = z.object({ + renameNeeded: z.boolean().describe("Indicates if the name needs to be changed. "), + originalName: z + .string() + .describe("The exact original name given by user to check whether it needs to be changed."), + suggestedNames: z + .array(z.string()) + .describe( + "An array of suggested names if it needs to be changed. The suggested names should meet the requirements provided by user and can describe itself well. The suggested names should be listed in a way that the first one is the most preferred name. Provide 3 suggestions at most. Double check you are not suggesting the original name as one of the suggestions.", + ), +}); +export type RenameCheckResult = z.infer; diff --git a/packages/azure-linter/src/testing/index.ts b/packages/azure-linter/src/testing/index.ts new file mode 100644 index 00000000000..f21d212b01a --- /dev/null +++ b/packages/azure-linter/src/testing/index.ts @@ -0,0 +1,11 @@ +import { resolvePath } from "@typespec/compiler"; +import { + createTestLibrary, + TypeSpecTestLibrary, +} from "@typespec/compiler/testing"; +import { fileURLToPath } from "url"; + +export const AzureLinterTestLibrary: TypeSpecTestLibrary = createTestLibrary({ + name: "azure-linter", + packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../../"), +}); diff --git a/packages/azure-linter/src/utils.ts b/packages/azure-linter/src/utils.ts new file mode 100644 index 00000000000..93a3c30288e --- /dev/null +++ b/packages/azure-linter/src/utils.ts @@ -0,0 +1,136 @@ +import { createHash } from "crypto"; +import { readFile, stat, writeFile } from "fs/promises"; +import { jsonrepair } from "jsonrepair"; +import { ZodType } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { logger } from "./log/logger.js"; + +export function tryParseConnectionString(cs: string) { + const result: Record = {}; + cs.split(";") + .filter((s) => s.trim().length > 0) + .forEach((part) => { + const index = part.indexOf("="); + if (index < 0) { + logger.error(`Invalid connection string: ${cs}`); + return undefined; + } + const key = part.substring(0, index).trim(); + const value = part.substring(index + 1).trim(); + if (!key || !value) { + logger.error(`Invalid connection string: ${cs}`); + return undefined; + } + result[key] = value; + }); + return result; +} + +export function tryRepairAndParseJson(jsonStr: string | undefined): T | undefined { + if (!jsonStr) { + return undefined; + } + let cleaned = jsonStr.trim(); + + // Remove triple backticks at the start and end + if (cleaned.startsWith("```")) { + cleaned = cleaned.replace(/^```(\w*\n)?/, ""); + } + if (cleaned.endsWith("```")) { + cleaned = cleaned.replace(/```$/, ""); + } + + try { + cleaned = jsonrepair(cleaned); + const parsed = JSON.parse(cleaned); + return parsed; + } catch (e) { + logger.error(`Error to repare and parsing JSON: ${e}`); + return undefined; + } +} + +export function toJsonSchemaString(obj: ZodType) { + return JSON.stringify(zodToJsonSchema(obj), undefined, 2); +} + +export async function isFileExists(filePath: string): Promise { + try { + const s = await stat(filePath); + return s.isFile(); + } catch { + return false; + } +} + +export async function tryReadJsonFile( + filePath: string, + zType?: ZodType, +): Promise { + const data = await tryReadFile(filePath); + if (!data) { + return undefined; + } + const parsed = tryRepairAndParseJson(data); + if (!parsed) { + logger.debug(`Failed to parse JSON from file: ${filePath}`); + return undefined; + } + if (!zType) { + return parsed; + } + + const safeParsed = zType.safeParse(parsed); + if (!safeParsed.success) { + logger.debug(`Failed to parse JSON from file: ${filePath}. Error: ${safeParsed.error}`); + return undefined; + } + return safeParsed.data as T; +} + +export async function tryReadFile(filePath: string): Promise { + try { + const data = await readFile(filePath, "utf-8"); + return data; + } catch (e) { + logger.debug(`Failed to read file ${filePath}: ${e}`); + return undefined; + } +} + +export async function tryWriteFile(filePath: string, content: string): Promise { + try { + await writeFile(filePath, content, "utf-8"); + return true; + } catch (e) { + logger.error(`Failed to write file ${filePath}: ${e}`); + return false; + } +} + +export function getRecordValue(record: Record, key: string): T | undefined { + return record[key]; +} + +export function setRecordValue(record: Record, key: string, value: T): void { + record[key] = value; +} + +export function hasRecordKey(record: Record, key: string): boolean { + return key in record; +} + +export function foreachRecord( + record: Record, + callback: (key: string, value: T) => void, +): void { + for (const key in record) { + if (Object.prototype.hasOwnProperty.call(record, key)) { + callback(key, record[key]); + } + } +} + +export function md5(str: string): string { + return createHash("md5").update(str).digest("hex"); +} diff --git a/packages/azure-linter/test/rules/no-interfaces.rule.test.ts b/packages/azure-linter/test/rules/no-interfaces.rule.test.ts new file mode 100644 index 00000000000..bf714fcf92a --- /dev/null +++ b/packages/azure-linter/test/rules/no-interfaces.rule.test.ts @@ -0,0 +1,34 @@ +import { + LinterRuleTester, + createLinterRuleTester, + createTestRunner, +} from "@typespec/compiler/testing"; +import { beforeEach, describe, it } from "node:test"; +import { noInterfaceRule } from "../../src/rules/no-interfaces.rule.js"; + +describe("noInterfaceRule", () => { + let ruleTester: LinterRuleTester; + + beforeEach(async () => { + const runner = await createTestRunner(); + ruleTester = createLinterRuleTester( + runner, + noInterfaceRule, + "azure-linter", + ); + }); + + describe("models", () => { + it("emit diagnostics if using interfaces", async () => { + await ruleTester.expect(`interface Test {}`).toEmitDiagnostics({ + code: "azure-linter/no-interface", + message: + "Interface shouldn't be used with this library. Keep operations at the root.", + }); + }); + + it("should be valid if operation is at the root", async () => { + await ruleTester.expect(`op test(): void;`).toBeValid(); + }); + }); +}); diff --git a/packages/azure-linter/test/test-host.ts b/packages/azure-linter/test/test-host.ts new file mode 100644 index 00000000000..20c2561cc4f --- /dev/null +++ b/packages/azure-linter/test/test-host.ts @@ -0,0 +1,16 @@ +import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; +import { AzureLinterTestLibrary } from "../src/testing/index.js"; + +export async function createAzureLinterTestHost() { + return createTestHost({ + libraries: [AzureLinterTestLibrary], + }); +} + +export async function createAzureLinterTestRunner() { + const host = await createAzureLinterTestHost(); + + return createTestWrapper(host, { + autoUsings: ["AzureLinter"], + }); +} diff --git a/packages/azure-linter/tsconfig.json b/packages/azure-linter/tsconfig.json new file mode 100644 index 00000000000..63c79b0b4b0 --- /dev/null +++ b/packages/azure-linter/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "rootDir": ".", + "outDir": "dist", + "sourceMap": true, + "declaration": true, + "skipLibCheck": true, + + /* Linting */ + "strict": true + }, + "include": ["src", "test"] +} diff --git a/packages/compiler/src/core/linter.ts b/packages/compiler/src/core/linter.ts index edac393fbf3..136a6b95187 100644 --- a/packages/compiler/src/core/linter.ts +++ b/packages/compiler/src/core/linter.ts @@ -1,3 +1,4 @@ +import { isPromise } from "../utils/misc.js"; import { DiagnosticCollector, compilerAssert, createDiagnosticCollector } from "./diagnostics.js"; import { getLocationContext } from "./helpers/location-context.js"; import { defineLinter } from "./library.js"; @@ -7,7 +8,7 @@ import { createDiagnostic } from "./messages.js"; import { NameResolver } from "./name-resolver.js"; import type { Program } from "./program.js"; import { EventEmitter, mapEventEmitterToNodeListener, navigateProgram } from "./semantic-walker.js"; -import { startTimer, time } from "./stats.js"; +import { startTimer } from "./stats.js"; import { Diagnostic, DiagnosticMessages, @@ -27,7 +28,7 @@ type LinterLibraryInstance = { linter: LinterResolvedDefinition }; export interface Linter { extendRuleSet(ruleSet: LinterRuleSet): Promise; registerLinterLibrary(name: string, lib?: LinterLibraryInstance): void; - lint(): LinterResult; + lint(): Promise; } export interface LinterStats { @@ -158,7 +159,7 @@ export function createLinter( return diagnostics.diagnostics; } - function lint(): LinterResult { + async function lint(): Promise { const diagnostics = createDiagnosticCollector(); const eventEmitter = new EventEmitter(); const stats: LinterStats = { @@ -174,19 +175,37 @@ export function createLinter( ); const timer = startTimer(); + // Because we are hooking multiple callback to the event emitter and won't be able to return a result to the walker + // so try to collect back all the promise and await them here explicitly + const promisesMap: Map, Promise[]> = new Map(); for (const rule of enabledRules.values()) { const createTiming = startTimer(); const listener = rule.create(createLinterRuleContext(program, rule, diagnostics)); stats.runtime.rules[rule.id] = createTiming.end(); + const promises: Promise[] = []; + promisesMap.set(rule, promises); for (const [name, cb] of Object.entries(listener)) { const timedCb = (...args: any[]) => { - const duration = time(() => (cb as any)(...args)); - stats.runtime.rules[rule.id] += duration; + const timer = startTimer(); + const result = (cb as any)(...args); + if (isPromise(result)) { + const rr = result.then(() => { + const duration = timer.end(); + stats.runtime.rules[rule.id] += duration; + }); + promises.push(rr); + } else { + const duration = timer.end(); + stats.runtime.rules[rule.id] += duration; + } }; eventEmitter.on(name as any, timedCb); } } navigateProgram(program, mapEventEmitterToNodeListener(eventEmitter)); + for (const ps of promisesMap.values()) { + await Promise.all(ps); + } stats.runtime.total = timer.end(); return { diagnostics: diagnostics.diagnostics, stats }; } diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index 59fecec7ec0..efbbf64bc84 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -321,7 +321,7 @@ async function createProgram( } // Linter stage - const lintResult = linter.lint(); + const lintResult = await linter.lint(); runtimeStats.linter = lintResult.stats.runtime; program.reportDiagnostics(lintResult.diagnostics); diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index 6342db3d26a..e4ab89f12d7 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -1,3 +1,4 @@ +import { isPromise } from "../utils/misc.js"; import type { Program } from "./program.js"; import { isTemplateDeclaration } from "./type-utils.js"; import { @@ -60,6 +61,8 @@ export function navigateProgram( context.emit("root", program); navigateNamespaceType(program.getGlobalNamespaceType(), context); + + context.emit("exitRoot", program); } /** @@ -148,7 +151,16 @@ function createNavigationContext( ): NavigationContext { return { visited: new Set(), - emit: (key, ...args) => (listeners as any)[key]?.(...(args as [any])), + emit: (key, ...args) => { + const r = (listeners as any)[key]?.(...(args as [any])); + if (isPromise(r)) { + // We won't await here to keep the API sync which is good enough for some scenarios which don't require await + // TODO: should we make the whole API async which will be a breaking change? + return undefined; + } else { + return r; + } + }, options: computeOptions(options), }; } @@ -476,6 +488,7 @@ export class EventEmitter any }> { const eventNames: Array = [ "root", + "exitRoot", "templateParameter", "exitTemplateParameter", "scalar", diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 912701f3a82..e6ec2165e92 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -2129,7 +2129,7 @@ export enum ListenerFlow { /** * Listener function. Can return false to stop recursion. */ -type TypeListener = (context: T) => ListenerFlow | undefined | void; +type TypeListener = (context: T) => ListenerFlow | undefined | void | Promise; type exitListener = T extends string ? `exit${T}` : T; type ListenerForType = T extends Type ? { [k in Uncapitalize | exitListener]?: TypeListener } @@ -2138,7 +2138,8 @@ type ListenerForType = T extends Type export type TypeListeners = UnionToIntersection>; export type SemanticNodeListener = { - root?: (context: Program) => void | undefined; + root?: (context: Program) => void | undefined | Promise; + exitRoot?: (context: Program) => void | undefined | Promise; } & TypeListeners; export type DiagnosticReportWithoutTarget< diff --git a/packages/compiler/src/experimental/index.ts b/packages/compiler/src/experimental/index.ts index ba9304d39a3..e675d4f7db8 100644 --- a/packages/compiler/src/experimental/index.ts +++ b/packages/compiler/src/experimental/index.ts @@ -1,4 +1,5 @@ export { createSourceLoader as unsafe_createSourceLoader } from "../core/source-loader.js"; +export { ChatCompleteOptions, ChatMessage, LmProvider } from "../server/types.js"; export { MutableType as unsafe_MutableType, Mutator as unsafe_Mutator, diff --git a/packages/compiler/src/server/client-config-provider.ts b/packages/compiler/src/server/client-config-provider.ts index 469cee74883..f4c77b4104e 100644 --- a/packages/compiler/src/server/client-config-provider.ts +++ b/packages/compiler/src/server/client-config-provider.ts @@ -23,20 +23,23 @@ export interface ClientConfigProvider { */ initialize(connection: Connection, host: ServerHost): Promise; + isInitialized: boolean; + config?: Config; } export function createClientConfigProvider(): ClientConfigProvider { let config: Config | undefined; + let initialized = false; async function initialize(connection: Connection, host: ServerHost): Promise { try { const configs = await connection.workspace.getConfiguration("typespec"); - host.log({ level: "debug", message: "VSCode settings loaded", detail: configs }); - // Transform the raw configuration to match our Config interface config = deepClone(configs); + host.log({ level: "debug", message: "vscode settings loaded", detail: config }); + connection.onDidChangeConfiguration(async (params) => { if (params.settings) { const newConfigs = params.settings?.typespec; @@ -45,6 +48,7 @@ export function createClientConfigProvider(): ClientConfigProvider { host.log({ level: "debug", message: "Configuration changed", detail: params.settings }); }); + initialized = true; } catch (error) { host.log({ level: "error", @@ -59,5 +63,8 @@ export function createClientConfigProvider(): ClientConfigProvider { get config() { return config; }, + get isInitialized() { + return initialized; + }, }; } diff --git a/packages/compiler/src/server/compile-service.ts b/packages/compiler/src/server/compile-service.ts index 57faf85faae..c0ec42ed265 100644 --- a/packages/compiler/src/server/compile-service.ts +++ b/packages/compiler/src/server/compile-service.ts @@ -29,7 +29,7 @@ import { ServerCompileOptions, } from "./server-compile-manager.js"; import { CompileResult, ServerHost, ServerLog } from "./types.js"; -import { UpdateManger, UpdateType } from "./update-manager.js"; +import { UpdateManager, UpdateType } from "./update-manager.js"; /** * Service managing compilation/caching of different TypeSpec projects @@ -73,7 +73,7 @@ export interface CompileServiceOptions { readonly fileService: FileService; readonly serverHost: ServerHost; readonly compilerHost: CompilerHost; - readonly updateManager: UpdateManger; + readonly updateManager: UpdateManager; readonly log: (log: ServerLog) => void; readonly clientConfigsProvider?: ClientConfigProvider; } @@ -105,7 +105,7 @@ export function createCompileService({ } function notifyChange(document: TextDocument | TextDocumentIdentifier, updateType: UpdateType) { - updateManager.scheduleUpdate(document, updateType); + void updateManager.scheduleUpdate(document, updateType); } /** @@ -137,7 +137,7 @@ export function createCompileService({ } const config = await getConfig(mainFile); configFilePath = config.filename; - log({ level: "debug", message: `config resolved`, detail: config }); + //log({ level: "debug", message: `config resolved`, detail: config }); const [optionsFromConfig, _] = resolveOptionsFromConfig(config, { cwd: getDirectoryPath(path), }); diff --git a/packages/compiler/src/server/lib-provider.ts b/packages/compiler/src/server/lib-provider.ts index 746fa83dc44..556726c934d 100644 --- a/packages/compiler/src/server/lib-provider.ts +++ b/packages/compiler/src/server/lib-provider.ts @@ -58,7 +58,7 @@ export class LibraryProvider { // don't add to cache when failing to load package.json which is unexpected if (!data) return false; if ( - (data.devDependencies && data.devDependencies["@typespec/compiler"]) || + (data.peerDependencies && data.peerDependencies["@typespec/compiler"]) || (data.dependencies && data.dependencies["@typespec/compiler"]) ) { const exports = await pkg.getModuleExports(); diff --git a/packages/compiler/src/server/server-compile-manager.ts b/packages/compiler/src/server/server-compile-manager.ts index 0304b4ee209..f2af34756be 100644 --- a/packages/compiler/src/server/server-compile-manager.ts +++ b/packages/compiler/src/server/server-compile-manager.ts @@ -10,7 +10,7 @@ import { } from "../index.js"; import { ENABLE_SERVER_COMPILE_LOGGING } from "./constants.js"; import { trackActionFunc } from "./server-track-action-task.js"; -import { UpdateManger } from "./update-manager.js"; +import { UpdateManager } from "./update-manager.js"; /** * core: linter and emitter will be set to [] when trigger compilation @@ -40,7 +40,7 @@ export class ServerCompileManager { private logDebug: (msg: string) => void; constructor( - private updateManager: UpdateManger, + private updateManager: UpdateManager, private compilerHost: CompilerHost, private log: (log: ServerLog) => void, ) { @@ -251,7 +251,7 @@ export class CompileTracker { static compile( id: number, - updateManager: UpdateManger, + updateManager: UpdateManager, host: CompilerHost, mainFile: string, options: CompilerOptions = {}, @@ -300,7 +300,7 @@ export class CompileTracker { private constructor( private id: number, - private updateManager: UpdateManger, + private updateManager: UpdateManager, private entrypoint: string, private compileResultPromise: Promise, private version: number, diff --git a/packages/compiler/src/server/server.ts b/packages/compiler/src/server/server.ts index 8cac5480d01..a45339e5321 100644 --- a/packages/compiler/src/server/server.ts +++ b/packages/compiler/src/server/server.ts @@ -16,7 +16,15 @@ import { NodeHost } from "../core/node-host.js"; import { typespecVersion } from "../manifest.js"; import { createClientConfigProvider } from "./client-config-provider.js"; import { createServer } from "./serverlib.js"; -import { CustomRequestName, Server, ServerHost, ServerLog } from "./types.js"; +import { + ChatCompleteOptions, + ChatMessage, + CustomRequestName, + LmProvider, + Server, + ServerHost, + ServerLog, +} from "./types.js"; let server: Server | undefined = undefined; @@ -153,6 +161,42 @@ function main() { documents.onDidClose(profile(s.documentClosed)); documents.onDidOpen(profile(s.documentOpened)); + let chatCompleteRequestId = 0; + const lmProvider: LmProvider = { + chatComplete: async ( + messages: ChatMessage[], + options: ChatCompleteOptions, + ): Promise => { + const start = new Date(); + const id = chatCompleteRequestId++; + host.log({ + level: "debug", + message: `[ChatComplete #${id}][${start.toTimeString()}] start sending custom/chatCompletion event`, + }); + try { + const r = await connection.sendRequest("custom/chatCompletion", { + messages, + lmOptions: options, + id: id.toString(), + }); + const end = new Date(); + host.log({ + level: "debug", + message: `[ChatComplete #${id}][${end.toTimeString()}] custom/chatCompletion event finished in ${end.getTime() - start.getTime()} ms`, + }); + return r as string; + } catch (e) { + host.log({ + level: "error", + message: `chatComplete failed with error: ${inspect(e)}`, + }); + return ""; + } + }, + }; + + (globalThis as any).TspExLmProvider = lmProvider; + documents.listen(connection); connection.listen(); } diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts index 955bd3997d8..3a5250c1472 100644 --- a/packages/compiler/src/server/serverlib.ts +++ b/packages/compiler/src/server/serverlib.ts @@ -134,7 +134,7 @@ import { ServerSourceFile, ServerWorkspaceFolder, } from "./types.js"; -import { UpdateManger } from "./update-manager.js"; +import { UpdateManager } from "./update-manager.js"; export function createServer( host: ServerHost, @@ -161,7 +161,12 @@ export function createServer( (exports) => exports.$linter !== undefined, ); - const updateManager = new UpdateManger(log); + const updateManager = new UpdateManager("doc-update", log); + + const signatureHelpUpdateManager = new UpdateManager( + "signature-help", + log, + ); const compileService = createCompileService({ fileService, @@ -179,6 +184,8 @@ export function createServer( let isInitialized = false; let pendingMessages: ServerLog[] = []; + let sId = 0; + return { get pendingMessages() { return pendingMessages; @@ -247,6 +254,12 @@ export function createServer( } }); + signatureHelpUpdateManager.setCallback(async (updates, triggeredBy) => { + // for signature help, we should always compile against the document that triggered the request + // debounce can help to avoid the unnecessary triggering and compiler cache should be able to avoid the duplicates compile + return await compileInCoreMode(triggeredBy); + }); + const capabilities: ServerCapabilities = { textDocumentSync: TextDocumentSyncKind.Incremental, definitionProvider: true, @@ -366,6 +379,8 @@ export function createServer( } function initialized(params: InitializedParams): void { + updateManager.start(); + signatureHelpUpdateManager.start(); isInitialized = true; log({ level: "info", message: "Initialization complete." }); } @@ -513,7 +528,7 @@ export function createServer( // There will be no event triggered if the renamed file is not opened in vscode, also even when it's opened // there will be only closed and opened event triggered for the old and new file url, so send fire the update // explicitly here to make sure the change is not missed. - updateManager.scheduleUpdate({ uri: fileService.getURL(mainFile) }, "renamed"); + void updateManager.scheduleUpdate({ uri: fileService.getURL(mainFile) }, "renamed"); // Add this method to resolve timing issues between renamed files and `fs.stat` // to prevent `fs.stat` from getting the files before modification. @@ -843,7 +858,14 @@ export function createServer( async function getSignatureHelp(params: SignatureHelpParams): Promise { if (isTspConfigFile(params.textDocument)) return undefined; - const result = await compileInCoreMode(params.textDocument); + const id = sId++; + log({ level: "debug", message: `getSignatureHelp start ${id}` }); + const result = await signatureHelpUpdateManager.scheduleUpdate(params.textDocument, "changed"); + log({ + level: "debug", + message: `getSignatureHelp got result ${id}: isUndefined = ${result === undefined}`, + }); + //const result = await compileInCoreMode(params.textDocument); if (result === undefined) { return undefined; } diff --git a/packages/compiler/src/server/types.ts b/packages/compiler/src/server/types.ts index 4b38151220c..42b257e25e8 100644 --- a/packages/compiler/src/server/types.ts +++ b/packages/compiler/src/server/types.ts @@ -210,3 +210,16 @@ export type InitProjectConfig = ScaffoldingConfig; export type InitProjectTemplate = InitTemplate; export type InitProjectTemplateLibrarySpec = InitTemplateLibrarySpec; export type InitProjectTemplateEmitterTemplate = EmitterTemplate; + +export interface ChatCompleteOptions { + modelPreferences?: string; +} + +export interface ChatMessage { + role: "user" | "assist"; + message: string; +} + +export interface LmProvider { + chatComplete(messages: ChatMessage[], options: ChatCompleteOptions): Promise; +} diff --git a/packages/compiler/src/server/update-manager.ts b/packages/compiler/src/server/update-manager.ts index 3c42c1b9243..e39ef08f24f 100644 --- a/packages/compiler/src/server/update-manager.ts +++ b/packages/compiler/src/server/update-manager.ts @@ -1,5 +1,6 @@ import { TextDocumentIdentifier } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; +import { DeferredPromise } from "../utils/deferred-promise.js"; import { ENABLE_UPDATE_MANAGER_LOGGING } from "./constants.js"; import { ServerLog } from "./types.js"; @@ -10,43 +11,66 @@ interface PendingUpdate { export type UpdateType = "opened" | "changed" | "closed" | "renamed"; -type UpdateCallback = (updates: PendingUpdate[]) => Promise; +type UpdateCallback = ( + updates: PendingUpdate[], + triggeredBy: TextDocument | TextDocumentIdentifier, +) => Promise; /** * Track file updates and recompile the affected files after some debounce time. */ -export class UpdateManger { +export class UpdateManager { #pendingUpdates = new Map(); - #updateCb?: UpdateCallback; + #updateCb?: UpdateCallback; // overall version which should be bumped for any actual doc change #docChangedVersion = 0; - #scheduleBatchUpdate: () => void; + #scheduleBatchUpdate: ( + triggeredBy: TextDocument | TextDocumentIdentifier, + ) => Promise; #docChangedTimesteps: number[] = []; + #isStarted = false; private _log: (sl: ServerLog) => void; - constructor(log: (sl: ServerLog) => void) { + /** + * + * @param name For logging purpose, identify different update manager if there are multiple + * @param log + */ + constructor( + private name: string, + log: (sl: ServerLog) => void, + ) { this._log = typeof process !== "undefined" && process?.env?.[ENABLE_UPDATE_MANAGER_LOGGING]?.toLowerCase() === "true" ? (sl: ServerLog) => { - log({ ...sl, message: `#FromUpdateManager: ${sl.message}` }); + log({ ...sl, message: `#FromUpdateManager(${this.name}): ${sl.message}` }); } : () => {}; - this.#scheduleBatchUpdate = debounceThrottle( - async () => { + this.#scheduleBatchUpdate = debounceThrottle< + T | undefined, + TextDocument | TextDocumentIdentifier + >( + async (arg: TextDocument | TextDocumentIdentifier) => { const updates = this.#pendingUpdates; this.#pendingUpdates = new Map(); - if (updates.size > 0) { - await this.#update(Array.from(updates.values())); - } + return await this.#update(Array.from(updates.values()), arg); }, + () => (this.#isStarted ? "ready" : "pending"), this.getAdaptiveDebounceDelay, this._log, ); } - public setCallback(callback: UpdateCallback) { + /** + * Callback will only be invoked after start() is called. + */ + public start() { + this.#isStarted = true; + } + + public setCallback(callback: UpdateCallback) { this.#updateCb = callback; } @@ -105,8 +129,15 @@ export class UpdateManger { return this.DEFAULT_DELAY; }; - public scheduleUpdate(document: TextDocument | TextDocumentIdentifier, UpdateType: UpdateType) { - if (UpdateType === "changed" || UpdateType === "renamed") { + /** + * T will be returned if the schedule is triggered eventually, if a newer scheduleUpdate + * occurs before the debounce time, the previous one will be cancelled and return undefined. + */ + public scheduleUpdate( + document: TextDocument | TextDocumentIdentifier, + updateType: UpdateType, + ): Promise { + if (updateType === "changed" || updateType === "renamed") { // only bump this when the file is actually changed // skip open this.bumpDocChangedVersion(); @@ -122,11 +153,21 @@ export class UpdateManger { existing.latest = document; existing.latestUpdateTimestamp = Date.now(); } - this.#scheduleBatchUpdate(); + return this.#scheduleBatchUpdate(document); } - async #update(updates: PendingUpdate[]) { - await this.#updateCb?.(updates); + async #update( + updates: PendingUpdate[], + arg: TextDocument | TextDocumentIdentifier, + ): Promise { + if (this.#updateCb === undefined) { + this._log({ + level: "warning", + message: `No update callback registered, skip invoking update.`, + }); + return undefined; + } + return await this.#updateCb(updates, arg); } } @@ -136,45 +177,72 @@ export class UpdateManger { * took too long and the whole timeout of the next call was eaten up already. * * @param fn The function + * @param getFnStatus Fn will only be called when this returns "ready" * @param milliseconds Number of milliseconds to debounce/throttle */ -export function debounceThrottle( - fn: () => void | Promise, +export function debounceThrottle( + fn: (arg: P) => T | Promise, + getFnStatus: () => "ready" | "pending", getDelay: () => number, log: (sl: ServerLog) => void, -): () => void { +): (arg: P) => Promise { let timeout: any; let lastInvocation: number | undefined = undefined; let executingCount = 0; let debounceExecutionId = 0; + const executionPromises = new Map>(); const UPDATE_PARALLEL_LIMIT = 2; - function maybeCall() { + function maybeCall(arg: P): Promise { + const promise = new DeferredPromise(); + const curId = debounceExecutionId++; + executionPromises.set(curId, promise); + maybeCallInternal(curId, arg, promise); + return promise.getPromise(); + } + + /** Clear all promises before the given id */ + function clearPromisesBefore(id: number) { + // clear all promises before with id < the given id + for (const k of executionPromises.keys()) { + if (k < id) { + executionPromises.get(k)?.resolvePromise(undefined); + executionPromises.delete(k); + } + } + } + + function maybeCallInternal(id: number, arg: P, promise: DeferredPromise) { clearTimeout(timeout); + clearPromisesBefore(id); timeout = setTimeout(async () => { - if ( - lastInvocation !== undefined && - (Date.now() - lastInvocation < getDelay() || executingCount >= UPDATE_PARALLEL_LIMIT) - ) { - maybeCall(); + const delay = getDelay(); + const tooSoon = lastInvocation !== undefined && Date.now() - lastInvocation < delay; + const notReady = getFnStatus() !== "ready"; + const tooManyParallel = executingCount >= UPDATE_PARALLEL_LIMIT; + if (notReady || tooSoon || tooManyParallel) { + maybeCallInternal(id, arg, promise); return; } - const curId = debounceExecutionId++; const s = new Date(); try { executingCount++; log({ level: "debug", - message: `Starting debounce execution #${curId} at ${s.toISOString()}. Current parallel count: ${executingCount}`, + message: `Starting debounce execution #${id} at ${s.toISOString()}. Current parallel count: ${executingCount}`, }); - await fn(); + const r = await fn(arg); + promise.resolvePromise(r); + } catch (e) { + promise.rejectPromise(e); } finally { + executionPromises.delete(id); executingCount--; const e = new Date(); log({ level: "debug", - message: `Finish debounce execution #${curId} at ${e.toISOString()}, duration=${e.getTime() - s.getTime()}. Current parallel count: ${executingCount}`, + message: `Finish debounce execution #${id} at ${e.toISOString()}, duration=${e.getTime() - s.getTime()}. Current parallel count: ${executingCount}`, }); } lastInvocation = Date.now(); diff --git a/packages/compiler/src/utils/deferred-promise.ts b/packages/compiler/src/utils/deferred-promise.ts new file mode 100644 index 00000000000..718e0c0c2fb --- /dev/null +++ b/packages/compiler/src/utils/deferred-promise.ts @@ -0,0 +1,24 @@ +export class DeferredPromise { + private promise: Promise; + private resolve!: (value: T) => void; + private reject!: (reason?: any) => void; + + constructor() { + this.promise = new Promise((res, rej) => { + this.resolve = res; + this.reject = rej; + }); + } + + getPromise(): Promise { + return this.promise; + } + + resolvePromise(value: T) { + this.resolve(value); + } + + rejectPromise(reason?: any) { + this.reject(reason); + } +} diff --git a/packages/compiler/src/utils/misc.ts b/packages/compiler/src/utils/misc.ts index 6adcb2b15b1..688f097f0e9 100644 --- a/packages/compiler/src/utils/misc.ts +++ b/packages/compiler/src/utils/misc.ts @@ -484,3 +484,7 @@ class RekeyableMapImpl implements RekeyableMap { return true; } } + +export function isPromise(value: any): value is Promise { + return value && typeof value.then === "function"; +} diff --git a/packages/compiler/test/core/linter.test.ts b/packages/compiler/test/core/linter.test.ts index 08137ace738..047bb43f4dc 100644 --- a/packages/compiler/test/core/linter.test.ts +++ b/packages/compiler/test/core/linter.test.ts @@ -82,7 +82,7 @@ describe("compiler: linter", () => { const linter = await createTestLinter(`model Foo {}`, { rules: [noModelFoo], }); - expectDiagnosticEmpty(linter.lint().diagnostics); + expectDiagnosticEmpty((await linter.lint()).diagnostics); }); it("enabling a rule that doesn't exists emit a diagnostic", async () => { @@ -159,7 +159,7 @@ describe("compiler: linter", () => { const linter = await createTestLinterAndEnableRules(files, { rules: [noModelFoo], }); - expectDiagnosticEmpty(linter.lint().diagnostics); + expectDiagnosticEmpty((await linter.lint()).diagnostics); }); it("emit diagnostic when in the user code", async () => { @@ -174,7 +174,7 @@ describe("compiler: linter", () => { const linter = await createTestLinterAndEnableRules(files, { rules: [noModelFoo], }); - expectDiagnostics(linter.lint().diagnostics, { + expectDiagnostics((await linter.lint()).diagnostics, { severity: "warning", code: "@typespec/test-linter/no-model-foo", message: `Cannot call model 'Foo'`, @@ -192,7 +192,7 @@ describe("compiler: linter", () => { enable: { "@typespec/test-linter/no-model-foo": true }, }), ); - expectDiagnostics(linter.lint().diagnostics, { + expectDiagnostics((await linter.lint()).diagnostics, { severity: "warning", code: "@typespec/test-linter/no-model-foo", message: `Cannot call model 'Foo'`, @@ -208,7 +208,7 @@ describe("compiler: linter", () => { enable: { "@typespec/test-linter/no-model-foo": true }, }), ); - expectDiagnosticEmpty(linter.lint().diagnostics); + expectDiagnosticEmpty((await linter.lint()).diagnostics); }); }); @@ -222,7 +222,7 @@ describe("compiler: linter", () => { extends: ["@typespec/test-linter/all"], }), ); - expectDiagnostics(linter.lint().diagnostics, { + expectDiagnostics((await linter.lint()).diagnostics, { severity: "warning", code: "@typespec/test-linter/no-model-foo", message: `Cannot call model 'Foo'`, @@ -242,7 +242,7 @@ describe("compiler: linter", () => { extends: ["@typespec/test-linter/custom"], }), ); - expectDiagnostics(linter.lint().diagnostics, { + expectDiagnostics((await linter.lint()).diagnostics, { severity: "warning", code: "@typespec/test-linter/no-model-foo", message: `Cannot call model 'Foo'`, diff --git a/packages/typespec-vscode/src/tsp-language-client.ts b/packages/typespec-vscode/src/tsp-language-client.ts index 76b79b67b3c..1de94f0183e 100644 --- a/packages/typespec-vscode/src/tsp-language-client.ts +++ b/packages/typespec-vscode/src/tsp-language-client.ts @@ -8,13 +8,22 @@ import type { } from "@typespec/compiler"; import { InternalCompileResult } from "@typespec/compiler/internals"; import { inspect } from "util"; -import { ExtensionContext, LogOutputChannel, RelativePattern, workspace } from "vscode"; +import { + ExtensionContext, + LanguageModelChat, + LanguageModelChatMessage, + lm, + LogOutputChannel, + RelativePattern, + workspace, +} from "vscode"; import { Executable, LanguageClient, LanguageClientOptions, TextDocumentIdentifier, } from "vscode-languageclient/node.js"; +import { ChatCompleteOptions, ChatMessage } from "../../compiler/dist/src/server/types.js"; import { TspConfigFileName } from "./const.js"; import logger from "./log/logger.js"; import telemetryClient from "./telemetry/telemetry-client.js"; @@ -269,6 +278,84 @@ export class TspLanguageClient { const name = "TypeSpec"; const id = "typespec"; const lc = new LanguageClient(id, name, { run: exe, debug: exe }, options); + + let aiParallel = 0; + const lmModelCache = new Map>(); + // pre-warm some well known models + // TODO: refine the list + const knownFamilies = ["gpt-4o", "o4-mini", "gpt-5-mini", "claude-sonnet-4"]; + for (const family of knownFamilies) { + const m = lm.selectChatModels({ family }); + lmModelCache.set(family, m); + } + lc.onRequest( + "custom/chatCompletion", + async (params: { messages: ChatMessage[]; lmOptions: ChatCompleteOptions; id?: string }) => { + const family = params.lmOptions.modelPreferences ?? "gpt-4o"; + const s = new Date(); + logger.debug( + `[ChatComplete #${params.id ?? "N/A"}][${s.toTimeString()}] start select model for family ${family}`, + ); + let mp = lmModelCache.get(family); + if (!mp) { + mp = lm.selectChatModels({ family }); + lmModelCache.set(family, mp); + } + const models = await mp; + const e = new Date(); + logger.debug( + `[ChatComplete #${params.id ?? "N/A"}][${e.toTimeString()}] Model selection for family ${family} took ${e.getTime() - s.getTime()} ms`, + ); + if (!models || models.length === 0) { + logger.error( + `[ChatComplete #${params.id ?? "N/A"}] No chat models returned. Please check whether language model is available. It may take some time for language models to be ready to use after vscode is started. Language model family used: ${family}.`, + ); + return undefined; + } + try { + aiParallel++; + logger.warning(`AI parallelism: ${aiParallel}`); + const ss = new Date(); + logger.debug( + `[ChatComplete #${params.id ?? "N/A"}][${ss.toTimeString()}] Sending chat completion request to model ${models[0].name}`, + ); + // we always use the first model for now. + const response = await models[0].sendRequest( + params.messages.map((m) => { + if (m.role === "user") { + return LanguageModelChatMessage.User(m.message); + } else if (m.role === "assist") { + return LanguageModelChatMessage.Assistant(m.message); + } else { + logger.error(`Unknown chat message role: ${m.role}. Default to use User role.`); + return LanguageModelChatMessage.User(m.message); + } + }), + ); + let fullResponse = ""; + for await (const chunk of response.text) { + fullResponse += chunk; + } + const es = new Date(); + logger.debug( + `[ChatComplete #${params.id ?? "N/A"}][${es.toTimeString()}] chat complete request took ${es.getTime() - ss.getTime()} ms`, + ); + // TODO: support stream mode? seems not necessary because we need to wait for the full response anyway and no way to + // show the progress to end user. + return fullResponse; + } catch (e) { + logger.error( + `Error when sending chat completion request to model ${models[0].name}: ${inspect(e)}`, + [e], + ); + return undefined; + } finally { + aiParallel--; + logger.warning(`AI parallelism: ${aiParallel}`); + } + }, + ); + return new TspLanguageClient(lc, exe); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19ed15825d1..b72a9c5c533 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -148,7 +148,7 @@ importers: version: 0.9.4(prettier-plugin-astro@0.14.1)(prettier@3.6.2)(typescript@5.9.2) '@astrojs/starlight': specifier: ^0.35.1 - version: 0.35.2(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) + version: 0.35.2(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) '@expressive-code/core': specifier: ^0.41.2 version: 0.41.3 @@ -157,7 +157,7 @@ importers: version: link:../playground astro-expressive-code: specifier: ^0.41.2 - version: 0.41.3(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) + version: 0.41.3(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) pathe: specifier: ^2.0.3 version: 2.0.3 @@ -173,7 +173,50 @@ importers: version: 18.3.24 astro: specifier: ^5.5.6 - version: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + version: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + + packages/azure-linter: + dependencies: + '@azure/identity': + specifier: ~4.10.2 + version: 4.10.2 + jsonrepair: + specifier: ^3.12.0 + version: 3.13.0 + openai: + specifier: ^5.9.0 + version: 5.21.0(ws@8.18.3)(zod@3.25.76) + zod: + specifier: ^3.25.76 + version: 3.25.76 + zod-to-json-schema: + specifier: ^3.24.6 + version: 3.24.6(zod@3.25.76) + devDependencies: + '@types/node': + specifier: latest + version: 24.5.2 + '@typescript-eslint/eslint-plugin': + specifier: ^8.15.0 + version: 8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.9.2))(eslint@9.35.0)(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.15.0 + version: 8.43.0(eslint@9.35.0)(typescript@5.9.2) + '@typespec/compiler': + specifier: workspace:^ + version: link:../compiler + '@typespec/library-linter': + specifier: latest + version: 0.74.0(@typespec/compiler@packages+compiler) + eslint: + specifier: ^9.15.0 + version: 9.35.0 + prettier: + specifier: ^3.3.3 + version: 3.6.2 + typescript: + specifier: ^5.3.3 + version: 5.9.2 packages/best-practices: devDependencies: @@ -459,7 +502,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.1.2 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@25.0.1)(tsx@4.20.5)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@25.0.1)(tsx@4.20.5)(yaml@2.8.1) web-tree-sitter: specifier: ^0.25.4 version: 0.25.9(@types/emscripten@1.41.2) @@ -774,7 +817,7 @@ importers: version: 14.1.0 inquirer: specifier: ^12.5.0 - version: 12.9.4(@types/node@24.3.1) + version: 12.9.4(@types/node@24.5.2) ora: specifier: ^8.1.1 version: 8.2.0 @@ -792,7 +835,7 @@ importers: version: 2.0.0 vitest: specifier: ^3.1.2 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@25.0.1)(tsx@4.20.5)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@25.0.1)(tsx@4.20.5)(yaml@2.8.1) yargs: specifier: ~18.0.0 version: 18.0.0 @@ -1616,7 +1659,7 @@ importers: version: 0.25.9 vitest: specifier: ^3.1.2 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@25.0.1)(tsx@4.20.5)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@25.0.1)(tsx@4.20.5)(yaml@2.8.1) packages/protobuf: devDependencies: @@ -1936,7 +1979,7 @@ importers: version: 18.3.7(@types/react@18.3.24) '@vitejs/plugin-react': specifier: ~5.0.2 - version: 5.0.2(vite@7.1.5(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1)) + version: 5.0.2(vite@7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)) rimraf: specifier: ~6.0.1 version: 6.0.1 @@ -1948,13 +1991,13 @@ importers: version: 5.9.2 vite: specifier: ^7.0.5 - version: 7.1.5(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1) + version: 7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) vite-plugin-checker: specifier: ^0.10.1 - version: 0.10.3(eslint@9.35.0)(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.5(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1)) + version: 0.10.3(eslint@9.35.0)(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)) vite-plugin-dts: specifier: 4.5.4 - version: 4.5.4(@types/node@24.3.1)(rollup@4.49.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1)) + version: 4.5.4(@types/node@24.5.2)(rollup@4.49.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)) packages/spector: dependencies: @@ -2462,10 +2505,10 @@ importers: version: 0.9.4(prettier-plugin-astro@0.14.1)(prettier@3.6.2)(typescript@5.9.2) '@astrojs/react': specifier: ^4.2.1 - version: 4.3.1(@types/node@24.3.1)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tsx@4.20.5)(yaml@2.8.1) + version: 4.3.1(@types/node@24.5.2)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tsx@4.20.5)(yaml@2.8.1) '@astrojs/starlight': specifier: ^0.35.1 - version: 0.35.2(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) + version: 0.35.2(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) '@docsearch/css': specifier: ^4.1.0 version: 4.1.0 @@ -2489,10 +2532,10 @@ importers: version: link:../packages/playground astro: specifier: ^5.5.6 - version: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + version: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) astro-rehype-relative-markdown-links: specifier: ^0.18.1 - version: 0.18.1(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) + version: 0.18.1(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -2589,7 +2632,7 @@ importers: version: link:../packages/xml astro-expressive-code: specifier: ^0.41.2 - version: 0.41.3(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) + version: 0.41.3(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) rehype-mermaid: specifier: ^3.0.0 version: 3.0.0(playwright@1.55.0) @@ -2601,7 +2644,7 @@ importers: version: 6.0.1 vite-plugin-node-polyfills: specifier: ^0.24.0 - version: 0.24.0(rollup@4.49.0)(vite@7.1.5(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1)) + version: 0.24.0(rollup@4.49.0)(vite@7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)) packages: @@ -2826,6 +2869,10 @@ packages: resolution: {integrity: sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==} engines: {node: '>=20.0.0'} + '@azure/identity@4.10.2': + resolution: {integrity: sha512-Uth4vz0j+fkXCkbvutChUj03PDCokjbC6Wk9JT8hHEUtpy/EurNKAseb3+gO6Zi9VYBvwt61pgbzn1ovk942Qg==} + engines: {node: '>=20.0.0'} + '@azure/identity@4.12.0': resolution: {integrity: sha512-6vuh2R3Cte6SD6azNalLCjIDoryGdcvDVEV7IDRPtm5lHX5ffkDlIalaoOp5YJU08e4ipjJENel20kSMDLAcug==} engines: {node: '>=20.0.0'} @@ -6027,6 +6074,9 @@ packages: '@types/node@24.3.1': resolution: {integrity: sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==} + '@types/node@24.5.2': + resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -6193,6 +6243,12 @@ packages: resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typespec/library-linter@0.74.0': + resolution: {integrity: sha512-65+e/+RLUD2SoS18xMMDc/O/8Eh4hlmDFUg+D20cni6TSmXYwQB492WOouQKfxGCCcC1jW9jFAR/GqEwGD295Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@typespec/compiler': ^1.4.0 + '@typespec/ts-http-runtime@0.3.0': resolution: {integrity: sha512-sOx1PKSuFwnIl7z4RN0Ls7N9AQawmR9r66eI5rFCzLDIs8HTIYrIpH9QjYWoX0lkgGrkLxXhi4QnK7MizPRrIg==} engines: {node: '>=20.0.0'} @@ -9609,6 +9665,10 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonrepair@3.13.0: + resolution: {integrity: sha512-5YRzlAQ7tuzV1nAJu3LvDlrKtBFIALHN2+a+I1MGJCt3ldRDBF/bZuvIPzae8Epot6KBXd0awRZZcuoeAsZ/mw==} + hasBin: true + jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} @@ -10555,6 +10615,18 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openai@5.21.0: + resolution: {integrity: sha512-E9LuV51vgvwbahPJaZu2x4V6SWMq9g3X6Bj2/wnFiNfV7lmAxYVxPxcQNZqCWbAVMaEoers9HzIxpOp6Vvgn8w==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -12471,6 +12543,9 @@ packages: undici-types@7.10.0: resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + undici-types@7.12.0: + resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + undici@7.16.0: resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} engines: {node: '>=20.18.1'} @@ -13607,12 +13682,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@4.3.5(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1))': + '@astrojs/mdx@4.3.5(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1))': dependencies: '@astrojs/markdown-remark': 6.3.6 '@mdx-js/mdx': 3.1.1 acorn: 8.15.0 - astro: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + astro: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) es-module-lexer: 1.7.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -13630,15 +13705,15 @@ snapshots: dependencies: prismjs: 1.30.0 - '@astrojs/react@4.3.1(@types/node@24.3.1)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tsx@4.20.5)(yaml@2.8.1)': + '@astrojs/react@4.3.1(@types/node@24.5.2)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tsx@4.20.5)(yaml@2.8.1)': dependencies: '@types/react': 18.3.24 '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@vitejs/plugin-react': 4.7.0(vite@6.3.6(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1)) + '@vitejs/plugin-react': 4.7.0(vite@6.3.6(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) ultrahtml: 1.6.0 - vite: 6.3.6(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1) + vite: 6.3.6(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -13659,17 +13734,17 @@ snapshots: stream-replace-string: 2.0.0 zod: 3.25.76 - '@astrojs/starlight@0.35.2(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1))': + '@astrojs/starlight@0.35.2(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1))': dependencies: '@astrojs/markdown-remark': 6.3.6 - '@astrojs/mdx': 4.3.5(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) + '@astrojs/mdx': 4.3.5(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) '@astrojs/sitemap': 3.5.1 '@pagefind/default-ui': 1.4.0 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) - astro-expressive-code: 0.41.3(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) + astro: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + astro-expressive-code: 0.41.3(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 @@ -13788,6 +13863,22 @@ snapshots: fast-xml-parser: 5.2.5 tslib: 2.8.1 + '@azure/identity@4.10.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.0 + '@azure/core-client': 1.10.0 + '@azure/core-rest-pipeline': 1.22.0 + '@azure/core-tracing': 1.3.0 + '@azure/core-util': 1.13.0 + '@azure/logger': 1.3.0 + '@azure/msal-browser': 4.22.1 + '@azure/msal-node': 3.7.3 + open: 10.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@azure/identity@4.12.0': dependencies: '@azure/abort-controller': 2.1.2 @@ -16002,6 +16093,16 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/checkbox@4.2.2(@types/node@24.5.2)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.5.2) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@24.5.2) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/confirm@5.1.16(@types/node@24.3.1)': dependencies: '@inquirer/core': 10.2.0(@types/node@24.3.1) @@ -16009,6 +16110,13 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/confirm@5.1.16(@types/node@24.5.2)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.5.2) + '@inquirer/type': 3.0.8(@types/node@24.5.2) + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/core@10.2.0(@types/node@24.3.1)': dependencies: '@inquirer/figures': 1.0.13 @@ -16022,6 +16130,19 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/core@10.2.0(@types/node@24.5.2)': + dependencies: + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@24.5.2) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/editor@4.2.18(@types/node@24.3.1)': dependencies: '@inquirer/core': 10.2.0(@types/node@24.3.1) @@ -16030,6 +16151,14 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/editor@4.2.18(@types/node@24.5.2)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.5.2) + '@inquirer/external-editor': 1.0.1(@types/node@24.5.2) + '@inquirer/type': 3.0.8(@types/node@24.5.2) + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/expand@4.0.18(@types/node@24.3.1)': dependencies: '@inquirer/core': 10.2.0(@types/node@24.3.1) @@ -16038,6 +16167,14 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/expand@4.0.18(@types/node@24.5.2)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.5.2) + '@inquirer/type': 3.0.8(@types/node@24.5.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/external-editor@1.0.1(@types/node@24.3.1)': dependencies: chardet: 2.1.0 @@ -16045,6 +16182,13 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/external-editor@1.0.1(@types/node@24.5.2)': + dependencies: + chardet: 2.1.0 + iconv-lite: 0.6.3 + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/figures@1.0.13': {} '@inquirer/input@4.2.2(@types/node@24.3.1)': @@ -16054,6 +16198,13 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/input@4.2.2(@types/node@24.5.2)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.5.2) + '@inquirer/type': 3.0.8(@types/node@24.5.2) + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/number@3.0.18(@types/node@24.3.1)': dependencies: '@inquirer/core': 10.2.0(@types/node@24.3.1) @@ -16061,6 +16212,13 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/number@3.0.18(@types/node@24.5.2)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.5.2) + '@inquirer/type': 3.0.8(@types/node@24.5.2) + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/password@4.0.18(@types/node@24.3.1)': dependencies: '@inquirer/core': 10.2.0(@types/node@24.3.1) @@ -16069,6 +16227,14 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/password@4.0.18(@types/node@24.5.2)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.5.2) + '@inquirer/type': 3.0.8(@types/node@24.5.2) + ansi-escapes: 4.3.2 + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/prompts@7.8.4(@types/node@24.3.1)': dependencies: '@inquirer/checkbox': 4.2.2(@types/node@24.3.1) @@ -16084,6 +16250,21 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/prompts@7.8.4(@types/node@24.5.2)': + dependencies: + '@inquirer/checkbox': 4.2.2(@types/node@24.5.2) + '@inquirer/confirm': 5.1.16(@types/node@24.5.2) + '@inquirer/editor': 4.2.18(@types/node@24.5.2) + '@inquirer/expand': 4.0.18(@types/node@24.5.2) + '@inquirer/input': 4.2.2(@types/node@24.5.2) + '@inquirer/number': 3.0.18(@types/node@24.5.2) + '@inquirer/password': 4.0.18(@types/node@24.5.2) + '@inquirer/rawlist': 4.1.6(@types/node@24.5.2) + '@inquirer/search': 3.1.1(@types/node@24.5.2) + '@inquirer/select': 4.3.2(@types/node@24.5.2) + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/rawlist@4.1.6(@types/node@24.3.1)': dependencies: '@inquirer/core': 10.2.0(@types/node@24.3.1) @@ -16092,6 +16273,14 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/rawlist@4.1.6(@types/node@24.5.2)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.5.2) + '@inquirer/type': 3.0.8(@types/node@24.5.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/search@3.1.1(@types/node@24.3.1)': dependencies: '@inquirer/core': 10.2.0(@types/node@24.3.1) @@ -16101,6 +16290,15 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/search@3.1.1(@types/node@24.5.2)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.5.2) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@24.5.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/select@4.3.2(@types/node@24.3.1)': dependencies: '@inquirer/core': 10.2.0(@types/node@24.3.1) @@ -16111,10 +16309,24 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/select@4.3.2(@types/node@24.5.2)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.5.2) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@24.5.2) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.5.2 + '@inquirer/type@3.0.8(@types/node@24.3.1)': optionalDependencies: '@types/node': 24.3.1 + '@inquirer/type@3.0.8(@types/node@24.5.2)': + optionalDependencies: + '@types/node': 24.5.2 + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -16241,6 +16453,14 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@microsoft/api-extractor-model@7.30.7(@types/node@24.5.2)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@24.5.2) + transitivePeerDependencies: + - '@types/node' + '@microsoft/api-extractor@7.52.12(@types/node@24.3.1)': dependencies: '@microsoft/api-extractor-model': 7.30.7(@types/node@24.3.1) @@ -16259,6 +16479,24 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@microsoft/api-extractor@7.52.12(@types/node@24.5.2)': + dependencies: + '@microsoft/api-extractor-model': 7.30.7(@types/node@24.5.2) + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@24.5.2) + '@rushstack/rig-package': 0.5.3 + '@rushstack/terminal': 0.16.0(@types/node@24.5.2) + '@rushstack/ts-command-line': 5.0.3(@types/node@24.5.2) + lodash: 4.17.21 + minimatch: 10.0.3 + resolve: 1.22.10 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + '@microsoft/applicationinsights-channel-js@3.3.9(tslib@2.8.1)': dependencies: '@microsoft/applicationinsights-common': 3.3.9(tslib@2.8.1) @@ -17708,6 +17946,19 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@rushstack/node-core-library@5.14.0(@types/node@24.5.2)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 11.3.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.10 + semver: 7.5.4 + optionalDependencies: + '@types/node': 24.5.2 + '@rushstack/rig-package@0.5.3': dependencies: resolve: 1.22.10 @@ -17720,6 +17971,13 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@rushstack/terminal@0.16.0(@types/node@24.5.2)': + dependencies: + '@rushstack/node-core-library': 5.14.0(@types/node@24.5.2) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 24.5.2 + '@rushstack/ts-command-line@5.0.3(@types/node@24.3.1)': dependencies: '@rushstack/terminal': 0.16.0(@types/node@24.3.1) @@ -17729,6 +17987,15 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@rushstack/ts-command-line@5.0.3(@types/node@24.5.2)': + dependencies: + '@rushstack/terminal': 0.16.0(@types/node@24.5.2) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + '@rushstack/worker-pool@0.4.9(@types/node@24.3.1)': optionalDependencies: '@types/node': 24.3.1 @@ -18369,6 +18636,10 @@ snapshots: dependencies: undici-types: 7.10.0 + '@types/node@24.5.2': + dependencies: + undici-types: 7.12.0 + '@types/normalize-package-data@2.4.4': {} '@types/parse-json@4.0.2': {} @@ -18573,6 +18844,10 @@ snapshots: '@typescript-eslint/types': 8.43.0 eslint-visitor-keys: 4.2.1 + '@typespec/library-linter@0.74.0(@typespec/compiler@packages+compiler)': + dependencies: + '@typespec/compiler': link:packages/compiler + '@typespec/ts-http-runtime@0.3.0': dependencies: http-proxy-agent: 7.0.2 @@ -18583,7 +18858,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.7.0(vite@6.3.6(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1))': + '@vitejs/plugin-react@4.7.0(vite@6.3.6(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -18591,7 +18866,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.6(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1) + vite: 6.3.6(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -18607,6 +18882,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-react@5.0.2(vite@7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.34 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 @@ -18653,6 +18940,14 @@ snapshots: optionalDependencies: vite: 7.1.3(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@7.1.3(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 7.1.3(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@7.1.5(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -19119,7 +19414,7 @@ snapshots: algoliasearch: 4.25.2 clipanion: 4.0.0-rc.4(typanion@3.14.0) diff: 5.2.0 - ink: 3.2.0(@types/react@18.3.24)(react@18.3.1) + ink: 3.2.0(@types/react@18.3.24)(react@17.0.2) ink-text-input: 4.0.3(ink@3.2.0(@types/react@18.3.24)(react@17.0.2))(react@17.0.2) react: 17.0.2 semver: 7.7.2 @@ -19312,7 +19607,7 @@ snapshots: '@yarnpkg/plugin-git': 3.1.3(@yarnpkg/core@4.4.3(typanion@3.14.0))(typanion@3.14.0) clipanion: 4.0.0-rc.4(typanion@3.14.0) es-toolkit: 1.39.10 - ink: 3.2.0(@types/react@18.3.24)(react@17.0.2) + ink: 3.2.0(@types/react@18.3.24)(react@18.3.1) react: 17.0.2 semver: 7.7.2 tslib: 2.8.1 @@ -19665,14 +19960,14 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.41.3(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)): + astro-expressive-code@0.41.3(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)): dependencies: - astro: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + astro: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) rehype-expressive-code: 0.41.3 - astro-rehype-relative-markdown-links@0.18.1(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)): + astro-rehype-relative-markdown-links@0.18.1(astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1)): dependencies: - astro: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + astro: 5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) catch-unknown: 2.0.0 debug: 4.4.1(supports-color@8.1.1) github-slugger: 2.0.0 @@ -19684,7 +19979,7 @@ snapshots: transitivePeerDependencies: - supports-color - astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.3.1)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1): + astro@5.13.7(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0)(@types/node@24.5.2)(encoding@0.1.13)(rollup@4.49.0)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.12.2 '@astrojs/internal-helpers': 0.7.2 @@ -19740,8 +20035,8 @@ snapshots: unist-util-visit: 5.0.0 unstorage: 1.17.1(@azure/identity@4.12.0)(@azure/storage-blob@12.28.0) vfile: 6.0.3 - vite: 6.3.6(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1) - vitefu: 1.1.1(vite@6.3.6(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1)) + vite: 6.3.6(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + vitefu: 1.1.1(vite@6.3.6(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)) xxhash-wasm: 1.1.0 yargs-parser: 21.1.1 yocto-spinner: 0.2.3 @@ -22655,6 +22950,18 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + inquirer@12.9.4(@types/node@24.5.2): + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.5.2) + '@inquirer/prompts': 7.8.4(@types/node@24.5.2) + '@inquirer/type': 3.0.8(@types/node@24.5.2) + ansi-escapes: 4.3.2 + mute-stream: 2.0.0 + run-async: 4.0.6 + rxjs: 7.8.2 + optionalDependencies: + '@types/node': 24.5.2 + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -23030,6 +23337,8 @@ snapshots: jsonparse@1.3.1: {} + jsonrepair@3.13.0: {} + jsonwebtoken@9.0.2: dependencies: jws: 3.2.2 @@ -24396,6 +24705,11 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openai@5.21.0(ws@8.18.3)(zod@3.25.76): + optionalDependencies: + ws: 8.18.3 + zod: 3.25.76 + openapi-types@12.1.3: {} optionator@0.9.4: @@ -26630,6 +26944,8 @@ snapshots: undici-types@7.10.0: {} + undici-types@7.12.0: {} + undici@7.16.0: {} unicode-properties@1.4.1: @@ -26863,6 +27179,27 @@ snapshots: - tsx - yaml + vite-node@3.2.4(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.1(supports-color@8.1.1) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-plugin-checker@0.10.3(eslint@9.35.0)(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.5(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1)): dependencies: '@babel/code-frame': 7.27.1 @@ -26880,6 +27217,23 @@ snapshots: optionator: 0.9.4 typescript: 5.9.2 + vite-plugin-checker@0.10.3(eslint@9.35.0)(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)): + dependencies: + '@babel/code-frame': 7.27.1 + chokidar: 4.0.3 + npm-run-path: 6.0.0 + picocolors: 1.1.1 + picomatch: 4.0.3 + strip-ansi: 7.1.2 + tiny-invariant: 1.3.3 + tinyglobby: 0.2.15 + vite: 7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + vscode-uri: 3.1.0 + optionalDependencies: + eslint: 9.35.0 + optionator: 0.9.4 + typescript: 5.9.2 + vite-plugin-dts@4.5.4(@types/node@24.3.1)(rollup@4.49.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1)): dependencies: '@microsoft/api-extractor': 7.52.12(@types/node@24.3.1) @@ -26899,6 +27253,25 @@ snapshots: - rollup - supports-color + vite-plugin-dts@4.5.4(@types/node@24.5.2)(rollup@4.49.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)): + dependencies: + '@microsoft/api-extractor': 7.52.12(@types/node@24.5.2) + '@rollup/pluginutils': 5.3.0(rollup@4.49.0) + '@volar/typescript': 2.4.23 + '@vue/language-core': 2.2.0(typescript@5.9.2) + compare-versions: 6.1.1 + debug: 4.4.1(supports-color@8.1.1) + kolorist: 1.8.0 + local-pkg: 1.1.2 + magic-string: 0.30.19 + typescript: 5.9.2 + optionalDependencies: + vite: 7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + vite-plugin-node-polyfills@0.24.0(rollup@4.49.0)(vite@7.1.5(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.49.0) @@ -26907,7 +27280,15 @@ snapshots: transitivePeerDependencies: - rollup - vite@6.3.6(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1): + vite-plugin-node-polyfills@0.24.0(rollup@4.49.0)(vite@7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)): + dependencies: + '@rollup/plugin-inject': 5.0.5(rollup@4.49.0) + node-stdlib-browser: 1.3.1 + vite: 7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + transitivePeerDependencies: + - rollup + + vite@6.3.6(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -26916,7 +27297,7 @@ snapshots: rollup: 4.49.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.3.1 + '@types/node': 24.5.2 fsevents: 2.3.3 tsx: 4.20.5 yaml: 2.8.1 @@ -26935,6 +27316,20 @@ snapshots: tsx: 4.20.5 yaml: 2.8.1 + vite@7.1.3(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1): + dependencies: + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.49.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.5.2 + fsevents: 2.3.3 + tsx: 4.20.5 + yaml: 2.8.1 + vite@7.1.5(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.9 @@ -26949,9 +27344,23 @@ snapshots: tsx: 4.20.5 yaml: 2.8.1 - vitefu@1.1.1(vite@6.3.6(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1)): + vite@7.1.5(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1): + dependencies: + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.49.0 + tinyglobby: 0.2.15 optionalDependencies: - vite: 6.3.6(@types/node@24.3.1)(tsx@4.20.5)(yaml@2.8.1) + '@types/node': 24.5.2 + fsevents: 2.3.3 + tsx: 4.20.5 + yaml: 2.8.1 + + vitefu@1.1.1(vite@6.3.6(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)): + optionalDependencies: + vite: 6.3.6(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@25.0.1)(tsx@4.20.5)(yaml@2.8.1): dependencies: @@ -26998,6 +27407,51 @@ snapshots: - tsx - yaml + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@25.0.1)(tsx@4.20.5)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.1.3(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.1(supports-color@8.1.1) + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.3(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 24.5.2 + '@vitest/ui': 3.2.4(vitest@3.2.4) + happy-dom: 18.0.1 + jsdom: 25.0.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vm-browserify@1.1.2: {} volar-service-css@0.0.62(@volar/language-service@2.4.23):