From 8fda87614feab32a56dcda55fe5be2fa5eacd3c9 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:44:07 -0500 Subject: [PATCH] feat(codemode): document numeric string and array constraints (#46510) --- packages/codemode/src/tool-schema.ts | 10 ++ packages/codemode/src/tool.ts | 9 ++ packages/codemode/test/signature.test.ts | 136 ++++++++++++++++++++ packages/core/test/codemode/catalog.test.ts | 3 +- 4 files changed, 157 insertions(+), 1 deletion(-) diff --git a/packages/codemode/src/tool-schema.ts b/packages/codemode/src/tool-schema.ts index 242e9363ba7e..98a902151610 100644 --- a/packages/codemode/src/tool-schema.ts +++ b/packages/codemode/src/tool-schema.ts @@ -63,8 +63,18 @@ const docTags = (schema: JsonSchema): Array => { } catch {} } if (typeof schema.format === "string") tags.push(`@format ${schema.format}`) + if (schema.type === "integer") tags.push("@integer") + if (typeof schema.minimum === "number") tags.push(`@minimum ${schema.minimum}`) + if (typeof schema.maximum === "number") tags.push(`@maximum ${schema.maximum}`) + if (typeof schema.exclusiveMinimum === "number") tags.push(`@exclusiveMinimum ${schema.exclusiveMinimum}`) + if (typeof schema.exclusiveMaximum === "number") tags.push(`@exclusiveMaximum ${schema.exclusiveMaximum}`) + if (typeof schema.multipleOf === "number") tags.push(`@multipleOf ${schema.multipleOf}`) + if (typeof schema.minLength === "number") tags.push(`@minLength ${schema.minLength}`) + if (typeof schema.maxLength === "number") tags.push(`@maxLength ${schema.maxLength}`) + if (typeof schema.pattern === "string") tags.push(`@pattern ${schema.pattern}`) if (typeof schema.minItems === "number") tags.push(`@minItems ${schema.minItems}`) if (typeof schema.maxItems === "number") tags.push(`@maxItems ${schema.maxItems}`) + if (schema.uniqueItems === true) tags.push("@uniqueItems true") return tags } diff --git a/packages/codemode/src/tool.ts b/packages/codemode/src/tool.ts index bfa4ba6babbd..1b23d20c3683 100644 --- a/packages/codemode/src/tool.ts +++ b/packages/codemode/src/tool.ts @@ -21,8 +21,17 @@ export type JsonSchema = { readonly default?: unknown readonly format?: string readonly deprecated?: boolean + readonly minimum?: number + readonly maximum?: number + readonly exclusiveMinimum?: number + readonly exclusiveMaximum?: number + readonly multipleOf?: number + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string readonly minItems?: number readonly maxItems?: number + readonly uniqueItems?: boolean readonly $ref?: string readonly $defs?: Readonly> readonly definitions?: Readonly> diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts index bde314c58859..ce57fc4efadd 100644 --- a/packages/codemode/test/signature.test.ts +++ b/packages/codemode/test/signature.test.ts @@ -139,6 +139,81 @@ describe("pretty signature rendering", () => { expect(pretty).toBe(["{", " size?: number,", "}"].join("\n")) }) + test.each([ + [{ type: "number", minimum: 0 }, "@minimum 0", "number"], + [{ type: "number", maximum: 0 }, "@maximum 0", "number"], + [{ type: "number", exclusiveMinimum: 0 }, "@exclusiveMinimum 0", "number"], + [{ type: "number", exclusiveMaximum: 0 }, "@exclusiveMaximum 0", "number"], + [{ type: "number", multipleOf: 0.25 }, "@multipleOf 0.25", "number"], + [{ type: "string", minLength: 0 }, "@minLength 0", "string"], + [{ type: "string", maxLength: 0 }, "@maxLength 0", "string"], + [{ type: "string", pattern: "^[a-z]+$" }, "@pattern ^[a-z]+$", "string"], + [{ type: "array", minItems: 0 }, "@minItems 0", "Array"], + [{ type: "array", maxItems: 0 }, "@maxItems 0", "Array"], + [{ type: "array", uniqueItems: true }, "@uniqueItems true", "Array"], + ] as const)("renders constraint %j without changing the compact type", (value, tag, type) => { + const schema = { type: "object", properties: { value } } + expect(jsonSchemaToTypeScript(schema, true)).toBe(["{", ` /** ${tag} */`, ` value?: ${type},`, "}"].join("\n")) + expect(jsonSchemaToTypeScript(schema)).toBe(`{ value?: ${type} }`) + }) + + test("documents integer numbers without adding redundant types or requiring uniqueness when false", () => { + expect( + jsonSchemaToTypeScript( + { + type: "object", + properties: { + count: { type: "integer" }, + amount: { type: "number" }, + name: { type: "string" }, + enabled: { type: "boolean" }, + values: { type: "array", uniqueItems: false }, + choice: { type: ["integer", "string"] }, + }, + }, + true, + ), + ).toBe( + [ + "{", + " /** @integer */", + " count?: number,", + " amount?: number,", + " name?: string,", + " enabled?: boolean,", + " values?: Array,", + " choice?: number | string,", + "}", + ].join("\n"), + ) + }) + + test.each([false, null, ""])("preserves default %j alongside constraint tags", (value) => { + expect(jsonSchemaToTypeScript({ properties: { value: { default: value, minLength: 0 } } }, true)).toContain( + ` * @default ${JSON.stringify(value)}\n * @minLength 0\n`, + ) + }) + + test("escapes comment terminators in tag values", () => { + expect( + jsonSchemaToTypeScript( + { properties: { value: { type: "string", default: "*/", format: "*/", pattern: "^a*/b$" } } }, + true, + ), + ).toBe( + [ + "{", + " /**", + ' * @default "* /"', + " * @format * /", + " * @pattern ^a* /b$", + " */", + " value?: string,", + "}", + ].join("\n"), + ) + }) + test("neutralizes */ inside descriptions so nothing closes the comment early", () => { const pretty = jsonSchemaToTypeScript( { type: "object", properties: { note: { type: "string", description: "Ends */ early" } } }, @@ -377,6 +452,67 @@ describe("union schemas render every alternative", () => { }) describe("JSDoc signatures in catalogs and search results", () => { + test.each([ + { + source: "JSON Schema", + schema: { + type: "object", + properties: { + count: { type: "integer", minimum: 0, maximum: 10 }, + name: { type: "string", minLength: 1, maxLength: 20, pattern: "^[a-z]+$" }, + labels: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 5 }, + }, + required: ["count", "name", "labels"], + }, + }, + { + source: "Effect", + schema: Schema.Struct({ + count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(10)), + name: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(20), Schema.isPattern(/^[a-z]+$/)), + labels: Schema.Array(Schema.String).check(Schema.isMinLength(1), Schema.isMaxLength(5)), + }), + }, + ])("$source constraints survive input/output catalog and search signatures", async ({ schema }) => { + const runtime = CodeMode.make({ + tools: { + constrained: Tool.make({ + description: "Constrained tool", + input: schema, + output: schema, + execute: () => Effect.succeed({ count: 1, name: "test", labels: ["test"] }), + }), + }, + }) + const type = [ + "{", + " /**", + " * @integer", + " * @minimum 0", + " * @maximum 10", + " */", + " count: number,", + " /**", + " * @minLength 1", + " * @maxLength 20", + " * @pattern ^[a-z]+$", + " */", + " name: string,", + " /**", + " * @minItems 1", + " * @maxItems 5", + " */", + " labels: Array,", + "}", + ].join("\n") + const signature = `tools.constrained(input: ${type}): Promise<${type}>` + expect(runtime.catalog()[0]?.signature).toBe(signature) + const result = await Effect.runPromise(runtime.execute('return search({ query: "tools.constrained" })')) + expect(result.ok).toBe(true) + if (!result.ok) throw new Error("search failed") + expect(result.value).toMatchObject({ items: [{ signature }] }) + }) + const runtime = CodeMode.make({ tools: { github: { list_issues: listIssues }, orders: { lookup: lookupOrder } } }) const search = async (query: string) => { diff --git a/packages/core/test/codemode/catalog.test.ts b/packages/core/test/codemode/catalog.test.ts index 308c15b16276..378476580d00 100644 --- a/packages/core/test/codemode/catalog.test.ts +++ b/packages/core/test/codemode/catalog.test.ts @@ -104,7 +104,8 @@ describe("CodeModeInstructions.render", () => { ) expect(partial).not.toContain("surrounding top-level agent tools") expect(partial).toContain("- search(input: {") - expect(partial).toContain(" limit?: number,\n offset?: number,") + expect(partial).toContain(" /**\n * @integer\n * @exclusiveMinimum 0\n */\n limit?: number,") + expect(partial).toContain(" /**\n * @integer\n * @minimum 0\n */\n offset?: number,") expect(partial).not.toContain("tools.orders.lookup(input:") })