From 59bdd7762417eb656ad4a3d18cf5388815f17b30 Mon Sep 17 00:00:00 2001 From: jottakka Date: Fri, 17 Jul 2026 00:27:30 -0300 Subject: [PATCH 1/3] fix: preserve toolkit docs on merge failures Co-authored-by: Cursor --- toolkit-docs-generator/src/cli/index.ts | 68 +++++++++++++++---- .../src/merger/data-merger.ts | 22 +++++- .../tests/merger/data-merger.test.ts | 25 ++++++- 3 files changed, 96 insertions(+), 19 deletions(-) diff --git a/toolkit-docs-generator/src/cli/index.ts b/toolkit-docs-generator/src/cli/index.ts index bb05939f6..edc461d4f 100644 --- a/toolkit-docs-generator/src/cli/index.ts +++ b/toolkit-docs-generator/src/cli/index.ts @@ -855,6 +855,10 @@ const processProviders = async ( } } catch (error) { spinner.fail(`${pv.provider}: ${error}`); + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to process ${pv.provider}: ${message}`, { + cause: error, + }); } } @@ -1714,11 +1718,17 @@ program } } + const mergeFailures = allResults.filter((result) => result.error); + // Error results can still carry a last-known-good toolkit fallback from + // the merger. Keep them in the batch output so one failed merge cannot + // silently remove that toolkit from index.json. + const writableResults = allResults; + // Generate output files (batch mode if not incremental) - if (!useIncremental && allResults.length > 0) { + if (!useIncremental && writableResults.length > 0) { spinner.start("Writing output files..."); - const toolkits = allResults.map((r) => r.toolkit); + const toolkits = writableResults.map((result) => result.toolkit); const genResult = await generator.generateAll(toolkits); filesWritten.push(...genResult.filesWritten); @@ -1731,13 +1741,15 @@ program } } else if (useIncremental) { // Generate index file for incremental mode - if (allResults.length > 0 || skipToolkitIds.size > 0) { + if (writableResults.length > 0 || skipToolkitIds.size > 0) { spinner.start("Generating index file..."); try { const existingToolkits = await generator.getCompletedToolkitIds(); const allToolkitIds = new Set([ ...existingToolkits, - ...allResults.map((r) => r.toolkit.id.toLowerCase()), + ...writableResults.map((result) => + result.toolkit.id.toLowerCase() + ), ]); // Load all toolkits for index @@ -1797,7 +1809,11 @@ program } // Print summary - if (filesWritten.length > 0 || writeErrors.length > 0) { + if ( + filesWritten.length > 0 || + writeErrors.length > 0 || + mergeFailures.length > 0 + ) { console.log(chalk.green("\n✓ Generation complete\n")); if (options.verbose) { console.log(chalk.dim("Files:")); @@ -1814,6 +1830,14 @@ program console.log(chalk.yellow(` ${error}`)); } } + if (mergeFailures.length > 0) { + console.log(chalk.red("\nFailed toolkits:")); + for (const failure of mergeFailures) { + console.log( + chalk.red(` ${failure.toolkit.id}: ${failure.error}`) + ); + } + } } if (options.verifyOutput) { @@ -1846,13 +1870,7 @@ program (count, result) => count + result.warnings.length, 0 ); - const failedToolkits = allResults - .filter((result) => - result.warnings.some((warning) => - warning.startsWith("Error processing toolkit") - ) - ) - .map((result) => result.toolkit.id); + const failedToolkits = mergeFailures.map((result) => result.toolkit.id); const failedTools = allResults.flatMap((result) => result.failedTools); const failedToolkitsFromTools = Array.from( new Set(failedTools.map((tool) => tool.toolkitId)) @@ -1905,6 +1923,9 @@ program title: "generate", details: runDetails, }); + if (mergeFailures.length > 0 || writeErrors.length > 0) { + process.exitCode = 1; + } } catch (error) { spinner.fail( `Error: ${error instanceof Error ? error.message : String(error)}` @@ -2415,6 +2436,11 @@ program spinner.start(progressTracker.getProgressString()); const results = await merger.mergeAllToolkits(); + const mergeFailures = results.filter((result) => result.error); + // Error results can still carry a last-known-good toolkit fallback. + // Preserve it in batch output rather than letting --clear-output + // remove the prior JSON artifact. + const writableResults = results; const summary = progressTracker.getSummary(); spinner.succeed( `Processed ${summary.completed} toolkit(s) with ${summary.totalTools} tools in ${summary.elapsed}` @@ -2435,11 +2461,20 @@ program console.log(chalk.dim(` - ${warning}`)); } } + if (mergeFailures.length > 0) { + console.log(chalk.red("\nFailed toolkits:")); + for (const failure of mergeFailures) { + console.log( + chalk.red(` ${failure.toolkit.id}: ${failure.error}`) + ); + } + process.exitCode = 1; + } // Generate output (batch mode if not incremental) - if (!useIncremental && results.length > 0) { + if (!useIncremental && writableResults.length > 0) { spinner.start("Writing output files..."); - const toolkits = results.map((r) => r.toolkit); + const toolkits = writableResults.map((result) => result.toolkit); const genResult = await generator.generateAll(toolkits); filesWritten.push(...genResult.filesWritten); @@ -2461,7 +2496,9 @@ program const existingToolkits = await generator.getCompletedToolkitIds(); const allToolkitIds = new Set([ ...existingToolkits, - ...results.map((r) => r.toolkit.id.toLowerCase()), + ...writableResults.map((result) => + result.toolkit.id.toLowerCase() + ), ]); const toolkitsForIndex: MergedToolkit[] = []; @@ -2553,6 +2590,7 @@ program for (const warning of writeErrors) { console.log(chalk.yellow(` ${warning}`)); } + process.exitCode = 1; } } catch (error) { spinner.fail( diff --git a/toolkit-docs-generator/src/merger/data-merger.ts b/toolkit-docs-generator/src/merger/data-merger.ts index 6696dfd89..b73f9139c 100644 --- a/toolkit-docs-generator/src/merger/data-merger.ts +++ b/toolkit-docs-generator/src/merger/data-merger.ts @@ -93,6 +93,7 @@ export interface MergeResult { toolkit: MergedToolkit; warnings: string[]; failedTools: FailedTool[]; + error?: string; } export interface ToolExampleResult { @@ -987,6 +988,15 @@ export class DataMerger { message: string, previousToolkit?: MergedToolkit ): MergeResult { + if (previousToolkit) { + return { + toolkit: previousToolkit, + warnings: [`Error processing toolkit: ${message}`], + failedTools: [], + error: message, + }; + } + return { toolkit: { id: toolkitId, @@ -1005,13 +1015,14 @@ export class DataMerger { }, auth: null, tools: [], - documentationChunks: previousToolkit?.documentationChunks ?? [], - customImports: previousToolkit?.customImports ?? [], - subPages: previousToolkit?.subPages ?? [], + documentationChunks: [], + customImports: [], + subPages: [], generatedAt: new Date().toISOString(), }, warnings: [`Error processing toolkit: ${message}`], failedTools: [], + error: message, }; } @@ -1049,6 +1060,11 @@ export class DataMerger { return result; } catch (error) { const message = error instanceof Error ? error.message : String(error); + if (this.requireCompleteData) { + throw new Error(`Failed to process ${toolkitId}: ${message}`, { + cause: error, + }); + } const previousToolkit = this.getPreviousToolkit(toolkitId); return this.buildMergeErrorResult(toolkitId, message, previousToolkit); } diff --git a/toolkit-docs-generator/tests/merger/data-merger.test.ts b/toolkit-docs-generator/tests/merger/data-merger.test.ts index 70b0afe0c..eb4d69177 100644 --- a/toolkit-docs-generator/tests/merger/data-merger.test.ts +++ b/toolkit-docs-generator/tests/merger/data-merger.test.ts @@ -1928,7 +1928,8 @@ describe("DataMerger", () => { const results = await merger.mergeAllToolkits(); const result = results[0]; - // Error result must preserve previous custom sections, not return empty arrays + expect(result?.error).toBe("Custom sections source unavailable"); + expect(result?.toolkit).toEqual(previousResult.toolkit); expect(result?.toolkit.documentationChunks).toHaveLength(1); expect(result?.toolkit.documentationChunks[0]?.content).toBe( "Critical: GitHub Apps only." @@ -1955,6 +1956,7 @@ describe("DataMerger", () => { const results = await merger.mergeAllToolkits(); const result = results[0]; + expect(result?.error).toBe("Custom sections source unavailable"); expect(result?.toolkit.documentationChunks).toHaveLength(0); expect(result?.toolkit.customImports).toHaveLength(0); expect(result?.toolkit.subPages).toHaveLength(0); @@ -2053,6 +2055,27 @@ describe("DataMerger", () => { expect(results[0]?.toolkit.id).toBe("Github"); }); + it("fails strict runs when a complete toolkit cannot be merged", async () => { + const toolkitDataSource = createCombinedToolkitDataSource({ + toolSource: new InMemoryToolDataSource([githubTool1]), + metadataSource: new InMemoryMetadataSource([githubMetadata]), + }); + const merger = new DataMerger({ + toolkitDataSource, + customSectionsSource: { + getCustomSections: async () => { + throw new Error("Custom sections source unavailable"); + }, + }, + toolExampleGenerator: createStubGenerator(), + requireCompleteData: true, + }); + + await expect(merger.mergeAllToolkits()).rejects.toThrow( + "Failed to process Github: Custom sections source unavailable" + ); + }); + it("should return empty array when no tools", async () => { const toolkitDataSource = createCombinedToolkitDataSource({ toolSource: new InMemoryToolDataSource([]), From c918790d2885578634c3caf64bc71a4636e6e4f5 Mon Sep 17 00:00:00 2001 From: jottakka Date: Fri, 17 Jul 2026 01:09:07 -0300 Subject: [PATCH 2/3] fix: retain fallback toolkits in incremental output Co-authored-by: Cursor --- toolkit-docs-generator/src/cli/index.ts | 4 ---- toolkit-docs-generator/src/merger/data-merger.ts | 10 +++++++++- .../tests/merger/data-merger.test.ts | 5 +++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/toolkit-docs-generator/src/cli/index.ts b/toolkit-docs-generator/src/cli/index.ts index edc461d4f..da6a075db 100644 --- a/toolkit-docs-generator/src/cli/index.ts +++ b/toolkit-docs-generator/src/cli/index.ts @@ -855,10 +855,6 @@ const processProviders = async ( } } catch (error) { spinner.fail(`${pv.provider}: ${error}`); - const message = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to process ${pv.provider}: ${message}`, { - cause: error, - }); } } diff --git a/toolkit-docs-generator/src/merger/data-merger.ts b/toolkit-docs-generator/src/merger/data-merger.ts index b73f9139c..ef089999a 100644 --- a/toolkit-docs-generator/src/merger/data-merger.ts +++ b/toolkit-docs-generator/src/merger/data-merger.ts @@ -1066,7 +1066,15 @@ export class DataMerger { }); } const previousToolkit = this.getPreviousToolkit(toolkitId); - return this.buildMergeErrorResult(toolkitId, message, previousToolkit); + const result = this.buildMergeErrorResult( + toolkitId, + message, + previousToolkit + ); + if (this.onToolkitComplete) { + await this.onToolkitComplete(result); + } + return result; } } diff --git a/toolkit-docs-generator/tests/merger/data-merger.test.ts b/toolkit-docs-generator/tests/merger/data-merger.test.ts index eb4d69177..a4b26f7b4 100644 --- a/toolkit-docs-generator/tests/merger/data-merger.test.ts +++ b/toolkit-docs-generator/tests/merger/data-merger.test.ts @@ -1917,12 +1917,16 @@ describe("DataMerger", () => { }), createStubGenerator() ); + const completedToolkitIds: string[] = []; const merger = new DataMerger({ toolkitDataSource, customSectionsSource: makeFailingCustomSectionsSource(), toolExampleGenerator: createStubGenerator(), previousToolkits: new Map([["github", previousResult.toolkit]]), + onToolkitComplete: async (result) => { + completedToolkitIds.push(result.toolkit.id); + }, }); const results = await merger.mergeAllToolkits(); @@ -1939,6 +1943,7 @@ describe("DataMerger", () => { expect(result?.warnings[0]).toContain( "Custom sections source unavailable" ); + expect(completedToolkitIds).toEqual(["Github"]); }); it("returns empty custom sections in error result when no previous toolkit exists", async () => { From 8011b5d7fa2e67c8e71856c4ef3b0b5b4921673d Mon Sep 17 00:00:00 2001 From: jottakka Date: Fri, 17 Jul 2026 01:13:07 -0300 Subject: [PATCH 3/3] fix: avoid writing empty toolkit fallbacks Co-authored-by: Cursor --- toolkit-docs-generator/src/merger/data-merger.ts | 2 +- toolkit-docs-generator/tests/merger/data-merger.test.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/toolkit-docs-generator/src/merger/data-merger.ts b/toolkit-docs-generator/src/merger/data-merger.ts index ef089999a..d278b18ca 100644 --- a/toolkit-docs-generator/src/merger/data-merger.ts +++ b/toolkit-docs-generator/src/merger/data-merger.ts @@ -1071,7 +1071,7 @@ export class DataMerger { message, previousToolkit ); - if (this.onToolkitComplete) { + if (this.onToolkitComplete && previousToolkit) { await this.onToolkitComplete(result); } return result; diff --git a/toolkit-docs-generator/tests/merger/data-merger.test.ts b/toolkit-docs-generator/tests/merger/data-merger.test.ts index a4b26f7b4..e2277f399 100644 --- a/toolkit-docs-generator/tests/merger/data-merger.test.ts +++ b/toolkit-docs-generator/tests/merger/data-merger.test.ts @@ -1951,11 +1951,15 @@ describe("DataMerger", () => { toolSource: new InMemoryToolDataSource([githubTool1]), metadataSource: new InMemoryMetadataSource([githubMetadata]), }); + const completedToolkitIds: string[] = []; const merger = new DataMerger({ toolkitDataSource, customSectionsSource: makeFailingCustomSectionsSource(), toolExampleGenerator: createStubGenerator(), + onToolkitComplete: async (result) => { + completedToolkitIds.push(result.toolkit.id); + }, }); const results = await merger.mergeAllToolkits(); @@ -1968,6 +1972,7 @@ describe("DataMerger", () => { expect(result?.warnings[0]).toContain( "Custom sections source unavailable" ); + expect(completedToolkitIds).toEqual([]); }); });