From 0a9ff29b17267b3184521e12ed2e125aff10f2ec Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 4 Aug 2026 20:18:52 +0000 Subject: [PATCH] fix(omptype): emit postfix bound for min-only numeric typebox schemas tNumber rendered a min-only bound as the left-bound-only DSL "1 <= number.integer", which parseBounded rejects because that range spelling requires a trailing right bound. Standard TypeBox schemas like Type.Integer({ minimum: 1 }) therefore failed to build, breaking extension tool loading. Emit the postfix "number.integer >= 1" form when there is no upper bound instead. Fixes #7648 --- packages/omptype/CHANGELOG.md | 4 ++++ packages/omptype/src/typebox.ts | 16 +++++++++++++--- packages/omptype/test/typebox.test.ts | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/omptype/CHANGELOG.md b/packages/omptype/CHANGELOG.md index 9c6e03621..555d1af30 100644 --- a/packages/omptype/CHANGELOG.md +++ b/packages/omptype/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the TypeBox adapter emitting an invalid left-bound-only DSL for min-only numeric schemas (e.g. `Type.Integer({ minimum: 1 })`), which threw `left bound requires a corresponding right bound` and broke extension tool loading ([#7648](https://github.com/can1357/oh-my-pi/issues/7648)). + ## [17.2.8] - 2026-08-04 ### Added diff --git a/packages/omptype/src/typebox.ts b/packages/omptype/src/typebox.ts index 2654f5372..7e252a556 100644 --- a/packages/omptype/src/typebox.ts +++ b/packages/omptype/src/typebox.ts @@ -267,9 +267,19 @@ function tNumber(opts?: NumberOpts, integer = false): TNumber { upper = { value: opts.exclusiveMaximum, exclusive: true }; } const keyword = integer ? "number.integer" : "number"; - const lowerDsl = lower ? `${lower.value} ${lower.exclusive ? "<" : "<="} ` : ""; - const upperDsl = upper ? ` ${upper.exclusive ? "<" : "<="} ${upper.value}` : ""; - let schema = asRuntime(type.raw(`${lowerDsl}${keyword}${upperDsl}`)); + // The `LO <= TYPE <= HI` range spelling requires both bounds; a min-only + // bound must use the postfix `TYPE >= LO` form (see parseBounded in ir.ts). + let src: string; + if (lower && upper) { + src = `${lower.value} ${lower.exclusive ? "<" : "<="} ${keyword} ${upper.exclusive ? "<" : "<="} ${upper.value}`; + } else if (lower) { + src = `${keyword} ${lower.exclusive ? ">" : ">="} ${lower.value}`; + } else if (upper) { + src = `${keyword} ${upper.exclusive ? "<" : "<="} ${upper.value}`; + } else { + src = keyword; + } + let schema = asRuntime(type.raw(src)); if (opts?.multipleOf !== undefined) { const divisor = opts.multipleOf; schema = schema.narrow((value, ctx) => { diff --git a/packages/omptype/test/typebox.test.ts b/packages/omptype/test/typebox.test.ts index f41e7911f..0ded2d33f 100644 --- a/packages/omptype/test/typebox.test.ts +++ b/packages/omptype/test/typebox.test.ts @@ -56,6 +56,22 @@ describe("TypeBox adapter", () => { expect(valid(exclusive, 1)).toBe(false); expect(valid(exclusive, 3)).toBe(false); expect(exclusive.toJsonSchema()).toEqual({ type: "number", exclusiveMinimum: 1, exclusiveMaximum: 3 }); + + const minOnly = Type.Integer({ minimum: 1 }); + expect(valid(minOnly, 1)).toBe(true); + expect(valid(minOnly, 0)).toBe(false); + expect(valid(minOnly, 1.5)).toBe(false); + expect(minOnly.toJsonSchema()).toEqual({ type: "integer", minimum: 1 }); + const exclusiveMinOnly = Type.Number({ exclusiveMinimum: 0 }); + expect(valid(exclusiveMinOnly, 0)).toBe(false); + expect(valid(exclusiveMinOnly, 0.5)).toBe(true); + const maxOnly = Type.Number({ maximum: 5 }); + expect(valid(maxOnly, 5)).toBe(true); + expect(valid(maxOnly, 6)).toBe(false); + const minWithMultiple = Type.Integer({ minimum: 1, multipleOf: 2 }); + expect(valid(minWithMultiple, 4)).toBe(true); + expect(valid(minWithMultiple, 3)).toBe(false); + expect(valid(minWithMultiple, 0)).toBe(false); }); test("arrays, tuples, objects, records and intersections validate", () => {