Repository navigation
Phase 7a: serialization — documentation and phase record - #89
Conversation
Phase 7a, Tasks 2-16 and 19's wiring. In dexpace-core, under lib/dexpace/serde/: DecodeContext -- the `ctx` every witness receives, a frozen Data carrying the RFC 6901 path and the decode's target name, with the eight `!` checks and the ONE raise site that makes SERDE-13/21/22 properties of one method; witness.rb reopening Dexpace::Serde for WITNESS_METHOD, DUMP_METHOD, .witness? and .witness! (a respond_to? on .dexpace_load, never on #call); Native and OMIT, the encode walk that drops an Absent key, writes nil for one in an Array or at the top level, and raises SerializationError naming the class where ::JSON.generate would stringify (P7-9); the private scalar table and the named BOOLEAN witness; Tristate with ABSENT, NULL, Present (validating in #initialize, .new and .[] private, Model#with keeping the fourth state closed on 3.2) and the private Combinator behind .of with its two entry points; List, Map and Nullable, frozen Datas by value from a concrete element witness, failing at construction (SERDE-8); Instant, the ISO-8601 witness with P7-8's microsecond domain; DecodingHandler (SERDE-27: the body's own #source, one ensure-close, BufferedSource#eof? for an empty body) and StatusAwareHandler (SERDE-28: 2xx delegates, 4xx/5xx raises the factory's error over Recovery.buffer_error_body's copy with no second close, anything else closes and raises leading with the code and the raw ETag/Location). Body.serialized is the ninth factory (SERDE-2). Phase 2's interface _Codec is edited in place -- #media_type a MediaType or a String, #load over the new _Witness, #dump_to an Integer -- never written twice. In dexpace-serde-json: the gemspec's json >= 2.19.9 line, the only place that floor is stated and the first NFR-2 third-party half spent; the entry file asserting MINIMUM_JSON_VERSION at require time (P7-7), registering under :json with REQUIRED_CORE, and the .default / .build factories; and Codec, the six seam methods over one private ::JSON::Coder per instance (P7-4) built with keywords only, strict: true and allow_duplicate_key: false fixed, encoders: never forwarded, options as one positional Hash over a five-key allowlist, #dump_into's explicit fit check raising IndexError cause: nil, #load draining through #read_utf8 under the materialisation ceiling (P7-1), validating UTF-8 (P7-6) and rescuing ::JSON::JSONError around the parse alone (SERDE-12 structurally). Every new file has a sig/ mirror; the Steepfile's :serde_json target alone downgrades Ruby::UnknownConstant to :information for the one JSON::Coder rbs 4.2.0 does not declare (json 3.0.2 ships no sig/). The surface manifests are regenerated once, 86 rows. Five core pins the require-time registration invalidates change here: seam_surface_test.rb's two seam pins and serde_test.rb's "starts empty" pin assert in a child process that requires dexpace alone, and its two swap pins assert the override is gone. The adapter's smoke test snapshots after `require "dexpace"` and pins the gem's four constants.
Review round 0, R0-1. The feat commit rewrote three lines of the file's header comment to correct its stale "json arrives with the codec in phase 7" sentence. The file is a shared one this phase's brief lists out of bounds -- phase 8a adds the first row to it -- and no gate reads the comment, so the correction rode a scope breach for nothing. Restore the file to main's content; the stale sentence is routed by date and content to phase 10's inbound list on the docs branch, for whichever lane adds the first row to close. The Steepfile's :serde_json relaxation, which is what settles the JSON::Coder reference, is unchanged.
Review round 1, R1-4. DecodeContext.root gives an anonymous class no target (P7-70), so that `#error!` keeps its plain form and no `#<Class:0x...>` reaches a message -- but DecodingHandler#missing_body and StatusAwareHandler#unhandled_message interpolated that nil directly, and a 204 through a `Class.new` witness read "no body to decode into : the response carried none". Each handler now derives the name through one private #target_name that falls back to the literal "an anonymous witness", declared in the two sig/ mirrors. A named witness's messages are byte-for-byte what they were; P7-70's rule on the context is unchanged.
Phase 7a, Tasks 1-18's tests. Thirteen core suites under test/dexpace/serde/ -- one mirror per lib file plus tristate_decode_test.rb and the SEAM-2 scan no_concrete_codec_test.rb (Ripper-tokenised, comments dropped) -- and http/body_serialized_test.rb beside 3b's body suite: SERDE-21's nine coercions as nine fixtures, SERDE-22's two permissions, SERDE-13's target naming at the root frame, SERDE-14 closed on construction, .[] and #with, SERDE-15/16/17/20's three states both ways, SERDE-24's domain and P7-8's truncation, SERDE-27's five-case matrix over 3b's FakeResponseBody (raw close counts) and a real Response, and SERDE-28's three branches with the non-canonical 599 in the second. In dexpace-serde-json: codec_test.rb (the four encode profiles, SERDE-4's four-part buffer matrix with cause: nil asserted from inside a rescue, P7-5, the failure model, SERDE-25/26/29, the option allowlist against json 2.19.9 and 3.0), codec_load_test.rb (SERDE-3's zero close count under every option, SERDE-5/12/13/20/21/22/23, P7-6, the over-ceiling StreamError), defaults_test.rb (SERDE-1/19/24 through the real codec, two seeded property samples), seam_conformance_test.rb over the phase-9 lift target test/support/serde_seam_assertions.rb, and composition_test.rb walking Operation -> Pipeline.standard -> TypedResponse over a recording lambda transport. Two new close-counting doubles. Thirty-four mutations run red on 4.0.6 and 3.2.11 (two equivalent, one floor-only), recorded in the checklist.
Review round 0, R0-2. Dropping the explicit `allow_duplicate_key: false` default from Codec#initialize survived the whole suite on the bundle's json 3.0.2, where a duplicate key is a ParserError by default, and turned into a warning-plus-last-wins only at the 2.19.9 floor, which no gate row runs. The option is now pinned where it lives rather than through the engine's behaviour: a fourth nested class in codec_test.rb runs a child process (context_store_config_test.rb's shape -- the recorder is a permanent prepend on a library class's singleton, so it never enters the suite's own process) that captures every keyword ::JSON::Coder.new receives across four constructions and asserts `allow_duplicate_key: false` and `strict: true` on the default, the caller's opt-in on the second, `max_nesting` forwarded and `encoders:` withheld on the third, and the two booleans on the fourth. The child is handed the core the parent loaded explicitly, so the case reads the same tree under `bundle exec` and under a bare `ruby -I` run at the floor. Measured: the mutant is red on 4.0.6 with json 3.0.2 and on 3.4.10 with json 2.19.9 pinned unbundled; guard 19 (`strict: true` dropped), an equivalent mutant until now, is red at the keyword level too; forwarding `encoders:` is red on both.
Review round 1, R1-1, R1-2 and R1-4's proof. R1-1: the empty-body case in decoding_handler_test.rb asserted only that the message names PetWitness, which the witness's OWN shape failure over the drained "" also does, so reducing `raise missing_body if source.eof?` to a bare probe left every suite green. It now asserts /no body/ beside the target, and composition_test.rb drives an empty 200 through Pipeline.standard and the real JSON codec, asserting the handler's target-naming error and never the parser's "malformed JSON: unexpected end of input". Both are red under that mutant. R1-2: P7-7's require-time floor assertion was exercised by no test -- every gate row runs under the bundle's json, above the floor. The new json/floor_test.rb runs a child process with RUBYOPT, RUBYLIB and the BUNDLE_*/BUNDLER_* keys cleared (GEM_HOME and GEM_PATH kept, so the bundle's json stays findable), pins a json by exact version with `gem` and requires the entry file: the interpreter's default json (2.6.3, 2.7.2, 2.9.1, 2.18.0 across the matrix) is refused with the SeamError naming the floor and the active version, and the json this process runs loads and registers under :json. Deleting the block turns the first case red on every row -- json 2.18.0 loads and registers, the silently-unpatched case the deviation exists for. R1-4: one case per handler drives a `Class.new` witness and asserts the message reads "an anonymous witness", never an empty name and never `#<Class`.
Phase 7a, Task 19's records. The checklist, written from the build: thirty SERDE rows, all implemented, nine stating a clause and nine touched by a deviation; twenty cross-reference rows; the matrix facts re-run on all four interpreters against json 3.0.2 and 2.19.9; thirty-four guards and four gate mutations; thirty-one departures from the plan; findings routed. The design gains an As-built addendum, P7-61-P7-72, withdrawing the "_Codec never written" finding with the evidence. docs/sdk-documentation/serde.md is the fifteenth page, every example run on 4.0.6 and 3.2.11; architecture.md, both READMEs, the gem README and docs/README.md point at it. CLAUDE.md gains the built-phase sentence, the layer paragraph, 184 -> 195 lib files, fifteen checklists, the adapter's lib/ sentence and five constraints. docs/first-release.md's 7a P7-1 entry records its first owed half closed. docs/knowledge/notes/serde.md is new with two entries. The roadmap gains the 2026-09-20 status note, one phase-10 inbound bullet (gates:clean_bundle's install location) and a bracketed correction on the _Codec residue bullet; the knowledge-lookup skill's thirteenth audit row gains constraints,conclusions.
Review round 0 returned changes_requested with two should-fix findings and three nits. The records catch up with the two fixes below this commit and take the three nits: - R0-1: rbs_collection.yaml is as main has it again; the checklist's What was built and deviation 8 no longer say its comment was corrected, and the stale "json arrives with the codec in phase 7" sentence is one new phase-10 inbound bullet, by date and content, for whichever lane adds the first row to close (Findings routed). - R0-2: the keyword pin on the codec's JSON::Coder construction is guard 34 (red on 4.0.6 with json 3.0.2 and on 3.4.10 with json 2.19.9 pinned unbundled), with 34.5 for encoders: forwarded; guard 19, an equivalent mutant behaviourally, is red at the keyword level through the same pin. The battery is thirty-five, thirty-three caught on both rows, guard 3 on 3.2.11 alone, guard 21 the one equivalent mutant. The SERDE-9 row cites the pin; CLAUDE.md's constraint line says why the option is pinned at the keyword level and not through the engine's behaviour. - R0-3: serde.md's last example defines the five responses it reads, over Body.buffer from one helper; every ruby block of the page runs as written on 4.0.6 and 3.2.11 with no fixture pre-defined (12 blocks, 69 checks). - R0-4: CLAUDE.md now says which serde pin runs in a child process (the "starts empty" one) and that the two swap pins assert the override is gone. - R0-5: the roadmap's status note names the Steep relaxation as the second route of the manager's decision (1), as P7-62 and the checklist do. The design's As-built addendum carries a round-1 paragraph; no ledger row is added, because the round found no behaviour the document states that the code fails to honour. The roadmap gains a round-1 status paragraph. Probe clean.
Review round 1 returned changes_requested with two should-fix findings and two nits. The checklist's guard table gains the round's three rows (23.5: the eof? raise reduced to a bare probe; 35: the P7-7 floor block deleted; 36: the anonymous-witness fallback dropped), corrects guard 23's row -- its third failure was the eof?-probe case, not the empty-body case, which the round found green under that guard -- and restates the battery at thirty-eight; the SERDE-27 and SERDE-28 rows cite the new assertions and the floor test joins What was built. The design's As-built addendum carries a round-2 paragraph and P7-70's row is amended in place for the two handler messages. The roadmap's status line carries the docs tip's current run count (R1-3) and a round-2 paragraph; CLAUDE.md's two serde constraint lines say why the empty-body message is pinned on both halves and where P7-7's raise is observable; serde.md names the stock 4.0 json and the anonymous witness. No frozen tree, no earlier phase's document and no other lane's file is touched.
Phase 7a was built off c53638b concurrently with 7b and 7c, whose stacks landed first. The three 7a branches were rebased onto 7c's reconciled docs tip with every 7a commit preserved; the files both sides changed were reconciled inside the rebased 7a commits, with every count re-derived from the combined tree. This commit carries the two records no 7a commit could: the roadmap's dated reconciliation paragraph, and a dated "Reconciled" note at the head of 7a's checklist saying which of its count sentences describe its own base and what the combined tree's figures are. Phase 7 is complete with this stack.
Review record for the phase 7a stack (#87 → #88 → #89)3 independent reviews, each by a fresh agent with no memory of the previous one, each re-running every gate itself on every tip (4.0.6 every gate individually at the code tip and the full
Nothing was skipped. Round 0 → fixed in round 1
Round 1 → fixed in round 2
Round 2 (final) — approve
What the final reviewer verified by experiment, both interpreters
Tips reviewed at the final round: code Reconciliation after reviewThe three reviews above ran on the stack as built off |
Closes #26. Third PR of phase 7a's stack — the phase record and the documentation — on top of #88.
What lands
12 files on top of 7b's and 7c's
main(the 7a record plus the reconcile pass's dated paragraphs). Phase 7 is complete with this PR — umbrella #25 closes by hand.docs/work/mvp/phase7/phase7a/2026-09-10-phase7a-serialization-checklist.md— 30 own rows, all ✅ (SERDE-1–SERDE-30; nine carry a stated clause rather than a bare tick), plus 20 cross-reference rows; the roadmap's legend verbatim; sections: requirement rows, what was built, guards run red (38), audit groups run, deviations from the plan, findings routed, postponed work (phase 9's lift ofserde_seam_assertions.rb)._Codecnever written" finding withdrawn with the evidence (c881f92). The consolidation into design §10 is a human's —docs/sdk-design-ruby/is frozen — and the addendum says so.docs/sdk-documentation/serde.md(new; every example executed on 4.0.6 and 3.2.11 — the one block that needsrequire "stringio"is R2-1 below),architecture.md,gems/dexpace-serde-json/README.md(no longer a skeleton's),README.md,docs/README.md(whose "what the tree holds" sentence had omitted 6b's redirect layer onmain— corrected while adding 7a's).docs/knowledge/notes/serde.md— two new reference entries (a decoded JSON String can be UTF-8-tagged and invalid;JSON::Coder's option handling drifted between 2.19.9 and 3.0), keys cited in support, none overriding;ruby scripts/verify_knowledge_structure.rbOK..claude/skills/knowledge-lookup/SKILL.md— the serde audit row's query widened torules,constraints,conclusions(--section rulesalone missesSERDE-17,24,25and30).docs/first-release.md—SERDE-27's no-materialization entry: the first owed half closed byserde.mdon 2026-09-20; the phase-8 pull-parser check stays open;dexpace-serde-oj's post-v1 entry verified, not re-filed.CLAUDE.md— the built-phases sentence reads… 6c, 7b, 7c and 7a are built — the whole of phase 6 and the whole of phase 7; the opening paragraph carries 7b's, 7c's then 7a's layer sentences;dexpace-serde-jsonleaves the "phase-0 skeleton" sentence; every count re-derived from the combined tree by the reconcile pass — 219 lib files besideversion.rb, 219sig/mirrors, the same nineteenprivate_constanttest-mirror exceptions (7a adds none), seventeen checklists, seventeen pages, eighteen gates; 7b's four, 7c's four then 7a's five "Constraints that will bite" lines.gates:clean_bundle's BUNDLE_PATH, dated 2026-09-20) and the dated bracketed correction to the false_Codecbullet.docs/product-spec/,docs/sdk-design-ruby/,docs/knowledge/harvested/,docs/deviations.mdand every other phase's documents are untouched.Reconciliation onto 7b's and 7c's
mainThis stack was reviewed on
c53638band rebased onto5e2cb21by a reconcile pass that resolved five prose collisions (CLAUDE.md, README.md, docs/README.md, architecture.md, the roadmap) by keeping every lane's content in merge order and re-deriving every count from the tree; two omissions in 7a's own docs were corrected from the tree while there (README.md's status sentence had not gained 7a; architecture.md's page list stopped at fourteen withoutserde.md); the roadmap gains a dated "Phase 7a reconciled onto main after 7b and 7c" paragraph and 7a's checklist a dated "Reconciled 2026-09-20" note (its own numbers describe its old base). One omission is recorded, not fixed:gems/dexpace-core/README.mdnames the SSE and pagination layers and not the serialization layer (7a's branch never touched that file) — a one-paragraph addition owed to the next docs pass. 8a (#30) is not onmainyet: it converts the same twoseam_surface_test.rbpins (one copy survives its reconcile), edits its ownSteepfiletargets beside this lane's:serde_jsonblock, and its guarded generator-slice test un-guards once it is rebased onto this.Verification
Docs tip (rebased):
bundle exec rake(eighteen gates) green on 4.0.6;ruby .claude/skills/housekeeping/probe.rbexit 0 (all eight checks);ruby scripts/verify_knowledge_structure.rbOK; the housekeeping and knowledge tooling suites green.Known follow-ups
R2-1, R2-2, R2-3 (nits, all docs) — see the code PR's list.