Path: blob/main/extensions/copilot/src/platform/remoteCodeSearch/common/githubCodeSearchService.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import { RequestType } from '@vscode/copilot-api';5import { shouldInclude } from '../../../util/common/glob';6import { Result } from '../../../util/common/result';7import { TelemetryCorrelationId } from '../../../util/common/telemetryCorrelationId';8import { raceCancellationError } from '../../../util/vs/base/common/async';9import { CancellationToken } from '../../../util/vs/base/common/cancellation';10import { isCancellationError } from '../../../util/vs/base/common/errors';11import { URI } from '../../../util/vs/base/common/uri';12import { Range } from '../../../util/vs/editor/common/core/range';13import { createDecorator, IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';14import { IAuthenticationService } from '../../authentication/common/authentication';15import { FileChunk, FileChunkAndScore } from '../../chunking/common/chunk';16import { stripChunkTextMetadata, truncateToMaxUtf8Length } from '../../chunking/common/chunkingStringUtils';17import { EmbeddingType } from '../../embeddings/common/embeddingsComputer';18import { ICAPIClientService } from '../../endpoint/common/capiClient';19import { IEnvService } from '../../env/common/envService';20import { GithubRepoId, toGithubNwo } from '../../git/common/gitService';21import { makeGitHubAPIRequest } from '../../github/common/githubAPI';22import { getGithubMetadataHeaders } from '../../github/common/githubApiFetcherService';23import { IIgnoreService } from '../../ignore/common/ignoreService';24import { ILogService } from '../../log/common/logService';25import { IFetcherService, Response } from '../../networking/common/fetcherService';26import { postRequest } from '../../networking/common/networking';27import { ITelemetryService } from '../../telemetry/common/telemetry';28import { CodeSearchOptions, LexicalCodeSearchResult, RemoteCodeSearchError, RemoteCodeSearchIndexState, RemoteCodeSearchIndexStatus, SemanticCodeSearchResult } from './remoteCodeSearch';293031interface ResponseShape {32readonly results: readonly SemanticSearchResult[];33readonly embedding_model: string;34}3536type SemanticSearchResult = {37chunk: {38hash: string;39text: string;40// Byte offset range of the chunk41range: { start: number; end: number };42line_range: { start: number; end: number };43embedding?: { embedding: number[] };44};45distance: number;46location: {47path: string; // file path48commit_sha: string;49ref_name: string;50repo: {51nwo: string;52url: string;53};54};55};5657export interface GithubCodeSearchRepoInfo {58readonly kind: 'repo';59readonly githubRepoId: GithubRepoId;60readonly localRepoRoot: URI | undefined;61readonly indexedCommit: string | undefined;62}6364export interface GithubCodeSearchOrgInfo {65readonly kind: 'org';66readonly org: string;67}6869export type GithubCodeSearchScope = GithubCodeSearchRepoInfo | GithubCodeSearchOrgInfo;7071export const IGithubCodeSearchService = createDecorator('IGithubCodeSearchService');7273export interface IGithubCodeSearchService {74readonly _serviceBrand: undefined;7576/**77* Gets the state of the remote index for a given repo.78*/79getRemoteIndexState(80authOptions: { readonly silent: boolean },81githubRepoId: GithubRepoId,82telemetryInfo: TelemetryCorrelationId,83token: CancellationToken,84): Promise<Result<RemoteCodeSearchIndexState, RemoteCodeSearchError>>;8586/**87* Requests that a given repo be indexed.88*/89triggerIndexing(90authOptions: { readonly silent: boolean },91triggerReason: 'auto' | 'manual' | 'tool',92githubRepoId: GithubRepoId,93telemetryInfo: TelemetryCorrelationId,94): Promise<Result<true, RemoteCodeSearchError>>;9596/**97* Semantic searches a given github repo for relevant code snippets98*99* The repo must have been indexed first. Make sure to check {@link getRemoteIndexState} or call {@link triggerIndexing}.100*/101semanticSearch(102authOptions: { readonly silent: boolean },103embeddingType: EmbeddingType,104scope: GithubCodeSearchRepoInfo,105query: string,106maxResults: number,107options: CodeSearchOptions,108telemetryInfo: TelemetryCorrelationId,109token: CancellationToken,110): Promise<SemanticCodeSearchResult>;111112/**113* Lexical searches a given github repo or org for relevant code snippets114*/115lexicalSearch(116authOptions: { readonly silent: boolean },117scope: GithubCodeSearchScope,118query: string,119maxResults: number,120options: CodeSearchOptions,121telemetryInfo: TelemetryCorrelationId,122token: CancellationToken,123): Promise<LexicalCodeSearchResult>;124}125126export class GithubCodeSearchService implements IGithubCodeSearchService {127128declare readonly _serviceBrand: undefined;129130constructor(131@IAuthenticationService private readonly _authenticationService: IAuthenticationService,132@ICAPIClientService private readonly _capiClientService: ICAPIClientService,133@IEnvService private readonly _envService: IEnvService,134@IFetcherService private readonly _fetcherService: IFetcherService,135@IIgnoreService private readonly _ignoreService: IIgnoreService,136@ILogService private readonly _logService: ILogService,137@ITelemetryService private readonly _telemetryService: ITelemetryService,138@IInstantiationService private readonly _instantiationService: IInstantiationService,139) { }140141async getRemoteIndexState(auth: { readonly silent: boolean }, githubRepoId: GithubRepoId, telemetryInfo: TelemetryCorrelationId, token: CancellationToken): Promise<Result<RemoteCodeSearchIndexState, RemoteCodeSearchError>> {142const repoNwo = toGithubNwo(githubRepoId);143144if (repoNwo.startsWith('microsoft/simuluation-test-')) {145return Result.ok({ status: RemoteCodeSearchIndexStatus.NotYetIndexed });146}147148const authToken = await this.getGithubAccessToken(auth.silent);149if (!authToken) {150this._logService.error(`GithubCodeSearchService::getRemoteIndexState(${repoNwo}). Failed to fetch indexing status. No valid github auth token.`);151return Result.error<RemoteCodeSearchError>({ type: 'not-authorized' });152}153154try {155const statusRequest = await raceCancellationError(this._capiClientService.makeRequest<Response>({156method: 'GET',157headers: {158Authorization: `Bearer ${authToken}`,159...getGithubMetadataHeaders(telemetryInfo.callTracker, this._envService),160}161}, { type: RequestType.EmbeddingsIndex, repoWithOwner: repoNwo }), token);162if (!statusRequest.ok) {163/* __GDPR__164"githubCodeSearch.getRemoteIndexState.error" : {165"owner": "mjbvz",166"comment": "Information about failed remote index state requests",167"statusCode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The response status code" }168}169*/170this._telemetryService.sendMSFTTelemetryEvent('githubCodeSearch.getRemoteIndexState.error', {}, {171statusCode: statusRequest.status,172});173174this._logService.error(`GithubCodeSearchService::getRemoteIndexState(${repoNwo}). Failed to fetch indexing status. Response: ${statusRequest.status}. ${await statusRequest.text()}`);175return Result.error<RemoteCodeSearchError>({ type: 'generic-error', error: new Error(`Failed to fetch indexing status. Response: ${statusRequest.status}.`) });176}177178const preCheckResult = await raceCancellationError(statusRequest.json(), token);179if (preCheckResult.semantic_code_search_ok && preCheckResult.semantic_commit_sha) {180const indexedCommit = preCheckResult.semantic_commit_sha;181this._logService.trace(`GithubCodeSearchService::getRemoteIndexState(${repoNwo}). Found indexed commit: ${indexedCommit}.`);182return Result.ok({183status: RemoteCodeSearchIndexStatus.Ready,184indexedCommit,185});186}187188if (preCheckResult.semantic_indexing_enabled) {189if (await raceCancellationError(this.isEmptyRepo(authToken, githubRepoId, token), token)) {190this._logService.trace(`GithubCodeSearchService::getRemoteIndexState(${repoNwo}). Semantic indexing enabled but repo is empty.`);191return Result.ok({192status: RemoteCodeSearchIndexStatus.Ready,193indexedCommit: undefined194});195}196197this._logService.trace(`GithubCodeSearchService::getRemoteIndexState(${repoNwo}). Semantic indexing enabled but not yet indexed.`);198199return Result.ok({ status: RemoteCodeSearchIndexStatus.BuildingIndex });200} else {201this._logService.trace(`GithubCodeSearchService::getRemoteIndexState(${repoNwo}). semantic_indexing_enabled was false. Repo not yet indexed but possibly can be.`);202return Result.ok({ status: RemoteCodeSearchIndexStatus.NotYetIndexed });203}204} catch (e: unknown) {205if (isCancellationError(e)) {206throw e;207}208209this._logService.error(`GithubCodeSearchService::getRemoteIndexState(${repoNwo}). Error: ${e}`);210return Result.error<RemoteCodeSearchError>({ type: 'generic-error', error: e instanceof Error ? e : new Error(String(e)) });211}212}213214public async triggerIndexing(215auth: { readonly silent: boolean },216triggerReason: 'auto' | 'manual' | 'tool',217githubRepoId: GithubRepoId,218telemetryInfo: TelemetryCorrelationId,219): Promise<Result<true, RemoteCodeSearchError>> {220const authToken = await this.getGithubAccessToken(auth.silent);221if (!authToken) {222return Result.error({ type: 'not-authorized' });223}224225const response = await this._capiClientService.makeRequest<Response>({226method: 'POST',227headers: {228Authorization: `Bearer ${authToken}`,229...getGithubMetadataHeaders(telemetryInfo.callTracker, this._envService),230},231body: JSON.stringify({232auto: triggerReason === 'auto',233})234}, { type: RequestType.EmbeddingsIndex, repoWithOwner: toGithubNwo(githubRepoId) });235236if (!response.ok) {237this._logService.error(`GithubCodeSearchService.triggerIndexing(${triggerReason}). Failed to request indexing for '${githubRepoId}'. Response: ${response.status}. ${await response.text()}`);238239/* __GDPR__240"githubCodeSearch.triggerIndexing.error" : {241"owner": "mjbvz",242"comment": "Information about failed trigger indexing requests",243"workspaceSearchSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Caller of the search" },244"workspaceSearchCorrelationId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Correlation id for the search" },245"triggerReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Reason why the indexing was triggered" },246"statusCode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The response status code" }247}248*/249this._telemetryService.sendMSFTTelemetryEvent('githubCodeSearch.triggerIndexing.error', {250workspaceSearchSource: telemetryInfo.callTracker.toString(),251workspaceSearchCorrelationId: telemetryInfo.correlationId,252triggerReason253}, {254statusCode: response.status,255});256257return Result.error({ type: 'generic-error', error: new Error(`Failed to request indexing for '${githubRepoId}'. Response: ${response.status}.`) });258}259260/* __GDPR__261"githubCodeSearch.getRemoteIndexState.success" : {262"owner": "mjbvz",263"comment": "Information about failed remote index state requests",264"workspaceSearchSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Caller of the search" },265"workspaceSearchCorrelationId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Correlation id for the search" },266"triggerReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Reason why the indexing was triggered" }267}268*/269this._telemetryService.sendMSFTTelemetryEvent('githubCodeSearch.getRemoteIndexState.success', {270workspaceSearchSource: telemetryInfo.callTracker.toString(),271workspaceSearchCorrelationId: telemetryInfo.correlationId,272triggerReason,273}, {});274275return Result.ok(true);276}277278async semanticSearch(279auth: { readonly silent: boolean },280embeddingType: EmbeddingType,281repo: GithubCodeSearchRepoInfo,282searchQuery: string,283maxResults: number,284options: CodeSearchOptions,285telemetryInfo: TelemetryCorrelationId,286token: CancellationToken287): Promise<SemanticCodeSearchResult> {288const authToken = await this.getGithubAccessToken(auth.silent);289if (!authToken) {290throw new Error('No valid auth token');291}292293const response = await raceCancellationError(294this._instantiationService.invokeFunction(postRequest, {295endpointOrUrl: { type: RequestType.EmbeddingsCodeSearch },296secretKey: authToken,297intent: 'copilot-panel',298requestId: '',299body: {300scoping_query: `repo:${toGithubNwo(repo.githubRepoId)}`,301// The semantic search endpoint only supports prompts of up to 8k bytes (in utf8)302// For now just truncate but we should consider a better way to handle this, such as having a model303// generate a short prompt304prompt: truncateToMaxUtf8Length(searchQuery, 7800),305include_embeddings: false,306limit: maxResults,307embedding_model: embeddingType.id,308} satisfies {309scoping_query: string;310prompt: string;311include_embeddings: boolean;312limit: number;313embedding_model: string;314} as any,315additionalHeaders: getGithubMetadataHeaders(telemetryInfo.callTracker, this._envService),316cancelToken: token,317}),318token);319320if (!response.ok) {321/* __GDPR__322"githubCodeSearch.searchRepo.error" : {323"owner": "mjbvz",324"comment": "Information about failed code searches",325"workspaceSearchSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Caller of the search" },326"workspaceSearchCorrelationId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Correlation id for the search" },327"statusCode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The response status code" }328}329*/330this._telemetryService.sendMSFTTelemetryEvent('githubCodeSearch.searchRepo.error', {331workspaceSearchSource: telemetryInfo.callTracker.toString(),332workspaceSearchCorrelationId: telemetryInfo.correlationId,333}, {334statusCode: response.status,335});336337throw new Error(`Code search semantic search failed with status: ${response.status}`);338}339340const body = await raceCancellationError(response.json(), token);341if (!Array.isArray(body.results)) {342throw new Error(`Code search semantic search unexpected response json shape`);343}344345const result = await raceCancellationError(parseGithubCodeSearchResponse(body, repo, options, this._ignoreService), token);346347/* __GDPR__348"githubCodeSearch.searchRepo.success" : {349"owner": "mjbvz",350"comment": "Information about successful code searches",351"workspaceSearchSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Caller of the search" },352"workspaceSearchCorrelationId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Correlation id for the search" },353"resultCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of returned chunks from the search" },354"resultOutOfSync": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Tracks if the commit we think code search has indexed matches the commit code search returns results from" }355}356*/357this._telemetryService.sendMSFTTelemetryEvent('githubCodeSearch.searchRepo.success', {358workspaceSearchSource: telemetryInfo.callTracker.toString(),359workspaceSearchCorrelationId: telemetryInfo.correlationId,360}, {361resultCount: body.results.length,362resultOutOfSync: result.outOfSync ? 1 : 0,363});364365return result;366}367368async lexicalSearch(369auth: { readonly silent: boolean },370scope: GithubCodeSearchScope,371query: string,372maxResults: number,373options: CodeSearchOptions,374telemetryInfo: TelemetryCorrelationId,375token: CancellationToken376): Promise<LexicalCodeSearchResult> {377const authToken = await this.getGithubAccessToken(auth.silent);378if (!authToken) {379throw new Error('No valid auth token');380}381382const scopeQualifier = scope.kind === 'org' ? `org:${scope.org}` : `repo:${toGithubNwo(scope.githubRepoId)}`;383const searchQuery = `${query} ${scopeQualifier}`;384const routeSlug = `search/code?q=${encodeURIComponent(searchQuery)}&per_page=${maxResults}`;385386const body = await raceCancellationError(makeGitHubAPIRequest(387this._fetcherService,388this._logService,389this._telemetryService,390this._capiClientService.dotcomAPIURL,391routeSlug,392'GET',393authToken,394{395accept: 'application/vnd.github.text-match+json',396additionalHeaders: getGithubMetadataHeaders(telemetryInfo.callTracker, this._envService),397callSite: 'github-code-search-lexical',398},399), token);400401if (!body) {402/* __GDPR__403"githubCodeSearch.lexicalSearch.error" : {404"owner": "mjbvz",405"comment": "Information about failed lexical code searches",406"workspaceSearchSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Caller of the search" },407"workspaceSearchCorrelationId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Correlation id for the search" }408}409*/410this._telemetryService.sendMSFTTelemetryEvent('githubCodeSearch.lexicalSearch.error', {411workspaceSearchSource: telemetryInfo.callTracker.toString(),412workspaceSearchCorrelationId: telemetryInfo.correlationId,413});414415throw new Error(`Code search lexical search failed`);416}417if (!Array.isArray(body.items)) {418throw new Error(`Code search lexical search unexpected response json shape`);419}420421const result = await raceCancellationError(parseLexicalSearchResponse(body, scope, options, this._ignoreService), token);422423/* __GDPR__424"githubCodeSearch.lexicalSearch.success" : {425"owner": "mjbvz",426"comment": "Information about successful lexical code searches",427"workspaceSearchSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Caller of the search" },428"workspaceSearchCorrelationId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Correlation id for the search" },429"resultCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of returned items from the search" }430}431*/432this._telemetryService.sendMSFTTelemetryEvent('githubCodeSearch.lexicalSearch.success', {433workspaceSearchSource: telemetryInfo.callTracker.toString(),434workspaceSearchCorrelationId: telemetryInfo.correlationId,435}, {436resultCount: body.items.length,437});438439return result;440}441442private async getGithubAccessToken(silent: boolean) {443return (await this._authenticationService.getGitHubSession('permissive', { silent }))?.accessToken444?? (await this._authenticationService.getGitHubSession('any', { silent }))?.accessToken;445}446447448private async isEmptyRepo(authToken: string, githubRepoId: GithubRepoId, token: CancellationToken): Promise<boolean> {449const response = await raceCancellationError(fetch(this._capiClientService.dotcomAPIURL + `/repos/${toGithubNwo(githubRepoId)}`, {450headers: {451'Authorization': `Bearer ${authToken}`,452'Accept': 'application/vnd.github.v3+json'453}454}), token);455456if (!response.ok) {457this._logService.error(`GithubCodeSearchService.isEmptyRepo(${toGithubNwo(githubRepoId)}). Failed to fetch repo info. Response: ${response.status}. ${await response.text()}`);458return false;459}460461const data: any = await response.json();462463// Check multiple indicators of an empty repo:464// - size of 0 indicates no content465// - missing default_branch often means no commits466return data.size === 0 || !data.default_branch;467}468}469470export async function parseGithubCodeSearchResponse(body: ResponseShape, repo: GithubCodeSearchRepoInfo, options: CodeSearchOptions & { skipVerifyRepo?: boolean }, ignoreService: IIgnoreService): Promise<SemanticCodeSearchResult> {471let outOfSync = false;472const outChunks: FileChunkAndScore[] = [];473474const embeddingsType = new EmbeddingType(body.embedding_model);475476await Promise.all(body.results.map(async (result): Promise<FileChunkAndScore | undefined> => {477if (!options.skipVerifyRepo && result.location.repo.nwo.toLowerCase() !== toGithubNwo(repo.githubRepoId)) {478return;479}480481let fileUri: URI;482if (repo.localRepoRoot) {483fileUri = URI.joinPath(repo.localRepoRoot, result.location.path);484if (await ignoreService.isCopilotIgnored(fileUri)) {485return;486}487} else {488// Non-local repo, make up a URI489fileUri = URI.from({490scheme: 'githubRepoResult',491path: '/' + result.location.path492});493}494495if (!shouldInclude(fileUri, options.globPatterns)) {496return;497}498499outOfSync ||= !!repo.indexedCommit && result.location.commit_sha !== repo.indexedCommit;500outChunks.push({501chunk: {502file: fileUri,503text: stripChunkTextMetadata(result.chunk.text),504rawText: undefined,505range: new Range(result.chunk.line_range.start, 0, result.chunk.line_range.end, 0),506isFullFile: false, // TODO: get this from github507},508distance: {509embeddingType: embeddingsType,510value: result.distance,511}512});513}));514515// Extract the remote URL and ref name from the first result516const firstResult = body.results[0];517let remoteUrl: string | undefined;518let refName: string | undefined;519if (firstResult) {520// Derive the web URL from the API URL (e.g. https://api.github.com/repos/o/r -> https://github.com/o/r)521const apiUrl = firstResult.location.repo.url;522const nwo = firstResult.location.repo.nwo;523try {524const parsed = URI.parse(apiUrl);525const host = parsed.authority === 'api.github.com' ? 'github.com' : parsed.authority.replace(/^api\./, '');526remoteUrl = `https://${host}/${nwo}`;527} catch {528// Fall back to constructing from nwo529remoteUrl = `https://github.com/${nwo}`;530}531532// Extract branch name from ref_name (e.g. "refs/heads/main" -> "main")533const rawRef = firstResult.location.ref_name;534if (rawRef?.startsWith('refs/heads/')) {535refName = rawRef.slice('refs/heads/'.length);536} else if (rawRef) {537refName = rawRef;538}539}540541return { chunks: outChunks, outOfSync, remoteUrl, refName };542}543544interface LexicalSearchResponseShape {545readonly total_count: number;546readonly incomplete_results: boolean;547readonly items: readonly LexicalSearchItem[];548}549550type LexicalSearchItem = {551readonly path: string;552readonly repository: {553readonly full_name: string;554};555readonly text_matches?: readonly {556readonly fragment: string;557readonly matches: readonly { readonly text: string; readonly indices: readonly [number, number] }[];558readonly object_type: string;559readonly property: string;560}[];561readonly score: number;562};563564export async function parseLexicalSearchResponse(body: LexicalSearchResponseShape, scope: GithubCodeSearchScope & { skipVerifyRepo?: boolean }, options: CodeSearchOptions & { skipVerifyRepo?: boolean }, ignoreService: IIgnoreService): Promise<LexicalCodeSearchResult> {565const outChunks: FileChunk[] = [];566567await Promise.all(body.items.map(async (item): Promise<void> => {568if (!options.skipVerifyRepo && scope.kind === 'repo' && item.repository.full_name.toLowerCase() !== toGithubNwo(scope.githubRepoId)) {569return;570}571if (!options.skipVerifyRepo && scope.kind === 'org' && item.repository.full_name.toLowerCase().split('/')[0] !== scope.org.toLowerCase()) {572return;573}574575const localRepoRoot = scope.kind === 'repo' ? scope.localRepoRoot : undefined;576let fileUri: URI;577if (localRepoRoot) {578fileUri = URI.joinPath(localRepoRoot, item.path);579if (await ignoreService.isCopilotIgnored(fileUri)) {580return;581}582} else {583fileUri = URI.from({584scheme: 'githubRepoResult',585path: '/' + item.repository.full_name + '/' + item.path586});587}588589if (!shouldInclude(fileUri, options.globPatterns)) {590return;591}592593const textMatches = item.text_matches?.filter(m => m.property === 'content');594if (textMatches && textMatches.length > 0) {595for (const match of textMatches) {596outChunks.push({597file: fileUri,598text: match.fragment,599rawText: undefined,600range: new Range(0, 0, 0, 0),601isFullFile: false,602});603}604} else {605// No text matches, include the file as a whole-file result606outChunks.push({607file: fileUri,608text: '',609rawText: undefined,610range: new Range(0, 0, 0, 0),611isFullFile: true,612});613}614}));615616return { chunks: outChunks, outOfSync: false };617}618619620