Tags: oasdiff/oasdiff
Tags
Add response header schema checks: type, format, nullability (#1116) * Add response header schema checks: type, format, nullability (#1094) Response headers had existence-level checks only (added, removed, became-optional); nothing walked a response header's schema diff, so a type or format change on a header was silent in breaking and changelog. This adds the first slice of the missing checks, walking responses' HeadersDiff.Modified schema diffs: - response-header-type-changed / -generalized / -specialized / -compatible - response-header-became-nullable / -became-not-nullable Severity mirrors the response body/property rules through the shared responseTypeChangeId and nullabilityChangeId classifiers. A header value is text on the wire, so it is treated non-strongly-typed, the same as a scalar parameter: a bare string to integer swap is backward compatible, while dropping a format a client parses (Retry-After string/date-time to integer) is breaking, and widening the returned type is breaking. Total rule ids 500 -> 506. * Pass strong-typing as a bool, not via a fake empty media type The response header check reached non-strongly-typed behavior by passing mediaType "" (relying on isStronglyTyped("") == false) and then patched the compatible comment after the fact. Both were kludges. Make the real axis explicit: requestTypeFormatBreaking / responseTypeFormatBreaking and requestTypeChangeId / responseTypeChangeId now take stronglyTyped bool, and the type-change helpers take the loose-swap comment as a parameter. Body and property callers pass isStronglyTyped(mediaType) + the media-type comment; the header caller passes false + the header comment directly. No behavior change. * Note content-serialized headers are out of scope A header serialized via content (e.g. content: application/json) produces a ContentDiff, not a SchemaDiff, and would be strongly typed. This slice walks only the simple schema serialization (text on the wire).
checker: a required request property with a default is breaking (erro… …r), not warning (#1110) v1.25.0 moved new-required-request-property-with-default and request-property-became-required-with-default from info to warning. That still under-reported. Breaking-ness is a property of the contract, not of a lenient runtime: a request that omits a required property is invalid under the new contract whether or not the property has a default. The default is a server-side fallback, it does not make the omitted property valid. Whether a particular server applies it and accepts the request anyway is runtime tolerance, not contract validity (and generated SDKs and strict gateways reject it regardless). Both checks are now ERR, same as their no-default siblings, with a comment explaining why the default does not make them safe. The ids stay distinct so a team can downgrade exactly these via --severity-levels if the change is safe for their ecosystem. Adds a 'How oasdiff decides what is breaking' section to BREAKING-CHANGES.md documenting the contract-not-runtime principle. Comment message updated in all four locales. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs: validate's duplicate-enum lint; allOf's dropped keywords (#1108) * docs: validate's duplicate-enum lint; allOf's dropped keywords VALIDATE.md predates the duplicate-enum-value lint (#1023); it joins the warning examples. ALLOF.md said some fields are "not merged" but if/then/else, dependentSchemas, and unevaluatedProperties are dropped outright, with semantic consequences; state it and link #1097. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: validate also lints ambiguous parameter serialization (#1103) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
feat(review): per-endpoint structural block extraction for large specs ( #1068) * feat(review): scaffold per-endpoint structural block extraction [WIP] Render one self-contained card per change instead of the full spec — the large-spec /review fix (a 250k-line spec currently commits ~1M DOM nodes and takes minutes to become interactive; cards cut that ~99%). New reviewblocks package: group changes by their smallest enclosing structural block and slice each block's source text using kin-openapi origin end-positions (Origin.Key.EndLine/EndColumn), so a block's full extent is known, not just its start. Repins getkin/kin-openapi to master HEAD for that support (pseudo-version) pending the upstream release. WIP: change grouping, the origin-end line slice, and the component-schema case are wired and tested. Still to do: operation / path-item / top-level node resolution (operations don't carry Origin, so the path-item key origin is the handle), overlap dedup, the "Other changes" fallback, and feeding the blocks into the encrypted review bundle so the browser renders cards from the decrypted payload (the server never sees the spec). * feat(review): slice endpoint (operation + path) blocks PathItem and Operation both carry Origin (with Key.EndLine), so resolve an endpoint change to its operation block directly: "<METHOD> <path>" -> the operation's source span (falling back to the whole path block if the operation has no recorded origin); a path-level change -> the whole path block. End-to-end tests parse with IncludeOrigin and assert the POST block excludes the sibling GET, the other path, and the rest of the document. Corrects the earlier note that operations don't carry Origin — they do. * feat(review): key blocks by source line so $ref'd-component changes card correctly A change inside a $ref'd component is reported by oasdiff under the referencing operation, but its source line is in the component. Keying by (operation, path) sliced the operation block, which only shows the $ref, not the change. Switch to source-line containment: index each document's blocks (operations, path items, named component schemas) by their Origin span and map each change to the smallest block containing its BaseSource/RevisionSource line. This follows the $ref (cards the component) and dedupes the same component change reported across several endpoints into one card. (operation, path) plus the rule Area become the fallback when no source line resolves. New test asserts a property change inside a $ref'd component cards as components/schemas/User, not POST /users. * test(review): real-pipeline flatten-allof case keys to the operation Runs the actual diff + checker with --flatten-allof: the merged schema produces the readable request-property-became-required change with no source line, so the change falls back to the operation it names and the slice is the real operation block (whose origin survives the flatten). This is the path the review page runs; closes the test TODO. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(review): carry per-change blocks in the encrypted --open bundle Add a `blocks` field to the review payload, filled by reviewblocks.Extract from the resolved specs and the raw spec text. The review page renders one card per block instead of the full spec. Works with and without --flatten-allof: flattened changes have no source line and fall back to the operation block, whose line survives the flatten. JSON tags added to Block so it serializes into the bundle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(review): carry per-change fingerprints on each block The review page joins each change to its card by fingerprint (the stable key), not the rule id (which repeats). Extract now records each change's fingerprint alongside its id. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(source): emit origin end position (endLine/endColumn) Source now carries the origin's end position alongside the start, so -f json/yaml expose endLine/endColumn for each change. A multi-line element (an operation, an added parameter, an object schema) reports its full span; a single-line element (an added enum value, a single field) reports endLine == line (omitted when zero). Populated from openapi3.Origin.Key end positions and field/sequence locations. Lets a consumer highlight exactly the changed lines. Updated the operation-level golden source assertions to include the new end fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(reviewblocks): add package doc.go Move the package overview into doc.go (the per-package convention), refreshed to the current state: what Extract returns, source-line block keying with the $ref-following rationale, origin-end slicing, and the known gaps (top-level sections, multi-file, schema-shape completeness). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(review): centralize the review bundle in package review Rename reviewblocks -> review and move the bundle's domain logic out of internal/open_review.go into it: the Payload wire type, the AES-256-GCM Encrypt, and the fingerprint Manifest now live alongside Extract, so the package is the single source of truth for the bundle format. internal/ open_review.go keeps only CLI orchestration (flags, HTTP upload, browser, changelog rendering). The package comments are product-agnostic (a generic encrypted review bundle); the oasdiff.com / OASDIFF_URL defaults stay in internal. Moved the encryption round-trip and manifest unit tests into the review package; the upload-flow tests stay in internal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * checker: add Source.WithEnd so end-bearing sources keep using NewSource Adding EndLine/EndColumn to Source meant the golden assertions with a span couldn't use the 3-arg NewSource and had become raw struct literals. Add a fluent WithEnd(endLine, endColumn) so they read NewSource(f,l,c).WithEnd(el,ec), restoring the constructor convention. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(review): 'receives' not 'stores' a blob it cannot read The package produces ciphertext and uploads it; whether the server persists it is the caller's/server's concern, so 'receives' is the accurate framing for the library. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(review): drop 'side-by-side', a renderer-specific layout The review package is renderer-agnostic; side-by-side is one UI's choice. Describe the generic behavior (render only what changed; fall back to the full spec) instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(review): gloss 'rule Area' in the block-selection fallback Spell out what the Area fallback is (the OpenAPI object a rule concerns, e.g. security/tags) so the doc is self-explanatory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(review): index the top-level sections (info/servers/tags/security) buildIndex now adds a block for each top-level section, spanning from its key line to just before the next top-level key (from the document origin's field locations). Key-line based on purpose: it works uniformly even for security, whose items are bare maps with no origin of their own. changeSources now reads the sources via the Change interface instead of type-asserting ApiChange, so SecurityChange / ComponentChange resolve to their block too. Closes the "top-level sections not indexed" gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(review): state the real multi-file cause (kin drops cross-file origins) The limitation isn't a missing text thread: kin resolves an external $ref's value but discards its origin, so the change is sourced at the referencing operation, not the external file. Document that, and that the fix is upstream (carry origins across file resolution) plus a file-aware index here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(review): correct the multi-file limitation A $ref into a valid OpenAPI sub-document keeps its origin (external file + line), so the standard multi-file shape has coordinates; the gap is this index being single-file (needs file-aware index + per-file texts), not a kin limitation. Only a bare-fragment ref loses its origin in kin's generic-map (JSON round-trip) resolution path, which would need an upstream fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(load): capture referenced file texts (NewSpecInfoWithCapture) For multi-file slicing, the review path needs the text of every file that contributed to the spec (the root and each $ref'd file). kin reads them all through ReadFromURIFunc; NewSpecInfoWithCapture installs a recording wrapper (on both the file and git paths) and returns them in SpecInfo.Sources, keyed by resolved path -- which matches each element's origin File, so a consumer can slice by origin file. Off by default; plain NewSpecInfo records nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(review): slice multi-file specs (file-aware index + per-file texts) Make block extraction file-aware so a change in a schema $ref'd from another file cards to that block and slices from the external file, not the referencing operation in the root. - span carries the file it lives in; smallestContaining matches (file, line) since line numbers aren't unique across files; blockKey keys on the change's source File. - indexExternalSchemas (via WalkSchemas) indexes schemas $ref'd from another file by their ref, with the external file's origin span. - Extract takes per-file text maps (base/revision) and slices each block from the file it lives in. open_review passes SpecInfo.Sources (captured on --open); normalDiff loads with capture only when --open is set. Verified end to end: a cross-file role-type change cards to "other.yaml#/components/schemas/User" and slices from other.yaml. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(review): multi-file is sliced now (only bare fragments remain) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(review): the multi-file gap is narrow (only arbitrary-top-level-key refs) Tested: per-schema files (./User.yaml) and components-structured files (./defs.yaml#/components/schemas/User) slice correctly. Only a schema under an arbitrary top-level key (./schemas.yaml#/User) loses its origin in kin's generic-map path. Uncommon in idiomatic OpenAPI 3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(review): each block reports the file it was sliced from Add Block.BaseFile/RevFile (the source file's basename, git <rev>: prefix stripped) so the review page can label a block with the file it came from, which for a 'd-from-another-file block differs from the root spec. For a single-file block it's the spec's own filename. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(review): Extract indexes a set of docs (composed groundwork) buildIndex is variadic and Extract takes []*openapi3.T per side, so a set of specs (composed mode) share one file-aware index. Single-spec callers pass a one-element slice. No behavior change yet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(review): support composed mode with --open A composed diff (-c) can now be reviewed with --open. Previously it was rejected up front because the review compared exactly two specs; the cards view removed that constraint (each change is sliced from whichever spec in the set it lives in), so the rejection is gone. - composedDiff captures every matched spec's file texts on --open, via a new load.NewSpecInfoFromGlobWithCapture (off by default, like the single-spec NewSpecInfoWithCapture). - diffResult carries the per-side spec sets (one for a normal diff, N for composed); uploadAndOpen slices blocks from the whole set through one file-aware index, and reports each block's own file. - A composed review has no single spec, so the full-spec fields stay empty (the cards carry the diff) and each side is labelled 'composed'. Verified e2e: a composed changelog of two-file APIs cards a change to the right operation and slices it from the correct spec file in the set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(review): index component security schemes as blocks A component security-scheme change (scheme removed, type changed) is sourced inside components/securitySchemes, which buildIndex didn't cover (only components/schemas), so those changes got no block. SecurityScheme carries Origin, so index it the same way. Fixes two changes lacking a block in the review cards. * review: drop redundant comment on security-scheme indexing The securitySchemes loop mirrors the schemas loop above it and is covered by TestExtract_SecuritySchemeBlock. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): git-revision slices and composed duplicate-name blocks Two extraction bugs, both reproduced end to end by decrypting real bundles. 1. Git-revision sources produced blocks with no source slices, and lost the source-line-to-block mapping (a component change fell back to its operation/Area instead of its own block): - the root document is loaded from bytes (LoadFromDataWithPath), so the recording ReadFromURIFunc never fired for it; record it directly - origins carry a "./" prefix on git "<rev>:<path>" paths (net/url prepends it when a relative path's first segment contains a colon) while capture keys don't; normalize the capture key and the text lookup - checker sources are display paths (the "<rev>:" prefix stripped) while span files are raw; fileMatches accepts both forms 2. In composed mode, a component name defined in several specs (a shared "Error" schema) sliced whichever file the key map kept last: a change could render a diff pane with identical text on both sides, from the wrong file. The index now keeps every span per key, the resolved span is carried through to slicing instead of re-looked up by key, an ambiguous fallback yields no slice rather than a possibly wrong one, and a colliding key is qualified by filename ("pets.yaml: components/schemas/Error") so each file's block stays a separate card. Top-level sections (info/servers/tags/ security), which collide across every composed doc, get the same treatment. Regression tests: composed duplicate component name, rev-prefixed file matching, "./"-prefixed origin lookup, and git-revision capture of the root and $ref'd files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(review): trim comments to rationale Comments in the review bundle code narrated mechanics and enumerated cases that the code and tests already show. Trim each to the non-obvious "why": wire-shape constraints, the file-matching and "./"-prefix normalizations, why the fallback yields no slice on ambiguity, why top-level sections are key-line based. Fix the stale Extract signature in the package doc and list security schemes and composed mode in the indexed block types. Also gofmt two misformatted files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): never reach production or a real browser from unit tests Test_OpenWithComposedAllowed stubbed OASDIFF_API_URL (the authenticated endpoint) but ran the free path, which posts to OASDIFF_URL and defaults to production: every test run uploaded two real bundles and opened two browser tabs. Point the free upload at a non-connectable address, and add two binary-wide guards so the class can't recur: TestMain defaults both upload URLs to a non-connectable address (stub servers override per-test), and openBrowser defaults to a no-op for the whole test binary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(review): a component change groups per-endpoint reports, not dedupes them Each referencing endpoint's report stays on the component's card with its own operation and fingerprint; nothing is dropped or merged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(review): speak in blocks, not cards The review package produces blocks in a bundle; what a consumer renders them as is not its concern. Remove the renderer vocabulary that crept back in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(review): drop renderer vocabulary from the package docs The blocks section justified itself by a consumer's rendering costs (DOM nodes, what a UI shows). State what Extract produces instead: each change with its enclosing source text, without the full specs. What a consumer does with the bundle is its own concern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(review): say change, not report 'Report' names nothing in this package; its objects are changes, blocks, and fingerprints. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(review): compress the block-selection rationale to one sentence Keep the why of line keying (it exists for the $ref case); drop the step-by-step walkthrough, which TestExtract_RefdComponentFollowsSourceLine already demonstrates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(review): state when Blocks is empty instead of consumer guidance Blocks is empty iff the changelog is empty (every change resolves to some block via the fallback chain). The old sentence was consumer guidance and wrong for composed bundles, whose full-spec fields are empty too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(review): describe the manifest by function, not by tier Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: blocks, not cards, in the CLI comments too Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): mark composed bundles with an explicit field, not a magic filename The CLI filled the filename fields with "composed", which doubled as display text and, on the receiving side, as the composed-detection signal - an undocumented magic-string contract that a spec file literally named "composed" would trip. Payload.Composed now states the fact; the filename fields stay empty and presentation is the consumer's choice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): the filename qualifier is block identity, not title Qualifying only colliding titles made one block read differently from its siblings. Key keeps the file qualifier (identity must be unique); Title stays the plain name; consumers label each block's file from BaseFile/RevFile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: renderChangelogJSON serves any consumer, not a specific page Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: describe the payload changelog rendering without naming a private endpoint The comment referenced a specific server's route; the general fact is the standard object-wrapped JSON changelog shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: describe uploadAndOpen as it is, not against past designs 'There is no sign-in' documented the absence of a removed feature; the current facts are that the upload is anonymous and zero-knowledge. Also composed bundles carry no specs, so 'bundles the two specs' was stale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: from() records the capture on the file and git branches only The doc claimed every load is recorded; URL and stdin loads ignore the capture because the review bundle rejects those sources, so their text has no consumer. State the constraint so a branch that gains a consumer also gains the recorder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(review): support URL sources with --open URL sources were rejected at launch because the bundle needs the raw spec bytes and the loader does not retain them; re-fetching a remote, mutable URL could disagree with the content the diff was computed from. The capture dissolved that: the recorder now also wraps the URL branch (keyed by the full URL, matching the origin File kin records for remote documents), and readSpecSource serves URL bytes from the captured Sources, byte-identical to what was diffed, with no second fetch. Blocks slice from the same texts. Stdin remains unsupported (single-read stream; no capture consumer). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(review): index all named component types Responses, parameters, request bodies, and headers carry Origin like schemas and security schemes did, but weren't indexed, so a change inside one (e.g. a shared Error response) fell back to the referencing operation, whose slice only shows the $ref. One generic helper indexes every named-component section. Links, callbacks, and examples are left out: no check sources changes inside them today. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix ci: yaml tags on Block, check f.Close in the stdin test The repo requires yaml tags wherever json tags appear, and errcheck flags the unchecked deferred Close that came in with the both-stdin test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix lint: unchecked Setenv in the TestMain guard Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: gate OS-path-specific tests behind //go:build unix (fix Windows build) TestNewSpecInfoWithCapture_RecordsAllFiles and TestExtract_CrossFileSchemaSlicedFromExternalFile assert on OS-native $ref path resolution, which uses a different separator on Windows. Move each into a //go:build unix file so it runs on Linux and macOS but not Windows, instead of comparing platform-specific paths everywhere. * review: drop dead base/revision versions from renderChangelogJSON The encrypted-review path renders the changelog as JSON, and the JSON formatter does not render versions (only HTML/Markup do, now via constructor injection in #1085). So plumbing baseVersion/revVersion through renderChangelogJSON, and computing specSetVersion to feed it, was dead. Remove the params, the FormatterOpts versions, and the now-orphaned specSetVersion. * review: drop no-op ColorNever and its comment from renderChangelogJSON JSON output is never colored (the JSON formatter ignores opts.ColorMode), so forcing ColorNever, and the comment explaining it, were both irrelevant. * review: use maps.Copy for the source-text union in specSetDocsAndSources Replaces the manual key/value copy loop; identical last-write-wins behavior on duplicate keys (same file path across a composed spec set resolves to the same text, so overwrite is a no-op). * review: document why the source-text union is safe Sources is keyed by resolved file path, so a repeated key across a composed spec set is the same shared $ref'd file with identical text, making the last-write overwrite a no-op. Records the invariant the merge relies on. * load: combine NewSpecInfo and NewSpecInfoWithCapture They differed only in whether a source recorder is installed. Extract a shared newSpecInfoFromSource(loader, source, capture bool, options...) and make both public constructors thin wrappers, mirroring the existing NewSpecInfoFromGlob / newSpecInfoFromGlob pair. Public API unchanged. * review: drop unreachable source-type guard in readSpecSource With stdin and URL handled by the early returns above, only file and git-revision sources can reach the guard, which is exactly what it permitted, so it never fired. ReadRaw's own default already rejects any other type. * diff: name the --open loader choice (loaderForOpen) normalDiff and composedDiff both branched on --open to pick the capturing loader variant. Extract loaderForOpen so the name and its doc state the --open-to-capture relationship once, instead of an unnamed if in each. * load: split git-revision loading into git.go; add withCapture helper Move the eight git-revision functions (loadFromGitRevision, readGitRefContent, hintMissingObject, refBeforeColon, gitFetch, commitExistsLocally, blobHashFromRef, gitShow) out of load.go into git.go, leaving load.go with just the from dispatcher and the URL helpers. Also extract withCapture so from's URL and file branches stop repeating the copy-loader-and-install-recorder dance; behavior unchanged. * keep comment on one line to fix alignment * load: fix sourceCapture doc — drop caller naming, ground the origin-key match The old comment named a single caller (NewSpecInfoWithCapture), which is both inaccurate (the glob-with-capture path populates it too) and the kind of caller-listing a type doc shouldn't carry. Restate the install condition behaviorally (a capture is passed to the load), and say the key matches origin.Key.File because recordingReader replicates kin's derivation, rather than asserting the match as if automatic. * load: move the recorder install-cost note onto recordingReader The 'installed only when capturing; ordinary loads pay nothing' note belongs on the recorder, not on the sourceCapture data structure. * load: key captured sources by location.String() to match origin.Key.File recordingReader keyed local files by filepath.FromSlash(location.Path); on Windows that yields backslashes while kin sets origin.Key.File to location.String() (forward slashes), so review.Extract's lookup by origin File missed and cross-file blocks came back empty. Key by location.String() everywhere (the git root's manual record too), matching kin exactly; the '.' / './' trim on both the record and the lookup handles git's colon-prefixed paths. Add cross-platform correspondence tests (local file, URL, git revision) that assert a $ref'd element's text is findable by its origin File via the same lookup review.Extract uses. These replace prose with proof and run on Windows, where the old //go:build unix capture tests could not. * test: trim capture correspondence header comment * review: use checker.Fingerprint after the move out of formatters Adapts review/payload.go and review/blocks.go to #1086 (ComputeFingerprint moved to checker), switching to checker.Fingerprint(change) and dropping the formatters import, so the review package no longer depends on the output layer. * load: simplify blobHashFromRef with strings.Cut (gopls modernize) * load, review: drop vestigial './' normalization; keys are origin.Key.File verbatim recordingReader keys captured sources by location.String(), which is exactly what kin records as origin.Key.File -- including the './' net/url prepends to colon-first git paths. So record()'s TrimPrefix('./') and textFor()'s './' fallback were leftovers from the old FromSlash(location.Path) keying: they cancel out (the strip broke the exact match the fallback then repaired). Remove both, so a captured file is found by a change's origin File with a direct lookup. textFor was then a one-line map access, so inline it. Verified: exact-match-only lookup passes for local-file, URL, and git-revision loads (the load/ correspondence tests). * review: test the external-ref './' strip; inline redundant textForOrigin The './' strip in indexExternalSchemas is load-bearing, not vestigial: kin preserves the authored './' in sr.Ref, so a ref written 'other.yaml' on the base side and './other.yaml' on the revision side would key differently without it and resolve() would emit delete + add instead of pairing the two into one block. No test covered that asymmetric case (existing fixtures use the same spelling on both sides), so add TestExtract_CrossFileRefStyleDiffersBetweenSides -- it fails if the strip is removed. Also inline textForOrigin: once its './' fallback was dropped (keys now match the origin File verbatim) it was a bare comma-ok map access. * test: consolidate capture<->origin correspondence into one source of truth The capture-key == origin.Key.File invariant (what review.Extract's slice lookup depends on) was spread across four overlapping partial tests, two keyed on hardcoded literals rather than the actual origin. Consolidate into a single assertCapturedByOrigin used by the local-file / URL / git-revision tests: it checks both the root and the $ref'd file by each element's origin File, with a direct lookup, plus the exact captured-file count. Removed as subsumed: TestNewSpecInfoWithCapture_URL (hardcoded URL key), TestLoadInfo_GitRevisionCapture (hardcoded ./HEAD: keys), and the unix-only TestNewSpecInfoWithCapture_RecordsAllFiles (the consolidated local-file test now covers it cross-platform). Kept TestNewSpecInfo_NoCapture (capture-off). Verified it catches both regressions this work identified: re-adding record()'s './' strip fails the git case; reverting the recorder to FromSlash(location.Path) fails the URL and git cases. * test: cover the composed (glob) capture path in the correspondence test Composed --open loads specs via NewSpecInfoFromGlobWithCapture, whose per-spec capture was untested. Add TestCaptureFoundByOriginFile_ComposedGlob: a two-root glob sharing a $ref'd file, running the same assertCapturedByOrigin per returned spec. The capture<->origin invariant is now pinned for the local-file, URL, git-revision, and composed source types under one assert. Confirmed per-spec capture is complete: kin re-reads a shared $ref on each top-level load, so every spec captures it, not just the first. * repin kin-openapi to v0.141.0 The end-position (getkin#1207) and WalkSchemas (getkin#1206) support this branch builds on is now in a tagged release; drop the master pseudo-version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review fixes: composed same-basename disambiguation, capture dedup Composed sets commonly repeat a basename across directories (one openapi.yaml per service). The block-key qualifier and the cross-side span lookup disambiguated by basename, so same-named components in same-named files merged into one block showing one file's slices, and the cross-side lookup could pick the other file's span. The qualifier now uses a directory-and-file display ("svc-a/openapi.yaml") and the span lookup matches the full origin path; regression test with committed fixtures. Also: a sourced change now upgrades the span-less resolution a fallback-keyed change stored first for the same key; the two inlined copies of the capture-wrap in the glob and git load paths use withCapture; a paragraph wrap in the package doc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: pin cross-file source locations; document the arbitrary-key $ref gap Two external-$ref shapes, exercised through the real diff + checker so the asserted source is the one users get in -f json and the review bundle: - Whole-file ref (./user.yaml): passes; pins that a property change inside the external file sources at that file and line. - Arbitrary top-level key ref (./schemas.yaml#/User, the Swagger-2-era definitions bag): the released kin-openapi resolves it through a generic map that drops the origin, so the change sources at the operation in the ROOT file (wrong file, wrong line) and its review block slices from there. Asserted at both levels (checker Source, review.Extract), skipped with the reason, ready to unskip when the kin fix is released. Verified both ways: the skipped tests fail against kin v0.141.0 exactly as described, and pass against the pending kin origin fix via a local replace. * bump kin-openapi past the arbitrary-key origin fix; unskip its tests kin now preserves origins for a $ref to a schema under an arbitrary top-level key, so TestCrossFileSource_ArbitraryTopLevelKeyRef and TestExtract_ArbitraryTopLevelKeyRefSlicedFromExternalFile run live: the change sources at the schema's own file and line, and its review block slices from that file. Master pseudo-version pending the next kin release; it includes the StringMap removal, so mappingToStringMap takes the plain map type. * review: doc.go reflects the upstream arbitrary-key origin fix The definitions-bag shape now slices from its own file, so it moves from the known-gaps list to the supported external-ref shapes, pointing at its test. * load: record why glob members bypass from() and capture per member Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: fmt.Appendf instead of []byte(fmt.Sprintf) (gopls modernize) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: document docIndex fields Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: dedup docIndex comments Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: split the cross-file test into index and extract concerns Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: split the ref-style test into index and extract concerns Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: cross-side file correspondence by longest path suffix; cover blocks.go branches The strict full-path match in spanFor could never succeed across sides that live in sibling directories (base/svc-b/openapi.yaml vs revision/svc-b/openapi.yaml), leaving the other pane empty for one-sided changes in composed sets. Correspondence is now the longest path-suffix match in segments, unique or nil: exact paths win outright, a shared directory segment tells same-named files apart, and a tie stays nil (a missing slice is safe, the wrong file's slice is not). Also: remove an unreachable resolution-upgrade branch (a fallback key with no spans cannot collide with a sourced change's key), and cover the remaining branches with unit tests: fileBase/fileDisplay forms, the Area fallback, nil docs in buildIndex, the last top-level section running to EOF, and the cross-side lookup itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: pin spanFor tie safety Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: cover the git-prefix strip in suffix correspondence Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: build the id->Area map lazily, not at package init Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: explain why schema-area changes skip the Area bucket Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: single-source the Area-to-section correspondence fallbackKey bucketed a locator-less change by its Area name unless the area was schema, while addTopLevelSections indexed four section names; the correspondence between the two lists was implicit, and areas in the gap (components, paths) produced sliceless buckets with location-shaped titles. The indexed section names are now one shared list, and fallbackKey buckets by Area only when the area names one of them, so every Area bucket is a real block with a slice and everything else goes to "Other". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: anchor topLevelSections to the Area constants Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * checker: add AreaInfo and AreaServers; derive topLevelSections from areas The area taxonomy now covers every top-level document section a rule could target. info is annotation-only and servers changes are treated as deployment metadata, so neither has rules yet; the review package's top-level section list is now derived entirely from the Area constants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
checker: phrase type generalize/specialize messages as "widened"/"nar… …rowed" (#1067) "the type was specialized from number to integer" reads awkwardly. Reword the nine type generalize/specialize messages to "was widened" / "was narrowed", plain English that matches the wording the list-of-types messages already use. Covers request body/property/parameter and response body/property, in all four locales. Only the human-facing message text changes; the rule ids and the Action taxonomy keep the generalize/specialize terms (as do the -description labels, which mirror the ids). Updates the affected test assertions. Claude-Session: https://claude.ai/code/session_01BqJA1X6suZYtR3tRbX8sLj Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
diff: model security requirement alternatives as structured, index-ke… …yed values (#1044) * diff: model security requirement alternatives as structured, index-keyed values Follow-up to #1043. A security list is an unordered set of OR-alternatives that can't be keyed by a string (several may share a scheme). Model it like SubschemasDiff/ModifiedSubschemas instead of stringly-typed lists: - Added/Deleted are now SecurityAlternatives ([]SecurityAlternative), each carrying its index plus its schemes and scopes, with a String() method for rendering (so the formatting lives on the type, not in the diff loop). - Modified is now ModifiedSecurityRequirements ([]*ModifiedSecurityRequirement) (a slice, like ModifiedSubschemas, to avoid a composite map key), each with Base/Revision identity and the per-scheme scope diff. This makes a removed/added alternative unambiguous when several share a scheme (the diff carries the scopes structurally, e.g. {index: 2, schemes: {petstore_auth: [admin:pets]}}), and the changelog renders it via String() as `petstore_auth: [admin:pets]`. Note: this changes the diff output schema for security added/deleted (string -> object) and modified (map -> list). Checker and report consumers updated; the changelog message wording carries the scopes (index kept in the structured output only, since it is positional). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * diff: clarify the security-alternative identity comment "Alternatives have no name" was imprecise: a scheme name like petstore_auth is the name of a scheme inside the alternative. The accurate point is that an alternative (a map of scheme->scopes) has no identity of its own: scheme names aren't a unique key (several alternatives may share one), and an alternative may AND several schemes. Hence index-based identity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * diff: expand the security-requirements doc comment with the OR/AND semantics Spell out the rules that drive the index-based modeling: the list is an OR, schemes within an item are AND-ed, scopes within a scheme are AND-ed, so an OR of scopes for one scheme requires repeating the scheme. Note that a scheme name is only a reference into components.securitySchemes (an author-chosen label whose meaning is diffed separately in SecuritySchemesDiff), so it isn't an identity here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * diff: render an empty security requirement ({}) explicitly in String() A security alternative can be the empty requirement object `{}` (anonymous / no-auth alternative), which has no schemes at all, so String() previously returned "" and the global-security changelog message showed empty backticks. Render it as "{}" and note this in the doc comment, which previously only covered a scheme with empty scopes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * diff: unit-test SecurityAlternative.String() Table test covering each branch: empty requirement ({}) and nil schemes, a scheme with no scopes (bare name), a scheme with scopes (sorted), and several schemes AND-ed (sorted by name). The {} and multi-scheme cases were only exercised indirectly before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * report: label a modified security requirement by scheme name, not full String() The text report showed "Modified security requirements: petstore_auth: [read:pets]" right above "Added scopes: [list:pets]", so the base scopes read as noise and were easy to misread against the delta beneath. Add SecurityAlternative.SchemeNames() (sorted, AND-joined scheme names) and use it for the modified label, restoring the pre-remodel "petstore_auth" header. Added/Deleted keep the full String() (with scopes), where the scopes are the identity and there's no detail below. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * diff: lock the invariant that a modified requirement has a unique scheme set The report labels a modified security requirement by scheme name only, which is unambiguous because the scope-modification pass only fires for an unambiguous scheme set (exactly one unmatched alternative with that set per side). Add a test proving that two same-scheme alternatives both changing scopes are reported as deleted + added (carrying full scopes), not as same-scheme modified entries, so a future matcher change can't silently make the label ambiguous. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
refactor(checker): rename SCREAMING_SNAKE_CASE stability constants to… … MixedCaps (#1032) STABILITY_DRAFT / ALPHA / BETA / STABLE use a C/Python casing convention, not Go. Rename them to StabilityDraft / StabilityAlpha / StabilityBeta / StabilityStable and migrate all internal usages. The values ("draft", "alpha", "beta", "stable") are unchanged, so no CLI or serialized behavior changes. The old names are kept as deprecated aliases, since they are exported and a library consumer may reference them; they can be dropped in a future major release. Closes #1031. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
fix(open): write --open URL and browser notice to stderr, not stdout (#… …1011) `oasdiff changelog --format json --open > out.json` appended the human-facing "Opening <url> (expires ...)" line (and any browser-fallback notice) to stdout, right after the rendered changelog, producing invalid JSON/YAML. The recent non-fatal-warning work already routes --open *errors* to stderr; this does the same for the success path. uploadAndOpen now writes its URL/guidance to a writer the caller sets to os.Stderr, so stdout carries only the rendered changelog. Verified: `changelog --format json --open` now emits valid JSON on stdout with the URL/notice on stderr. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
PreviousNext