Skip to content

Phase 3a: I/O contracts — documentation and phase record - #49

Merged
Wahbeh-Mohammad merged 8 commits into
mainfrom
11-phase-3a-io-contracts-docs
Sep 15, 2026
Merged

Wahbeh-Mohammad merged 8 commits into
mainfrom
11-phase-3a-io-contracts-docs

Conversation

@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor

Closes #11. Third of three phase 3a PRs — the documentation and the phase record for #47 and #48. Targets #48's branch.

What lands

11 files, +856 / −50, all Markdown.

  • The checklist, docs/work/mvp/phase3/phase3a/2026-09-08-phase3a-io-contracts-checklist.md, written from the build: 42 rows (IO-1–IO-42) — 34 ✅ and 8 🚫 (IO-30–IO-36 and IO-39, the byte-stream provider apparatus retired by design §10.1; IO-30's behavioural clause survives as a property of BufferedSource.of_bytes), no ⏳ and no N/A. Plus what was built, the guards run red with their messages and the Tasks 5–9 ladder, the audit groups run, nineteen deviations from the plan's text (none lowers a gate: #clear_tap dropped, the cop row flipped, cascading invalidation, the NFR-11 allowlist with its fixture and gate test, the private view constructor, the one-fill view contract, the string-eval shadow probe, …), the findings routed (the view-retention cost → 3b's plan Task 13; the plan's RuboCop-baseline finding, which does not reproduce; #read_line_utf8's bound → 7b's plan Task 12; the include Dexpace shadow → docs/first-release.md), and the postponed work re-read (IO-38 on a GVL-free interpreter stays event-gated under the post-release triggers; nothing further postponed).
  • The phase 3a design's ledger gains an "As built, 2026-09-15" addendum: P3-13 (#read / #readpartial leave outbuf BINARY) carried in from the plan, and As-built notes on P3-3, P3-5, P3-7 and P3-8. docs/sdk-design-ruby/ §10 is frozen; its consolidation is a human's, stated in the roadmap note.
  • docs/sdk-documentation/io.md — new as-built page for the byte-streaming layer, every example run verbatim on 4.0.6 and 3.2.11 (the views example was corrected in review round 0 so its last line raises ClosedError as stated); architecture.md, quality-gates.md (the seventh cop's IO and one-segment watch), the core README, README.md, docs/README.md updated; docs/first-release.md's include Dexpace blocker narrowed to its release-decision half now the documentation half is written.
  • CLAUDE.md count sentences re-derived from the tree (fifty-one phase-1/2/3a files beside version.rb, the sig/ and test/ mirrors, seven cops, a 366-line manifest); the roadmap gains the phase 3a status note.

Verification

  • bundle exec rake on Ruby 4.0.6 at this tip: exit 0, all seventeen gates green (783 / 4,669, 99.95%, YARD 0 undocumented).
  • ruby .claude/skills/housekeeping/probe.rb: no drift, all eight checks; verify_knowledge_structure.rb OK; the housekeeping suite (108 runs) and the knowledge suite (92 runs) green.
  • Empty diff under docs/product-spec/, docs/sdk-design-ruby/ and docs/knowledge/.
  • The reviewer re-derived every count the status note and CLAUDE.md state against the checklist and the tree, and ran every io.md example by hand on both interpreters.

Known follow-ups from the final review (not blocking)

  • The design's As-built addendum (…-design.md:1196) still quotes the pre-review shadow probe class_eval("::IO.pipe.first.is_a?(IO)", …); as built the probe evals ->(x) { x.is_a?(IO) } and is handed a pipe end the test owns (checklist deviation 14 already says so).
  • Checklist deviation 19 names async_transport.rbs:17 as the pre-existing over-100-column .rbs line; eight lines from phases 1 and 2 exceed it (async/future.rbs:22, bridge/async_over.rbs:10, http/media_type.rbs:20 and :26, http/request_options/builder.rbs:6, http/request_options.rbs:11 and :14), none in a file 3a touched.

Dexpace::IO shadows ::IO for every file inside `module Dexpace` -- and
core defines it, so the hazard is live from the first require. Inside
that namespace `x.is_a?(IO)` and `IO === x` are silently false for a
real ::IO on 3.2.11, 3.4.10 and 4.0.6 (design P3-7).

SHADOWED gains exactly IO; File, StringIO and Tempfile are deliberately
absent, under the standing rule that a constant joins the list in the
same change that creates its shadow. WATCHED becomes the one-segment
Dexpace, so the rule is "inside module Dexpace, anywhere", over every
gem's lib/ (.rubocop.yml's Include: widens from two core namespaces to
gems/*/lib/**/*.rb). A definition-site guard skips the constant a
`module`/`class` node declares, because `module Dexpace; module IO` is
itself a bare IO inside module Dexpace and the cop would otherwise
reject the file that creates the hazard it exists for.

The cop's own suite gains a second nested class with eight rejected and
ten accepted sources, and phase 2's accepted row -- a bare Thread inside
Dexpace::Transport -- moves to rejected, which the one-segment watch
implies: a tightening, never a narrowing.
Dexpace::StreamError < ::IOError, with the two BODY-13 message helpers
short_transfer and zero_read, and Dexpace::EndOfStreamError < ::EOFError
(IO-4, IO-9, IO-11, IO-12, IO-15, IO-16, IO-17, IO-28; P3-3). The
superclass is load-bearing: IO.copy_stream -- what Net::HTTP#body_stream=
calls -- terminates only on an EOFError subclass from a duck-typed
#readpartial. Dexpace::IOError and Dexpace::EOFError are never defined.

Dexpace::Closeable#closed? reads the latch under the close mutex (IO-38,
P3-6): the first requirement that reads the flag from a second thread,
and design §3.1 declines to rely on the GVL.

Dexpace::IO with MAX_MATERIALIZED_BYTES (IO-9, §10.18) and the RBS
interfaces _Source, _Sink and _Chunked -- the last being design §10.2's
canonical body representation as a type. TypedReads supplies the read
vocabulary over one private #fill hook: #read_into, IO-1's tail-append
primitive, deliberately not #read (P3-1), because #read, #readpartial,
#getbyte and #each keep Ruby's semantics and are IO-16's bridge (P3-2,
P3-13); the typed reads under the one materialisation guard (P3-4); the
hand-written line machine (IO-14); #peek and #slice, views that pin the
parent's cursor and never make it retain (P3-5), with invalidation
cascading through a view's own views on close. TypedWrites supplies the
write vocabulary over #deliver: #write_from, the ::IO-compatible #write,
#write_all with IO-17's unconditional zero-read rule, the charset writes,
#emit and #flush (P3-9). BufferedSource (three factories, one ownership
rule read from IO-6 and design §10.12, never SEAM-3; P3-11, P3-12),
Buffer (IO-7..IO-10, exempt from use-after-close on its own surface and
not for its views, P3-10), BufferedSink and TeeSink (IO-25..IO-29,
without #clear_tap: 3b builds a fresh tee per write). lib/dexpace.rb
requires the nine files in dependency order; every sig/ mirror declares
every method for the strict core Steep target.
…fest

tools/rbs_surface.rb's stdlib allowlist gains EOFError and IOError -- the
superclasses IO-16 and IO-4/IO-17/IO-42 give the two stream failure types
-- and Encoding, which IO-13's charset reads take: core Ruby names phase
0's list never met, the false positive phase 1 fixed for Data,
ArgumentError and StringScanner, with a fixture the gate test pins.

The runtime surface manifest of dexpace-core grows from 318 to 367
lines, exactly the public constants and methods the nine files define;
no private_constant appears, BufferedSource.__dexpace_view does under its
underscore name. The core smoke suite's constant list gains the
streaming layer and pins that Dexpace::IOError and Dexpace::EOFError are
never defined.
Through #peek and #slice, #fill_from_parent asked the parent to buffer
`behind + want` bytes, so a view's #read_into, #readpartial and #each
blocked until the whole requested count had arrived: a peek over a pipe
holding 10 bytes with the writer still open hung on read_into(count:
100) where the root returned the 10 at once. The design's R2 fixes the
primitive at one fill and R4 says the bridge claim holds for views, so
the parent-side entry point is now #dexpace_fill_beyond(behind, want),
which fills until one byte past `behind` is buffered or the upstream is
done, asking for the view's own count each time, and the view takes
what is there. #read(n) on a view still blocks to n through the view's
own #ensure_buffered loop, which is Ruby's contract (review round 0,
R0-1).

BufferedSource.__dexpace_view becomes a private class method with a
private RBS declaration, reached from TypedReads#build_view through
#send as Headers#== reaches #values, so the underscore name leaves the
surface manifest before the first tag locks it (R0-3). The two RBS
declarations over 100 columns are wrapped in the sig's own multi-line
shape (R0-5).
Ten suites mirroring the nine lib/ files plus the one change to
closeable.rb, every test file opening with the IDs it exercises and
nesting one class per behaviour group for the 100-line cap. The three
fakes under test/support/ -- FakeChunked (an #each-shaped body that
yields frozen literals and an empty chunk, and records whether its
ensure ran), FakeSource (#read_into only, scriptable to the zero read no
real stream produces) and FakeSink (#write only, scriptable to a short
count and to raise partway) -- are required by the suites that use them.

The guards run red first: P3-6's fiber-held-mutex assertion against
phase 2's unsynchronised #closed? (ThreadError expected but nothing was
raised, on 3.2.11, 3.4.10 and 4.0.6); the fill(count) amendment's count
guards against the pre-amendment fill(1) (Expected: 4096 Actual: 1); the
Tasks 5-9 ladder against the finished TypedReads suite; the transitive
view-invalidation test against the plan's single-flag protocol; and the
IO-38 cross-thread close, sequenced through Thread::Queues. The
Dexpace::IO shadow is probed with string evals, the consumer case
included, because the block form of module_eval resolves a constant in
the block's own lexical scope on every interpreter in the matrix.

test/gates/rbs_surface_test.rb pins EOFError, IOError and Encoding as
admitted by the NFR-11 scan.
Nine tests. typed_reads_test.rb gains a ViewFills group over the
scripted parent, whose #fills counter is the assertion: #read_into,
#readpartial and #each on a peek fill the parent once and return what
arrived, a slice past the buffered bytes fills until its first byte is
reachable and no further, and #read(n) and #read_exactly on a view
still fill to the count, which is Ruby's contract. buffered_source_test
gains a Views group over a live IO.pipe with the writer left open -- a
watchdog closes it after two seconds so the pre-fix shape fails on
`writer.closed?` instead of hanging -- plus the count a view passes to
the upstream, offset included, and the view constructor's absence from
the public singleton methods (review round 0, R0-1 and R0-3). Run red
against the pre-fix `ensure_buffered(behind + want)`: four ViewFills
failures and the pipe test's closed writer.

io_test.rb's two string-eval shadow probes each created an IO.pipe
inside the eval and never closed it (R0-4); each now evals a lambda and
hands it the pipe end the test owns and closes in its ensure.
…nters

The checklist, docs/work/mvp/phase3/phase3a/2026-09-08-phase3a-io-contracts-checklist.md,
written from the build: forty-two rows, 34 implemented and 8 not built
(the retired provider apparatus, design §10.1), the guards run red with
what each said, the Tasks 5-9 ladder, the audit groups re-checked,
seventeen departures from the plan's text and the findings routed. The
design's ledger gains P3-13 and an "As built" addendum: #clear_tap
dropped, view invalidation cascading, the nil window, the untyped
duck-checked parameters, the NFR-11 allowlist, and the string-eval
shadow probe.

docs/sdk-documentation/io.md is the as-built page, every example run
against the code, opening with the Dexpace::IO shadow a consumer that
includes Dexpace meets; architecture.md, quality-gates.md, the core
README, README.md and docs/README.md point at it, and
docs/first-release.md's include-Dexpace blocker narrows to its
release-decision half. The roadmap gains the phase 3a status note, and
CLAUDE.md's built-phases paragraph, claim sentences and the
constraints-that-bite line on the seventh cop are rewritten from the
tree. The consolidation of P3-1..P3-13 into design §10 is a human's.
The views example on docs/sdk-documentation/io.md claimed the final
slice.read raised Dexpace::ClosedError; as built it returned "", because
the slice had already pulled its whole window and P3-5's "behind"
failure needs a byte still unread (R0-2). The example now takes a
three-byte slice and reads one byte before the parent passes it, and
was re-run on 4.0.6 and 3.2.11; the section also states the one-fill
rule a view now shares with the root.

The checklist gains deviations 18 (a view's fill is one fill past its
position, never a loop to the count) and 19 (the two wrapped .rbs
lines), rewrites deviation 7 for the now-private view constructor,
extends deviation 14 with the lambda probes, adds the round-0 guard-red
table and the routed findings, renames #dexpace_ensure_buffered in the
IO-22 row and deviation 3, cites the new suites from IO-1, IO-16 and
IO-19, and carries the round-1 tip's numbers: 783 runs / 4,669
assertions, 2,222 / 2,223 lines, 261 documented methods, a 366-line
manifest, 77.91% and 78.74% on the code tip. The design's As-built
addendum corrects its P3-8 note and gains the view-fill note; the
roadmap's status note follows.
@Wahbeh-Mohammad Wahbeh-Mohammad added type:docs Documentation only area:core Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-* labels Sep 15, 2026
@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor Author

Review record for the phase 3a stack (#47 → #48 → #49)

Two independent reviews, each by a fresh agent with no memory of the previous one, each re-running the gates itself on every tip and both interpreters; one fix round between them. Every finding got a stable id, a severity, a file:line and the command output that was its evidence. approve required zero blocking and zero should-fix; the run was capped at four reviews and stopped at the second because it approved.

Round Verdict Blocking Should-fix Nits Disposition
0 changes required 0 2 3 all 5 fixed
1 (final) approve 0 0 3 carried into the PR bodies

No finding was ever blocking; nothing was skipped or disputed.

Round 0 → fixed in round 1

  • R0-1 should-fix — through a #peek or #slice view, #read_into, #readpartial and #each blocked until the full requested count was buffered in the parent, where the root returned after one fill: fill_from_parent asked parent.dexpace_ensure_buffered(behind + want), straight from the plan's own Task 10 fence. Reproduced on both interpreters — a pipe holding 10 bytes with the writer open: root.readpartial(100) returned the 10 bytes in 0.00 s, root.peek.readpartial(100) hung until a 2 s watchdog closed the writer. Design R2 says the primitive fills once and the rename "keeps the bridge claim true of views"; 3b's per-attempt logging drain and 7b's SSE preview would both have hung on a peer that sends fewer than count bytes and waits. Fixed on Phase 3a: I/O contracts — the byte-streaming layer #47: TypedReads#dexpace_fill_beyond(behind, want) fills only while nothing is buffered past the view's position; #read(n) and #read_exactly still fill to n. Nine tests on Phase 3a: I/O contracts — tests and fakes #48, including a live-pipe case; reverting the fix turns four ViewFills tests and the pipe test red on 3.2.11.
  • R0-2 should-fix — io.md's views example claimed slice.read raises Dexpace::ClosedError after the parent's #read; run verbatim it returned "", because the slice had already pulled its whole 2-byte window. Example reshaped on Phase 3a: I/O contracts — documentation and phase record #49 (slice(offset: 1, count: 3), one byte read, the parent reads past it) so the last line raises ClosedError "window starts 2 bytes behind" on both interpreters.
  • R0-3 BufferedSource.__dexpace_view was NFR-4-locked public surface (manifest row, RBS, YARD) against the design's "internal" — now private_class_method with a private def self. declaration, reached via send; the manifest lost the row (367 → 366). R0-4 the two string-eval shadow probes each leaked an IO.pipe pair — they now eval ->(x) { x.is_a?(IO) } and are handed a pipe end the test closes. R0-5 two new .rbs lines at 127 and 105 columns — wrapped.

Round 1 (final) — open, carried into the PR bodies

  • R1-1 the design's As-built addendum (…-design.md:1196) still quotes the pre-R0-4 probe string; checklist deviation 14 has the lambda shape, the design bullet does not. On Phase 3a: I/O contracts — documentation and phase record #49.
  • R1-2 checklist deviation 19 names async_transport.rbs:17 as the pre-existing over-100-column .rbs line; eight from phases 1–2 exceed it (async/future.rbs:22, bridge/async_over.rbs:10, http/media_type.rbs:20/:26, http/request_options/builder.rbs:6, http/request_options.rbs:11/:14), none in a file 3a touched. Round 0's own "no existing .rbs exceeds it" was wrong too. On Phase 3a: I/O contracts — documentation and phase record #49.
  • R1-3 tee_sink_test.rb:44 asserts BINARY over an ASCII-only fixture, which passes under exactly the append-retag bug; the property is proven with non-ASCII bytes by two other tests. On Phase 3a: I/O contracts — tests and fakes #48.

What the final reviewer verified, in its own runs

  1. Gates — Phase 3a: I/O contracts — the byte-streaming layer #47 tip 2e18bb5: all seventeen individually on 4.0.6, sixteen green, test:gems red on the coverage floor alone (77.91%, 530 runs, 0 failures, 0 errors); the matrix set on 3.2.11, same shape (78.78%). Phase 3a: I/O contracts — tests and fakes #48 tip d053fd4: full bundle exec rake green on 4.0.6 (783 runs / 4,669 assertions, 2,222 / 2,223 lines, 99.95%; cops:test 100 runs; test:gates 129; YARD 261 methods, 0 undocumented); the matrix set green on 3.2.11, 3.3.12 and 3.4.10 (783 runs each, 99.95%). Phase 3a: I/O contracts — documentation and phase record #49 tip 0d8ab49: full rake green, probe exit 0, knowledge verifier OK. The gem suite on seeds 1, 99991 and 424242 on 4.0.6 and 31337 on 3.2.11 — 766 runs, 0 failures, identical run count on every seed. RuboCop with --ignore-parent-exclusion (the nested-worktree vacuity is phase 10 inbound): 200 files, no offenses.
  2. Every prior finding individually — R0-1 re-probed by experiment on both interpreters (root, peek and slice over a live pipe, nine calls, all 0.00 s), then its fix reverted and the new tests run red; R0-2's example run verbatim on both; R0-3 confirmed by singleton_methods/private_methods and a NoMethodError on the direct call; R0-4 by grep and by the tests; R0-5 by a column scan of the nine new .rbs files.
  3. Concurrency discipline — the only two synchronize blocks in the whole layer are Closeable's flag flip and flag read; no lock is held across a fill, a read, a drain, a #release or a #close. The seven thread- and fiber-bearing tests (two-fiber interleave, IO-38 view invalidation, IO-38 blocked read, mutex-not-held-across-#release, two-thread #close, the live-pipe view, the fiber-held-mutex guard) each run ten times under timeout 120: 70/70, no hang, no flake. No Timeout.timeout, Thread#raise/#kill, timeout/deadline/cancellation parameter (IO-40), or resource acquisition inside an Enumerator block — the only ensures in lib/dexpace/io/ are the two .wrapping block forms and the two staging resets.
  4. Seven guards reverted and run red, on 3.2.11 and 4.0.6 where the point is interpreter-sensitive, each with the predicted message: Closeable#closed? read outside the mutex (ThreadError expected but nothing was raised, both interpreters); EndOfStreamError < ::StandardError (IO.copy_stream propagates instead of terminating, 3 errors); the ingress chunk.b.freeze → chunk.dup.freeze (three Over tests expected BINARY, got UTF-8); #read_into's << → replace ("seedabc" vs "abc", and the two-assertion overwrite-versus-append test); the IO-9 guard deleted from #read_exactly (EndOfStreamError where StreamError was expected); the IO-42 close check moved below the zero-count return (ClosedError expected but nothing was raised); the one-fill view contract (above).
  5. The layer by experiment, identical on 4.0.6 and 3.2.11: the cop rejecting a throwaway file's bare is_a?(IO), IO ===, IO.pipe and Mutex.new under module Dexpace (four offenses) while accepting ::IO, File and io.rb's own module IO definition site, and reporting 0 offenses over all 63 gem lib/ files; IO.copy_stream over BufferedSource.wrapping(pipe read end) with a 130,000-byte payload carrying é/ö/\xFF\xFE from a producer thread — bytes intact, BINARY, the pipe closed? only after #close; a StringIO-backed source drained and read through #read(3, utf8_outbuf), #read_into and #readpartial — BINARY on both, identical sha (the StringIO#read(n, buf) encoding change straddles the floor, P3-13); the 64 MiB + 1 Buffer whose #snapshot refuses, run unconditionally on all four rows; an over-ceiling slice constructing and refusing only on the read (IO-21); #read_line_utf8 unbounded by design and routed to 7b's plan Task 12 (Dexpace::SSE::MAX_LINE_BYTES).
  6. Encoding fixtures classified — 21 encoding-asserting tests under test/dexpace/io/: 15 carry non-ASCII bytes, two are empty-string cases, three assert a stored chunk's tag (red under the bug regardless), one is R1-3.
  7. Layering — main ⊂ 2e18bb5 ⊂ d053fd4 ⊂ 0d8ab49; eight commits, 0 merges, subjects 41–74 chars, no attribution lines; every file in each diff classified (27 code, 14 tests, 11 docs — every gate-read file on Phase 3a: I/O contracts — the byte-streaming layer #47, nothing but suites and doubles on Phase 3a: I/O contracts — tests and fakes #48, nothing but .md on Phase 3a: I/O contracts — documentation and phase record #49); empty diff under docs/product-spec/, docs/sdk-design-ruby/ and docs/knowledge/; no Gemfile.lock/.bundle/coverage/tmp in any commit.
  8. Global constraints — frozen_string_literal + SPDX on all 21 new .rb; no plain require added (only require_relative); zero add_dependency; no is_a?(IO), no bare shadowed constant, force_encoding only on egress copies; keywords everywhere but the four host-native bridge methods (P3-9); the four rubocop:disable directives are ClassLength/ModuleLength/SymbolProc/NumericPredicate with reasons and phase-2 precedent; sig/ mirrors lib/ 9-for-9, test/ 9 mirrors + 3 fakes; the manifest's 48 added rows are exactly the plan's Task 15 list plus the six singleton factories its abbreviated list omitted, minus #clear_tap and __dexpace_view; public signatures name only String, Integer, Encoding, Range, Enumerator, Float and Dexpace:: constants plus the two superclasses (NFR-11).
  9. Plan and spec coverage — the 15 tasks' files and outputs; each Scope bullet of Phase 3a: I/O Contracts #11 mapped; all 42 IO IDs have a row (34 ✅ / 8 🚫, every 🚫 citing §10.1, IO-30's independent-copy clause kept), and the cited test read for IO-1/2/4/6/9/14/16/17/19/22/24/26/27/30/31/37/38/40/42 — proven except IO-38, partially proven exactly as its row says (the mechanism guard is the only assertion that distinguishes the synchronised reader from phase 2's; the rest passes with or without the lock under the GVL; the GVL-free run stays event-gated in docs/first-release.md). The nineteen recorded deviations checked against the code; the findings routed to named owners (3b Task 13, 7b Task 12, phase 0 Task 3 gaining nothing); no register section anywhere.
  10. Phase record — every io.md code block (34 assertions) and the core README example run verbatim on both interpreters; CLAUDE.md counts re-derived (51 lib/ files beside version.rb, 51 sig/ mirrors, every lib/ file but hooks.rb mirrored in test/, 11 phase directories, 40 harvested topics, 366-line manifest); the roadmap note's 783 / 99.95% / 77.91% / 51 / 366 / nineteen match the measurements; every test file and group the checklist cites exists at the Phase 3a: I/O contracts — tests and fakes #48 tip.
  11. Report discrepancies — two coverage figures the fix report stated differed from the reviewer's own run by one race-only line (78.74% vs 78.78% on 3.2.11); round 0's R0-5 evidence was wrong about main's .rbs lines (R1-2). Both noted, neither material.

Run: workflow wf_41158730-0b8, 4 agents (1 implementer, 2 reviewers, 1 fixer), 1.9M tokens, 3.0 h.

@Wahbeh-Mohammad
Wahbeh-Mohammad changed the base branch from 11-phase-3a-io-contracts-tests to main September 15, 2026 20:42
@Wahbeh-Mohammad
Wahbeh-Mohammad merged commit 462a123 into main Sep 15, 2026
5 checks passed
@Wahbeh-Mohammad
Wahbeh-Mohammad deleted the 11-phase-3a-io-contracts-docs branch September 15, 2026 20:43
@Wahbeh-Mohammad Wahbeh-Mohammad mentioned this pull request Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-* type:docs Documentation only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 3a: I/O Contracts

1 participant