MarcusDunn
`exclusiveMinimum` / `exclusiveMaximum` are lowered into `minimum` / `maximum`, so the exclusivity is lost by the time a plugin sees the IR. In OpenAPI 3.1+ (JSON Schema 2020-12) `exclusiveMinimum` is a **number** and is a distinct keyword from `minimum`. A generator reading `PrimitiveConstraints` cannot tell the two apart, so a schema that says `> 0` produces a check that admits `0`. ## Reproduction `forge 0.1.27`, IR contract `0.1.27`. ```json { "openapi": "3.2.0", "info": { "title": "exclusive-bound-repro", "version": "1" }, "paths": {}, "components": { "schemas": { "Rate": { "x-kotlin-source": { "module": "repro", "class": "ca.repro.Rate", "generated": true }, "type": "object", "properties": { "exclusive_bound": { "type": "number", "exclusiveMinimum": 0 }, "inclusive_bound": { "type": "number", "minimum": 0 } }, "required": ["exclusive_bound", "inclusive_bound"] } } } } ``` Run any generator that reads numeric bounds. With a build of `generator-kotlin-types` that emits `require()` for them: ``` forge generate --input openapi.json --generator generator_kotlin_types.wasm --out ./out ``` ## Observed The two fields generate **identical** guards: ```kotlin init { require(exclusiveBound >= 0) { "exclusive_bound must be at least 0" } require(inclusiveBound >= 0) { "inclusive_bound must be at least 0" } } ``` ## Expected `exclusive_bound` should produce a strict comparison — `> 0` — distinguishable from the inclusive case. ## Where it shows in the IR `forge_ir::PrimitiveConstraints` has all four fields: ```rust pub minimum: Option<ValueRef>, pub maximum: Option<ValueRef>, pub exclusive_minimum: Option<ValueRef>, pub exclusive_maximum: Option<ValueRef>, ``` but for the schema above, `exclusive_minimum` is `None` and `minimum` is `Some(0)`. This is deducible from the output rather than only from reading the source: the generator emits `>=` solely from `minimum` and `>` solely from `exclusive_minimum`, and it emitted `>=` for a property whose schema carries **only** `exclusiveMinimum`. Scale in our repo: one document declares 11 `exclusiveMinimum`/`exclusiveMaximum` keywords, and the generated output contains **zero** strict comparisons. ## Why it matters The generated check is **weaker than the specification it is generated from**. A field documented as "must be greater than zero" is guarded as "must be at least zero", and a message derived from the constraint misstates the rule. Anything relying on the generated guard as the enforcement point silently admits the boundary value. It is also silent — there is no diagnostic to say a keyword was dropped, so a plugin author reading `exclusive_minimum` reasonably concludes the schema did not set it. ## Possible causes I have not read the parser, so this is a guess: OpenAPI 3.0 spells the same idea as `minimum` plus a **boolean** `exclusiveMinimum`, and a normalisation that folds the 3.0 form into `minimum` would produce exactly this if it also catches the 3.1 numeric form. If so, the 3.0 shape could set `exclusive_minimum` from `minimum` when the boolean is `true`, rather than the reverse. ## Workaround None from the plugin side — the distinction is gone before the plugin runs. A generator that wants to be honest about it can only avoid claiming exclusivity, or read the raw document separately.