Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The model is one file, [`openapi-changes-model.yaml`](openapi-changes-model.yaml

- **The vocabulary**: every term the model uses, defined in the file itself: the location grammar and the claim syntax built on it, the seven actions a document edit can perform (add, remove, set, unset, increase, decrease, change), the wire directions, the areas and kinds that situate each change in the OpenAPI object model, the six effects a change can have on the set of valid payloads, the guards (document states that qualify a verdict), the severity levels, and the statuses and categories of the coverage listing.
- **The severity law**: one rule that derives every verdict. Guards apply first; then narrowing breaks request consumers, widening breaks response consumers, an incomparable change breaks both, and an unknown one is a warning. When a change cannot be proven safe, it is breaking.
- **The transitions**: multi-edit document shapes recognized as one semantic change (a schema wrapped to become nullable, a schema replaced by the boolean `false`), each with the kinds of raw findings it explains away and the changes that report it instead.
- **681 named changes**, each with its direction, effect, guards, derived severity, human-readable message, and its **claims**: the exact document locations and actions it covers, as `location:action` patterns over the OpenAPI object model.
- **The full edit space**: 15,255 possible edits, enumerated mechanically from the OpenAPI specification's object model. Every edit is covered by named changes, waived with a written reason, or classified as non-contract (unable to affect which payloads are valid). None are undecided.

Expand Down
4 changes: 4 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ Every level is derived, never assigned. Guards apply first, each nullifying or r

The asymmetry is deliberate: reporting a safe change as breaking costs a reviewer one look, while reporting a breaking change as safe ships it to production. A change is declared safe only when it is provably safe for every consumer that conformed to the old contract; any gap in that proof resolves to breaking. Where the specification itself lacks the information to decide, the verdict is a warning that says what is missing, never a guess.

## Transitions

Some edits arrive together as one semantic change. Wrapping a schema in `oneOf: [{type: "null"}, X]` to make it nullable is a single decision, but the raw diff shows several edits: the type changed, an enum moved, a `oneOf` appeared. A **transition** names such a shape. At a recognized shape, raw findings of the transition's *claimed kinds* are echoes of the one change and are suppressed; findings of other kinds are independent changes and still report. The transition itself is reported by its listed changes, so nothing is silently dropped: the finding moves from the echoes to the recognition.

## Coverage dispositions

Every edit in the space has exactly one disposition:
Expand Down
7 changes: 3 additions & 4 deletions generator/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,20 @@ module github.com/oasdiff/openapi-changes-model/generator
go 1.26.2

require (
github.com/oasdiff/oasdiff v1.31.0
github.com/oasdiff/oasdiff v1.31.1-0.20260905155854-e887d0d25541
gopkg.in/yaml.v3 v3.0.1
)

require (
cloud.google.com/go v0.123.0 // indirect
github.com/TwiN/go-color v1.4.1 // indirect
github.com/getkin/kin-openapi v0.149.0 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/jsonpointer v1.0.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/oasdiff/yaml v0.1.1 // indirect
github.com/oasdiff/yaml3 v0.0.14 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/gjson v1.19.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
Expand Down
22 changes: 10 additions & 12 deletions generator/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,18 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/getkin/kin-openapi v0.149.0 h1:ZbhmVJ4yq5RZDUsyP8lcBcGMsjsaTqXEFt6isdtMDfA=
github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg=
github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY=
github.com/go-openapi/testify/v2 v2.7.0 h1:bycOreEj6wfBvijg3YFogZ/sFjTCDmQnwSodSzHa3X8=
github.com/go-openapi/testify/v2 v2.7.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/oasdiff/oasdiff v1.31.0 h1:4ggpCCxYK5ABpT387yb2wvCI3+1mc9d75i8tcVV5dkQ=
github.com/oasdiff/oasdiff v1.31.0/go.mod h1:25aWQvm5UvK1SJE34s3MPpAyhQUQwf7Q/t/gWDIrHM8=
github.com/oasdiff/oasdiff v1.31.1-0.20260905155854-e887d0d25541 h1:fsM+dtUOEP7gxR47aNl/zTP88UrGFkgMjwMc1P0o7cI=
github.com/oasdiff/oasdiff v1.31.1-0.20260905155854-e887d0d25541/go.mod h1:zrOVqxQfWeiPci5XvFhtnCQrJVLov2v8+oEZhAKAHtA=
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
Expand All @@ -33,8 +31,8 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCw
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
Expand All @@ -47,8 +45,8 @@ github.com/wI2L/jsondiff v0.7.1 h1:Fg9+yj+1/x3UtPBJhR91TKEzRkrEEWcAcLbg9dzEaNM=
github.com/wI2L/jsondiff v0.7.1/go.mod h1:yAt2W7U6Jd4HK0RA8DGSGk0zDtfEtOUUJVnH/xICpjo=
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
Expand Down
55 changes: 37 additions & 18 deletions generator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"os"
"runtime/debug"
"slices"
"sort"

"github.com/oasdiff/oasdiff/checker"
Expand All @@ -18,22 +19,17 @@ import (
)

type Model struct {
Model string `yaml:"model"`
Version string `yaml:"version"`
GeneratedFrom string `yaml:"generated_from"`
Vocabulary Vocabulary `yaml:"vocabulary"`
SeverityLaw SeverityLaw `yaml:"severity_law"`
Changes []Change `yaml:"changes"`
// Coverage is the full edit space of an OpenAPI document with each
// edit's disposition: covered by named changes, waived with a reason,
// or non-contract.
Coverage []coverage.Edit `yaml:"coverage"`
Model string `yaml:"model"`
Version string `yaml:"version"`
GeneratedFrom string `yaml:"generated_from"`
Vocabulary Vocabulary `yaml:"vocabulary"`
SeverityLaw SeverityLaw `yaml:"severity_law"`
Transitions []Transition `yaml:"transitions"`
Changes []Change `yaml:"changes"`
Coverage []coverage.Edit `yaml:"coverage"`
}

type Vocabulary struct {
// Locations describes the location grammar and the claim syntax built
// on it; locations are an open set derived from the OpenAPI object
// model, not an enumeration.
Locations string `yaml:"locations"`
Actions map[string]string `yaml:"actions"`
Directions map[string]string `yaml:"directions"`
Expand All @@ -42,7 +38,6 @@ type Vocabulary struct {
Effects map[string]string `yaml:"effects"`
Guards map[string]string `yaml:"guards"`
Levels map[string]string `yaml:"levels"`
// Statuses and Categories describe the coverage dispositions.
Statuses map[string]string `yaml:"statuses"`
Categories map[string]string `yaml:"categories"`
}
Expand All @@ -64,6 +59,13 @@ type VerdictRule struct {
Level string `yaml:"level"`
}

type Transition struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
ClaimedKinds []string `yaml:"claimed_kinds"`
ReportedBy []string `yaml:"reported_by"`
}

type Change struct {
Id string `yaml:"id"`
Level string `yaml:"level"`
Expand Down Expand Up @@ -116,6 +118,23 @@ func main() {
}
sort.Slice(changes, func(i, j int) bool { return changes[i].Id < changes[j].Id })

transitions := make([]Transition, 0)
for _, tr := range checker.GetTransitions() {
kinds := make([]string, 0, len(tr.Claims))
for _, k := range tr.Claims {
kinds = append(kinds, k.String())
}
reportedBy := slices.Clone(tr.ReportedBy)
sort.Strings(reportedBy)
transitions = append(transitions, Transition{
Name: tr.Name,
Description: tr.Description,
ClaimedKinds: kinds,
ReportedBy: reportedBy,
})
}
sort.Slice(transitions, func(i, j int) bool { return transitions[i].Name < transitions[j].Name })

model := Model{
Model: "OpenAPI Changes Model",
Version: "0.1.0-draft",
Expand Down Expand Up @@ -218,8 +237,9 @@ func main() {
{Effect: "none", Level: "info"},
},
},
Changes: changes,
Coverage: coverage.Analyze(metadata),
Transitions: transitions,
Changes: changes,
Coverage: coverage.Analyze(metadata),
}

data, err := yaml.Marshal(model)
Expand All @@ -236,5 +256,4 @@ func main() {
fmt.Printf("wrote %s: %d changes, %d edits\n", *out, len(changes), len(model.Coverage))
}

// ensure the law encoded above matches the implementation
var _ = rules.DeriveLevel
var _ = rules.DeriveLevel // the law encoded above is the one this implementation runs
Loading
Loading