From 5a2d0b423d3fba135bcca79e6cb6c151d2a5081e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:14:13 +0000 Subject: [PATCH 1/5] Initial plan From 3682fffebdf345e5eb660e2fc0da0dbf85c69bf9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:17:39 +0000 Subject: [PATCH 2/5] feat: add Script.declareVersion and Versions type output Agent-Logs-Url: https://github.com/counterfact/api-simulator/sessions/368d3892-1abe-4eec-b0c7-b6b603da28b6 --- src/typescript-generator/script.ts | 84 ++++++++++++++++++++++-- test/typescript-generator/script.test.ts | 58 ++++++++++++++++ 2 files changed, 138 insertions(+), 4 deletions(-) diff --git a/src/typescript-generator/script.ts b/src/typescript-generator/script.ts index e006c6030..a69152c45 100644 --- a/src/typescript-generator/script.ts +++ b/src/typescript-generator/script.ts @@ -39,6 +39,7 @@ export class Script { public repository: Repository; public comments: string[]; public exports: Map; + public versions: Map>; public imports: Map; public externalImport: Map; public cache: Map; @@ -49,6 +50,7 @@ export class Script { this.repository = repository; this.comments = []; this.exports = new Map(); + this.versions = new Map(); this.imports = new Map(); this.externalImport = new Map(); this.cache = new Map(); @@ -248,17 +250,69 @@ export class Script { return this.export(coder, true); } + public declareVersion(coder: Coder, name: string): void { + const version = + (coder as Coder & { + version?: string; + }).version ?? ""; + + const versions = this.versions.get(name) ?? new Map(); + this.versions.set(name, versions); + + if (versions.has(version)) { + return; + } + + const versionStatement: ExportStatement = { + beforeExport: "", + done: false, + id: coder.id, + isDefault: false, + isType: true, + jsdoc: "", + typeDeclaration: coder.typeDeclaration(this.exports, this), + }; + + versionStatement.promise = coder + .delegate() + .then((availableCoder) => { + versionStatement.code = availableCoder.write(this); + + return availableCoder; + }) + .catch((error: Error) => { + versionStatement.code = `{/* error declaring version "${name}" (${version}) for ${this.path}: ${error.stack} */}`; + versionStatement.error = error; + return undefined; + }) + .finally(() => { + versionStatement.done = true; + }); + + versions.set(version, versionStatement); + } + /** `true` while at least one export promise is still pending. */ public isInProgress(): boolean { - return Array.from(this.exports.values()).some( - (exportStatement) => !exportStatement.done, + return ( + Array.from(this.exports.values()).some( + (exportStatement) => !exportStatement.done, + ) || + Array.from(this.versions.values()) + .flatMap((versions) => Array.from(versions.values())) + .some((versionStatement) => !versionStatement.done) ); } /** Returns a promise that resolves when all pending export promises settle. */ public finished(): Promise<(Coder | undefined)[]> { return Promise.all( - Array.from(this.exports.values(), (value) => value.promise!), + [ + ...Array.from(this.exports.values(), (value) => value.promise!), + ...Array.from(this.versions.values()) + .flatMap((versions) => Array.from(versions.values())) + .map((value) => value.promise!), + ], ); } @@ -318,12 +372,32 @@ export class Script { ); } + public versionsTypeStatements(): string[] { + if (this.versions.size === 0) { + return []; + } + + const names = Array.from(this.versions, ([name, versions]) => { + const mappedVersions = Array.from( + versions, + ([version, versionStatement]) => + `"${version}": ${versionStatement.typeDeclaration || (versionStatement.code as string)}`, + ); + + return `"${name}": { ${mappedVersions.join(", ")} }`; + }); + + return [`export type Versions = { ${names.join(", ")} };`]; + } + /** * Formats the fully assembled script source with Prettier and returns it. * * All pending export promises are awaited before formatting. */ - public contents(): Promise { + public async contents(): Promise { + await this.finished(); + return format( [ this.comments.map((comment) => `// ${comment}`).join("\n"), @@ -331,6 +405,8 @@ export class Script { this.externalImportStatements().join("\n"), this.importStatements().join("\n"), "\n\n", + this.versionsTypeStatements().join("\n"), + this.versions.size > 0 ? "\n\n" : "", this.exportStatements().join("\n\n"), ].join(""), { parser: "typescript" }, diff --git a/test/typescript-generator/script.test.ts b/test/typescript-generator/script.test.ts index c874984b8..e320f8a6a 100644 --- a/test/typescript-generator/script.test.ts +++ b/test/typescript-generator/script.test.ts @@ -228,6 +228,45 @@ describe("a Script", () => { ]); }); + it("declares versioned code grouped by name and coder version", async () => { + const repository = new Repository("/base/path"); + + class V1AccountTypeCoder extends Coder { + public version = "1.0.0"; + + *names() { + yield "AccountV1"; + } + + write() { + return "{ id: string }"; + } + } + + class V2AccountTypeCoder extends Coder { + public version = "2.0.0"; + + *names() { + yield "AccountV2"; + } + + write() { + return "{ id: string, email: string }"; + } + } + + const script = repository.get("export-to-me.ts"); + + script.declareVersion(new V1AccountTypeCoder({}), "Account"); + script.declareVersion(new V2AccountTypeCoder({}), "Account"); + + await script.finished(); + + expect(script.versionsTypeStatements()).toStrictEqual([ + 'export type Versions = { "Account": { "1.0.0": { id: string }, "2.0.0": { id: string, email: string } } };', + ]); + }); + it("outputs the contents (import and export statements)", async () => { const repository = new Repository("/base/path"); @@ -246,4 +285,23 @@ describe("a Script", () => { '// This is a comment.\n\nimport { foo } from "./foo.js";\n\nexport const bar = "Bar";\n\nexport default class {}\n', ); }); + + it("outputs the contents including Versions when versions are declared", async () => { + const repository = new Repository("/base/path"); + const script = repository.get("script.ts"); + + class UserCoder extends Coder { + public version = "2025-01"; + + write() { + return "{ id: string }"; + } + } + + script.declareVersion(new UserCoder({}), "User"); + + await expect(script.contents()).resolves.toContain( + 'export type Versions = { User: { "2025-01": { id: string } } };\n', + ); + }); }); From 01f87d563720060d9d31326bab4208d0a7acce9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:19:09 +0000 Subject: [PATCH 3/5] chore: format script version declarations Agent-Logs-Url: https://github.com/counterfact/api-simulator/sessions/368d3892-1abe-4eec-b0c7-b6b603da28b6 --- src/typescript-generator/script.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/typescript-generator/script.ts b/src/typescript-generator/script.ts index a69152c45..b465c246b 100644 --- a/src/typescript-generator/script.ts +++ b/src/typescript-generator/script.ts @@ -252,11 +252,14 @@ export class Script { public declareVersion(coder: Coder, name: string): void { const version = - (coder as Coder & { - version?: string; - }).version ?? ""; + ( + coder as Coder & { + version?: string; + } + ).version ?? ""; - const versions = this.versions.get(name) ?? new Map(); + const versions = + this.versions.get(name) ?? new Map(); this.versions.set(name, versions); if (versions.has(version)) { @@ -306,14 +309,12 @@ export class Script { /** Returns a promise that resolves when all pending export promises settle. */ public finished(): Promise<(Coder | undefined)[]> { - return Promise.all( - [ - ...Array.from(this.exports.values(), (value) => value.promise!), - ...Array.from(this.versions.values()) - .flatMap((versions) => Array.from(versions.values())) - .map((value) => value.promise!), - ], - ); + return Promise.all([ + ...Array.from(this.exports.values(), (value) => value.promise!), + ...Array.from(this.versions.values()) + .flatMap((versions) => Array.from(versions.values())) + .map((value) => value.promise!), + ]); } public externalImportStatements(): string[] { From 6b42c8ebab668ec7fad6918cdad7a1c632f0204b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:20:56 +0000 Subject: [PATCH 4/5] fix: use valid type fallback for declareVersion errors Agent-Logs-Url: https://github.com/counterfact/api-simulator/sessions/368d3892-1abe-4eec-b0c7-b6b603da28b6 --- src/typescript-generator/script.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typescript-generator/script.ts b/src/typescript-generator/script.ts index b465c246b..6a78d08b9 100644 --- a/src/typescript-generator/script.ts +++ b/src/typescript-generator/script.ts @@ -284,7 +284,7 @@ export class Script { return availableCoder; }) .catch((error: Error) => { - versionStatement.code = `{/* error declaring version "${name}" (${version}) for ${this.path}: ${error.stack} */}`; + versionStatement.code = `unknown /* error declaring version "${name}" (${version}) for ${this.path}: ${error.stack} */`; versionStatement.error = error; return undefined; }) From 4f328b17ec87c1c2fabda25aec301498a7f654c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:22:13 +0000 Subject: [PATCH 5/5] chore: sanitize declareVersion error fallback message Agent-Logs-Url: https://github.com/counterfact/api-simulator/sessions/368d3892-1abe-4eec-b0c7-b6b603da28b6 --- src/typescript-generator/script.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typescript-generator/script.ts b/src/typescript-generator/script.ts index 6a78d08b9..9e262ec4d 100644 --- a/src/typescript-generator/script.ts +++ b/src/typescript-generator/script.ts @@ -284,7 +284,7 @@ export class Script { return availableCoder; }) .catch((error: Error) => { - versionStatement.code = `unknown /* error declaring version "${name}" (${version}) for ${this.path}: ${error.stack} */`; + versionStatement.code = `unknown /* error declaring version "${name}" (${version}) for ${this.path}: ${error.message} */`; versionStatement.error = error; return undefined; })