diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3767203
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 MiddleAPI
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index e12aba3..94896b3 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,38 @@
-# OAS
+
OpenAPI Spec
+
+
+
+TypeScript tooling for the [OpenAPI Specification](https://spec.openapis.org/), maintained by [middleapi](https://github.com/middleapi). It lets you work with OpenAPI 3.0, 3.1, and 3.2 documents from one place: precise types for each version, and converters that move a document from a newer version to an older one without losing anything the older version can still express.
+
+| Package | Description |
+| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
+| [`@openapi-spec/types`](https://github.com/middleapi/openapi-spec/blob/main/packages/types/README.md) | TypeScript types for OpenAPI 3.0, 3.1, and 3.2, with the specification text as JSDoc on every field |
+| [`@openapi-spec/downgrader`](https://github.com/middleapi/openapi-spec/blob/main/packages/downgrader/README.md) | Downgrade documents and Schema Objects one minor version at a time: 3.2 to 3.1 and 3.1 to 3.0 |
+
+## Development
+
+```bash
+pnpm install
+pnpm test
+pnpm lint
+pnpm type:check
+```
## Sponsors
@@ -83,3 +117,7 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
With thanks to [36 past sponsors](https://htmlpreview.github.io/?https://github.com/middleapi/static/blob/main/sponsors.svg) who helped get openapi-spec here.
+
+## License
+
+Distributed under the MIT License. See [LICENSE](https://github.com/middleapi/openapi-spec/blob/main/LICENSE) for more information.
diff --git a/packages/downgrader/README.md b/packages/downgrader/README.md
index fe14099..2887047 100644
--- a/packages/downgrader/README.md
+++ b/packages/downgrader/README.md
@@ -1,10 +1,30 @@
-# @openapi-spec/downgrader
-
-Downgrade [OpenAPI Specification](https://spec.openapis.org/) documents one minor version at a time: 3.2 → 3.1 and 3.1 → 3.0. Each converter works on an entire document or on a single Schema Object.
-
-- **Never throws**: malformed parts are deep-copied through unchanged instead of failing the whole conversion, and cyclic object graphs (e.g. the output of a `$ref` dereferencer) don't recurse forever — they are converted with their cycles preserved, a subtree that cycles back into an ancestor pointing at that ancestor's converted form. Only pathologically deep nesting (thousands of levels) can still exhaust the call stack.
-- **Never mutates**: the input document is left untouched.
-- **Extension-preserving, never extension-inventing**: existing `x-` keys and unknown keys always survive, while constructs the target version cannot express are converted where an equivalent exists and removed otherwise.
+OpenAPI Spec
+
+
+
+`@openapi-spec/downgrader` downgrades [OpenAPI Specification](https://spec.openapis.org/) documents one minor version at a time: 3.2 to 3.1 and 3.1 to 3.0. Use it when you author against a newer version than your tools accept, such as a code generator, gateway, or validator that stops at 3.0 or 3.1. Each converter handles a whole document or a single Schema Object.
+
+Every converter follows the same contract:
+
+- **Never throws.** Malformed parts are deep-copied through unchanged instead of failing the whole conversion. Cyclic object graphs, such as the output of a `$ref` dereferencer, convert with their cycles preserved. Only pathologically deep nesting (thousands of levels) can still exhaust the call stack.
+- **Never mutates.** The input is left untouched and the result is a new object.
+- **Preserves extensions, never invents them.** `x-` keys and unknown keys survive. Constructs the target version cannot express are converted where an equivalent exists and removed otherwise.
## Usage
@@ -19,68 +39,102 @@ import {
const v31 = downgradeSpecV32ToV31(v32Document)
const v30 = downgradeSpecV31ToV30(v31Document)
-// There is intentionally no direct 3.2 → 3.0 converter; compose the steps:
+// There is no direct 3.2 to 3.0 converter on purpose. Compose the steps:
const downgraded = downgradeSpecV31ToV30(downgradeSpecV32ToV31(v32Document))
-// Schema Objects can be converted standalone:
-const schema = downgradeSchemaV31ToV30({ type: ['string', 'null'] })
-// { type: "string", nullable: true }
+// Schema Objects convert on their own:
+downgradeSchemaV31ToV30({ type: ['string', 'null'] })
+// { type: 'string', nullable: true }
```
-## 3.2 → 3.1
+| Function | Input | Output |
+| ------------------------- | ------------------- | --------------------------------------- |
+| `downgradeSpecV32ToV31` | 3.2 `OpenAPIObject` | 3.1 `OpenAPIObject` |
+| `downgradeSchemaV32ToV31` | 3.2 `SchemaObject` | 3.1 `SchemaObject` |
+| `downgradeSpecV31ToV30` | 3.1 `OpenAPIObject` | 3.0 `OpenAPIObject` |
+| `downgradeSchemaV31ToV30` | 3.1 `SchemaObject` | 3.0 `SchemaObject` or `ReferenceObject` |
-Schema Objects pass through unchanged: the 3.2 Schema Object keyword set is identical to 3.1's (3.2 defines its own dialect URI, but only the OAS base vocabulary gained fields), and the 3.2-only fields (discriminator `defaultMapping`, XML `nodeType`) are deliberately retained. Two caveats: the standard OpenAPI 3.1 document schema tolerates them (Schema Object internals are open there), but the strict OAS 3.1 base-vocabulary meta-schema closes the XML and Discriminator Objects to their fixed fields plus `x-`, so a base-vocabulary validator will flag them; and 3.1 tooling will not act on them — in particular a `defaultMapping` fallback stops taking effect (`nodeType` is recovered on the 3.1 → 3.0 hop).
+All types come from [`@openapi-spec/types`](https://github.com/middleapi/openapi-spec/blob/main/packages/types/README.md).
-Converted:
+## 3.2 → 3.1
-| 3.2 construct | 3.1 result |
-| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `openapi: 3.2.x` | `openapi: 3.1.2` |
-| `jsonSchemaDialect` naming a 3.2 OAS dialect | `https://spec.openapis.org/oas/3.1/dialect/base` (the 3.2 dialect only extends the 3.1 base vocabulary); other dialects pass through |
-| `components.mediaTypes` and content-map `$ref`s to them | references inlined, the component map removed; content entries whose reference cannot be inlined (external, unknown, or cyclic targets) are removed, as 3.1 content maps cannot hold references — a parameter or header losing its entire `content` that way is removed with it (3.1 requires exactly one entry there) |
-| Media type `itemSchema` without a sibling `schema` | `schema: { type: "array", items: … }` (the 3.2 sequential media type data model) |
-| Response `summary` when no `description` exists | promoted to `description` (required in 3.1, so `""` is synthesized as a last resort) |
-| Example `dataValue` / `serializedValue` when `value` and `externalValue` are absent | promoted to `value` (in that order) |
-| Parameter `style: "cookie"` | removed, letting the 3.1 default `form` apply |
+Schema Objects pass through unchanged. 3.2 keeps the 3.1 JSON Schema keyword set and only adds two fields to the OAS vocabulary, `discriminator.defaultMapping` and `xml.nodeType`, and both are kept. 3.1 tooling ignores them, so a `defaultMapping` fallback stops taking effect, while `nodeType` is picked up again on the 3.1 → 3.0 hop. The standard OpenAPI 3.1 document schema accepts them, but the strict OAS 3.1 base-vocabulary meta-schema closes the XML and Discriminator Objects and will flag them.
-Removed (no 3.1 equivalent): `$self`, server `name`, tag `summary`/`parent`/`kind`, the `query` operation and `additionalOperations` of Path Items, `in: "querystring"` parameters (from parameter lists and `components.parameters`, together with references to the removed component entries, following chains of reference aliases), `allowReserved` on non-query parameters, media type `description`, media type / encoding `prefixEncoding`, `itemEncoding`, and nested `encoding`, a media type `itemSchema` beside an existing `schema`, response `summary` beside an existing `description`, OAuth `deviceAuthorization` flows, and security scheme `oauth2MetadataUrl` and `deprecated`.
+Converted:
-Known limitations: security requirements using URI keys and `$self`-relative reference resolution are passed through unchanged, and so is a `$schema` keyword inside a Schema Object that names the 3.2 dialect.
+| 3.2 construct | 3.1 result |
+| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `openapi: 3.2.x` | `openapi: 3.1.2` |
+| `jsonSchemaDialect` naming a 3.2 OAS dialect | `https://spec.openapis.org/oas/3.1/dialect/base`; other dialects pass through |
+| `components.mediaTypes` and content-map `$ref`s to it | references inlined and the component map removed. Entries whose target cannot be inlined (external, unknown, or cyclic) are removed, since 3.1 content maps cannot hold references. A parameter or header that loses its entire `content` that way is removed too, because 3.1 requires exactly one entry there |
+| media type `itemSchema` without a sibling `schema` | `schema: { type: "array", items: … }`, the sequential media type data model |
+| response `summary` without a `description` | promoted to `description`; `""` when neither exists, since 3.1 requires it |
+| example `dataValue` / `serializedValue` without `value` or `externalValue` | promoted to `value`, `dataValue` taking precedence |
+| parameter `style: "cookie"` | removed so the 3.1 default `form` applies |
+
+Removed, with no 3.1 equivalent:
+
+- `$self`
+- server `name`
+- tag `summary`, `parent`, and `kind`
+- the Path Item `query` operation and `additionalOperations`
+- `in: "querystring"` parameters, in parameter lists and in `components.parameters`, together with references to removed component parameters and headers (chains of reference aliases included)
+- `allowReserved` on non-query parameters
+- media type `description`
+- `prefixEncoding`, `itemEncoding`, and nested `encoding` on media types and encodings
+- `itemSchema` beside an existing `schema`, and response `summary` beside an existing `description`
+- OAuth `deviceAuthorization` flows
+- security scheme `oauth2MetadataUrl` and `deprecated`
+
+Known limitations: security requirements keyed by URI, `$self`-relative reference resolution, and a `$schema` keyword inside a Schema Object that names the 3.2 dialect all pass through unchanged.
## 3.1 → 3.0
Converted:
-| 3.1 construct | 3.0 result |
-| ----------------------------------------------- | ---------------------------------------------------------------------- |
-| `openapi: 3.1.x` | `openapi: 3.0.4` |
-| missing `paths` | `{}` (required in 3.0) |
-| missing operation `responses` | `{ "default": { "description": "" } }` (required and non-empty in 3.0) |
-| Reference `summary` / `description` overrides | removed (3.0 references stand alone) |
-| Security requirement roles on non-OAuth schemes | emptied (`[]`) |
+| 3.1 construct | 3.0 result |
+| ---------------------------------------------------------- | ---------------------------------------------------------------------- |
+| `openapi: 3.1.x` | `openapi: 3.0.4` |
+| missing `paths` | `{}` (required in 3.0) |
+| missing operation `responses` | `{ "default": { "description": "" } }` (required and non-empty in 3.0) |
+| path parameters without `required: true` | `required: true` added (mandatory for `in: "path"`) |
+| Reference Object `summary` / `description` | removed (3.0 references carry no overrides) |
+| security requirement scopes on `apiKey` and `http` schemes | emptied to `[]` |
+
+Removed, with no 3.0 equivalent:
-Removed (no 3.0 equivalent): `webhooks`, `components.pathItems` (Path Item `$ref`s pointing at it, in `paths` and in callbacks, are inlined instead — following chains of references, with the referencing Path Item's own fields winning over inlined ones where both define a field; a reference that cannot be inlined, such as an unknown or cyclic target, is left untouched and will dangle), `jsonSchemaDialect`, `info.summary`, `license.identifier`, and `mutualTLS` security schemes (reference aliases to them included) — their names are stripped from every security requirement, requirements that referenced only such schemes are removed, and a `security` list emptied that way is removed entirely, since an explicit empty list means "no security required" and would make an operation public.
+- `webhooks`
+- `jsonSchemaDialect`
+- `info.summary` and `license.identifier`
+- `components.pathItems`. Path Item `$ref`s to it, in `paths` and in callbacks, are inlined first, following reference chains, with the referencing Path Item's own fields winning over inlined ones. A reference that cannot be inlined (unknown or cyclic target) is left as is and will dangle.
+- `mutualTLS` security schemes, reference aliases included. Their names are stripped from every security requirement, a requirement left empty is removed, and a `security` list left empty is removed entirely, since an explicit empty list means "no security required" and would make the operation public.
Schema Objects:
-| 3.1 construct | 3.0 result |
-| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `true` / `false` boolean schemas | `{}` / `{ not: {} }` |
-| `$ref` with sibling keywords | siblings kept, `$ref` wrapped into `allOf` |
-| `type: ["T", "null"]` | `type: "T"` plus `nullable: true` |
-| `enum: []` / duplicate `required` entries | `enum` removed / `required` deduplicated (3.0 requires a non-empty `enum` and unique `required`) |
-| `type: "null"` | `nullable: true` plus `enum: [null]`; a sibling `enum`/`const` is intersected with the null type — an `enum` containing `null` collapses to `[null]`, and a sibling excluding `null` yields a match-nothing schema (`not: {}`), since the source accepted no value |
-| `type` with several non-null entries | `anyOf` of single-type schemas |
-| `const` | single-value `enum` |
-| numeric `exclusiveMinimum` / `exclusiveMaximum` | bound plus boolean flag (the tighter bound wins) |
-| `examples` | first entry becomes `example` when none exists |
-| `contentEncoding: base64` | `format: byte` |
-| `contentMediaType: application/octet-stream` | `format: binary` |
-| `type: "array"` without `items` | `items: {}` is added (required in 3.0) |
-| XML `nodeType` (carried over from a 3.2 chain) | `attribute: true` / `wrapped: true` where expressible, then removed (3.0 forbids unknown XML Object fields) |
-| `$schema`, `$id`, `$defs`, `$anchor`, `$dynamicRef`/`$dynamicAnchor`, `$vocabulary`, `$comment`, `if`/`then`/`else`, `dependentSchemas`/`dependentRequired`, `prefixItems` (and its trailing `items`), `contains`/`minContains`/`maxContains`, `patternProperties` (and its sibling `additionalProperties`, whose meaning would otherwise tighten onto the pattern-matched keys), `propertyNames`, `unevaluatedItems`/`unevaluatedProperties`, `contentSchema` | removed — in positive schema positions dropping these only loosens validation, the safe direction for a downgrade |
-
-Known limitations: `$ref`s that point into dropped keywords (`#/…/$defs/…` pointers, `$anchor` targets, `$id`-based bases) will dangle — hoist reusable subschemas into `components.schemas` before downgrading. Arbitrary non-standard schema keywords are preserved per the extension-preserving contract, even though the official 3.0 schema forbids unknown Schema Object fields. Dropping keywords inside `not` (where loosening the operand tightens the whole) or inside `oneOf` branches (where loosening one branch can break exclusivity) can shift what validates.
+| 3.1 construct | 3.0 result |
+| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `true` / `false` boolean schemas | `{}` / `{ not: {} }` |
+| `$ref` with sibling keywords | siblings kept, `$ref` moved into `allOf` |
+| `type: ["T", "null"]` | `type: "T"` plus `nullable: true` |
+| `type` with several non-null entries | `anyOf` of single-type schemas, each `nullable` when `null` was listed |
+| `type: "null"` | `nullable: true` plus `enum: [null]`. A sibling `enum` or `const` is intersected with the null type: an `enum` containing `null` collapses to `[null]`, and one excluding it yields `not: {}`, since the source accepted no value |
+| `const` | single-value `enum`, plus `nullable: true` when the value is `null` |
+| numeric `exclusiveMinimum` / `exclusiveMaximum` | `minimum` / `maximum` plus the boolean flag; a tighter existing bound wins |
+| `examples` | first entry becomes `example` when none exists |
+| `contentEncoding: base64` | `format: byte` when no `format` exists |
+| `contentMediaType: application/octet-stream` without `contentEncoding` | `format: binary` when no `format` exists |
+| `type: "array"` without `items` | `items: {}` added (required in 3.0) |
+| `enum: []` | removed (3.0 requires a non-empty `enum`) |
+| `required: []` / duplicate `required` entries | removed / deduplicated (3.0 requires a non-empty, unique `required`) |
+| XML `nodeType`, carried over from a 3.2 chain | `attribute: true` / `wrapped: true` where expressible, then removed (3.0 forbids unknown XML Object fields) |
+
+Removed, with no 3.0 equivalent: `$schema`, `$id`, `$defs`, `$anchor`, `$dynamicRef`, `$dynamicAnchor`, `$vocabulary`, `$comment`, `if` / `then` / `else`, `dependentSchemas`, `dependentRequired`, `prefixItems` (with its trailing `items`), `contains`, `minContains`, `maxContains`, `patternProperties` (with its sibling `additionalProperties`, whose meaning would otherwise tighten onto the pattern-matched keys), `propertyNames`, `unevaluatedItems`, `unevaluatedProperties`, and `contentSchema`. In positive schema positions dropping these only loosens validation, the safe direction for a downgrade.
+
+Known limitations:
+
+- `$ref`s into dropped keywords (`#/…/$defs/…` pointers, `$anchor` targets, `$id`-based bases) will dangle. Hoist reusable subschemas into `components.schemas` before downgrading.
+- Non-standard schema keywords are preserved per the extension contract, even though the official 3.0 schema forbids unknown Schema Object fields.
+- Dropping keywords inside `not`, where loosening the operand tightens the whole, or inside `oneOf` branches, where loosening one branch can break exclusivity, can change what validates.
## Sponsors
@@ -165,3 +219,7 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
With thanks to [36 past sponsors](https://htmlpreview.github.io/?https://github.com/middleapi/static/blob/main/sponsors.svg) who helped get openapi-spec here.
+
+## License
+
+Distributed under the MIT License. See [LICENSE](https://github.com/middleapi/openapi-spec/blob/main/LICENSE) for more information.
diff --git a/packages/downgrader/package.json b/packages/downgrader/package.json
index 5396da8..c91ba42 100644
--- a/packages/downgrader/package.json
+++ b/packages/downgrader/package.json
@@ -58,7 +58,6 @@
}
},
"scripts": {
- "build": "unbuild",
"prepack": "unbuild",
"type:check": "tsc -b"
},
diff --git a/packages/types/README.md b/packages/types/README.md
index c006eab..cf035be 100644
--- a/packages/types/README.md
+++ b/packages/types/README.md
@@ -1,28 +1,50 @@
-# @openapi-spec/types
-
-TypeScript types for the [OpenAPI Specification](https://spec.openapis.org/), covering versions 3.0, 3.1, and 3.2 with complete inline documentation.
-
-Each version module is authored against the latest patch release of its minor line:
-
-- `@openapi-spec/types/v3.0` — OpenAPI [3.0.4](https://spec.openapis.org/oas/v3.0.4.html)
-- `@openapi-spec/types/v3.1` — OpenAPI [3.1.2](https://spec.openapis.org/oas/v3.1.2.html), reusing 3.0 types where unchanged
-- `@openapi-spec/types/v3.2` — OpenAPI [3.2.0](https://spec.openapis.org/oas/v3.2.0.html), reusing 3.1 types where unchanged
+OpenAPI Spec
+
+
+
+`@openapi-spec/types` provides TypeScript types for the [OpenAPI Specification](https://spec.openapis.org/), covering versions 3.0, 3.1, and 3.2. Every object and field is modelled after the specification, and every field carries the specification's own description as JSDoc with a link to its section, so the spec is readable from your editor. The package ships types only, no runtime code.
+
+Each version module targets the latest patch release of its minor line:
+
+| Module | Specification | Notes |
+| -------------------------- | ---------------------------------------------------------- | ------------------------------------------------ |
+| `@openapi-spec/types/v3.0` | [OpenAPI 3.0.4](https://spec.openapis.org/oas/v3.0.4.html) | |
+| `@openapi-spec/types/v3.1` | [OpenAPI 3.1.2](https://spec.openapis.org/oas/v3.1.2.html) | Reuses 3.0 types for objects that did not change |
+| `@openapi-spec/types/v3.2` | [OpenAPI 3.2.0](https://spec.openapis.org/oas/v3.2.0.html) | Reuses 3.1 types for objects that did not change |
## Usage
```ts
-// Namespaces via the root export
-import type {
- OpenAPIV3_0,
- OpenAPIV3_1,
- OpenAPIV3_2,
-} from '@openapi-spec/types'
-
-// or one version's module directly
+// Every version as a namespace
+import type { OpenAPIV3_0, OpenAPIV3_1, OpenAPIV3_2 } from '@openapi-spec/types'
+
+// Or one version directly
import type { OpenAPIObject, SchemaObject } from '@openapi-spec/types/v3.1'
-// SchemaObject accepts an optional data type for its data-carrying fields
-// (`enum`, `default`, `example`, and in 3.1+ `const` and `examples`).
+const doc: OpenAPIObject = {
+ openapi: '3.1.2',
+ info: { title: 'Pet Store', version: '1.0.0' },
+ paths: {},
+}
+
+// SchemaObject takes an optional data type for its data-carrying fields:
+// `enum`, `default`, `example`, and from 3.1 on `const` and `examples`
const status = {
type: 'string',
enum: ['available', 'pending', 'sold'],
@@ -30,7 +52,13 @@ const status = {
} satisfies SchemaObject
```
-Type names follow the specification's section names (`InfoObject`, `PathItemObject`, `SchemaObject`, ...), and every field carries its specification description as JSDoc, linked back to the relevant spec section.
+## Conventions
+
+- Type names follow the specification's section names: `InfoObject`, `PathItemObject`, `SchemaObject`, and so on.
+- Fields the specification marks as deprecated carry an `@deprecated` tag.
+- Rules the type system can express are enforced: allowed fields, value shapes, and version-specific literals such as `style` values. Rules it cannot express, like mutually exclusive fields or "at least one of", are stated in the JSDoc instead.
+
+The types are checked against the official example documents and the specification's own schema test corpus. See [tests/README.md](https://github.com/middleapi/openapi-spec/blob/main/packages/types/tests/README.md).
## Sponsors
@@ -115,3 +143,7 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
With thanks to [36 past sponsors](https://htmlpreview.github.io/?https://github.com/middleapi/static/blob/main/sponsors.svg) who helped get openapi-spec here.
+
+## License
+
+Distributed under the MIT License. See [LICENSE](https://github.com/middleapi/openapi-spec/blob/main/LICENSE) for more information.
diff --git a/packages/types/tests/README.md b/packages/types/tests/README.md
index e6f538e..05402cd 100644
--- a/packages/types/tests/README.md
+++ b/packages/types/tests/README.md
@@ -1,13 +1,13 @@
# Real-document fixtures
-Type-level fixtures generated from official OpenAPI documents. Each file embeds a complete document as an object literal typed as the `OpenAPIObject` of the version module matching its `openapi` field, so `tsc` at the repository root re-validates the whole corpus and rejects properties the specification does not allow.
+Official OpenAPI documents embedded as TypeScript object literals. Each file types its document as the `OpenAPIObject` of the version module matching its `openapi` field, so `pnpm type:check` at the repository root compiles the whole corpus and fails whenever the types reject a valid document. The downgrader's corpus tests reuse the same fixtures as conversion input.
Sources (Apache-2.0, © the OpenAPI Initiative):
-- `examples/` — the official example documents from [OAI/learn.openapis.org](https://github.com/OAI/learn.openapis.org/tree/main/examples) (v3.0, v3.1, and v3.2 sets).
-- `schema-tests-3.1/` — the `tests/schema/pass` documents from the [OAI/OpenAPI-Specification `v3.1-dev` branch](https://github.com/OAI/OpenAPI-Specification/tree/v3.1-dev/tests/schema/pass).
-- `schema-tests-3.2/` — the `tests/schema/pass` documents from the [OAI/OpenAPI-Specification `v3.2-dev` branch](https://github.com/OAI/OpenAPI-Specification/tree/v3.2-dev/tests/schema/pass).
+- `examples/`: the official example documents from [OAI/learn.openapis.org](https://github.com/OAI/learn.openapis.org/tree/main/examples), 3.0, 3.1, and 3.2 sets
+- `schema-tests-3.1/`: the `tests/schema/pass` documents from the [OAI/OpenAPI-Specification `v3.1-dev` branch](https://github.com/OAI/OpenAPI-Specification/tree/v3.1-dev/tests/schema/pass)
+- `schema-tests-3.2/`: the `tests/schema/pass` documents from the [OAI/OpenAPI-Specification `v3.2-dev` branch](https://github.com/OAI/OpenAPI-Specification/tree/v3.2-dev/tests/schema/pass)
-The corresponding `tests/schema/fail` documents are intentionally not committed: most of them violate semantic rules that these types document rather than encode (mutual exclusions, at-least-one-of containers, per-location field applicability, non-empty arrays, map-key syntax). The ones that are type-expressible (unknown top-level fields, wrong value shapes, excess fields on Header/Link Objects, 3.2-only style values used in 3.1 documents) were verified to produce compile errors when this corpus was generated.
+The matching `tests/schema/fail` documents are not committed. Most of them break semantic rules the types document rather than encode: mutual exclusions, at-least-one-of containers, per-location field applicability, non-empty arrays, and map-key syntax. The ones the types can catch (unknown top-level fields, wrong value shapes, excess fields on Header and Link Objects, 3.2-only `style` values in 3.1 documents) were verified to fail compilation when the corpus was generated.
-The files are generated — do not edit them by hand; refresh them from the sources above instead.
+The files are generated. Refresh them from the sources above instead of editing by hand.