feat(ir)!: close ten IR gaps found by SpaceAPI's integration study - #434
feat(ir)!: close ten IR gaps found by SpaceAPI's integration study#434fuad-daoud wants to merge 13 commits into
Conversation
Detect capped its search for `openapi`/`swagger` at the first 64 KiB, so a valid document that writes a large object before its version key was reported as an unrecognized format. Stripe's published spec3.json is one: `components` runs to megabytes and `openapi` lands at byte 2,593,401. Mapping key order carries no meaning, so the same document with its keys the other way round compiled fine — the format answer depended on where a writer put a key. The 64 KiB prefix keeps its place as the fast path, and every document that declares a key there is still answered without a full parse. When the prefix declares neither key, a byte scan of the whole source decides whether to read it whole: only bytes that name `openapi:` or `swagger:` as a top-level key reach the parse, so a source of another format still gets the fast path's silence and never a complaint from this compiler. That scan is what Detect already used to tell its own broken source from another format's, and it is no longer bounded to the prefix either. A document whose prefix does not parse and whose declaration sits past the cap is now reported as an undecodable OpenAPI source rather than declined, which is the answer the surrounding rule always intended; detection now reads the bytes it would have had to read to say otherwise. Fixes #420 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
When allOf branches declare one field with incompatible types the merge keeps the first declaration and warns, naming both pointers. The losing declaration was then dropped: it reached the IR in no form at all, so a consumer reading the document rather than the diagnostic stream saw no trace of it — and a diff across two revisions in which only the losing branch's type moved reported no change. GitHub's published spec writes this shape 102 times. Every other degradation in this compiler keeps what it could not model. This one now does too: the discarded ir.TypeRef is written to the merged property's Unmodeled under ReasonDegradedLowering, keyed by the redeclaration's own pointer so sibling branches never overwrite one another, and stamped with the losing declaration's provenance. The constraint half of the same diagnostic is deliberately left alone. It also discards the redeclaration's keyword, but the recorded direction there is to intersect the bounds so the merged field satisfies both branches (#10), and preserving the loser instead would settle a decision that already has one. The code comment on keepLosingType says so. Fixes #424 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
Request-body optionality survived only as an inverted sentinel: the OpenAPI compiler wrote Payload.Unmodeled["openapi:required"] = false when a body was not required and wrote nothing when it was, so recovering the fact meant knowing an OpenAPI-specific key and reading its absence as true. A consumer that reads typed fields alone saw every body as required — 563 times across GitHub's and Stripe's published specs. ir/unmodeled.go grades no_ir_home as "a gap expected to close, not a boundary", and this is that gap. ir.Payload now carries Required *bool. The pointer is the point: a format that expresses body optionality treats an unstated body as optional, so folding "the format is silent" onto the same value as "the document says no" would lose the distinction a non-OpenAPI compiler needs. Response and message payloads leave it nil, because only a request body can be omitted. The OpenAPI compiler always sets it, since OpenAPI's own default makes an undeclared `required` mean false rather than unstated, and it no longer writes the openapi:required entry or the info diagnostic that announced the degradation — the fact is modeled now, so neither describes anything. ir-design.md is normative on the field shapes, so §7.2's Payload and §14's OpenAPI lowering summary are updated with it. BREAKING CHANGE: a consumer reading Payload.Unmodeled["openapi:required"] must read Payload.Required instead; the Unmodeled entry and its openapi/degraded-construct info diagnostic are no longer emitted. The per-reason reachability test moves its no_ir_home witness to a parameter's allowEmptyValue, which still has no typed home. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
ir.Parameter was the last lowered node carrying no Provenance, and two things followed from that. A parameter's vendor extensions were stranded. ir-design §12 rule 4 says a node with no provenance is not promoted into, because a promotion that cannot be marked Inferred cannot be audited — so the parameter position was the one ir.Deprecation carrier PromoteDeprecation was not wired at, and a deprecated parameter's x-sunset sat unread beside an empty Deprecation. It is wired now, and extension-promotion.yaml gains the parameter row so the sweep fails at that carrier rather than being covered by a neighbour. Parameter origin was erased. mergeParameters merges a path item's parameters into every operation on the path, and nothing afterwards recorded that a given parameter was inherited rather than declared. The stamp uses the pointer internal/operation already threads per parameter for the interning fix (#36, #107): an operation's own entry points under that operation, a $ref'd one at the component it names, and a path-item one at the path item — one declaration named by every operation that inherits it, which is what tells the two apart. BREAKING CHANGE: ir.Parameter gains a Provenance field, serialized without omitempty like every other node's. Every golden carrying a parameter moves, and a consumer decoding the IR sees a new object on each one. Closes #423 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
ir.ErrorCase and ir.Response are two lowerings of one Response Object, but
only one of them could say what the source declared. Response carries a
Name whose Hint is the status spelling, Headers, and a Payload holding
every media type; ErrorCase carried none of the three — no name at all, no
headers, and one bare TypeRef where the content map belongs.
Everything that fell outside those fields went to ErrorCase.Unmodeled with
an info diagnostic, so a consumer's behaviour changed with the status
class and nothing said so:
- Retry-After and the rate-limit family live on 429 and 503, precisely the
side with no typed home for a header.
- A 4xx declaring application/json and application/problem+json kept the
first schema and lost the media-type key entirely; a 4xx declaring one
media type lost the key it was written under.
- "5XX" and "default" had no faithful round-trip: StatusRange renders
{500,599} and {0,0} with no record of how the source spelled them.
ErrorCase now has Name Naming, Headers []Property and Payload *Payload in
place of Type, each spelled as Response spells it, and the error path
lowers through the same responseName, lowerHeaders and lowerPayload the
success path uses. preserveErrorHeaders, fillErrorType, preserveErrorContent
and errorContentMessage existed only to soften this gap and are gone with
it, along with the two info diagnostics they emitted.
pass.checkEncodingKeys grows a fourth Payload carrier, reached at both
positions an ErrorCase hangs from — an operation's Errors and a service's
CommonErrors — since a check walking only the first would resolve a
service-level error's encoding keys against nothing in silence.
BREAKING CHANGE: ErrorCase.Type is removed; an error case's models are its
Payload.Contents entries' types. The JSON gains "name", "payload" and
"headers" and loses "type". ir.IRVersion is deliberately not moved here:
per ir-design.md §2.1 a line of work bumps it once, where it lands on main,
and two earlier shape changes on this branch left it alone for the same
reason.
The normative rows in docs/ir-design.md that described the old behaviour
are updated, as is the error-taxonomy example in docs/emitter-design.md.
The per-status-errors conformance fixture gains a 429 declaring two media
types and two rate-limit headers, which is what makes the new fields
witnessed rather than merely present.
Closes #422
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
x-sunset echoes RFC 8594's Sunset header, which is a date by definition, but the default promotion mapping read it into Deprecation.RemovalVersion — a field whose name, doc comment and sibling all say version. A consumer deciding whether removing a deprecated operation is breaking compares a sunset against a release date, and could not tell which spelling it had been handed without re-parsing the string. Take issue #417's option 1: a distinct RemovalDate beside RemovalVersion, with x-sunset promoting to the date. A version and a date are two facts, not two spellings of one — a document may state both ("gone in 3.0.0", "gone on 2026-08-01"), and neither is derivable from the other without a release calendar the IR does not have. A single field carrying which spelling it holds (option 2) would have to drop whichever fact it read second, so it costs losslessness to buy nothing a second field does not already give: the field a value arrives in is what says which fact it is. Deliberately out of scope, and stated in ir-design.md and at the reading site: RemovalDate is the source's own text, neither parsed nor normalized. No source format defines the field, so none defines its format; and the key→field mapping is caller policy, so a key pointed at the date field is the caller's statement that it holds a date. Morphic records which fact was stated and leaves the calendar to the consumer. BREAKING CHANGE: Deprecation gains removalDate, and x-sunset now fills it instead of removalVersion. A consumer reading removalVersion for a sunset reads an empty field until it moves. No default key names RemovalVersion any more — a document stating a removal version names its own key, per promotion rule 1 — so the corpus stops witnessing that field and it joins unwitnessed.golden.txt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
ir.Enum has carried a Closed bool since the IR was written, and the OpenAPI compiler set it true at both construction sites unconditionally. So the one key the format has for saying an enum is open, x-extensible-enum, survived only as a generic vendor_extension entry, and every consumer reading typed fields saw a closed enum whatever the document said. Open versus closed decides whether a generator emits a fallback member and whether a differ calls an added value breaking, so this was a wiring gap, not a modelling one. Add TargetEnumOpen to the promotion vocabulary, map x-extensible-enum onto it by default, and apply it in attachDeclaredAnnotations beside the deprecation promotion — the point at which a declaration's extensions have reached the node's map, which is what makes "the extension survives its own promotion" structural here as it is there. Every promotion property holds unchanged: the entry stays put with its vendor_extension reason, the node records extension-promotion in Provenance.Inferred, and a disabled policy writes nothing. The target names the fact rather than the field, which the rest of the vocabulary does not. Openness is the only half of that bool a document ever declares — a schema's `enum` is closed by definition — so a target named for Closed could only ever be written false and would read as its own opposite at every mapping naming it. For the same reason the key's presence is the statement rather than its value: the established spelling writes the member list as the value, and a list of members says nothing about openness the key naming it has not already said. A boolean is the one shape that does state it alone, so an explicit `false` is read as written rather than inverted. Deliberately out of scope, and stated in ir-design.md and at the reading site: a document writing x-extensible-enum *instead* of `enum`, with the members in the extension, lowers to no ir.Enum at all and there is no node to open. Minting one would be reading a member list out of a vendor key rather than promoting a field; the entry survives verbatim for a consumer that wants to. The corpus can now witness the matrix's open-enums row, so its matrixRowsUncovered reason is deleted rather than left to go stale, and extension-promotion.yaml gains the three enums that pin the three answers the reading has: the convention opens one, an explicit false declines to, and an enum naming no such key is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
The 2020-12 content vocabulary was two-thirds modelled: contentEncoding reached Encoding.Name and contentMediaType Encoding.MediaType, while contentSchema had no field at any IR position and was always kept verbatim under Unmodeled. A consumer saw an opaque string where the source declared a full shape, and had to special-case one of three keywords (GitHub #426). ir.Encoding gains Schema *TypeRef. contentSchema's value is a schema, so it lowers like every other sub-schema position: hoisted at its own source pointer and referenced by ID, never carried beside the encoding as a raw payload a consumer would have to re-parse. The pointer it hoists at is the one the source wrote it at, which only that declaration can name, so the minted node needs no namespace of its own. The three keywords now share one home, so a position keeps them all or lowers them all: contentSchema joins contentKeywords, and the schema package decides its fate by asking the node that was built rather than the keyword that was written. That is why annotation.noIRHomeAt goes — whether a content keyword reached ir.Encoding is a question only the lowering can answer, and it was answering "never" from outside. Adding the scalar hoisters to the schema walk's recursion is what lets a contentSchema nest; the walk's depth counter already bounds it, and internal/archtest pins the widened cycle. BREAKING CHANGE: contentSchema no longer appears as an openapi:contentSchema Unmodeled entry at a position that lowers to a Scalar; it is Encoding.Schema there. A position with no Encoding field still keeps it verbatim, now alongside its two neighbours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
At a $ref use site the compiler merges the referent's documentation, deprecation and default onto the referencing Property/Parameter, and leaves its constraints where they were declared. The split was deliberate and tested but written down nowhere a consumer reads, so an empty Constraints at a use site could be read as "this value is unbounded" when it means "this position declared no bound". The split is kept, because the two halves are not the same kind of fact. An annotation is a single value one position may restate for another, so use-site precedence is the only sensible rule and applying it once in the compiler keeps every carrier alike. A bound is not: maxLength 64 on the referent and maxLength 100 beside the $ref are both in force and the narrower wins, so merging under use-site precedence would publish 100 as the whole truth and lose the bound the document enforces. What changes is that the rule is now stated where it is read: a new ir-design §12.2, the Constraints, Property.Constraints, Parameter.Constraints and TypeRef field docs, and the two lowering sites that implement it. An absent Constraints at a use site means that position declared no bound; the effective bound is its conjunction with every node reached from its TypeRef. param-ref-inheritance now declares a bound beside the $ref at both carriers, so the split is witnessed rather than merely absent: the use site's 100 lands on the carrier, the referent's 64 stays on the referent, and the case reddens if either is copied onto the other. Fixes #428 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
In JSON Schema 2020-12 minimum and exclusiveMinimum are independent keywords that both apply; a schema may legally declare both, and the same holds on the upper side. ir.Constraints held one bound plus one exclusivity flag per side, so a co-declared pair had to be reconciled: the tighter keyword took the slot and the other was kept verbatim under Unmodeled as degraded_lowering. The loser was preserved, so nothing was lost outright. But a consumer comparing constraints across two revisions of a spec reads the fields, not the Unmodeled map: a revision that moved only the dropped keyword read as no change, and one that swapped which keyword was tighter read as a change of a different kind than the one that happened (#425). ir.Constraints now holds four bounds — Min, ExclusiveMin, Max, ExclusiveMax, each a *BigVal, each the keyword of the same name. A co-declared pair reaches two fields, keeps nothing beside them, and reports nothing: there is no degradation left to announce. The reconciliation, its exact-decimal tighter-of-two comparison, and its diagnostics are gone; merge adopts and compares each of the four the way it already did multipleOf. BREAKING CHANGE: ExclusiveMin and ExclusiveMax change from bool to *BigVal and their JSON keys gain omitempty, so `"exclusiveMin": false` no longer appears and an exclusive bound serializes as its literal rather than as a flag on `min`. The OpenAPI 3.0 spelling — a boolean modifying the minimum beside it — now lowers to the bound it means: `{minimum: 5, exclusiveMinimum: true}` becomes ExclusiveMin "5" with Min unset, which is what the 3.1 spelling of the same restriction produces, so a 3.0 document and its 3.1 translation no longer differ in the IR. A 3.0 modifier written with no bound to modify (invalid under draft-4, and unchecked by the loader) is kept verbatim under Unmodeled and reported as a warning rather than setting a flag over an absent bound. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
The whole-source read #420 added got two answers wrong, both by reusing machinery written for a cut prefix. The flow decoder reports "this is a flow mapping" for anything opening with `{`, and drops the error that ended its walk. For a cut prefix that is right: the cut always breaks the token stream, so the error describes the cut and not the document. For a whole document it hides the document's own break. A JSON source past the cap whose `openapi` key sits behind a syntax error came back with a nil error, so Detect saw no failure to report and declined it as an unrecognized format — the very answer #420 set out to replace, still standing for every JSON source, which is the style the motivating spec is written in. The decoder now returns the error that stopped it and treats stopping on the mapping's own closing delimiter or on the entry cap as no error at all; sniffPrefix drops it along with the cut that caused it, and sniffWhole keeps it. The key scan was widened to the whole source without being scoped to the top level. Its block arm reads column 0 and always was top-level, but its quoted arm matched `"openapi":` at any depth, anywhere in the buffer. Bounded to the first 64 KiB that cost a needless parse; over a whole source it makes a claim, and a wrong one — another format's document that nests such a key and does not parse was reported as an undecodable OpenAPI source. Saying nothing about bytes that are not this compiler's own is the rule detection is built on. The quoted spelling is how flow style writes every key, so flow structure is what scopes it: a depth-tracking scan reads the root mapping's own entries and nothing under them, and a source that opens no mapping at all declares nothing here. It is a lexer rather than a parser because the case it exists for is a document broken before the key that names it, where there is no tree to ask. A block document that quotes its top-level key is no longer seen and is declined in silence, which is the direction to be wrong in. A valid document past the cap that declares its version last still compiles, which is what #420 was about. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
§12's fourth promotion rule read as if it had none left: `Parameter` was the instance it named, and the sentence recording that `Parameter` has since gained a `Provenance` left the rule with nothing to point at. `Variant` (§4.4) and `EnumMember` (§4.5) each still carry a `Deprecation` with no provenance of their own, so the rule governs them today. Name them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
Six commits on this branch change the JSON shape of a Document and none bumped the constant, each correctly deferring per ir-design 2.1: a line of work bumps it ONCE, where it lands on main. This is that bump. The GoDoc on IRVersion names exactly the failure a missing bump causes -- "a shape change that reaches main without a bump leaves a consumer pinned to the old version accepting a document it cannot read, which is the one thing this constant exists to prevent" -- and nothing in the gate can see it, because TestVerify_CurrentIRVersionIsClean, TestVerify_IncompatibleIRVersionIsAViolation and openapi_test.go all compare against the same constant and stay green whatever it says. Found by review, not by CI. The log paragraph records all six, each framed as what a 0.3.0 consumer gets wrong rather than as a feature: ErrorCase loses Type and gains Name/Payload/ Headers; Payload gains Required; Parameter gains Provenance; Deprecation gains RemovalDate and x-sunset routes there; Encoding gains Schema; and Constraints.ExclusiveMin/Max change from bool to a decimal string, which is the one that fails a consumer's decode rather than degrading it. TestCompatibleVersion's neighbour rows were spelled against 0.3.0, so the bump made "a later generation" 0.4.0 assert that the build rejects its own documents. Re-anchored, with a comment saying they move with the constant. 79 goldens regenerated; the only key that moved is irVersion. BREAKING CHANGE: IR documents now stamp 0.4.0 and CompatibleVersion refuses 0.3.0. Consumers must recompile rather than migrate stored documents. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
|
Superseded by a stack of eight, at your request — this one was too big to review.
Each targets the one below it; review and merge bottom-up. The commits are unchanged and not rebased — deliberately, because every one of them passed The one cost of not reordering: #441 fixes two real defects in #435 (the fix missed JSON, and the key scan was not top-level-scoped) and sits at the top rather than folded into #435, because that is where the review found them. Both PRs cross-reference each other. The branch |
DO NOT SQUASH-MERGE THIS AS-IS, and do not merge it at all without reading the two notes at the bottom. Thirteen commits closing ten issues. It is opened for review, not because it is ready to land.
Closes #417, #420, #421, #422, #423, #424, #425, #426, #427, #428.
Why one branch
Every issue here was filed by the same study:
dexpace/spaceapiis evaluating morphic IR as its input format, and is the first prospective consumer that is not an emitter. The gaps were found together, several of them interact (ErrorCaseandPayloadboth touchlowerPayload;RemovalDateandParameter.Provenanceboth touch promotion), and the IR version bump has to cover all of them at once. Splitting into ten PRs after the fact would mean ten golden regenerations of the same files.If you would rather have them split, say so and I will split them — the commits are already one-per-issue and each passed the gate on its own.
The gate
GOTOOLCHAIN=go1.26.3 make gate→ exit 0 on the final tree. All ten targets, 100% coverage every package, fuzz and bench-smoke included. Each commit was also gated individually as it landed.Note the toolchain: this machine's Go 1.27 fails the gate for reasons unrelated to any change here (golangci-lint panics building IR for the stdlib, plus one test pinning a Go 1.26 JSON escape). Both reproduce on a pristine
main. Filed as #431.BREAKING CHANGES — the union, because a squash loses the commit bodies
ir.IRVersion0.3.0 → 0.4.0.CompatibleVersionrefuses 0.3.0, so consumers recompile rather than migrate stored documents.ErrorCaselosesType; gainsName,Payload,Headers(ir.ErrorCase is second-class beside ir.Response: no Name, no Headers, no per-media-type payload #422). It is nowResponse's sibling and lowers through the same helpers. A 0.3.0 consumer finds notypeon an error case and cannot reach its models at all. This deletespreserveErrorHeaders,fillErrorType,preserveErrorContentand the 703-occurrence "error response media type has no ErrorCase home" diagnostic.PayloadgainsRequired(ir.Payload has no Required: body optionality survives only as an inverted Unmodeled sentinel #421), a tri-state*bool. Body optionality stops being an invertedUnmodeled["openapi:required"]sentinel read by absence — a consumer still reading that key now finds nothing and reads every body as required.ParametergainsProvenance, non-omitempty (ir.Parameter carries no Provenance, which strands x-sunset and erases parameter origin #423), and with itx-sunsetpromotion at the parameter position.DeprecationgainsRemovalDate(Deprecation.RemovalVersion holds a date when x-sunset promotes into it #417);x-sunsetpromotes there rather than intoRemovalVersion. A consumer reading a removal date off the version field now finds it empty. Rationale recorded in the code: a version and a date are two facts, not two spellings of one, and neither is derivable from the other without a release calendar the IR does not have.EncodinggainsSchema(contentSchema has no IR field at any position #426), givingcontentSchemaa home at scalar positions.Constraints.ExclusiveMin/ExclusiveMaxchange fromboolto a decimal string (A co-declared minimum + exclusiveMinimum pair loses one keyword to degraded_lowering #425) carrying the bound itself. This is the one that fails a consumer's decode rather than degrading it — the JSON type changed.Non-breaking: #420 (detection past the sniff cap), #424 (a conflicting redeclaration's losing type is kept), #427 (
x-extensible-enum→Enum.Closed; the field's meaning is unchanged, only the compiler's computed value), #428 (documented as deliberate rather than changed).Two things I want your judgement on
1. #428 was answered with a doc, not a fix. The issue asked why a
$ref'd schema's constraints do not reach the use site while docs and defaults do. The agent working it concluded the split is deliberate and documented it (9861f2b) rather than changing behaviour. That is a defensible reading and it is also the answer that required no work — worth a second opinion.2. A deliberate recall loss in detection.
e9ad16cscopes the key scan to the top level, which means a block YAML document quoting its top-level key ("openapi": 3.0.0at column 0) past the 64 KiB cap is no longer detected. Admitting"openapi":at column 0 would re-open the hole for zero-indent JSON, whichjson.MarshalIndent(v, "", "")really produces. Againstmainnothing regresses — that shape was declined before too — but it narrows a capability this branch itself added.Review found these, and they are fixed here
An adversarial pass over the whole branch (which re-ran the gate itself) caught four things the per-commit gates could not:
openapipast 64 KiB: Stripe's published spec is rejected #420's fix missed JSON, the motivating case:decodeFlowEntriesswallowed a mid-stream error, correct for a cut prefix and wrong for a whole document. Malformed JSON withopenapipast the cap still reportedunrecognized-format. Fixed ine9ad16c, mutation-checked."openapi"key could be claimed asopenapi/undecodable-source— this compiler claiming bytes that are not its own, whichdetect.go's own comments forbid. Fixed in the same commit.ir-design.md§12 sentence that read as if rule 4 had no live instances whenVariantandEnumMemberstill are ones. Fixed in68977f4.It also found one defect that is not ours: a
components/responsesentry mounted at both a success and an error status interns its schema order-dependently, andmorphic-harnessreddens on it onmaintoo. Filed separately as #433.Verification beyond the gate
Per CLAUDE.md's "verify by executing", the agents mutation-checked their own work rather than trusting green:
NamefromlowerErrorCasereddenedirverify's naming rule across the whole corpusper-status-errorsfixture reddened the conformance case, proving the fixture addition is load-bearingDetectA differential
Unmodeled-key census between amainbinary and a branch binary over everytestdata/**spec confirms each key delta maps to exactly one commit's intent, with no double-writes and no orphans:openapi:content5→0,openapi:headers1→0,openapi:required7→0,openapi:contentSchema2→1,openapi:exclusiveMinimum2→0,openapi:conflicting-redeclaration0→2. All other 60 keys unchanged.🤖 Generated with Claude Code
https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1