diff --git a/.claude/skills/housekeeping/SKILL.md b/.claude/skills/housekeeping/SKILL.md index 9216fd7..820c5a5 100644 --- a/.claude/skills/housekeeping/SKILL.md +++ b/.claude/skills/housekeeping/SKILL.md @@ -41,7 +41,7 @@ ruby .claude/skills/housekeeping/probe.rb --warn-only Findings are grouped by check and printed as `path:line: [severity] message`, followed by a summary line. `act` is drift to fix; `note` is a judgement call. -Eight checks. Each is a small class with a `name` and a `run(repo)`, and each derives the +Nine checks. Each is a small class with a `name` and a `run(repo)`, and each derives the repository fact **once, from the repository**, then compares every document that states it against that one derivation — never one document against another. @@ -54,6 +54,7 @@ against that one derivation — never one document against another. | `links` | Broken relative links in `docs/**/*.md`, `README.md`, `CLAUDE.md` and `gems/*/README.md`. `http(s)://`, `mailto:` and anchor-only targets are skipped. Also a **backticked** chapter of a normative tree — `docs/product-spec/NN-….md`, `docs/sdk-design-ruby/NN-….md` — that does not resolve: this repository writes chapter references in backticks, not link syntax, so nothing else reads them. Scoped to those two complete trees, so a `gems/…` path a later phase creates is not reported; a wildcard or an elided path is not a claim about one file and is skipped | | `registers` | An aggregate `## Open Findings` / `## Deferred Items` / `## Open Items` section living inside a spec, design or plan document. No find-list register exists any more: a finding belongs with its owner — the numbered plan task whose scope it falls in, phase 10's inbound list in the roadmap, or a `docs/first-release.md` entry | | `citations` | Two namespaces, and both prefixes are now **retired**: every `DEF-` and every `OI-` citation, anywhere. `docs/deferred-items.md` and `docs/open-items.md` were both retired on 2026-09-13 with every row resolved — a finding is no longer registered, it is routed to its owner when it is found: a numbered plan task, phase 10's inbound list, a `docs/first-release.md` entry, or simply the fix. A retired prefix never no-ops, because a missing file is exactly what would hide the leftovers; if either old file itself lingers, that is one finding, not one per row. The **live** half of the check stays — a register resolves only against its own file, and no-ops when that file does not exist — because a future register is one entry in `REGISTERS`. A **backticked** ID counts, and so does one on a 4-space-indented continuation line — this repository's ID convention backticks every ID, and a 4-space indent here is list-item continuation, not code. Only a ``` fence is an example, and only a fence is blanked | +| `chapters` | A requirement ID a document attributes to a `docs/product-spec/NN-….md` chapter that does not carry it (phase 10's Task 7). The unit is a clause: a line is split at `;`, each ID pairs with the nearest preceding chapter reference in its clause, a range is expanded whether or not its endpoints are backticked, and a two-line window carrying a negation ("appears nowhere", "is not in", …) is skipped. Appendix C is exempt as a target, and phase 10's own documents, which quote the pre-correction lines on purpose, are exempt as sources. Two blind spots are stated in `Chapters::GAPS`: a chapter reference on the preceding line, and a chapter named by an ellipsis or a variable | | `guard` | The frozen list and the writable surface overlapping, a path the guard has quietly stopped refusing, or a frozen entry that has become a symlink — the three ways the apply stage could eat a normative document | The `claims` check is a **declarative table**: file, a pattern whose first capture is the @@ -187,12 +188,14 @@ checks rot. No case count is written here on purpose; `test/run.rb` reports it. .claude/skills/housekeeping/ SKILL.md this file guard.rb the frozen-path guard; every write site goes through it - probe.rb stage 1 — eight read-only checks + probe.rb stage 1 — nine read-only checks + chapters.rb the ninth check, `chapters`, in its own file apply.rb stage 2 — git mv only, guarded, dry by default test/ fixture.rb builds the throwaway repositories the tests probe guard_test.rb prefix, traversal, absolute, symlink, and the pinned list probe_test.rb every check has a fixture it fires on, plus the CLI + chapters_test.rb the clause rule, the ranges, the negations, the two regression fixtures apply_test.rb the target mapping, every batch refusal, and the CLI run.rb the whole suite ``` diff --git a/.claude/skills/housekeeping/chapters.rb b/.claude/skills/housekeeping/chapters.rb new file mode 100644 index 0000000..1709d18 --- /dev/null +++ b/.claude/skills/housekeeping/chapters.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +# SPDX-License-Identifier: MIT +# .claude/skills/housekeeping/chapters.rb +# +# The probe's ninth check, `chapters` (phase 10's plan, Task 7; design R10 and addendum A10). +# +# A document asserting that a requirement ID is stated in a named `docs/product-spec/` chapter, +# where that chapter does not carry the ID. The unit is a CLAUSE, not a line: "A.md for X, Y; +# B.md for Z" pairs every ID with every chapter under a same-line rule, which is why a naive rule +# fires 23 times for 4 real defects (phase 10's Fact 6). So a line is split at `;`, each ID is +# associated with the NEAREST PRECEDING chapter reference in its clause -- or, for an ID run +# followed by "appears in", with the chapter that phrase introduces -- a range is expanded +# whether or not its endpoints are backticked, and a line whose two-line window carries a negation +# -- prose whose own subject is that the ID is NOT in that chapter -- is skipped. Appendix C +# carries every ID and can never be wrong about one, so it is exempt as a target. +# +# Two stated blind spots, printed in the check's own `GAPS` so a reader is never told the check +# saw something it did not: `continued_clause` (a chapter reference on the PRECEDING line is +# invisible to a line-oriented scanner -- 8c's design :70, `SEAM-15`, was caught by hand), and +# `dynamic_chapter_path` (a chapter named by an ellipsis such as `13-…md`, or by a variable, +# resolves to no file and is skipped). + +module Housekeeping + module Checks + # 9. A requirement ID attributed to a spec chapter that does not carry it. + class Chapters < Check + NAME = 'chapters' + PREFIXES = %w[SEAM HTTP IO BODY CTX PIPE RECOV RETRY REDIR AUTH PAGE SSE SERDE OBS CFG + TRANSPORT ASYNC XCUT NFR].freeze + ID = /\b(#{PREFIXES.join('|')})-(\d+)\b/ + # `SEAM-11`–`SEAM-15`, SEAM-11–SEAM-15, SEAM-11–15: backticks tolerated on either end, + # which is what hid 8a's `SEAM-13` from a range pattern written for bare text. + RANGE = /`?\b(#{PREFIXES.join('|')})-(\d+)`?\s*[–—]\s*`?(?:\1-)?(\d+)\b`?/ + CHAPTER = %r{docs/product-spec/(\d\d-[a-z0-9-]+\.md)} + BOUNDARY = ';' + # Prose whose subject is that the ID is NOT in the named chapter, over the line and the + # next one, because such a sentence routinely crosses a line break. + # A union of plain phrases, never an /x pattern: under /x the spaces inside a phrase are + # stripped and "appears nowhere" silently becomes "appearsnowhere" (found writing this). + NEGATION = Regexp.union( + 'appear nowhere', 'appears nowhere', 'harvested nowhere', 'appear in no', 'appears in no', + 'appendix C is their only', 'appendix C is its only', 'appendix C alone', + 'does not carry', 'do not carry', 'carries neither', 'carries none', 'carries no ', + 'carry none', 'no prose chapter', 'appendix-C row', 'unfollowable', 'from appendix C', + 'read out of appendix C', 'read out of **appendix C', 'neither ID appears', 'is not in', + 'are not in', 'not stated in', 'only normative statement', 'only prose home', + 'not appear in', 'does not appear', 'do not appear', 'never appear', "n't appear" + ).then { |union| Regexp.new(union.source, Regexp::IGNORECASE) } + # "X appears in ": the one verb that puts the chapter AFTER the IDs it is about. + # An ID run followed by it binds FORWARD, to the chapter the verb introduces, instead of to + # the nearest preceding one. Phase 10's review round 0 (R0-8) found the phrase listed as a + # negation, which silenced this -- the usual positive attribution -- to hide one line of the + # phase-5 segmentation design (:538-539) whose second run bound backward to the first + # run's chapter; binding forward is what that line needed. + FORWARD = /\bappears?\s+in\b/i + EXEMPT_TARGET = 'appendix-c-consolidated-normative-requirement-index.md' + # Documents that quote the pre-correction attributions ON PURPOSE, as the record of the + # defect this check exists for: phase 10's own design, plan and checklist. + EXEMPT_DOCUMENTS = %r{\Adocs/work/mvp/phase10/} + SCANNED = ['docs'].freeze + SKIPPED = %r{\Adocs/(?:product-spec|knowledge/harvested)/} + GAPS = %i[continued_clause dynamic_chapter_path].freeze + + def run(repo) + carried = chapter_ids(repo) + documents(repo).flat_map { |path| scan(path, repo.read(path).lines, carried) } + end + + # The pairs a single line asserts, [chapter, id], in clause order. Public so a test can + # read the clause scoping directly. + def self.pairs(line) + line.split(BOUNDARY).flat_map { |clause| clause_pairs(clause) } + end + + # A run of consecutive IDs binds to the nearest preceding chapter, unless the run is + # followed by FORWARD, when it binds to the chapter immediately after the verb (or to + # nothing, when the chapter is on the next line: the continued-clause gap). + def self.clause_pairs(clause) + list = tokens(clause) + chapter = nil + pairs = [] + index = 0 + while index < list.length + kind, value = list[index] + if kind == :chapter + chapter = value + index += 1 + elsif kind == :forward + index += 1 + else + run_end = index + run_end += 1 while run_end < list.length && list[run_end][0] == :id + target = run_target(list, run_end, chapter) + list[index...run_end].each { |_kind, id| pairs << [target, id] } if target + index = run_end + end + end + pairs + end + + def self.run_target(list, after, chapter) + return chapter unless list.dig(after, 0) == :forward + + list.dig(after + 1, 0) == :chapter ? list.dig(after + 1, 1) : nil + end + + # Chapters and IDs in textual order, every range expanded in place. + def self.tokens(clause) + found = [] + clause.scan(CHAPTER) { found << [Regexp.last_match.begin(0), :chapter, Regexp.last_match[1]] } + clause.scan(FORWARD) { found << [Regexp.last_match.begin(0), :forward, nil] } + covered = [] + clause.scan(RANGE) do + match = Regexp.last_match + covered << (match.begin(0)...match.end(0)) + (match[2].to_i..match[3].to_i).each { |n| found << [match.begin(0), :id, "#{match[1]}-#{n}"] } + end + clause.scan(ID) do + match = Regexp.last_match + next if covered.any? { |span| span.cover?(match.begin(0)) } + + found << [match.begin(0), :id, "#{match[1]}-#{match[2]}"] + end + found.sort_by.with_index { |(position, _kind, _value), index| [position, index] } + .map { |_position, kind, value| [kind, value] } + end + + private + + def chapter_ids(repo) + repo.tracked('docs/product-spec/*.md').to_h do |path| + [File.basename(path), repo.read(path).scan(ID).map { |prefix, n| "#{prefix}-#{n}" }.uniq] + end + end + + def documents(repo) + repo.tracked(*SCANNED).select { |path| path.end_with?('.md') } + .reject { |path| SKIPPED.match?(path) || EXEMPT_DOCUMENTS.match?(path) } + end + + def scan(path, lines, carried) + lines.each_with_index.flat_map do |line, index| + next [] if NEGATION.match?(line + lines.fetch(index + 1, '')) + + self.class.pairs(line).filter_map do |chapter, id| + next if chapter == EXEMPT_TARGET || !carried.key?(chapter) || carried[chapter].include?(id) + + act(path, index + 1, "attributes #{id} to docs/product-spec/#{chapter}, which does not " \ + "carry it (gaps: #{GAPS.join(', ')})") + end + end + end + end + end +end diff --git a/.claude/skills/housekeeping/probe.rb b/.claude/skills/housekeeping/probe.rb index a4ea042..e6a9e98 100644 --- a/.claude/skills/housekeeping/probe.rb +++ b/.claude/skills/housekeeping/probe.rb @@ -202,7 +202,8 @@ def strip_inline_code(text) end end - # The eight read-only checks. Each is a class with `name` and `run(repo) -> [Finding]`. + # The nine read-only checks. Each is a class with `name` and `run(repo) -> [Finding]`. The + # ninth, `chapters`, lives in chapters.rb (phase 10): one concern per file, as guard.rb. module Checks # Base class: supplies `name` from the subclass's `NAME` and a `Finding` factory. class Check @@ -732,11 +733,14 @@ def symlinks(repo) end end + require_relative 'chapters' + # Runs the checks and collects their findings. class Probe ALL = [ Checks::Inbox, Checks::Root, Checks::Claims, Checks::Readmes, - Checks::Links, Checks::Registers, Checks::Citations, Checks::GuardCheck + Checks::Links, Checks::Registers, Checks::Citations, Checks::GuardCheck, + Checks::Chapters ].freeze NAMES = ALL.map { |check| check::NAME }.freeze diff --git a/.claude/skills/housekeeping/test/chapters_test.rb b/.claude/skills/housekeeping/test/chapters_test.rb new file mode 100644 index 0000000..111c4f7 --- /dev/null +++ b/.claude/skills/housekeeping/test/chapters_test.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +# SPDX-License-Identifier: MIT +# .claude/skills/housekeeping/test/chapters_test.rb +# +# The probe's ninth check, `chapters` (phase 10's plan, Task 7). Measured over docs/ on +# 2026-09-25: the clause-scoped form fires on exactly the true positives, and the two blind spots +# are asserted here so the check never claims to have seen what it did not. The two +# phase-8 governing-documents lines it was designed against were corrected on 2026-09-13, so they +# live on as committed regression fixtures -- a check whose only evidence is a live defect loses +# its evidence the moment the defect is fixed. + +require 'minitest/autorun' + +require_relative '../probe' +require_relative 'fixture' + +class ChaptersTest < Minitest::Test + Fixture = Housekeeping::Fixture + Chapters = Housekeeping::Checks::Chapters + + FIXTURES = File.expand_path('fixtures/chapters', __dir__) + # Chapter 03 as the spec carries it, trimmed to the SEAM IDs that matter: 13, 15 and 22 are not + # in it (02 carries SEAM-13; appendix C alone carries 15 and 22). + CHAPTER_03 = "# 3\n\nSEAM-11 SEAM-12 SEAM-14 SEAM-16 SEAM-17 SEAM-24 SEAM-25 SEAM-29 SEAM-30\n" + CHAPTER_02 = "# 2\n\nSEAM-13 SEAM-29\n" + + def run_over(document) + overrides = { + 'docs/product-spec/03-pluggable-seams-and-extension-model.md' => CHAPTER_03, + 'docs/product-spec/02-architectural-principles.md' => CHAPTER_02, + 'docs/work/mvp/x-design.md' => document + } + Fixture.with(overrides: overrides) do |root| + Housekeeping::Probe.new(Housekeeping::Repo.new(root), only: ['chapters']).run + end + end + + def ids(findings) = findings.map { |finding| finding.message[/SEAM-\d+/] }.sort + + def test_a_governing_documents_list_naming_an_appendix_c_only_id_is_a_finding + findings = run_over("`docs/product-spec/03-pluggable-seams-and-extension-model.md` for `SEAM-11`, `SEAM-15`.\n") + + assert_equal %w[SEAM-15], ids(findings) + end + + def test_prose_whose_subject_is_the_absence_is_not_a_finding + assert_empty run_over( + "`docs/product-spec/03-pluggable-seams-and-extension-model.md` carries 22 of the 30 IDs; " \ + "`SEAM-15` and `SEAM-22` appear nowhere in the specification's prose.\n" + ) + end + + def test_a_clause_boundary_reassigns_the_chapter + findings = run_over("`docs/product-spec/03-pluggable-seams-and-extension-model.md` for `SEAM-11`; " \ + "`docs/product-spec/02-architectural-principles.md` for `SEAM-13`.\n") + + assert_empty findings + end + + # 8a's design :67, verbatim: the backticked range is what hid SEAM-13 from a pattern written for + # bare text, and with the range read all three wrong IDs are found. + def test_the_phase8a_pre_correction_line_fires_on_all_three_wrong_ids + findings = run_over(File.read(File.join(FIXTURES, 'phase8a_design_67_pre_correction.md'))) + + assert_equal %w[SEAM-13 SEAM-15 SEAM-22], ids(findings) + end + + # 8c's design :69-70, verbatim: SEAM-13 is on the chapter's own line and is found; SEAM-15 is + # too. A clause continued onto the NEXT line would be the stated blind spot -- see below. + def test_the_phase8c_pre_correction_line_fires_on_both_wrong_ids + findings = run_over(File.read(File.join(FIXTURES, 'phase8c_design_69_pre_correction.md'))) + + assert_equal %w[SEAM-13 SEAM-15], ids(findings) + end + + def test_a_clause_continued_onto_the_next_line_is_a_stated_blind_spot + findings = run_over("- `docs/product-spec/03-pluggable-seams-and-extension-model.md` for\n `SEAM-15`.\n") + + assert_empty findings + assert_includes Chapters::GAPS, :continued_clause + end + + def test_the_range_vocabulary_tolerates_backticks_and_a_bare_upper_bound + assert_equal [%w[03-x.md SEAM-11], %w[03-x.md SEAM-12], %w[03-x.md SEAM-13]], + Chapters.pairs('docs/product-spec/03-x.md for `SEAM-11`–`SEAM-13`').first(3) + assert_equal 3, Chapters.pairs('docs/product-spec/03-x.md for SEAM-11–13').size + end + + # Phase 10's review round 0 (R0-8): "appears in" was listed as a negation, which silenced the + # usual positive attribution. It is a FORWARD binding now: the IDs before it belong to the + # chapter after it, so a wrong one fires and a right one does not. + def test_an_appears_in_attribution_binds_forward_and_fires_when_wrong + wrong = "`SEAM-15` appears in `docs/product-spec/03-pluggable-seams-and-extension-model.md`.\n" + right = "`SEAM-11` appears in `docs/product-spec/03-pluggable-seams-and-extension-model.md`.\n" + + assert_equal %w[SEAM-15], ids(run_over(wrong)) + assert_empty run_over(right) + end + + # The phase-5 segmentation design's :538-539 shape, the line the negation entry was hiding: a + # second run followed by "appears in" and a chapter on the NEXT line must not bind backward to + # the first run's chapter (40 false fires under the backward rule). + def test_a_second_appears_in_run_does_not_bind_backward + document = "every one of `SEAM-11`–`SEAM-12` appears in\n" \ + "`docs/product-spec/03-pluggable-seams-and-extension-model.md` and every one of `SEAM-13` appears in\n" \ + "`docs/product-spec/02-architectural-principles.md`.\n" + + assert_empty run_over(document) + assert_equal [%w[03-x.md SEAM-11]], Chapters.pairs('`SEAM-11` appears in docs/product-spec/03-x.md') + end + + # Phase 10's review round 1 (R1-4): the forward binding matched "appear in" inside a negative + # sentence, so a correct statement that an ID is ABSENT from a chapter fired as a wrong + # attribution. Each spelling of the negated verb is a negation, not a binding. Round 2 (R2-2) + # added the two it still bound forward: 'never appears in' and "doesn't appear in". + def test_a_negated_appears_in_is_not_an_attribution + ['does not appear in', 'do not appear in', 'did not appear in', 'never appears in', + "doesn't appear in", "don't appear in", 'appears nowhere in'].each do |verb| + document = "`SEAM-15` #{verb} `docs/product-spec/03-pluggable-seams-and-extension-model.md`.\n" + + assert_empty run_over(document), verb + end + end + + def test_appendix_c_is_never_a_target + document = "`docs/product-spec/appendix-c-consolidated-normative-requirement-index.md` for `SEAM-99`.\n" + + assert_empty run_over(document) + end +end diff --git a/.claude/skills/housekeeping/test/fixtures/chapters/phase8a_design_67_pre_correction.md b/.claude/skills/housekeeping/test/fixtures/chapters/phase8a_design_67_pre_correction.md new file mode 100644 index 0000000..804754d --- /dev/null +++ b/.claude/skills/housekeeping/test/fixtures/chapters/phase8a_design_67_pre_correction.md @@ -0,0 +1,3 @@ +- `docs/product-spec/03-pluggable-seams-and-extension-model.md` for `SEAM-11`–`SEAM-15`, `SEAM-22`, `SEAM-29` + -- the governing-documents line phase 8a's design carried at :67 before phase 10's planning + corrected it on 2026-09-13, kept verbatim as a regression fixture. diff --git a/.claude/skills/housekeeping/test/fixtures/chapters/phase8c_design_69_pre_correction.md b/.claude/skills/housekeeping/test/fixtures/chapters/phase8c_design_69_pre_correction.md new file mode 100644 index 0000000..6ef1092 --- /dev/null +++ b/.claude/skills/housekeeping/test/fixtures/chapters/phase8c_design_69_pre_correction.md @@ -0,0 +1,2 @@ +- `docs/product-spec/03-pluggable-seams-and-extension-model.md` — `SEAM-11`, `SEAM-13`, `SEAM-14`, `SEAM-15`, `SEAM-16`, + `SEAM-17`, `SEAM-24`, `SEAM-25`, `SEAM-30` diff --git a/.claude/skills/housekeeping/test/probe_test.rb b/.claude/skills/housekeeping/test/probe_test.rb index 4b5f93c..86f366e 100644 --- a/.claude/skills/housekeeping/test/probe_test.rb +++ b/.claude/skills/housekeeping/test/probe_test.rb @@ -50,8 +50,8 @@ def test_a_clean_fixture_reports_nothing_on_every_check assert_empty on_fixture end - def test_check_names_are_the_eight_the_documentation_states - assert_equal %w[inbox root claims readmes links registers citations guard], Probe::NAMES + def test_check_names_are_the_nine_the_documentation_states + assert_equal %w[inbox root claims readmes links registers citations guard chapters], Probe::NAMES end # --- numerals -------------------------------------------------------------------------- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 799f440..2281db2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,9 @@ jobs: - run: bundle exec rake gates:cause_walk - run: bundle exec rake gates:bounded_map - run: bundle exec rake gates:seam_names + - run: bundle exec rake gates:ledger_audit + - run: bundle exec rake gates:spdx_rbs + - run: bundle exec rake gates:sole_parse - run: bundle exec rake gates:rbs_surface - run: bundle exec rake gates:sig_diff - run: bundle exec rake gates:surface_snapshot diff --git a/CLAUDE.md b/CLAUDE.md index af69b34..d0a64f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,12 +13,14 @@ three-state PATCH — solved exactly once, and it deliberately does not compete Work here is **spec-driven, not feature-driven**. `docs/product-spec/` is normative: 645 numbered requirements across 19 prefixes. Before implementing anything, find the requirement IDs it must satisfy. -**Phases 0, 1, 2, 3a, 3b, 4a, 4b, 4c, 5a, 5b, 5c, 6a, 6b, 6c, 7b, 7c, 7a, 8a, 8b, 8c and 9 are built — the whole of +**Phases 0, 1, 2, 3a, 3b, 4a, 4b, 4c, 5a, 5b, 5c, 6a, 6b, 6c, 7b, 7c, 7a, 8a, 8b, 8c, 9 and 10 are built — the whole of phase 6, the whole of phase 7, whose three sub-phases were built concurrently off one base and landed in that order, 7a last (umbrella #25 closes by hand), and the whole of phase 8, whose three sub-phases were built 8a first, off the same base as phase 7's and reconciled onto the tree that holds all three, then 8b and 8c concurrently off the tree that holds 8a, 8c landing second, and phase 9, the audit phase, which ships no -`lib/` code outside `dexpace-conformance` and repairs nothing it finds; the domain model, the seam layer, the +`lib/` code outside `dexpace-conformance` and repairs nothing it finds, and phase 10, the reconciliation +phase, which repairs what phase 9 and every earlier review found, flips `docs/deviations.md` and adds no new +layer; the domain model, the seam layer, the byte-streaming layer, the body layer, the execution context, the recovery layer, the stage pipeline, the configuration layer, the tracing and metrics layer, the logging facade with its redaction, the retry layer, the authentication layer, the redirect layer, the server-sent-events layer, the pagination layer, the @@ -255,11 +257,13 @@ the synchronous transport is the first thing here that talks to a socket, and th on the async path — the two meet in `dexpace-async-thread`'s composed suite over a real socket. Both transports talk to a socket; the async one needs a running reactor on the calling thread and creates none. The workspace root carries the `Gemfile`, `Rakefile`, `Steepfile`, `rbs_collection.yaml`, `.rubocop.yml`, `.yardopts`, `VERSIONS` and -the twenty-one blocking gates — phase 0's seventeen +the twenty-four blocking gates — phase 0's seventeen (`docs/work/mvp/phase0/2026-09-05-phase0-scaffold-and-quality-gates-checklist.md`), phase 7b's -`gates:serde_boundary` and phase 9's three repository-wide invariant scans, `gates:cause_walk`, +`gates:serde_boundary`, phase 9's three repository-wide invariant scans, `gates:cause_walk`, `gates:bounded_map` and `gates:seam_names`, over one `RubyVM::AbstractSyntaxTree` walker in -`tools/ast_scan.rb`. +`tools/ast_scan.rb`, and phase 10's three, `gates:ledger_audit` (`docs/deviations.md` against design §10), +`gates:spdx_rbs` (the SPDX header on every shipped `.rbs`) and `gates:sole_parse` (`AstScan.parse` is the +one `parse_file`). Beyond that, what exists is the specification, the port design, the process tooling and the register, `docs/deviations.md`. Ruby **>= 3.2** is the floor (`required_ruby_version` in every gemspec but one, asserted by `gates:versions`; `dexpace-transport-async_http` alone declares **>= 3.3**, read from `VERSIONS`' per-gem @@ -302,8 +306,8 @@ makes an older Bundler try to install Bundler 4. ```bash bundle install -bundle exec rake # the default task: all twenty-one gates, in order (NFR-17) -bundle exec rake gates:list # the twenty-one names, in the order CI and `rake` both use +bundle exec rake # the default task: all twenty-four gates, in order (NFR-17) +bundle exec rake gates:list # the twenty-four names, in the order CI and `rake` both use bundle exec rake rubocop # NFR-7, findings fatal, no autocorrection bundle exec rake rubocop:fix # safe autocorrections only, never the gate bundle exec rake cops:test # the custom cops' own suite (.rubocop/test/) @@ -316,6 +320,9 @@ bundle exec rake gates:serde_boundary # SSE-37, spec-forced boundary bundle exec rake gates:cause_walk # XCUT-9: Dexpace.each_cause is the only walk of a cause chain bundle exec rake gates:bounded_map # XCUT-14: only Dexpace::BoundedMap holds a caller-keyed map bundle exec rake gates:seam_names # SEAM-2: core never names a concrete seam implementation +bundle exec rake gates:ledger_audit # NFR-17: docs/deviations.md still describes design §10 +bundle exec rake gates:spdx_rbs # NFR-13, NFR-3: every shipped .rbs has the header and declares something +bundle exec rake gates:sole_parse # NFR-6: AstScan.parse is the one RubyVM::AbstractSyntaxTree.parse_file bundle exec rake gates:clean_bundle # the scratch-Gemfile isolation run, all six gems bundle exec rake gates:rbs_surface # NFR-11 bundle exec rake gates:sig_diff # NFR-4, RBS half (vacuous until the first v* tag) @@ -338,7 +345,7 @@ than a subprocess and a message lives in `tools/` — `gates:clean_bundle` and ` The process tooling has its own commands and its own tests: ```bash -ruby .claude/skills/housekeeping/probe.rb # read-only documentation drift. Eight checks. +ruby .claude/skills/housekeeping/probe.rb # read-only documentation drift. Nine checks. ruby .claude/skills/housekeeping/probe.rb --only claims,links ruby .claude/skills/housekeeping/apply.rb # dry run: prints the git mv commands ruby .claude/skills/housekeeping/apply.rb --delivery mvp --phase 5a --write @@ -364,7 +371,9 @@ ruby -w .claude/skills/housekeeping/test/run.rb -n /guard/ # Minitest flags pa ``` `scripts/` and `.claude/` are outside the RuboCop gate — `NFR-7`'s one documented exception, with its re-enable -condition in `.rubocop.yml` and its repair on phase 10's inbound list in the roadmap. +condition in `.rubocop.yml` and its repair a post-release trigger in `docs/first-release.md` (phase 10's file +list did not reach either tree). The `rubocop` gate runs with `--ignore-parent-exclusion`, so a worktree nested +under a checkout whose `.rubocop.yml` excludes `.claude/**` is still linted (phase 10's repairs). The gate table is `docs/sdk-design-ruby/09-toolchain-and-quality-gates.md`: RuboCop (`rubocop-minitest`, `rubocop-performance`, findings fatal), `ruby -w` plus `RUBYOPT=-W:deprecated` with warnings failing the build, @@ -444,7 +453,9 @@ that lands inside after normalisation, an absolute path, and a symlink whose tar **Where a finding goes.** Nothing is registered and looked up later. A finding is **routed to its owner when it is found**, and there are four owners. Work that falls inside a phase's scope → a numbered task in that phase's plan, cited by path and task number. Audit-or-repair work against a phase that is already planned → -phase 10's inbound list in `docs/work/mvp/2026-09-05-ruby-sdk-v1-roadmap-design.md`. Anything that belongs to +phase 10's inbound list in `docs/work/mvp/2026-09-05-ruby-sdk-v1-roadmap-design.md` — until phase 10 drained it +on 2026-09-25; phase 10 is the roadmap's last phase, so such work found now takes the release owner below, the +way phase 10's own unrepaired residues did. Anything that belongs to the release — a blocker, something v1 ships without, a step on the release path, a post-release trigger → `docs/first-release.md`. And if the thing it reports is in material you may write, it is not a finding at all: fix it. Something consciously postponed while building the SDK takes the first or the third of those, and the @@ -541,9 +552,11 @@ invariants no tool catches. `Ractor.make_shareable` deep-freezes but **freezes in place and returns the same object**, so it is applied only to a collection the model has already `dup`ed and therefore owns — never to a caller's live hash. - **`private_class_method :new` plus a validating `.build`**, and the gap stated honestly (P8): `Req.send(:new, - …)` reaches the generated constructor anyway, because `send` bypassing `private` is a documented Ruby feature; - and any object responding to `#method`/`#url`/`#headers`/`#body` duck-types past the builder entirely. Neither - hole can be closed. Do not build a fake proof that they are. + …)` bypasses `.build` because `send` bypassing `private` is a documented Ruby feature — but not validation, + because every model's `#initialize` override validates and calls `super`; what stays open is `.allocate`, + which yields an instance whose members are all nil, and any object responding to + `#method`/`#url`/`#headers`/`#body`, which duck-types past the builder entirely (phase 10's amendment `C19`). + Neither hole can be closed. Do not build a fake proof that they are. - **The mitigation that matters is wire-boundary re-validation.** Header name and outbound value validation (`HTTP-17`, `HTTP-18`, `XCUT-18`) runs **again** immediately before dispatch, inside every transport adapter, so a forged model cannot smuggle a CRLF into a header name even if it never met a builder. That makes the @@ -620,8 +633,9 @@ Each is one line plus the chapter to read before touching the area. charset through `MediaType#charset` (already `nil` for absent or unknown), **retag** the BINARY bytes with 3a's `#read_string(encoding)`, then transcode with the target **named**, `encode(encoding, invalid: :replace, undef: :replace)`. Design §3.1's one-step sentence mangles every - non-ASCII byte and a target-less `#encode` follows the host's `Encoding.default_internal`; the sentence is on - phase 10's inbound list, and the suite's hostile-global tests are what catch a revert (phase 3b's Task 8). + non-ASCII byte and a target-less `#encode` follows the host's `Encoding.default_internal`; the sentence's + correction is amendment `C1` in `docs/deviations.md`'s amendment set, and the suite's hostile-global tests + are what catch a revert (phase 3b's Task 8). - **A body closes exactly the sources it opened, and `close: true` at `Body.stream` forces single-use** — the body layer's ownership rule (design §10.12, `BODY-8`) is deliberately not the I/O layer's wrapping-takes-ownership rule (`IO-6`); a body that closes its stream cannot rewind it, so @@ -988,10 +1002,11 @@ Each is one line plus the chapter to read before touching the area. - **After a mid-stream failure, `BufferedSource.over`'s enumerator RESTARTS `#each`** — `Enumerator#next` on a fiber that died by exception starts over, so a second `#getbyte` re-delivers the body's first byte rather than nil or a second raise, on every row; the SSE facade never gets there because it closed itself - first, and a bare `Reader` driven again after a raise would (phase 3a's residue, on phase 10's inbound - list). And the per-byte read path costs ~0.9 µs a byte through `BufferedSource#getbyte` — a mutex + first, and a bare `Reader` driven again after a raise would (phase 3a's residue, moved by phase 10 to + `docs/first-release.md`). And the per-byte read path costs ~0.9 µs a byte through `BufferedSource#getbyte` — a mutex acquisition and a one-byte String per call — against ~0.3 µs through a plain duck, which is why the - at-scale cap tests run over `FakeByteSource` and why the bulk path is a phase-10 item. + at-scale cap tests run over `FakeByteSource` and why a bulk path is a post-release trigger in + `docs/first-release.md`. - **A bare `ensure` that closes a page INVERTS `PAGE-13`/`PAGE-32`'s primary, and `$!` cannot repair it** — a close raised from an `ensure` replaces the in-flight consumer error as the primary (its `#cause`), and `$!` is thread-dynamically scoped, so it is a CALLER's unrelated error inside anything called from @@ -1198,7 +1213,7 @@ Each is one line plus the chapter to read before touching the area. it, `Numeric`'s own protocol — because a NaN answers false to both `negative?` and `zero?`, and one that reached the timer's deadline-ordered list killed its thread and turned every later `#delay` into a bare `ArgumentError`, while a NaN budget raised one out of `#close` with the latch flipped (P8-77; core's - `Async.validate_delay` and `Clock::Guard.duration` share the hole and are phase 10's). Phase 2's + `Async.validate_delay` and `Clock::Guard.duration` shared the hole and phase 10 closed it the same way). Phase 2's `Bridge::AsyncOver` checks the token BEFORE dispatch as well as after, so a task cancelled while queued never reaches the transport — every "window" test gates on the double's `entered` queue before cancelling, or it is red three runs in twelve. @@ -1231,6 +1246,13 @@ Each is one line plus the chapter to read before touching the area. second `class MatrixFactsTest` in an adapter gem aborts the whole run at load with "is not a module"; the pool gem's is `PoolMatrixFactsTest` and its doubles `PoolFakeTransport`, `PoolRecordingSink`, `PoolStubClock` and `PoolProbeScheduler` (8a's checklist item 35, widened by 8b). +- **`docs/deviations.md` is a gate input, not only a register** — `gates:ledger_audit` reads it against + design §10 on every `rake`: row N's `IDs touched` must EQUAL the IDs §10 entry N names (a range + expanded), and a row carrying a verdict must cite a `gems/…` path that exists and name only + `Dexpace::` constants that resolve with core and every floor-admitted adapter loaded — so renaming a + constant or moving a file a verdict cites turns the gate red, and a verdict citing only `docs/…` never + passes (phase 10's `P10-2`, `P10-33`). A shipped `.rbs` likewise opens with the SPDX line on line 1 + and declares something (`gates:spdx_rbs`); an `.rbs` file with only comments is an offence. ## Public API surface @@ -1286,7 +1308,8 @@ all three read the corpus first. path or the post-release triggers — and the phase document records the reason and the pick-up condition beside that pointer. A finding the phase is not acting on is not registered either: it goes to the plan task whose scope it falls in, to phase 10's inbound list in the roadmap when it is audit-or-repair work against an - already-planned phase, or to `docs/first-release.md` when it belongs to the release — and when it is in + already-planned phase (drained by phase 10; after it, `docs/first-release.md`), or to `docs/first-release.md` + when it belongs to the release — and when it is in material the phase may write, it is simply fixed. **Never leave an aggregate register section inside the spec or the plan.** 7. **Housekeeping before handover.** Run the probe, fix what it reports, then apply. @@ -1306,8 +1329,9 @@ ruby .claude/skills/housekeeping/apply.rb --delivery mvp --phase 5a --write ``` The probe derives each repository fact **once, from the repository**, then checks every document that states it -against that one derivation — never one document against another. Eight checks: `inbox`, `root`, `claims`, -`readmes`, `links`, `registers`, `citations`, `guard`. Exit code is 1 when anything is found. +against that one derivation — never one document against another. Nine checks: `inbox`, `root`, `claims`, +`readmes`, `links`, `registers`, `citations`, `guard`, `chapters` — the last, phase 10's, a requirement ID +attributed to a `docs/product-spec/` chapter that does not carry it. Exit code is 1 when anything is found. The apply stage does exactly one thing: `git mv` from the inbox into `docs/work/`, so `git log --follow` resolves each file across the move. It refuses the **whole batch** if any source or target is under a frozen @@ -1379,7 +1403,7 @@ probe compares each against the live tree, and a count written anywhere else in carry that phase's design, plan and checklist; `phase3/` carries its segmentation design, `docs/work/mvp/phase3/2026-09-08-phase3-segmentation-design.md`, and two sub-phase directories — `phase3/phase3a/` and `phase3/phase3b/`, each holding that sub-phase's design, plan and checklist — - twenty-one checklists written so far, each at implementation; `phase4/` + twenty-two checklists written so far, each at implementation; `phase4/` carries its segmentation design, `docs/work/mvp/phase4/2026-09-08-phase4-segmentation-design.md`, and three sub-phase directories — `phase4/phase4a/`, `phase4/phase4b/` and `phase4/phase4c/`; each holds that sub-phase's @@ -1433,30 +1457,36 @@ probe compares each against the live tree, and a count written anywhere else in phase 9's own suites, `B.3`, `B.4`, `B.6` and `B.7` are lifted, extended or driven, and `B.1`, `B.2` and `B.5` are dispositioned by reference to the owning phase's suite with a committed 61-row coverage map as the artifact. Phase 9 reports and phase 10 repairs. - `phase10/` likewise carries **no segmentation design and no sub-phase** — its design and plan sit - directly at `docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-design.md` - and `docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness.md`. + `phase10/` likewise carries **no segmentation design and no sub-phase** — its design, plan and checklist sit + directly at `docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-design.md`, + `docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness.md` and + `docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-checklist.md`, + the last written at implementation on 2026-09-25. Its scope is the largest in the roadmap by ID count and its design argues that the ID is the wrong unit: **124 own rows** — every ID design §10's nineteen entries name, plus `RETRY-28` from its closing note, 108 MUST / 15 SHOULD / 1 MAY — of which **fifty-one come from §10.1 alone**, whose retirement of the byte-stream provider seam names `SEAM-3`–`SEAM-10`, all forty-two `IO` IDs and - `XCUT-23` in one argument. Counted by ledger entry the phase is 19 entries plus a closing note plus - 32 inbound bullets — **52 units**, the same order as phase 1's 42 unsegmented rows — and it ships + `XCUT-23` in one argument. Counted by ledger entry the phase was designed at 19 entries plus a closing note + plus 32 inbound bullets — **52 units** (the list had grown to sixty-five bullets by implementation), the same order as phase 1's 42 unsegmented rows — and it ships **no new gem**, so only one of the segmentation rule's three triggers fires. It carries **64 cross-reference rows** beside the 124, one per ID an inbound bullet touches or a phase-10 repair reaches - whose row belongs to an earlier phase, for 188 in all. It is the phase that flips all nineteen rows of `docs/deviations.md` + whose row belongs to an earlier phase, for 188 in all; its checklist carries seventeen more cross-reference + rows the as-built list reached, for 205. It is the phase that flips all nineteen rows of `docs/deviations.md` from `design only — not yet built`, by the method the roadmap fixes for it — **re-deriving every - claim from as-built source, never from another document** — and the phase that writes the thirteen - frozen-chapter amendments `C1`–`C13` out, because `docs/sdk-design-ruby/` and `docs/product-spec/` are + claim from as-built source, never from another document** — and the phase that writes the + frozen-chapter amendments out, the design's thirteen `C1`–`C13`, `C14` reconciled from phase 4b's filing + and five the as-built audit found, `C15`–`C19`, in `docs/deviations.md`'s amendment set, because `docs/sdk-design-ruby/` and `docs/product-spec/` are frozen and only a human may apply them. It ships repair code in `dexpace-core`, `dexpace-transport-async_http` and `dexpace-conformance`, reaches - `dexpace-transport-net_http` only through the `sig/` header its `NFR-13` repair adds to every gem, - plans three further blocking gates (`gates:ledger_audit`, `gates:spdx_rbs`, - `gates:sole_parse`) and a ninth probe check for chapter attribution — none of the four built yet — - and closes or narrows five `docs/first-release.md` lines while publishing nothing: every gem stays - at `0.0.0`. + `dexpace-transport-net_http` through the `sig/` header its `NFR-13` repair adds to every gem, two YARD + comments in its `lib/` and two tests the `Protocol` repair invalidated, per its design's R1 addendum, + builds three further blocking gates (`gates:ledger_audit`, `gates:spdx_rbs`, + `gates:sole_parse`) and a ninth probe check for chapter attribution, dispositions every one of the + inbound list's sixty-five bullets (repaired, verified already fixed, moved to `docs/first-release.md`, + or withdrawn — none carried forward), and closes or narrows `docs/first-release.md` lines while + publishing nothing: every gem stays at `0.0.0`. Every checklist but phase 0's, phase 1's, phase 2's, phase 3a's, phase 3b's, phase 4a's, phase 4b's, phase 4c's, phase 5a's, phase 5b's, phase 5c's, phase 6a's, phase 6b's, phase 6c's, phase 7b's, - phase 7c's, phase 7a's, phase 8a's, phase 8b's, phase 8c's and phase 9's is still to be written at - execution time — which leaves phase 10's alone. + phase 7c's, phase 7a's, phase 8a's, phase 8b's, phase 8c's, phase 9's and phase 10's is still to be + written at execution time — which leaves none: every phase of the v1 roadmap has its checklist. - There are 40 harvested topics under `docs/knowledge/harvested/`; the harvest ran here on 2026-09-05. diff --git a/README.md b/README.md index 1fc16a4..7de1e3f 100644 --- a/README.md +++ b/README.md @@ -123,8 +123,9 @@ idempotent bounded close that emits the lifecycle event phase 2 postponed | `dexpace-conformance` | `Dexpace::Conformance` | `dexpace-core` | What phase 0 does ship is the gate set every later phase is written under: seventeen blocking -checks in one `bundle exec rake` — eighteen since phase 7b added `gates:serde_boundary` — each proven -by a deliberately failing input, and a CI matrix that runs the real suite on Ruby 3.2, 3.3, 3.4 and +checks in one `bundle exec rake` — eighteen since phase 7b added `gates:serde_boundary`, twenty-one +since phase 9's three invariant scans and twenty-four since phase 10's `gates:ledger_audit`, +`gates:spdx_rbs` and `gates:sole_parse` — each proven by a deliberately failing input, and a CI matrix that runs the real suite on Ruby 3.2, 3.3, 3.4 and 4.0 (`docs/work/mvp/phase0/2026-09-05-phase0-scaffold-and-quality-gates-checklist.md`). ## Working in the repository diff --git a/Rakefile b/Rakefile index 1de1c93..d335bc9 100644 --- a/Rakefile +++ b/Rakefile @@ -27,6 +27,9 @@ DEFAULT_GATES = %w[ gates:cause_walk gates:bounded_map gates:seam_names + gates:ledger_audit + gates:spdx_rbs + gates:sole_parse gates:clean_bundle gates:rbs_surface gates:sig_diff diff --git a/docs/README.md b/docs/README.md index dcb370e..1038633 100644 --- a/docs/README.md +++ b/docs/README.md @@ -80,8 +80,9 @@ not a find-list, and nothing else here is one either. A finding is **not registered; it is routed to its owner when it is found**. Work that falls inside a phase's scope becomes a numbered task in that phase's plan, cited by path and task -number. Audit-or-repair work against a phase that is already planned goes to phase 10's inbound -list in [the roadmap](./work/mvp/2026-09-05-ruby-sdk-v1-roadmap-design.md). Anything belonging to +number. Audit-or-repair work against a phase that is already planned went to phase 10's inbound +list in [the roadmap](./work/mvp/2026-09-05-ruby-sdk-v1-roadmap-design.md) until phase 10, the +roadmap's last phase, drained it on 2026-09-25; such work now goes to the release register below. Anything belonging to the release — a blocker, something v1 ships without, a step on the release path, a post-release trigger — goes to [`first-release.md`](./first-release.md). And when the thing reported is in material you may write, it is not routed anywhere: it is fixed. Work consciously postponed @@ -157,7 +158,9 @@ the asynchronous transport, the suite's second driver, is what phase 8c built. P of its own: it ships no gem, and what it built extended two that already existed — [`sdk-documentation/conformance.md`](./sdk-documentation/conformance.md) gains a section on its four suites and [`sdk-documentation/quality-gates.md`](./sdk-documentation/quality-gates.md) three rows, -one per repository-wide invariant scan. +one per repository-wide invariant scan. Phase 10 added no page either: +[`sdk-documentation/quality-gates.md`](./sdk-documentation/quality-gates.md) gains three more rows, +one per gate it built, and the pages whose worked examples it re-ran were corrected in place. ## Keeping this file true @@ -167,6 +170,7 @@ ruby .claude/skills/housekeeping/probe.rb The [`housekeeping`](../.claude/skills/housekeeping/SKILL.md) skill probes every claim here against the repository — the gem count, the phase-directory count, the harvested-topic count, -a README on every gem, broken relative links, register text in the wrong document, and a -citation that resolves to nothing. It is a hand-run tool, not a CI step. Run it before +a README on every gem, broken relative links, register text in the wrong document, a +citation that resolves to nothing, and a requirement ID attributed to a specification chapter +that does not carry it. It is a hand-run tool, not a CI step. Run it before claiming the documentation is current. diff --git a/docs/deviations.md b/docs/deviations.md index 8e8913d..a35b782 100644 --- a/docs/deviations.md +++ b/docs/deviations.md @@ -6,34 +6,40 @@ the design document that promises it. Design §10 is frozen (see `docs/README.md frozen"); this file is where the routine, ongoing question "has entry N actually landed the way §10 says it would?" gets answered and kept current. -**Nothing has been built yet.** Every row below is therefore `design only — not yet built`: the -argument in §10 is recorded, but there is no shipped code to audit it against. A row moves to -`as-built: confirmed` (or is flagged otherwise) once a gem implementing it exists and has been -checked against source, the same way `docs/deviations.md` is used in the sibling Node SDK. +**Audited 2026-09-25 by phase 10, against the tree at the end of phase 10.** Every row was +re-derived from as-built source, never from another document: each Status cell names the gem path and +the constant that is its evidence, and `gates:ledger_audit` (phase 10's plan, Task 2) fails the build if +a row's IDs stop equalling its entry's, if two rows drift out of their entry's position, or if a verdict +cites no `gems/…` path that exists or names a `Dexpace::` constant that is not defined. The verdicts are +phase 10's design `R1`'s four: *confirmed*, *confirmed, narrower* (true, and narrower than the sentence -- +each names the amendment below that carries the narrowing), *contradicted* (none) and *unverifiable* +(none). The closing note's claim -- nothing unified, both retry stacks, both transport seams and both +bridges survive -- is confirmed too (`gems/dexpace-core/lib/dexpace/resilience/`, `Dexpace::Bridge::SyncOver` +and `AsyncOver`), and is phase 10's checklist row `RETRY-28`. ## The audit | # | Title | IDs touched | Status | |---|---|---|---| -| 1 | The byte-stream provider seam is retired; its behavioural contract is not | SEAM-3–SEAM-10, IO-1–IO-42, XCUT-23 | design only — not yet built | -| 2 | The canonical body is a duck type, not a nominal interface | SEAM-3, BODY-1, BODY-35 | design only — not yet built | -| 3 | The async pivot is a core-owned future rather than an ecosystem primitive | SEAM-1, SEAM-16, SEAM-17, ASYNC-1, ASYNC-2, NFR-11 | design only — not yet built | -| 4 | Cancellation is cooperative; the orphaned-response close moves to the producer | SEAM-13, SEAM-30, XCUT-1–XCUT-3, CFG-17, CFG-20, CFG-21, RETRY-23, TRANSPORT-3, ASYNC-5 | design only — not yet built | -| 5 | Two MUSTs are not satisfied, and a third holds vacuously | ASYNC-3, ASYNC-4, PIPE-33 | design only — not yet built | -| 6 | Suppressed exceptions are a core-owned trail, not a host facility | RECOV-12, PAGE-13, PAGE-15, SSE-29, SSE-30, SSE-36, RETRY-34, XCUT-9 | design only — not yet built | -| 7 | "Standard library" is narrowed to what is stable across the supported Ruby range | SEAM-1, NFR-1, AUTH-14, OBS-2 | design only — not yet built | -| 8 | Discovery's substrate is require-time self-registration, not classpath scanning | SEAM-5–SEAM-9, XCUT-23 | design only — not yet built | -| 9 | SEAM-10's multi-loader de-duplication is vacuous and is replaced by a version-skew guard | SEAM-10 | design only — not yet built | -| 10 | Runtime encapsulation of models is partially unachievable | HTTP-2/SEAM-29, HTTP-4, HTTP-7, HTTP-17, HTTP-18, IO-28/BODY-37, XCUT-18 | design only — not yet built | -| 11 | Read-only collection exposure is computed once, not wrapped per access | HTTP-5, XCUT-15 | design only — not yet built | -| 12 | One stream-ownership rule for bodies, resolving a reference inconsistency | BODY-8, SEAM-3, SEAM-20, SEAM-21 | design only — not yet built | -| 13 | The serde seam ships four encode profiles, two of which are one Ruby type | SEAM-20 | design only — not yet built | -| 14 | The serde witness is a class-object-and-combinator protocol, not a reflective type token | SEAM-22, SEAM-23, SERDE-5–SERDE-8, SERDE-16, SERDE-17 | design only — not yet built | -| 15 | The cross-origin redirect marker lives on the per-hop cursor, not on the request | REDIR-11, AUTH-29, PIPE-16 | design only — not yet built | -| 16 | The configuration chain keeps four tiers with a substituted third source | CFG-1, CFG-3, CFG-4, CFG-24, CFG-26, OBS-35 | design only — not yet built | -| 17 | The interruptible sleep is a cancellable queue wait, not `Kernel#sleep` | CFG-15, CFG-17, CFG-18, RETRY-26, XCUT-3, XCUT-13 | design only — not yet built | -| 18 | Platform-constant substitutions where Ruby has no constant | IO-9, BODY-32, SSE-11, RECOV-34 | design only — not yet built | -| 19 | The dead-code-survival gate is retargeted, not deleted | NFR-8, NFR-9 | design only — not yet built | +| 1 | The byte-stream provider seam is retired; its behavioural contract is not | SEAM-3–SEAM-10, IO-1–IO-42, XCUT-23 | as-built: confirmed, narrower -- the apparatus is absent (`gems/dexpace-core/lib/dexpace/io.rb` defines `Dexpace::IO::Buffer`, `BufferedSource`, `BufferedSink`, `TeeSink` and no registration entry point; the only registries are `Dexpace::Transport::REGISTRY`, `Dexpace::AsyncTransport::REGISTRY` and `Dexpace::Serde::REGISTRY`) and the contract is 3a's `gems/dexpace-core/lib/dexpace/io/`; narrower where XCUT-23's third surviving instance is the async transport seam, not an executor (C17) | +| 2 | The canonical body is a duck type, not a nominal interface | SEAM-3, BODY-1, BODY-35 | as-built: confirmed -- `Dexpace::IO::BufferedSource.over` (`gems/dexpace-core/lib/dexpace/io/buffered_source.rb:91`) takes no ownership, measured: closing the source leaves the upstream open, where `.wrapping` (`:48`) closes it; every body yields BINARY chunks from `#each` (`gems/dexpace-core/lib/dexpace/http/body.rb`) | +| 3 | The async pivot is a core-owned future rather than an ecosystem primitive | SEAM-1, SEAM-16, SEAM-17, ASYNC-1, ASYNC-2, NFR-11 | as-built: confirmed -- the pivot is `Dexpace::Async::Future`, `Dexpace::Async::Completer` and `Dexpace::Async::Settlement` (`gems/dexpace-core/lib/dexpace/async/future.rb`), core-owned; `gates:rbs_surface` finds no foreign async type in any shipped signature | +| 4 | Cancellation is cooperative; the orphaned-response close moves to the producer | SEAM-13, SEAM-30, XCUT-1–XCUT-3, CFG-17, CFG-20, CFG-21, RETRY-23, TRANSPORT-3, ASYNC-5 | as-built: confirmed, narrower -- `Dexpace::Async::Completer#fulfil` closes an undelivered result (`gems/dexpace-core/lib/dexpace/async/completer.rb:96`); the socket-closing hooks are the transports' (`gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/response_pump.rb:120`, `gems/dexpace-transport-async_http/lib/dexpace/transport/async_http/exchange.rb:134`) and the pool owns no socket, so the mitigation names a transport, not an adapter (C12) | +| 5 | Two MUSTs are not satisfied, and a third holds vacuously | ASYNC-3, ASYNC-4, PIPE-33 | as-built: confirmed as admitted, not re-opened -- `ASYNC-3` waived by ID as would-fail in `gems/dexpace-async-thread/test/dexpace/async/thread/conformance_test.rb`, `PIPE-33`'s interrupt clause unmet, `ASYNC-4` vacuous; the gap is `docs/first-release.md` § Unsatisfied MUSTs | +| 6 | Suppressed exceptions are a core-owned trail, not a host facility | RECOV-12, PAGE-13, PAGE-15, SSE-29, SSE-30, SSE-36, RETRY-34, XCUT-9 | as-built: confirmed, narrower -- the trail is `Dexpace::Suppressible` (`gems/dexpace-core/lib/dexpace/suppressible.rb:46`), included by `Dexpace::Error` and extended onto any exception by `Dexpace.attach_suppressed`, not a method of `Dexpace::Error` itself (C14); one cycle-safe walk, `Dexpace.each_cause` over `compare_by_identity` (`gems/dexpace-core/lib/dexpace/each_cause.rb:48`), kept alone by `gates:cause_walk` | +| 7 | "Standard library" is narrowed to what is stable across the supported Ruby range | SEAM-1, NFR-1, AUTH-14, OBS-2 | as-built: confirmed -- `gems/dexpace-core/dexpace-core.gemspec` declares no dependency; Basic is `pack("m0")` (`gems/dexpace-core/lib/dexpace/auth/basic_handler.rb:45`); no `require "logger"` in core; `gates:require_allowlist` and `gates:clean_bundle` green on every row | +| 8 | Discovery's substrate is require-time self-registration, not classpath scanning | SEAM-5–SEAM-9, XCUT-23 | as-built: confirmed, narrower -- adapters self-register at require time (`gems/dexpace-transport-net_http/lib/dexpace/transport/net_http.rb`) into `Dexpace::Registry`'s five branches (`gems/dexpace-core/lib/dexpace/registry.rb`); the permitted instrumentation auto-activation exists nowhere (`gems/dexpace-core/test/dexpace/seam_surface_test.rb`), so the restriction holds by absence | +| 9 | SEAM-10's multi-loader de-duplication is vacuous and is replaced by a version-skew guard | SEAM-10 | as-built: confirmed -- vacuous with a replacement: `Dexpace::Registry#register(key, factory, core:)` (`gems/dexpace-core/lib/dexpace/registry.rb:123`) is the version-skew guard; `gates:single_instance` green | +| 10 | Runtime encapsulation of models is partially unachievable | HTTP-2/SEAM-29, HTTP-4, HTTP-7, HTTP-17, HTTP-18, IO-28/BODY-37, XCUT-18 | as-built: confirmed, narrower -- `Dexpace::Request.send(:new, ...)` bypasses `.build` but not validation: it runs the validating `#initialize` (`gems/dexpace-core/lib/dexpace/http/request.rb:51`), so a GET with a body raises HTTP-7 and a non-`Headers` headers value is refused (measured on 4.0.6 and 3.2.11); the residual holes are `Dexpace::Request.allocate`, which yields a Request whose four members are nil, and duck typing, and the mitigation for both is that both transports re-validate header names and outbound values at the wire (`gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/request_mapper.rb:58`, `gems/dexpace-transport-async_http/lib/dexpace/transport/async_http/request_mapper.rb:50`) (C19) | +| 11 | Read-only collection exposure is computed once, not wrapped per access | HTTP-5, XCUT-15 | as-built: confirmed -- `Dexpace::Model.own` (`gems/dexpace-core/lib/dexpace/model.rb`) deep-freezes a copy it owns; a returned header value list refuses mutation (measured); phase 10 repaired its default-proc escape | +| 12 | One stream-ownership rule for bodies, resolving a reference inconsistency | BODY-8, SEAM-3, SEAM-20, SEAM-21 | as-built: confirmed, narrower -- a body closes only what it opened (`gems/dexpace-core/lib/dexpace/http/body/stream_body.rb`), ownership-on-wrap is `Dexpace::IO::BufferedSource.wrapping`'s, a codec closes nothing (`gems/dexpace-serde-json/lib/dexpace/serde/json/codec.rb`); §10.12 attributes the I/O rule to SEAM-3 where the live ID is IO-6 (C3) | +| 13 | The serde seam ships four encode profiles, two of which are one Ruby type | SEAM-20 | as-built: confirmed, narrower -- all four profiles ship (`gems/dexpace-serde-json/lib/dexpace/serde/json/codec.rb:145`), `dump_string` tagged UTF-8 and `dump_bytes` ASCII-8BIT (measured); each takes the value first, which §10.13 does not spell (C18) | +| 14 | The serde witness is a class-object-and-combinator protocol, not a reflective type token | SEAM-22, SEAM-23, SERDE-5–SERDE-8, SERDE-16, SERDE-17 | as-built: confirmed -- `Dexpace::Serde.witness!` over `Dexpace::Serde::WITNESS_METHOD` (`gems/dexpace-core/lib/dexpace/serde/witness.rb:17`); the hierarchy is `Dexpace::Serde::Error` with `Dexpace::Serde::SerializationError` and `Dexpace::Serde::DeserializationError` under it; no compile-time refusal, as admitted | +| 15 | The cross-origin redirect marker lives on the per-hop cursor, not on the request | REDIR-11, AUTH-29, PIPE-16 | as-built: confirmed -- `cursor.fork(state: { cross_origin: ... })` on every drive (`gems/dexpace-core/lib/dexpace/redirect/step.rb:166`), read only through `cursor.state` (`gems/dexpace-core/lib/dexpace/auth/step.rb:118`); `Dexpace::Pipeline::Cursor` has no state setter | +| 16 | The configuration chain keeps four tiers with a substituted third source | CFG-1, CFG-3, CFG-4, CFG-24, CFG-26, OBS-35 | as-built: confirmed -- `Dexpace::Configuration#string` (`gems/dexpace-core/lib/dexpace/configuration.rb:115`) reads override, environment, property, default; the property tier is `Dexpace.configure` (`gems/dexpace-core/lib/dexpace/config.rb:43`); `Dexpace::Proxy.resolve` reads the same chain; phase 10 bounded the chain a repeated configure grew | +| 17 | The interruptible sleep is a cancellable queue wait, not `Kernel#sleep` | CFG-15, CFG-17, CFG-18, RETRY-26, XCUT-3, XCUT-13 | as-built: confirmed -- `Dexpace::Clock#sleep` is a per-call queue pop woken by the token (`gems/dexpace-core/lib/dexpace/clock.rb:113`); phase 10 refused NaN and Complex in its guard and in `Dexpace::Async.delay` | +| 18 | Platform-constant substitutions where Ruby has no constant | IO-9, BODY-32, SSE-11, RECOV-34 | as-built: confirmed, narrower -- `Dexpace::IO::MAX_MATERIALIZED_BYTES` (`gems/dexpace-core/lib/dexpace/io.rb:27`), `Dexpace::SSE::MAX_RETRY_MS` (`gems/dexpace-core/lib/dexpace/sse.rb:58`), `Dexpace::Resilience::Policy::MAX_DURATION_NANOSECONDS` (`gems/dexpace-core/lib/dexpace/resilience/policy.rb:77`); 7b added `Dexpace::SSE::MAX_LINE_BYTES` and `Dexpace::SSE::MAX_EVENT_BYTES`, which the entry does not list (C15) | +| 19 | The dead-code-survival gate is retargeted, not deleted | NFR-8, NFR-9 | as-built: confirmed -- vacuous by its own text; retargeted at the require-allowlist audit and the clean-bundle run (`gems/dexpace-core/dexpace-core.gemspec`, `tasks/gates.rake`), both green on every row | Numbering follows §10's own list order and is not renumbered as entries are confirmed built; a row's number is a citation, the same as an item ID in `docs/first-release.md`, the one other register @@ -112,7 +118,7 @@ of `async-http`'s dependency closure, and appendix C's `SSE-19` row, the first e normative specification. Both are below, dated. **What this section adds up to, because the arithmetic was wrong once and is worth stating: thirteen -amendments in eleven notes.** Phase 10's design numbers them `C1`–`C13` and its plan, Task 17 writes each +amendments in eleven notes** (as of 2026-09-13; phase 10's table above completes the set at nineteen). Phase 10's design numbers them `C1`–`C13` and its plan, Task 17 writes each one's replacement text; the mapping is fixed here so neither document can drift from the other. Seven of the eleven notes below the retirement paragraph carry `C1`, `C3`, `C4`, `C5`, `C6`, `C7` and `C10`; the two dated 2026-09-13 carry `C8` and `C11`; and **the two notes above this paragraph, dated 2026-09-12, are @@ -126,6 +132,40 @@ condition is the `docs/first-release.md` blocker phase 10 files, and that blocker's enumeration names all thirteen — an amendment recorded here with no line in that blocker is exactly the outcome this section exists to prevent. +**The amendment set, written out -- phase 10, 2026-09-25. This is the section's closing condition.** +Phase 10 (its plan, Task 17) verified every code half below in as-built source before writing a +replacement, so applying each amendment is editorial: quote the sentence, paste the replacement. The set +is **`C1`-`C19`**: the thirteen of phase 10's design, `C14` (filed by phase 4b in `docs/first-release.md` +and missing here until now), and five the as-built audit added (`C15`-`C19`). The notes below this table +stand as each amendment's measured record; `C2`, `C9` and `C14`-`C19` had none and are written in the +table alone. Its closing condition is the `docs/first-release.md` blocker "The fourteen recorded +corrections ... applied", which now enumerates all nineteen and names the one further human act this set +cannot transcribe -- **the consolidation of every phase's as-built Deviation Ledger rows into §10** -- +and whose alternative form is that the release notes name the sentences, and the ledgers, a reader +should not trust. After that blocker is answered this section is a record, not a holding area. + +| # | File and sentence | Replacement | Code half, verified 2026-09-25 | +|---|---|---|---| +| C1 | §3.1 (`03-…md:78`): "applies the media type's charset via `String#encode(invalid: :replace, undef: :replace)`" | "retags the BINARY bytes to the media type's charset -- UTF-8 when it is absent or unknown -- and then transcodes with both encodings named, `#encode(target, invalid: :replace, undef: :replace)`" | `Dexpace::Response#body_string` (`gems/dexpace-core/lib/dexpace/http/response.rb`): `MediaType#charset`, `#read_string(encoding)`, then a named-target `#encode` | +| C2 | §4 (`04-…md:21`): "`Request`, `Response`, `Headers`, `Query`, `RequestOptions` and `Configuration` get real mutable `Builder` classes" | add "and the multipart body (`MultipartBody#new_builder`, `MultipartBody::Builder`), which **HTTP-3** names" | `Dexpace::MultipartBody::Builder` (`gems/dexpace-core/lib/dexpace/http/body/multipart_body.rb`) and its non-aliasing test | +| C3 | §3.1 (`03-…md:83`): "closes that `IO` (**SEAM-3**)"; §10.12 leans on the same attribution | cite **IO-6** (appendix C its only statement) beside or instead of SEAM-3 | `Dexpace::IO::BufferedSource.wrapping` closes the IO, measured | +| C4 | §8.1's code block (`08-…md:31`): "`#tag(key, value)`" | delete the method, or name the requirement it serves and its precedence against **OBS-5**'s three sources | `Dexpace::Instrumentation::Event` ships `#field`, `#event`, `#cause`, `#emit` and no `#tag` (5b's P5-18) | +| C5 | §3.2 (`03-…md:167-172`): the block-scoped `read_body` "satisfied literally" | "a per-response producer `Thread` over a `Thread::SizedQueue(1)`, drained through a `#readpartial`-shaped reader, keeps the body lazy and closable (**SEAM-11**, **TRANSPORT-25**); the block form buffers the body and kills the socket, measured" | `gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/response_pump.rb` (8a's P8-1) | +| C6 | §3.2 (`03-…md:172-173`), §11.18 and §12's `TRANSPORT` row: `Net::HTTP` "retries nothing on its own", "has no resend hook", TRANSPORT-2 vacuous | "`Net::HTTP#max_retries` defaults to 1 and re-sends idempotent requests; the adapter sets it to 0, so TRANSPORT-2 is satisfied by construction and leaves §12's vacuous count" | `http.max_retries = 0` in `gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/adapter.rb` | +| C7 | §12's `TRANSPORT` row, both directions | add TRANSPORT-14 to the adapter-scoped list (unreachable on `async-http`); record TRANSPORT-8 as satisfied on the async adapter and drop it from the vacuous count | the named waiver `WAIVED = %w[TRANSPORT-14 TRANSPORT-27]` and the "unreachable here" measurement in `gems/dexpace-transport-async_http/test/dexpace/transport/async_http/conformance_test.rb`; 8c's `Async::Cancel`/`Async::TimeoutError` discrimination | +| C8 | §8.3 (`08-…md:216`): "forbidden in every gem in this repository" | "forbidden in code this repository writes, which `Dexpace/NoThreadInterrupt` enforces over every gem's `lib/`. A dependency may use them: `net-http`'s connect phase is `Timeout.timeout(@open_timeout, Net::OpenTimeout)` on every supported Ruby (0.9.1 also carries `TCPSocket.open(open_timeout:)`), the first such call starts a process-wide thread a host that counts threads sees once, and the `async-http` closure has no `Timeout.timeout`, seven `Fiber#raise` sites at scheduler checkpoints, one `Thread.current.raise` and one `Thread#kill` on an unreachable helper" | none owed; the cop stands | +| C9 | §9.3 (`09-…md:71`): "it ships with the interpreter as a default gem" | "it ships with the interpreter as a **bundled** gem, needing an explicit bundle entry under Bundler -- `VERSIONS` carries it, pinned `~> 5.25` -- and the same argument follows" | `VERSIONS`' `tool minitest ~> 5.25` row and its reason beside it (`VERSIONS:22-28`), read by the root `Gemfile` | +| C10 | §9.3 (`09-…md:111`): the waiver "listing the requirement ID", the report's unit unstated | add "and the report's unit is the same requirement ID: one assertion per ID, an appendix-B item a many-to-one view whose status is the worst of its assertions" | `Dexpace::Conformance::Report`, one `Result` per assertion, and `gems/dexpace-conformance/APPENDIX_B.md` | +| C11 | appendix C's `SSE-19` and `OBS-29` rows, against their chapters | each row either carries the other's clause or says it is partial -- a recommendation to the specification author, in §11's idiom | 7b's configurable line cap (`Dexpace::SSE::MAX_LINE_BYTES`); 5c's documented `OBS-29` contract and 6a's emitted per-attempt group | +| C12 | §10.5 (`10-…md:52`): "`Completer#on_cancel` lets an adapter shorten that" | "lets a **transport** shorten that by closing its socket under the read; the thread-pool adapter owns no socket" | the transports' hooks at `net_http/response_pump.rb:120` and `async_http/exchange.rb:134` | +| C13 | §12's `PAGE` row (`12-…md:36`) | add "`PAGE-15`'s wrapping clause is vacuous by a false antecedent (`7c P7-1`)" beside PAGE-35 | 7c's `P7-1` | +| C14 | §10 item 6 (`10-…md:60`): "`Dexpace::Error#suppressed` supplies the list"; §5.2's placement | "`Dexpace::Suppressible` supplies the list -- `Dexpace::Error` includes it and `Dexpace.attach_suppressed` extends it onto any other exception" | `gems/dexpace-core/lib/dexpace/suppressible.rb` (4b's P4-12) | +| C15 | §10 item 18 (`10-…md:119-124`): the named substitutions | add "`SSE::MAX_LINE_BYTES` (1 MiB) and `SSE::MAX_EVENT_BYTES` (8 MiB), SSE-19's two rejecting caps, distinct from SSE-11's `MAX_RETRY_MS`" | `gems/dexpace-core/lib/dexpace/sse.rb:38`, `:48` (7b's P7-21) | +| C16 | §9's gate table (`09-…md:7-24`) | add the seven gates later phases built -- `gates:serde_boundary` (7b), `gates:cause_walk`, `gates:bounded_map`, `gates:seam_names` (phase 9, A4-A7), `gates:ledger_audit`, `gates:spdx_rbs`, `gates:sole_parse` (phase 10, A8, A9, A11) -- and the probe's `chapters` check (A10); the NFR-13 row gains "and `gates:spdx_rbs` over every shipped `.rbs`" | `DEFAULT_GATES` in the root `Rakefile`, twenty-four names | +| C17 | §11.8 (`11-…md:32`): XCUT-23's three instances "(transport, serde, executor)" | "(transport, async transport, serde)" -- there is no executor registry | `Dexpace::AsyncTransport::REGISTRY` (`gems/dexpace-core/lib/dexpace/async_transport.rb:22`) | +| C18 | §10 item 13 (`10-…md:93`): "`#dump_to(sink)` and `#dump_into(buffer, offset:)`" | "`#dump_to(value, sink)` and `#dump_into(value, buffer, offset: 0)`" (and `#dump_string(value)`, `#dump_bytes(value)`) | `gems/dexpace-serde-json/lib/dexpace/serde/json/codec.rb:145-200` | +| C19 | §4 (`04-…md:45`): "`Req.send(:new, ...)` reaches the generated constructor anyway"; §10 item 10 (`10-…md:78-79`): "`private_class_method :new` is bypassable by `send` by design" | "`send(:new, ...)` bypasses `.build` but not validation -- every model's `#initialize` override validates and calls `super`, so it re-runs HTTP-4, HTTP-7 and the member type checks; what remains open is `.allocate`, which yields an instance whose members are all nil, and duck typing" | `Dexpace::Request#initialize` (`gems/dexpace-core/lib/dexpace/http/request.rb:51`); `Request.send(:new, ...)` with a GET and a body raises HTTP-7, and `Request.allocate` returns all-nil members, measured on 4.0.6 and 3.2.11 (phase 10's review round 0, R0-6) | + **2026-09-08 — against design §3.1's decode recipe. Established by phase 3b's design.** The frozen sentence fixes one decode boundary as `Response#body_string`, "which applies the media type's charset via `String#encode(invalid: :replace, undef: :replace)` and falls back to UTF-8 when absent or unknown". The rule diff --git a/docs/first-release.md b/docs/first-release.md index 72f98a4..99edfcc 100644 --- a/docs/first-release.md +++ b/docs/first-release.md @@ -92,7 +92,13 @@ stated in the release notes rather than discovered at `bundle install`. beside what a green run proves; the box ticks when 8c's waiver is stated beside them. **Ticked 2026-09-21**: the preamble names a third omission, `TRANSPORT-8`, whose antecedent only an adapter's own suite can originate, and `conformance.md` states 8c's two waivers (`TRANSPORT-14` - and `TRANSPORT-27`, both by id, both this adapter's alone) beside the three omissions + and `TRANSPORT-27`, both by id, both this adapter's alone) beside the three omissions. **Narrowed + 2026-09-25 by phase 10 (`R11`)**: the two waivers were re-measured -- `conformance_test.rb`'s "the two + waived clauses are unreachable here: protocol-http1 refuses both heads out of the read" still passes + on 3.3.12, 3.4.10 and 4.0.6, so both still state the truth -- and the page must add the largest + thing a green run does not prove: `gems/dexpace-conformance/APPENDIX_B.md`'s rows dispositioned + **by reference**, where the evidence is another gem's test file and the row proves the ID is claimed, + never that the behaviour is asserted (`Aggregate::PREAMBLE` already prints it) - [ ] **Before release, `docs/sdk-documentation/` must document the `include Dexpace` constant-shadow hazard.** Filed 2026-09-08 by phase 3a's design; recorded here 2026-09-13. Phase 1 measured `Dexpace::Method` shadowing `::Method` and concluded "**verified inert outside core**"; the observation @@ -178,8 +184,12 @@ stated in the release notes rather than discovered at `bundle install`. nothing here. The same pass discharges the three narrow obligations this file already carries against that tree: the conformance-run caveat, the `include Dexpace` constant shadow, and `Dexpace::IO::MAX_MATERIALIZED_BYTES`. Cites `SEAM-26`, `SEAM-27`, `SERDE-28`, `RECOV-15`, - `PIPE-39` -- [ ] **The fourteen recorded corrections to `docs/sdk-design-ruby/` §3, §4, §5, §8, §9, §10, §11, §12 and to + `PIPE-39`. **Narrowed 2026-09-25 by phase 10 (`R7`, `P10-7`): the example must also show the + correlation chain** -- constructing a `Dexpace::DispatchContext`, promoting it with + `#promote_to_request` and `#promote_to_exchange`, and where the operation name goes -- because no + phase builds a call path that does it and this is the one place a generated client's author meets + it (`CTX-16`, `CTX-14`, `SEAM-28`; the *Behavioural asymmetries* entry below) +- [ ] **The eighteen recorded corrections (fourteen until phase 10 completed the set on 2026-09-25) to `docs/sdk-design-ruby/` §3, §4, §5, §8, §9, §10, §11, §12 and to appendix C's `SSE-19` row applied — or the release notes stating which design sentences a reader should not trust.** Filed 2026-09-13 by phase 10's design. Every one is a place where the **rule** is right, the **mechanism sentence** is wrong about Ruby or about a library, and the phase that @@ -215,14 +225,58 @@ stated in the release notes rather than discovered at `bundle install`. `Hooks.notify` hand the helper is a caller's exception, so the trail lives on `Dexpace::Suppressible`, which `Dexpace::Error` includes and `Dexpace.attach_suppressed` extends onto anything else (phase 4b's design, P4-12; the working code is `gems/dexpace-core/lib/dexpace/suppressible.rb`). Its replacement - sentence is written out in 4b's design ledger and awaits the same human hand as the thirteen + sentence is written out in 4b's design ledger and awaits the same human hand as the thirteen. + **Completed 2026-09-25 by phase 10, which wrote the set out in `docs/deviations.md` § Deviations + found outside a phase with every frozen sentence quoted, its replacement, its measurement and its + code half verified in as-built source -- `C1`-`C14` plus four the as-built audit added:** `C15`, + §10.18's list of platform-constant substitutions omitting 7b's `SSE::MAX_LINE_BYTES` and + `MAX_EVENT_BYTES`; `C16`, §9's gate table carrying none of the seven gates phases 7b, 9 and 10 added + (`gates:serde_boundary`, phase 9's addenda A4-A7, phase 10's A8-A11) or the probe's ninth check; + `C17`, §11.8 naming the third surviving `XCUT-23` instance "executor" where it is the async transport + seam (`Dexpace::AsyncTransport::REGISTRY`; there is no executor registry); and `C18`, §10.13 spelling + the four encode profiles without the value they encode (`dump_to(value, sink)`, not `dump_to(sink)`); + and `C19`, §4's and §10 item 10's "`send(:new, …)` reaches the generated constructor", where as built + `send(:new)` runs the validating `#initialize` and the residual holes are `.allocate` and duck typing + (phase 10's review round 0, R0-6). + **The same human act owes one thing more, named here so it has an owner: the consolidation of every + phase's as-built Deviation Ledger rows -- `P0-…` through `P9-42` and phase 10's `P10-1`-`P10-11` and + `P10-21`-`P10-38` -- into design §10.** Each phase's status note ends "consolidation into design §10 + is a human's"; phase 10 is the last phase and the chapter is frozen to it. **The alternative form holds + for this half too**: a release may ship with the ledgers unconsolidated if its notes list the phase + ledgers a reader must consult beside §10 - [ ] An RBS sig-diff baseline established, so a later release can be checked against it for an accidental breaking change. **Phase 10 is the phase that can**: its plan, Task 5 is the last change to `sig/` in every gem, so Task 18 establishes the baseline over a tree nothing else will move — **both** baselines, the `sig/**/*.rbs` tree and the runtime surface snapshot, because `rbs` - describes what someone wrote and not what `Data.define` generates. Establishing a baseline is not - satisfying `NFR-4`, whose subject is a **diff** against the previous release tag; that stays ⏳ - until a `v*` tag exists + describes what someone wrote and not what `Data.define` generates. This line is design §9's + release-tag mechanism, not `NFR-4` itself: appendix C's `NFR-4` is "a checked-in, + machine-comparable snapshot" with the build failing on drift, which `gates:surface_snapshot` over + the six manifests already is, so `NFR-4` is ✅ (phase 9's mark, kept by phase 10's `P10-35`) and + what waits for a `v*` tag is `gates:sig_diff`, vacuous until one exists. (Corrected 2026-09-25 by + phase 10's review round 2, R2-1: this sentence had read `NFR-4` as a diff against the previous + release tag that stayed ⏳ until a tag.) **Still open after phase 10, with the reason (2026-09-25, `P10-36`):** + `tools/sig_diff.rb` takes its baseline from `git describe --tags --match v*` and reads no committed + baseline file, and phase 10 may not tag -- so the `sig/` baseline cannot be established inside this + repository's design by any phase. Phase 10 did regenerate the six runtime surface manifests once, + deliberately (two rows: `Dexpace::Protocol::HTTP_1_0` and `Dexpace::Status#standard?`), and put the + SPDX header on every shipped `.rbs` -- the last change to `sig/` before a tag. The line closes with + the first `v*` tag +- [ ] **The one-time public-surface choices the phases flagged, each decided before `NFR-4` locks it + at the first `v*` tag.** Filed 2026-09-25 by phase 10, which is the last phase and cannot hand them + forward. Adding a public name later widens (`api-design/1d9e6e0b`); removing or narrowing one after + the tag is a break, so each is decided now or deliberately kept. The list, every row read off the + tree on 2026-09-25: `BufferedSource.__dexpace_view` as a private class method reached by `send` + (3a); `Dexpace::Body`'s identity `#==`/`#eql?`/`#hash` (3b); `ResponseBody.new` requiring `#peek` + beside `#read_into` (3b); `Bundle#remote` beside `Bundle#remote?` and `ContextStore#put`/`#[]` + with no caller in `lib/` (4a); `Recovery::Orchestrator#call`'s three required positionals (4b); + `Pipeline::Stage#with`, a public row whose only behaviour is to raise (4c); `Instrumentation::Scope.build`, + public and marked `@api private` (5c); **the body-replayability predicate's three spellings** -- + `Resilience::Resend.eligible?` and `.replayable_body?` public, `Auth::Step#replayable?` private + (6a/6b/6c; the roadmap's 2026-09-18 bullet); the two transports' public `Adapter.owning`/`.borrowing` + (8a, 8c); and **the default-port elision at the model boundary** -- `URL.parse!("https://h:443/y").to_s` + is `"https://h/y"`, so a caller's explicit default port and a redirect's are both elided and + `HTTP-46`'s textual comparison sees none (6b's P6-96; measured again 2026-09-25). None is a defect; + each is a choice a release owner signs off. Cites `NFR-4`, `HTTP-46`, `RETRY-5`, `REDIR-6`, `AUTH-31` - [ ] `SECURITY.md` contact confirmed reachable and monitored - [ ] RubyGems ownership settled for every gem name above, and trusted publishing configured (OIDC-based, no long-lived API key committed anywhere) @@ -526,6 +580,22 @@ MUST state every item here**, for the same reason the section above is stated. enumeration. **The recorded consequence**: `ContextStore`'s cap, `CTX-19`'s reachability and `CTX-9`'s eviction are exercised only by phase 4a's own tests in v1. +- **Port readings phase 10 left standing, each measured 2026-09-25 and each a behaviour a consumer can + meet -- `HTTP-51`, `HTTP-12`, `SSE-11`, `IO-1`.** Added by phase 10, the last phase, which decided not + to change them and has nowhere else to put them. *(a)* **A multipart part name or filename must be + ASCII**: `MultipartBody`'s part-header sweep is the outbound header grammar, so `"résumé.pdf"` raises + `InvalidArgumentError` at write time and the caller percent-encodes first (RFC 7578 §4.2's own advice; + browsers send raw UTF-8). Widening the sweep to obs-text is additive and cannot break `NFR-4`; the + trigger is the first consumer that needs a raw UTF-8 filename. *(b)* **`Query.parse` drops a trailing + NUL** (`"a=b\0"` parses as `"a=b"`), phase 1's review R3-3 -- an HTTP-grammar reading, not a crash; + trigger: a server whose query semantics depend on it. *(c)* **A zero-padded `retry:` field longer than + ten digits is ignored** -- `00000000005` leaves the hint unset where `0000000005` sets 5 -- by the reader's ten-digit width bound (7b's + review R1-2) -- `SSE-11`'s documented cap, read strictly. *(d)* **After a mid-stream failure, + `BufferedSource.over`'s enumerator restarts `#each`** (7b's finding): a caller that rescues and reads on + re-reads the body's first bytes. The SSE facade closes itself first and never meets it; latching + exhaustion on a failure is the one-line alternative, recorded as the pick-up if a consumer reads past + a failure. The release notes state all four. + ### Post-v1 gems Design §2.2 is the authority, and the roadmap's "Post-v1" paragraph already states the rule: these seven @@ -536,8 +606,9 @@ design of 2026-09-05 stated it. - **`dexpace-async-async`**, bridging the core async pivot to `Async::Task`. Trigger: a reactor-native async adapter is needed beyond the thread-pool-backed `dexpace-async-thread`, which is - the adapter that proves the pivot. `SEAM-24`'s cancellation bridge (above) and the `XCUT-12` - fiber-scheduler trigger (under Post-release triggers, below) ride on it. + the adapter that proves the pivot. `SEAM-24`'s cancellation bridge (above) rides on it; the `XCUT-12` + fiber-scheduler trigger that once did too was closed on 2026-09-25 by phase 10's fiber verification + (under Post-release triggers, below). - **`dexpace-async-concurrent_ruby`**, bridging the pivot to `Concurrent::Promises::Future`. Trigger: `concurrent-ruby` interop is needed beyond `dexpace-async-thread`. - **`dexpace-transport-httpx`**, HTTP/2 without a reactor; a third transport over properties @@ -669,6 +740,17 @@ the trigger, then the one job to do when it fires. also the sharpest available argument for `bundle exec`. The pin's owner does not change: phase 0's plan, Task 2. Phase 10's plan, Task 1 re-measures both facts at implementation time, because a fact that moved once in a day will move again, and its Task 18 carries the numbers here if they have. + **Measurement corrected again 2026-09-25 by phase 10's execution, outside the bundle (`RUBYOPT` and + `BUNDLE_*` cleared, the stock interpreters):** `Gem::Specification.find_all_by_name("minitest")` is + 5.27.0 and 5.25.1 on 3.2.11, 5.20.0 on 3.3.12, 6.0.6 and 5.25.4 on 3.4.10, 6.0.0 and 5.27.0 on 4.0.6 -- + a 5.x copy now sits beside the 6.x one on BOTH top rows, installed on this machine since the last + measurement -- and **`require "minitest/mock"` now succeeds on all four** (it activates 5.27.0 / 5.20.0 / + 5.25.4 / 5.27.0, the newest copy that ships the file). So the 2026-09-13 sentence "still raises + `LoadError` on 4.0.6" is no longer true on this machine, and it was never a fact about the + interpreter: what a bare `require` resolves depends on what happens to be installed beside it. The + pin's reason stands and is sharper for it -- the bundle, pinned `~> 5.25`, is the only resolution that + is the same on every row. None of this is asserted in a committed test, deliberately: it is machine + state, not interpreter behaviour. - **Lifting appendix `B.1`, `B.2` and `B.5` — a second implementation of the pagination engine, the SSE reader or the configuration chain exists** → lift their assertions into `dexpace-conformance`. Until then a lifted assertion over a single subject is a test with one subject living in a package whose @@ -687,8 +769,17 @@ the trigger, then the one job to do when it fires. rather than four. What fires this: a drain implementation whose non-conformance the cap-and-drain observation cannot see — a second eviction path, or an eviction reached only on a branch the behavioural test does not drive. Touches `XCUT-14`, `NFR-17`. -- **`XCUT-12` under a fiber scheduler, the fallback — the post-v1 `dexpace-async-async` reactor-native - adapter (above) ships** → run `XCUT-12`'s single-flight assertion under a fiber scheduler, driving +- ~~**`XCUT-12` under a fiber scheduler, the fallback — the post-v1 `dexpace-async-async` reactor-native + adapter (above) ships**~~ — **CLOSED 2026-09-25 by phase 10**: the fiber-scheduler form is asserted in + `gems/dexpace-transport-async_http/test/dexpace/transport/async_http/fiber_single_flight_test.rb`, eight + fibers of one reactor on one thread against the real `Auth::BearerStamper` and `AsyncBearerStamper`, + exactly one fetch, run red against a single-flight guard keyed by thread identity (phase 10's checklist, + Guards run red 19). The judgement this entry waited on was made -- as a VERIFICATION rather than the + repair the 2026-09-13 annotation below expected: the deadlock that annotation predicts does not happen + (a fiber blocking on a `Thread::Mutex` another fiber holds parks on the scheduler), so the shipped code + was already single-flight under a reactor, and what the fiber form catches that the thread form cannot + is a guard keyed by thread identity (phase 10's `P10-21`). The entry is kept, struck, as the record: + ~~→ run `XCUT-12`'s single-flight assertion under a fiber scheduler, driving `6c`'s bearer or digest cache through a reactor via the suite contract's clause 9 `around:` wrapper. The primary disposition is phase 10's, on whose inbound list it sits (the roadmap's 2026-09-13 status note): judge whether the thread-only form phase 9 ships (Tasks 7–8) suffices, a judgement phase 9 may @@ -709,7 +800,7 @@ the trigger, then the one job to do when it fires. event that no longer means anything. It stays here until the fiber run is recorded, because a decision in a plan is not evidence the work was done. Runs on **3.3.12, 3.4.10 and 4.0.6 only**: `async`, `async-http`, `io-event` and `protocol-http1` all declare `required_ruby_version >= 3.3`, re-verified - 2026-09-13, which is `P8-36` from the other side. + 2026-09-13, which is `P8-36` from the other side.~~ - **A second continued-clause true positive for the probe's chapter-attribution check — one appears that the check cannot see** → write the sentence-spanning form. Recorded 2026-09-13 by phase 10's design. @@ -728,4 +819,48 @@ the trigger, then the one job to do when it fires. by the check — and closing it needs a sentence-spanning parser over Markdown, which 3 fires at 0 false positives does not justify. The gap is printed in the check's own output, so a reader is never told it saw something it did not. **What fires this**: a second such instance. One is an anecdote; two are a - population. Touches the requirement-ID conventions, `NFR-17`. + population. Touches the requirement-ID conventions, `NFR-17`. **Built and re-measured 2026-09-25 by + phase 10** (`.claude/skills/housekeeping/chapters.rb`, the probe's ninth check): over the live tree + the clause-scoped form fired twice, both true positives -- `SEAM-13` and `SEAM-15` inside a + backticked range the phase-8 segmentation design attributed to chapter 03, fixed in place -- and zero + false positives once the negation vocabulary was written as a union of phrases (an `/x` pattern had + silently deleted the phrases' spaces). Review round 0 (R0-8) found `appears in` listed as a negation, + which silenced the usual positive attribution; it is now a forward binding (the IDs before it pair with + the chapter after it), and the live count is unchanged. The continued-clause blind spot is `Chapters::GAPS` and is + asserted by `chapters_test.rb`; it still has one known instance, and this trigger still waits for a + second. + +- **Residues phase 10 measured and did not repair, each with the event that makes it worth doing** + (added 2026-09-25; phase 10 is the last phase and none of these has a phase to go to). *The process + tooling outside RuboCop* -- `scripts/**` and `.claude/**` are excluded in `.rubocop.yml` (P0-11, + `NFR-7`'s one documented exception); phase 10's file list did not reach `.rubocop.yml` or `scripts/` + → when either tree is next changed for its own reasons, run the autocorrect pass and delete the two + `Exclude` lines. *The per-byte `#getbyte` cost* -- about 0.9 µs a byte through `BufferedSource#getbyte`, + so the SSE line machine parses about 1 MiB/s (7b's baseline) → a consumer streaming SSE at that rate. + *`Fiber[:"dexpace.current_span"]` left set on the main fiber across suites* under the one-process + `rake test:gems` (8b) → the next suite that reads the main fiber's storage raw. *The portable + `TRANSPORT-7` row's race* against a streaming adapter (8c's review R1-1) → the next change to + `dexpace-conformance`'s transport suite. *`XCUT-9`'s two stated residues* (a collect-then-yield walk + and a depth cap equal to the cycle length pass the black-box assertion; `gates:cause_walk` is the + second line) → a second cause walk proposed anywhere. *7a's `SEAM-21` evidence living inside another + assertion's body*, declared in no ID-keyed map → the next change to `dexpace-serde-json`'s suite. + *`tools/surface.rb` blind to a `private_constant`* (it walks `Module#constants(false)`) → a private + group module made public. *The 3.4+ unused-block warning `test:gems` cannot see* (suppressed + process-wide once a same-named block-taking method is compiled; `-W:strict_unused_block` would reach + it) → a new sink or callback method declared without `&`. *Two spellings of the bare-require child + process* (`seam_surface_test.rb`'s helper and `test/support/bare_require.rb`) → the next bare-require + pin written. *6a's async pump close-before-schedule order* unpinned (an equivalent mutation) → a change + to `AsyncRetryStep`'s pump. *Phase 3a's three `untyped` signature parameters and `TeeSink`'s Float + `tap_limit` refusal*, and 7c's non-`Response` duck never closed after a parse → the next change to + those files. *The six gate tests that resolve `ruby`/`bundle` from PATH, the category assertion run + once, the third-party sub-feature refusal* (phase 0's review R3-4/R3-6/R3-7) → the next gate added. + *The probe's `claims` check does not read `CLAUDE.md`'s spelled-out `lib/` file counts* ("two hundred + and twenty … files", 5c's leftover) -- phase 10 counted its own `CLAUDE.md` edit by hand → the next + change to `.claude/skills/housekeeping/probe.rb`'s `claims` check, or the first time a spelled count + drifts unnoticed. *`docs/sdk-documentation/sse.md`'s `#with` sentence and the fence names it leaves + undefined* (7b's review R1-1), not re-measured by phase 10 → the next edit to that page, when the + fences are run and the sentence checked against `Dexpace::SSE::Event#with`. + *Phase 9's aggregate run is a hand run*: no committed test calls `Aggregate.run` over the four suites + with real subjects, so the 43/1/0/1/0 verdict was not re-run as one report by phase 10 (each suite's + driver ran in `rake test:gems`) → the first release's conformance run. *8c's two stale design facts and + `async-http`'s server letting a peer's mid-head `EOFError` reach Console* → an upstream report. diff --git a/docs/sdk-documentation/architecture.md b/docs/sdk-documentation/architecture.md index 37dc5c6..1383fcf 100644 --- a/docs/sdk-documentation/architecture.md +++ b/docs/sdk-documentation/architecture.md @@ -232,9 +232,10 @@ not do. Written against the adapter phase 8b shipped; derives from entries 3, 4 and 5 of `docs/sdk-design-ruby/10-deliberate-deviations-from-the-reference-contract.md`. [quality-gates.md](./quality-gates.md) — every blocking gate this SDK runs, what each protects, -and how to run it locally; twenty-one of them, and the table says which fixture turns each red. -Written against the build phase 0 shipped, extended by phase 7b's `gates:serde_boundary` and by -phase 9's three repository-wide invariant scans; derives from +and how to run it locally; twenty-four of them, and the table says which fixture turns each red. +Written against the build phase 0 shipped, extended by phase 7b's `gates:serde_boundary`, by +phase 9's three repository-wide invariant scans and by phase 10's three (`gates:ledger_audit`, +`gates:spdx_rbs`, `gates:sole_parse`); derives from `docs/sdk-design-ruby/09-toolchain-and-quality-gates.md`. write-a-transport.md — implementing the `Transport` seam, and proving an implementation against diff --git a/docs/sdk-documentation/http.md b/docs/sdk-documentation/http.md index 3b7a1a5..c0f2b59 100644 --- a/docs/sdk-documentation/http.md +++ b/docs/sdk-documentation/http.md @@ -113,15 +113,18 @@ casing, accepted everywhere a `String` name is. ## The value types -- **`Status`** — total over 100–599: `Status.of(520)` is a status with no canonical name, never an - error; `Status.canonical_name(code)` is the separate lookup. Equality is by code alone. Range - predicates (`success?`, `error?`, …) are derived, and `Response` delegates the same six. +- **`Status`** — total over every Integer (`HTTP-10`): `Status.of(520)` and `Status.of(999)` are + statuses with no canonical name, never an error, and only a non-Integer is refused; + `Status.canonical_name(code)` is the separate lookup. Equality is by code alone. Range predicates + (`success?`, `error?`, …) are derived, `#standard?` says whether the code is inside the protocol's + `100`–`599` classes, and `Response` delegates the same six class predicates. - **`Method`** — an upper-cased RFC 7230 token, so extension methods are representable. `Method::IDEMPOTENT` and `#idempotent?` are the single source the retry layer derives from; `#body_forbidden?` is the classification the request builder asks. Inside `module Dexpace`, `Method` is this class; write `::Method` for Ruby's. -- **`Protocol`** — `http/1.1` or `http/2`; `Protocol.parse` accepts those and the aliases - `HTTP/2` and `HTTP/2.0`, case-insensitively, and raises on anything else. +- **`Protocol`** — `http/1.0`, `http/1.1` or `http/2` (`HTTP_1_0`, `HTTP_1_1`, `HTTP_2`); + `Protocol.parse` accepts those and the alias `HTTP/2.0`, case-insensitively, and raises on anything + else — `HTTP/1.2`, `http/0.9`, `spdy/3`. - **`MediaType`** — type, subtype and parameter keys folded, parameter values as given; `parse` splits parameters respecting quoted-strings and `render` re-quotes, so `parse(render(x)) == x`; `#charset` is `nil` for an absent or unknown charset, never a raise; diff --git a/docs/sdk-documentation/quality-gates.md b/docs/sdk-documentation/quality-gates.md index b67b62d..e302763 100644 --- a/docs/sdk-documentation/quality-gates.md +++ b/docs/sdk-documentation/quality-gates.md @@ -1,12 +1,13 @@ # Quality gates Every blocking gate this SDK runs, what each protects, and how to run it locally. As built by -phase 0 (`docs/work/mvp/phase0/`), extended by phase 7b (`gates:serde_boundary`) and by phase 9 -(`gates:cause_walk`, `gates:bounded_map`, `gates:seam_names`); the design it implements is +phase 0 (`docs/work/mvp/phase0/`), extended by phase 7b (`gates:serde_boundary`), by phase 9 +(`gates:cause_walk`, `gates:bounded_map`, `gates:seam_names`) and by phase 10 (`gates:ledger_audit`, +`gates:spdx_rbs`, `gates:sole_parse`); the design it implements is `docs/sdk-design-ruby/09-toolchain-and-quality-gates.md`, and this page describes the build as it is rather than as it was planned. -`bundle exec rake` runs all twenty-one, in the order below, and stops at the first failure. Each is +`bundle exec rake` runs all twenty-four, in the order below, and stops at the first failure. Each is a separately invocable task, because a gate you cannot run alone is a gate you debug by running everything. `bundle exec rake gates:list` prints the names; `bundle exec rake -T` describes them. @@ -24,15 +25,18 @@ everything. `bundle exec rake gates:list` prints the names; `bundle exec rake -T | 10 | `gates:cause_walk` | `XCUT-9`: `Dexpace.each_cause` is the ONLY walk of an error's cause chain, checked by a parsed scan over every gem's `lib/`. A send is three node types — `a.cause` is `:CALL`, `a&.cause` is `:QCALL`, a bare `cause` is `:VCALL` — and a reflective `send`/`__send__`/`public_send`/`method` naming `:cause` is seen too, by Symbol AND by String; a Symbol literal is `:LIT` on 3.2 and 3.3 and `:SYM` on 3.4 and 4.0, so both are named. A send carrying an ARGUMENT is not a walk, which is what keeps 5b's `Event#cause(error)` builder out. Allowlist: `each_cause.rb` itself (added by phase 9) | `error.cause` anywhere else, `e&.cause`, `errors.map(&:cause)`, `e.send(:cause)`, `e.public_send("cause")` | | 11 | `gates:bounded_map` | `XCUT-14`: only `Dexpace::BoundedMap` holds a map whose key space a caller or a server chooses, checked as a parsed scan for a `Hash` assigned to an instance variable — a literal, `Hash.new`, `::Hash.new`, a chained call on a literal and an `||=` are all seen. The allowlist is a path ⇒ REASON Hash and each of its six entries carries the adjudication that put it there, because an allowlist whose entries carry no argument is a list of things somebody once silenced; a test asserts every entry names a live file the scan still reports, so a stale entry cannot silently re-open a hole (added by phase 9) | `@by_origin = {}` in a new file, `@cache = Hash.new`, `@seen ||= {}` | | 12 | `gates:seam_names` | `SEAM-2`: core never names a concrete seam implementation, matched by the LEAF namespace each adapter gem owns — `Serde::JSON`, `Transport::NetHTTP`, `Transport::AsyncHTTP`, `Async::Thread` — anywhere in a constant path or a String or Symbol literal, so `const_get("Dexpace::Serde::JSON")` is caught beside the bare constant. A fixed list of fully qualified names caught 0 of 4 realistic shapes and matching any path under a seam namespace flagged 31 conforming references, so the rule is the leaf pair; a later adapter gem adds its leaf here or is invisible, which is the gate's stated gap (added by phase 9) | `Dexpace::Serde::JSON` in core, `Transport::NetHTTP`, `const_get(:"Dexpace::Async::Thread")` | -| 13 | `gates:clean_bundle` | `SEAM-1`, `NFR-1`, `NFR-10`: each gem loads inside a scratch bundle holding only itself (and core, for an adapter); Bundler refuses every undeclared gem | a `require` of a bundled gem nobody declared — `logger` on Ruby 4.0 | -| 14 | `gates:rbs_surface` | `NFR-11`: no constant outside `Dexpace::` and a stdlib allowlist in any public signature. The signatures are loaded as one set and every name resolved against it, the way `rbs validate` does, so a relative `Headers` inside `module Dexpace` and a type variable `T` are not leaks; every type tree is then walked, so a leak inside an optional, a union, a tuple, a record, a proc or a type argument is seen | `Async::Task` as a return type, superclass, mixin (name or type argument), type alias, class or module alias, class- or method-level generic bound, or anywhere inside a type; `DexpaceX::Thing`; a set that does not resolve | -| 15 | `gates:sig_diff` | `NFR-4`: every public declaration in `sig/`, parsed and flattened to one line per overload with `private` honoured, against the last `v*` tag, per gem; a removed **or narrowed** declaration fails | a declaration or file gone since the tag, a dropped overload, a method moved under `private` (vacuous, by stated design, until the first tag) | -| 16 | `gates:surface_snapshot` | `NFR-4`: the runtime constant and method tree each gem adds to `Dexpace`, walked from the root and not from the gem's own constant — so an export placed *beside* a namespace (`Dexpace::Transport::Shared`, a method on `Dexpace::Transport`) is in the manifest — `Data.define` readers (those still public on the model) and singleton methods included, a `private_constant` excluded, one line per method, against `test/fixtures/surface/.txt`; an adapter's manifest is what its entry file adds with core already loaded, so core's lines are in core's manifest only | any export not in the manifest, inside or beside the gem's namespace; `rake surface:regenerate` is the deliberate act | -| 17 | `gates:single_instance` | design §2.4: one resolved path per core file; `Dexpace::VERSION` equals the gemspec. A second copy of core cannot finish loading once core holds `Data.define` models — Ruby refuses the re-opened class with `superclass mismatch` — and the gate reports that in the same terms, beside the features loaded twice before it | one core feature resolved from two directories; a version literal that disagrees | -| 18 | `gates:versions` | `NFR-14`, `NFR-10`: `.ruby-version`, the CI matrix, six gemspec versions and floors and six `VERSION` literals all agree with `VERSIONS` | any consumer that drifted; a gemspec that does not load | -| 19 | `gates:reproducible` | `NFR-12`: each gem built twice under a fixed `SOURCE_DATE_EPOCH` is byte-identical | a gemspec whose build depends on anything but its inputs | -| 20 | `yard` | styleguide 14.1: every public object documented | `yard stats --list-undoc` below 100% | -| 21 | `bundler_audit` | a known CVE in the resolved bundle | `bundler-audit check --update` | +| 13 | `gates:ledger_audit` | `NFR-17`, and phase 10's `P10-2` made mechanical: `docs/deviations.md` still describes design §10 -- register row N's title is a word subsequence of §10 entry N's bolded subject, its `IDs touched` column (every range expanded) EQUALS the IDs entry N names, and a row carrying a verdict cites at least one `gems/…` path that exists and names only `Dexpace::` constants that are defined, with core and every adapter the running Ruby's floor admits loaded first. A row citing only `docs/…` fails: a design document is never evidence | `test/fixtures/gates/ledger/`: a row missing an ID, two swapped rows, a verdict citing only a document, a verdict naming `Dexpace::Completer`; `ledger/workspace` for the task | +| 14 | `gates:spdx_rbs` | `NFR-13`, `NFR-3`: line 1 of every shipped `sig/**/*.rbs` is `# SPDX-License-Identifier: MIT` (the RuboCop cop cannot parse RBS), and every shipped signature declares something -- read as the third element of `RBS::Parser.parse_signature`, because the Buffer it returns first survives a `flatten` | `test/fixtures/gates/spdx_rbs/`: no header, the header on line 3, a comment-only file; `spdx_rbs/workspace` for the task | +| 15 | `gates:sole_parse` | `NFR-6`: `AstScan.parse` -- the one `$VERBOSE = nil` window -- is the only `RubyVM::AbstractSyntaxTree.parse_file` in `tools/`, `tasks/`, `test/gates/` and `.claude/skills/`, direct or reflective (`send(:parse_file, …)`, `public_send("parse_file", …)`), exempting the owner METHOD and three test contrasts listed with reasons; `Prism.parse_file` returns its diagnostics and is not the hazard. Stated gap: a receiver or a method name held in a variable | `test/fixtures/gates/sole_parse/`: a direct call, two reflective ones, an owner file whose second method calls it; `sole_parse/workspace` for the task | +| 16 | `gates:clean_bundle` | `SEAM-1`, `NFR-1`, `NFR-10`: each gem loads inside a scratch bundle holding only itself (and core, for an adapter); Bundler refuses every undeclared gem | a `require` of a bundled gem nobody declared — `logger` on Ruby 4.0 | +| 17 | `gates:rbs_surface` | `NFR-11`: no constant outside `Dexpace::` and a stdlib allowlist in any public signature. The signatures are loaded as one set and every name resolved against it, the way `rbs validate` does, so a relative `Headers` inside `module Dexpace` and a type variable `T` are not leaks; every type tree is then walked, so a leak inside an optional, a union, a tuple, a record, a proc or a type argument is seen | `Async::Task` as a return type, superclass, mixin (name or type argument), type alias, class or module alias, class- or method-level generic bound, or anywhere inside a type; `DexpaceX::Thing`; a set that does not resolve | +| 18 | `gates:sig_diff` | `NFR-4`: every public declaration in `sig/`, parsed and flattened to one line per overload with `private` honoured, against the last `v*` tag, per gem; a removed **or narrowed** declaration fails | a declaration or file gone since the tag, a dropped overload, a method moved under `private` (vacuous, by stated design, until the first tag) | +| 19 | `gates:surface_snapshot` | `NFR-4`: the runtime constant and method tree each gem adds to `Dexpace`, walked from the root and not from the gem's own constant — so an export placed *beside* a namespace (`Dexpace::Transport::Shared`, a method on `Dexpace::Transport`) is in the manifest — `Data.define` readers (those still public on the model) and singleton methods included, a `private_constant` excluded, one line per method, against `test/fixtures/surface/.txt`; an adapter's manifest is what its entry file adds with core already loaded, so core's lines are in core's manifest only | any export not in the manifest, inside or beside the gem's namespace; `rake surface:regenerate` is the deliberate act | +| 20 | `gates:single_instance` | design §2.4: one resolved path per core file; `Dexpace::VERSION` equals the gemspec. A second copy of core cannot finish loading once core holds `Data.define` models — Ruby refuses the re-opened class with `superclass mismatch` — and the gate reports that in the same terms, beside the features loaded twice before it | one core feature resolved from two directories; a version literal that disagrees | +| 21 | `gates:versions` | `NFR-14`, `NFR-10`: `.ruby-version`, the CI matrix, six gemspec versions and floors and six `VERSION` literals all agree with `VERSIONS` | any consumer that drifted; a gemspec that does not load | +| 22 | `gates:reproducible` | `NFR-12`: each gem built twice under a fixed `SOURCE_DATE_EPOCH` is byte-identical | a gemspec whose build depends on anything but its inputs | +| 23 | `yard` | styleguide 14.1: every public object documented | `yard stats --list-undoc` below 100% | +| 24 | `bundler_audit` | a known CVE in the resolved bundle | `bundler-audit check --update` | ## Where they run @@ -55,6 +59,9 @@ DEXPACE_GATE_ROOT=test/fixtures/gates/gemspec_audit/two_third_party bundle exec DEXPACE_GATE_ROOT=test/fixtures/gates/invariants/workspace bundle exec rake gates:cause_walk DEXPACE_GATE_ROOT=test/fixtures/gates/invariants/workspace bundle exec rake gates:bounded_map DEXPACE_GATE_ROOT=test/fixtures/gates/invariants/workspace bundle exec rake gates:seam_names +DEXPACE_GATE_ROOT=test/fixtures/gates/ledger/workspace bundle exec rake gates:ledger_audit +DEXPACE_GATE_ROOT=test/fixtures/gates/spdx_rbs/workspace bundle exec rake gates:spdx_rbs +DEXPACE_GATE_ROOT=test/fixtures/gates/sole_parse/workspace bundle exec rake gates:sole_parse RUBYOPT=-r./test/fixtures/gates/serde_boundary/pending_probe.rb bundle exec rake gates:serde_boundary DEXPACE_SURFACE_EXTRA=Injected bundle exec rake gates:surface_snapshot DEXPACE_SURFACE_EXTRA=Dexpace::Transport::Shared bundle exec rake gates:surface_snapshot # beside a namespace diff --git a/docs/sdk-documentation/serde.md b/docs/sdk-documentation/serde.md index 869b436..1734b7c 100644 --- a/docs/sdk-documentation/serde.md +++ b/docs/sdk-documentation/serde.md @@ -326,6 +326,7 @@ CODEC.dump_into({ "a" => 1 }, buffer, offset: 2) # => 7 buffer # => "\x00\x00{\"a\":1}\x00\x00\x00" CODEC.dump_into({ "a" => 1 }, ("\0" * 4).b, offset: 0) # => IndexError: 7 bytes at offset 0 do not fit a 4-byte buffer +require "stringio" # StringIO is not loaded by the codec sink = StringIO.new(+"".b) CODEC.dump_to([1, 2], sink) # => 5 sink.closed? # => false diff --git a/docs/sdk-documentation/transport-async_http.md b/docs/sdk-documentation/transport-async_http.md index 3b9670d..f0d987b 100644 --- a/docs/sdk-documentation/transport-async_http.md +++ b/docs/sdk-documentation/transport-async_http.md @@ -383,8 +383,9 @@ server.requests # => [] ## Lenient inbound mapping, and the two clauses it waives -A vendor status inside `100`–`599` maps with its body readable (`TRANSPORT-24`); an HTTP/2 head maps -to the model's `http/2`. A header whose **value** carries a control byte is dropped alone and logged +A vendor status maps with its body readable (`TRANSPORT-24`) — inside `100`–`599` or outside it, a +`999` included, because `Status` is total over every Integer (`HTTP-10`) and `#standard?` is the range +test; an HTTP/2 head maps to the model's `http/2` and an HTTP/1.0 head to `http/1.0` (`HTTP-33`). A header whose **value** carries a control byte is dropped alone and logged at verbose while obs-text is preserved and a repeated `Set-Cookie` survives as two values (`TRANSPORT-14`'s value clauses); a malformed `Content-Type` downgrades to no media type (`TRANSPORT-27`'s media-type clause); an absent native length is the `-1` sentinel. Two clauses are diff --git a/docs/sdk-documentation/transport-net_http.md b/docs/sdk-documentation/transport-net_http.md index c3c36bf..b85b922 100644 --- a/docs/sdk-documentation/transport-net_http.md +++ b/docs/sdk-documentation/transport-net_http.md @@ -338,14 +338,14 @@ response.body.each { |chunk| chunks << chunk } chunks.join # => "0123456789" (all of it, never "01234") ``` -**Not lenient, and stated**: `Dexpace::Protocol` admits `HTTP/1.1` and `HTTP/2` only (`HTTP-33`), so a -server answering `HTTP/1.0` makes the adapter raise `Dexpace::InvalidArgumentError` after the head, with -the connection released first (`TRANSPORT-22`). Widening `Protocol::WIRE_FORMS` is a phase-1 surface -decision on phase 10's inbound list, not an adapter's to take. The status has the same shape: `Status` -is total over `100`–`599` (`HTTP-10`'s reading, phase 1's), while `Net::HTTP` parses any three digits -and delivers a `999` or a `600` as an `HTTPUnknownResponse` — such a head raises the same -`InvalidArgumentError` after the head with the connection released, `599` maps, and whether -`TRANSPORT-24`'s "any code" reaches `600`–`999` is the same kind of phase-1 question, on the same list. +**What maps, and what still raises**: `Dexpace::Protocol` admits `HTTP/1.0` (`Protocol::HTTP_1_0`), +`HTTP/1.1` and `HTTP/2` (`HTTP-33`; phase 10 added the first), so a server answering `HTTP/1.0` maps +to `http/1.0`. A version the model does not know — an `HTTP/1.2` head — makes the adapter raise +`Dexpace::InvalidArgumentError` after the head, with the connection released first (`TRANSPORT-22`). +The status never raises: `Status` is total over every Integer (`HTTP-10`), and `Net::HTTP` parses any +three digits and delivers a `999` or a `600` as an `HTTPUnknownResponse`, so such a head maps with its +body readable (`TRANSPORT-24`); `Status#standard?` answers whether the code is inside the protocol's +`100`–`599` classes, and a `999` answers `false` to it and to every class predicate. ## TLS and the proxy diff --git a/docs/work/mvp/2026-09-05-ruby-sdk-v1-roadmap-design.md b/docs/work/mvp/2026-09-05-ruby-sdk-v1-roadmap-design.md index 080595b..9e7ff87 100644 --- a/docs/work/mvp/2026-09-05-ruby-sdk-v1-roadmap-design.md +++ b/docs/work/mvp/2026-09-05-ruby-sdk-v1-roadmap-design.md @@ -1830,12 +1830,21 @@ place; the name `@by_origin` was itself wrong on the tree, so the example reads bullet's own correction is read beside it. The rule the paragraph states — a sentence in a plan is not evidence the work was done — is what the discharge confirms, and it stands.] +**2026-09-25 — phase 10 executed, and this list is empty by disposition.** The head note above counts +"all thirty-two"; the list reached **sixty-five** bullets, thirty-three of them added after phase 10's design +and never in its disposition table. Every one of the sixty-five now carries a dated bracket naming its +disposition -- repaired here with its test, verified already fixed with the evidence, moved to a named +`docs/first-release.md` line, or withdrawn -- and +[`phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-checklist.md`](./phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-checklist.md)'s +inbound table holds the same sixty-five by date and content. Nothing is carried forward: phase 10 is the last +phase, and nothing is appended to this list after it. + **From the reconciliation.** - **The audit of the §10.5 ledger** — cross-cutting constraint 8: `ASYNC-3` and `PIPE-33`'s interrupt clause unsatisfied, `ASYNC-4` vacuous, and the trade not re-opened (`docs/first-release.md` § What v1 ships without › Unsatisfied MUSTs, which since 2026-09-13 also names `CFG-20`'s cancel-with-interrupt clause as the same - unmet clause under a second ID). + unmet clause under a second ID). [2026-09-25, phase 10 -- moved to `docs/first-release.md`: §10.5 audited, not re-opened: `docs/deviations.md` row 5 carries the verdict with `ASYNC-3` waived-would-fail in the pool's driver; the gap stays `docs/first-release.md` § Unsatisfied MUSTs. Row 1 of the inbound table in phase 10's checklist.] - **`OBS-29`'s surface decision, which is three findings and one decision.** Its two residual halves — the operation-lifecycle triple and the transport-milestone group — are **one surface decision**: a `PRE_REDIRECT`-adjacent step and/or a deliberate `RequestOptions` widening. *(a) The triple cannot be emitted @@ -1879,7 +1888,7 @@ the work was done — is what the discharge confirms, and it stands.] a shared stateless `NO_TRACER` satisfies both MUSTs — and says nothing about a recording one. **What is owed:** the surface decision, and with it the cross-reference that makes the two factories two. Touches `CTX-14`, `CTX-20`, `OBS-25`, `OBS-28`, `OBS-29`, `PIPE-2`, `PIPE-11`, `PIPE-37`, `SEAM-11`, - `SEAM-16`, `NFR-4`. + `SEAM-16`, `NFR-4`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: decided with no surface (`R6`): `OBS-29`'s own follow-up clause; `Bundle#tracer_factory`'s YARD now says it is CTX-14's span-tracer factory and not OBS-29's, pinned by `http_tracer_test.rb`; the unwired groups are a *Behavioural asymmetries* entry. Row 2 of the inbound table in phase 10's checklist.] - **The probe's "this ID is stated in chapter X" claim check, which is not built.** Measured 2026-09-13, during the register retirement: the `links` check now catches a backticked normative chapter path that does not resolve (`Links#span_findings`, built the same day), but the second half — a document asserting that a @@ -1889,15 +1898,15 @@ the work was done — is what the discharge confirms, and it stands.] rule, and most of those are not claims at all** — a chapter and an unrelated ID that merely share a line, and appendix C, which carries every ID and so can never be wrong about one. **The job:** a claim grammar ("read out of X", "stated in X", an `X §N` adjacency), a vocabulary for ranges and placeholders, and an appendix-C - special case, tuned against those roughly forty live candidates before it is allowed to gate anything. + special case, tuned against those roughly forty live candidates before it is allowed to gate anything. [2026-09-25, phase 10 -- repaired: built as the probe's ninth check, `chapters` (`.claude/skills/housekeeping/chapters.rb`, `test/chapters_test.rb`); two live true positives fixed in the phase-8 segmentation design. Row 3 of the inbound table in phase 10's checklist.] - **The judgement whether the thread-only `XCUT-12` form phase 9 ships in Tasks 7–8 suffices**, with `dexpace-async-async`'s reactor adapter as the fallback trigger and `docs/first-release.md` § Post-release - triggers as the fallback home. + triggers as the fallback home. [2026-09-25, phase 10 -- verified already fixed: judged and verified: the fiber form passes on the untouched tree (no deadlock), now asserted in `fiber_single_flight_test.rb`; the fallback trigger is closed. Row 4 of the inbound table in phase 10's checklist.] **From phase 9's own hand-offs.** - **The repairs of every audit phase 9 reports `:failed`**, each earning, for a MUST, a `docs/first-release.md` - blocker (`P9-6`). + blocker (`P9-6`). [2026-09-25, phase 10 -- verified already fixed: the one `:failed` (`NFR-13`) is repaired by Task 5; `ASYNC-3`'s waiver stands per §10.5; no other `:failed` exists. Row 5 of the inbound table in phase 10's checklist.] - **`dexpace-transport-async_http`'s `Clients#@by_origin` is an uncapped per-origin client cache, which `XCUT-14` (MUST) forbids.** `gems/dexpace-transport-async_http/lib/dexpace/transport/async_http/clients.rb` (8c plan:1683-1756, the ivar at 1715) is a plain `Hash` behind a `Thread::Mutex`, keyed by @@ -1942,7 +1951,7 @@ the work was done — is what the discharge confirms, and it stands.] **verified false positive of the scan** and never as a silenced defect: the scan sees a Hash assigned to an instance variable and cannot see a cap, and the reason recorded beside the entry is the reading of the file. The `docs/first-release.md` blocker is ticked and closed 2026-09-23, `XCUT-14`'s checklist row is ✅, - and **no phase-10 repair is owed** — the fix this bullet specifies had already landed with `8c`.] + and **no phase-10 repair is owed** — the fix this bullet specifies had already landed with `8c`.] [2026-09-25, phase 10 -- verified already fixed: already closed by phase 9 (green `gates:bounded_map`, 2026-09-23); the allowlist reason now cites `clients_test.rb`'s three `XCUT-14` cases. Row 6 of the inbound table in phase 10's checklist.] - **~~8a's `Adapter#dispatch` leaves its rescue variable unused~~ — first half CLOSED 2026-09-13 — and every repository tool that parses a filed source carries the same exposure.** As filed, this bullet reported that `docs/work/mvp/phase8/phase8a/2026-09-11-phase8a-synchronous-transport-and-conformance.md:4858` @@ -1964,7 +1973,7 @@ the work was done — is what the discharge confirms, and it stands.] no gate asserts that `AstScan.parse` is the only caller of `parse_file` in the repository. Phase 9's own exposure is closed (`AstScan.parse` opens a `$VERBOSE = nil` window restored in `ensure`, phase 9 plan:4285-4292, with Task 13's test at 4219-4227). **What would close the general case:** a check asserting - sole use of `AstScan.parse`, or a shared parse helper the gates cannot bypass. Touches `NFR-6`, `NFR-17`. + sole use of `AstScan.parse`, or a shared parse helper the gates cannot bypass. Touches `NFR-6`, `NFR-17`. [2026-09-25, phase 10 -- repaired: the general half built as `gates:sole_parse` (`tools/sole_parse.rb`, `test/gates/sole_parse_test.rb`). Row 7 of the inbound table in phase 10's checklist.] - **`NFR-13`'s SPDX gate is a RuboCop cop, so it cannot reach `sig/**/*.rbs`, which ships inside every gem.** `NFR-13` (SHOULD) is "Every source file SHOULD carry the project's license/SPDX header block" and its conformance clause is "scan **all source files** for the required header"; phase 0 mechanises it as @@ -1978,10 +1987,10 @@ the work was done — is what the discharge confirms, and it stands.] small Rake gate over `sig/**/*.rbs` beside the cop — the same three-line assertion over a different glob — or a deliberate, stated decision that `NFR-13` covers `lib/` only, put somewhere a reader will find it rather than left as the silent consequence of the mechanism phase 0 chose. Phase 9 records the gap in `NFR-13`'s - disposition row rather than closing it, per its `P9-6`. Touches `NFR-13`, `NFR-3`, `NFR-17`. + disposition row rather than closing it, per its `P9-6`. Touches `NFR-13`, `NFR-3`, `NFR-17`. [2026-09-25, phase 10 -- repaired: the SPDX header on all 307 shipped `.rbs` files plus `gates:spdx_rbs`; `packaging_suite_test.rb`'s `NFR-13` pin flipped to `:passed`. Row 8 of the inbound table in phase 10's checklist.] - **The `PAGE-15`/`P7-1` and §10.5 attribution notes in `docs/deviations.md`**, to fold into design §10 — - joined on 2026-09-13 by the seven interim notes the frozen-chapter corrections below carry. -- **Any `gates:bounded_map` that runs red.** + joined on 2026-09-13 by the seven interim notes the frozen-chapter corrections below carry. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: written out as amendments `C12` and `C13` in `docs/deviations.md`; applying them is the frozen-chapter blocker's. Row 9 of the inbound table in phase 10's checklist.] +- **Any `gates:bounded_map` that runs red.** [2026-09-25, phase 10 -- verified already fixed: every run green (`gates:bounded_map`, phase 10's tips). Row 10 of the inbound table in phase 10's checklist.] **Frozen-chapter corrections.** Each is a sentence in a chapter this repository may not edit; in every case the phase that found it shipped the code half already, and what is owed is the corrected sentence the next @@ -2012,7 +2021,7 @@ then; the two against §12's `TRANSPORT` row share one note. **or** unknown-to-this-Ruby charset, so `HTTP-42`'s fallback needs no second validation and `Encoding.find` cannot raise), and `docs/knowledge/notes/io-and-byte-streams.md` overrides `io-and-byte-streams/fbcb4d19` so the next reader does not repeat the recipe. Not a deviation from the reference contract — `HTTP-42` is - satisfied exactly. Interim note in `docs/deviations.md`. Touches `HTTP-42`, `HTTP-24`, `IO-13`, `BODY-16`. + satisfied exactly. Interim note in `docs/deviations.md`. Touches `HTTP-42`, `HTTP-24`, `IO-13`, `BODY-16`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C1`, written out; the blocker. Row 11 of the inbound table in phase 10's checklist.] - **§4's builder list drops the multipart body, which `HTTP-3` names.** `docs/sdk-design-ruby/04-domain-model-construction.md` §4 quotes `HTTP-3`'s own split and then restates the builder side as "`Request`, `Response`, `Headers`, `Query`, `RequestOptions` and `Configuration` get real @@ -2029,7 +2038,7 @@ then; the two against §12's `TRANSPORT` row share one note. (its plan, Task 7), and carries `HTTP-3` as a cross-reference row while phase 1 keeps the ID. **The addition owed:** the multipart body named in §4's builder sentence. Not a deviation — the port satisfies `HTTP-3` in full once 3b lands; only the design's restatement of the requirement is short. Touches - `HTTP-3`, `HTTP-51`, `BODY-2`. + `HTTP-3`, `HTTP-51`, `BODY-2`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C2`, written out; the blocker. Row 12 of the inbound table in phase 10's checklist.] - **§3.1's ownership sentence and §10.12 attribute `IO-6`'s rule to the retired `SEAM-3`.** `docs/sdk-design-ruby/03-seam-by-seam-idiomatic-mapping.md` §3.1's "Two ownership rules, deliberately different" paragraph reads "At the I/O layer, wrapping takes ownership: closing a `BufferedSource` built over @@ -2045,7 +2054,7 @@ then; the two against §12's `TRANSPORT` row share one note. statement:** §3.1's ownership sentence and §10.12 cite `IO-6` rather than, or alongside, `SEAM-3`. **Code half:** 3a reads `IO-6` out of appendix C and implements ownership-on-wrap, and the gap paragraph above now names appendix C as its source. The corpus repeats the attribution at `message-bodies/8a1e7a7b`, where a note - under `docs/knowledge/notes/message-bodies.md` marks it. Touches `IO-6`, `IO-16`, `SEAM-3`, `BODY-8`. + under `docs/knowledge/notes/message-bodies.md` marks it. Touches `IO-6`, `IO-16`, `SEAM-3`, `BODY-8`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C3`, written out; `IO-6` audited on its own row (ownership-on-wrap measured). Row 13 of the inbound table in phase 10's checklist.] - **§8.1 names `Event#tag(key, value)` and no requirement in chapter 15 does.** `docs/sdk-design-ruby/08-instrumentation-and-configuration.md` §8.1 fixes the event object's surface in a code block — `#field(key, value)`, `#tag(key, value)`, `#event(name)`, `#cause(error)`, `#emit`. Four are @@ -2063,7 +2072,7 @@ then; the two against §12's `TRANSPORT` row share one note. relative to `OBS-5`'s three sources, or the line goes. **Code half:** phase 5b ships `#field`, `#event`, `#cause` and `#emit` and not `#tag`, recorded as deviation `P5-18` — and not shipping it costs nothing before the first release, because adding a method widens. Interim note in `docs/deviations.md`. Touches - `OBS-4`, `OBS-5`, `OBS-8`, `NFR-4`. + `OBS-4`, `OBS-5`, `OBS-8`, `NFR-4`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C4`, written out; the blocker. Row 14 of the inbound table in phase 10's checklist.] - **§3.2's block-scoped `read_body` construction cannot satisfy the two requirements it says it satisfies "literally".** `docs/sdk-design-ruby/03-seam-by-seam-idiomatic-mapping.md:167-171` reads: "Streaming is preserved end to end: `dexpace-transport-net_http` issues the request inside @@ -2085,7 +2094,7 @@ then; the two against §12's `TRANSPORT` row share one note. have its body read on the caller's thread, and `TRANSPORT-29`'s "confined to the returned response graph" would narrow to "confined to one thread"; the abandoned-`Fiber` `ensure` is real and is *not* what decides it. Interim note in `docs/deviations.md`. Touches `SEAM-11`, `TRANSPORT-25`, `TRANSPORT-19`, `TRANSPORT-29`, - `IO-41`, `BODY-15`, `HTTP-43`. + `IO-41`, `BODY-15`, `HTTP-43`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C5`, written out; the blocker. Row 15 of the inbound table in phase 10's checklist.] - **`Net::HTTP` has a built-in automatic retry that is on by default, and §3.2, §11.18 and §12 all record that it has none.** §3.2 says "The reference transport disables nothing for **TRANSPORT-1**/**TRANSPORT-2** because `Net::HTTP` follows no redirects and retries nothing on its own — those two requirements are vacuous @@ -2112,7 +2121,7 @@ then; the two against §12's `TRANSPORT` row share one note. statement:** §12's `TRANSPORT` row and its MUST-level count stop recording `TRANSPORT-2` as vacuous for this adapter, and §3.2's and §11.18's sentences follow. **Code half:** one line, `http.max_retries = 0`, which phase 8a writes, its checklist stating the corrected reason. Interim note in `docs/deviations.md`. Touches - `TRANSPORT-2`, `TRANSPORT-3`, `TRANSPORT-17`, `TRANSPORT-18`, `RETRY-13`, `PIPE-2`, `XCUT-4`. + `TRANSPORT-2`, `TRANSPORT-3`, `TRANSPORT-17`, `TRANSPORT-18`, `RETRY-13`, `PIPE-2`, `XCUT-4`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C6`, written out; the blocker. Row 16 of the inbound table in phase 10's checklist.] - **`TRANSPORT-14`'s malformed-inbound-header-*name* clause is unreachable on `dexpace-transport-async_http`, and §12 records `TRANSPORT-14` as satisfied without qualification.** `TRANSPORT-14` (MUST): "Inbound response headers MUST be copied leniently enough that a single malformed @@ -2132,7 +2141,7 @@ then; the two against §12's `TRANSPORT` row share one note. statement:** §12's `TRANSPORT` row gains `TRANSPORT-14` to its adapter-scoped list. **Code half:** phase 8c records deviation `P8-38` and the conformance run carries a **named waiver listing `TRANSPORT-14`** per §9.3's mechanism, so the gap is reported rather than restated. Interim note in `docs/deviations.md`. Touches - `TRANSPORT-14`, `XCUT-18`, `HTTP-17`, `NFR-8`. + `TRANSPORT-14`, `XCUT-18`, `HTTP-17`, `NFR-8`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C7` (first direction), written out; the waiver re-measured green by `conformance_test.rb`. Row 17 of the inbound table in phase 10's checklist.] - **`TRANSPORT-8` is satisfiable on `dexpace-transport-async_http`, and §12 counts it among the eight MUSTs that hold vacuously — the inverse direction, and equally a defect.** `docs/sdk-design-ruby/12-appendix-requirement-coverage-index.md:41` lists "TRANSPORT-1, TRANSPORT-2, @@ -2153,7 +2162,7 @@ then; the two against §12's `TRANSPORT` row share one note. **The correct statement:** §12's `TRANSPORT` row and its MUST-level count record `TRANSPORT-8` as satisfied on the async adapter. **Code half:** phase 8c implements and asserts the discrimination, its checklist row stating it. Interim note in `docs/deviations.md`. Touches `TRANSPORT-8`, `TRANSPORT-3`, `TRANSPORT-4`, - `XCUT-2`, `ASYNC-6`, `NFR-8`. + `XCUT-2`, `ASYNC-6`, `NFR-8`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C7` (second direction), written out. Row 18 of the inbound table in phase 10's checklist.] - **§8.3's prohibition is absolute and `net-http`'s connect phase uses `Timeout.timeout`, which the cop that enforces the ban cannot see.** `/usr/lib/ruby/3.4.0/net/http.rb:1657` is `s = Timeout.timeout(@open_timeout, Net::OpenTimeout) { TCPSocket.open(conn_addr, conn_port, @local_host, @local_port) }`. @@ -2168,7 +2177,7 @@ then; the two against §12's `TRANSPORT` row share one note. find nothing, and because the same question is owed of `async-http`'s dependency closure. **The correct statement:** §8.3 gains one clause scoping the prohibition to code this repository writes. **Code half:** none is owed — the cop and the ban stand as written. Touches `ASYNC-3`, `PIPE-33`, `XCUT-13`, - `TRANSPORT-4`, `NFR-2`. + `TRANSPORT-4`, `NFR-2`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C8`, written out with the per-row measurement (net-http 0.9.1 carries both the `Timeout.timeout` connect and `TCPSocket.open(open_timeout:)`). Row 19 of the inbound table in phase 10's checklist.] - **Phase 5a's exclusions table hands proxy use to phase 8 under two requirement IDs that are about something else, and it is the second document to make that substitution** *(added 2026-09-13, phase 8a review)*. The row reads "`TRANSPORT-3`, `TRANSPORT-8` — proxy *use* and header-drop reporting on a real @@ -2187,7 +2196,7 @@ then; the two against §12's `TRANSPORT` row share one note. already owned** — phase 8a's `R17` and its plan Task 19b implement proxy use, wire `Dexpace::Proxy.resolve` into the per-call client and disposition `TRANSPORT-30`, so nothing waits on this row; what is wrong is only the pointer a later audit would follow. Touches `TRANSPORT-3`, - `TRANSPORT-8`, `TRANSPORT-13`, `TRANSPORT-30`, `CFG-22`–`CFG-28`, `OBS-19`. + `TRANSPORT-8`, `TRANSPORT-13`, `TRANSPORT-30`, `CFG-22`–`CFG-28`, `OBS-19`. [2026-09-25, phase 10 -- verified already fixed: fixed in place on 2026-09-13 by phase 10's planning; re-read 2026-09-25. Row 20 of the inbound table in phase 10's checklist.] - **§9.3 calls Minitest a default gem; it is a bundled gem, and §2.4 is built on exactly that distinction.** §9.3's argument for choosing Minitest over RSpec is that "**it ships with the interpreter as a default gem**, so the same argument §2.4 makes about `base64` and `logger` applies to the test @@ -2201,7 +2210,7 @@ then; the two against §12's `TRANSPORT` row share one note. drivers reference `::Minitest` and `::RSpec` at call time and `require` neither. **The correct statement:** §9.3 calls Minitest a bundled gem and draws the same conclusion from it. **Code half:** phase 0's plan Task 2 carries the root `Gemfile`'s explicit `minitest` line — and, since 2026-09-13, its `~> 5.25` pin, - which the Minitest-6 measurement forced. Touches `NFR-2`, `NFR-17`, `SEAM-1`. + which the Minitest-6 measurement forced. Touches `NFR-2`, `NFR-17`, `SEAM-1`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C9`, written out. Row 21 of the inbound table in phase 10's checklist.] - **§9.3 fixes the requirement ID as the *waiver's* unit and leaves the *report's* unit unstated, and an appendix-B item cannot be it.** §9.3 reads: "A failing **item** that the port has decided not to satisfy is reported as a failure by the suite and suppressed in the port's own build through a named waiver listing @@ -2218,7 +2227,7 @@ then; the two against §12's `TRANSPORT` row share one note. requirement ID; an appendix-B item is a many-to-one view over them, and its status in the coverage map is the worst among its assertions with every contributing status listed — and the 61-row map phase 9 commits is the only place the real mapping is written down, which is why a porter reading §9.3 alone still builds the - wrong granularity. Interim note in `docs/deviations.md`. Touches `NFR-17`, `ASYNC-3`, `ASYNC-4`. + wrong granularity. Interim note in `docs/deviations.md`. Touches `NFR-17`, `ASYNC-3`, `ASYNC-4`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C10`, written out. Row 22 of the inbound table in phase 10's checklist.] - **Nothing in the MVP constructs or promotes an execution context, so `CTX-16`'s operation name never reaches the tracing seam it is defined to label.** Phase 4a ships the whole promotion chain — 20 `CTX` IDs, three flavours, the bounded store — and `CTX-16` makes the operation name "a schema-defined operation id @@ -2245,7 +2254,7 @@ then; the two against §12's `TRANSPORT` row share one note. `Pipeline#call` threaded into `Instrumentation::Step`), or whether driving the chain is documented as the SDK author's job in `docs/sdk-documentation/`. **Code half:** none written; phase 6a, Task 8 is the file and the widening shape either answer extends (`api-design/1d9e6e0b`, so `NFR-4` permits it). Touches - `CTX-14`, `CTX-16`, `SEAM-28`, `OBS-34`, `PIPE-11`, `NFR-4`. + `CTX-14`, `CTX-16`, `SEAM-28`, `OBS-34`, `PIPE-11`, `NFR-4`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: decided with no surface (`R7`); the correlation chain joins the worked-example blocker and the *Behavioural asymmetries* entry. Row 23 of the inbound table in phase 10's checklist.] - **`PAGE-14` and `SSE-26`/`SSE-40` guard the identical single-use latch with two different error families, and the divergence has to be settled before `NFR-4` locks either.** Phase 7's own segmentation design pairs them — "**`PAGE-14` and `SSE-26`'s single-use guards are two flags on two objects**" @@ -2264,7 +2273,7 @@ then; the two against §12's `TRANSPORT` row share one note. error family for the single-use latch across both subsystems — the `SSE-26`-shaped state error, with `PAGE-14` raising it too, or one shared core type both namespaces use. **Code half:** one `raise` line in each sub-phase's view task — `7c`'s is Task 12 — plus its `sig/`, and it is cheap only before the `NFR-4` - diff records two families. Touches `PAGE-14`, `SSE-26`, `SSE-40`, `NFR-4`, `SEAM-29`. + diff records two families. Touches `PAGE-14`, `SSE-26`, `SSE-40`, `NFR-4`, `SEAM-29`. [2026-09-25, phase 10 -- verified already fixed: the premise is gone: 7c raises `Page::PageStateError < ::StandardError` (`page/page_state_error.rb:17`), refuted as an ArgumentError by `page_state_error_test.rb`; `R8` not carried out (`P10-22`). Row 24 of the inbound table in phase 10's checklist.] **From phase 10's own planning read** *(all eight added 2026-09-13)*. Phase 10's design ran the sweep this list was built from rather than trusting the list, `grep -rn -i -e 'phase 10' -e 'phase-10' docs/work/`, and @@ -2283,7 +2292,7 @@ design. statement of `SSE-19` and nothing says so.** This is the first correction on this list against the **normative** specification rather than the design, so it is also a recommendation to the specification author in §11's own idiom. **Code half: already shipped** — 7b's configurable cap, `P7-21`. Interim note in - `docs/deviations.md`. Touches `SSE-19`, `SSE-11`, `SSE-12`. + `docs/deviations.md`. Touches `SSE-19`, `SSE-11`, `SSE-12`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C11`, written out; also a recommendation to the specification author. Row 25 of the inbound table in phase 10's checklist.] - **Phase 2's three other declared-and-unwritten `sig/` files, which no gate can see.** Handed forward by 7a, which settled one clause of one of them. `docs/work/mvp/phase2/2026-09-07-phase2-seam-foundations.md:4899-4900` (Step 6, "Write the four `sig/` mirrors", header at :4897) @@ -2301,7 +2310,7 @@ design. subclasses; 7a edited `_Codec` in place (`#media_type` to `(Dexpace::MediaType | String)`, `#load` over `Dexpace::Serde::_Witness`, `#dump_to` to `Integer`) rather than writing a second one, so there is no empty declaration to find and the "no gate can see it" hazard has no instance here; no action for - phase 10.] + phase 10.] [2026-09-25, phase 10 -- withdrawn: withdrawn by 7a on 2026-09-20 (premise false on the tree); `gates:spdx_rbs`'s empty-declarations check finds 0 of 307. Row 26 of the inbound table in phase 10's checklist.] - **`Dexpace::Protocol.parse` has no alias for `"http/1.0"`, so a real `HTTP/1.0` response makes `ResponseMapper` raise.** Handed forward by 8a, measured at its plan `:233` and restated at `:4323`. Only `http/1.1`, `http/2` and `http/2.0` fold into a recognised wire form, so `native.http_version` produces @@ -2309,13 +2318,13 @@ design. plan answers `HTTP/1.1`, so the suite never exercises it — and any real server can reach it. 8a routed it here rather than fixing it because widening `Protocol::WIRE_FORMS` is a **phase-1 surface decision no sub-phase should take alone**; the widening is additive, so `NFR-4` permits it. **Code half: none** — 8a - records one sentence in `ResponseMapper`'s YARD. Touches `HTTP-24`, `HTTP-43`, `NFR-4`. + records one sentence in `ResponseMapper`'s YARD. Touches `HTTP-24`, `HTTP-43`, `NFR-4`. [2026-09-25, phase 10 -- repaired: `Protocol` gains `http/1.0` in `WIRE_FORMS` and `ALIASES` and `Protocol::HTTP_1_0` (`HTTP-33`); both adapters map an HTTP/1.0 head. Row 27 of the inbound table in phase 10's checklist.] - **8a's forward-obligations row for the two declined `TRANSPORT` SHOULDs is half stale.** Handed forward by 8a at its design `:2219`, the only literal `| **Phase 10**, …` row in any phase document, naming `TRANSPORT-28`'s third clause **and `TRANSPORT-30`**. `TRANSPORT-30` is no longer declined: 8a's own `R17` and plan Task 19b implement proxy use, and `docs/first-release.md` narrowed it out of the ships-without entry on 2026-09-13. A forward row handing a later phase a decision already taken spends the audit budget - on rediscovery. **Fixed in this pass**, dated, in 8a's design. Touches `TRANSPORT-28`, `TRANSPORT-30`. + on rediscovery. **Fixed in this pass**, dated, in 8a's design. Touches `TRANSPORT-28`, `TRANSPORT-30`. [2026-09-25, phase 10 -- verified already fixed: fixed in place on 2026-09-13 by phase 10's planning. Row 28 of the inbound table in phase 10's checklist.] - **`APPENDIX_B.md`'s check 2 is weaker than the prose that specifies it.** Found by reading phase 9's plan against phase 9's design. The design settles on **per-row ID-set equality** against the set parsed from each appendix-B item's own text — "decidable, strictly stronger, and it makes the distinct-ID coverage check hold @@ -2326,24 +2335,24 @@ design. equality check needs**, per section and index, plus the assertion — and because `generate` carries hand-written rows over verbatim (:5726), the drift the check would catch is confined to those. Both the line citation and the size of the claim corrected 2026-09-13. So the 61-row map documents a check it does not perform, in the artifact that is "the only place - the real mapping is written down". **Code half: none** — the accessor does not exist. Touches `NFR-17`. + the real mapping is written down". **Code half: none** — the accessor does not exist. Touches `NFR-17`. [2026-09-25, phase 10 -- verified already fixed: already done by phase 9: `tools/appendix_b.rb` and `test/gates/appendix_b_test.rb`'s set-equality test. Row 29 of the inbound table in phase 10's checklist.] - **`TransportSuite` is not on `Runner`, leaving two status-deciding paths in one gem.** Phase 9's `P9-10` records it and its reason: phase 8 owns that file, so phase 9 deliberately did not refactor it, and "a future change to the five statuses must be made twice." Phase 10 is the first phase that owns every gem, so - the reason has expired. **Code half: none.** Touches `NFR-17`, `NFR-4`. + the reason has expired. **Code half: none.** Touches `NFR-17`, `NFR-4`. [2026-09-25, phase 10 -- repaired: `TransportSuite.run` delegates to `Runner.run` with the teardown supplied through `around:`; the five-status invariant test in `transport_suite_test.rb`. Row 30 of the inbound table in phase 10's checklist.] - **A design document that promised a shape the code did not take is phase 10's doc repair.** Phase 9's `R3` (its design:536-542) fixes the rule for the requirement half — assert against what arrived, record both shapes — and routes the document half here: "a document the next reader will trust wrongly". It is a **method rather than a subject**, which is why it is written out: phase 10's audit tasks each route their own instances, and a bullet that named none would read as though none existed. Touches every `XCUT`/`NFR` - subject phase 9 probed. + subject phase 9 probed. [2026-09-25, phase 10 -- verified already fixed: a method, applied: each audit row names its as-built evidence (`docs/deviations.md`), and every design found wrong is bracket-corrected in place. Row 31 of the inbound table in phase 10's checklist.] - **The `minitest` pin's ownership.** Phase 9's own open question 1: the pin is phase 0's artifact and `R6` makes a repair to phase 0's tree phase 10's, so "the decision may not be phase 9's at all". Phase 10 re-measured and the answer is that **the pin is right and its owner does not change** — what is owed is the corrected measurement, because two of the three per-interpreter versions phase 9 recorded are stale and the 3.4 row is now affected in a different way from the 4.0 row. Carried to `docs/first-release.md` § Post-release triggers, the Minitest 6 entry, dated. Touches `NFR-6`, `NFR-17`, - `NFR-10`, `NFR-2`. + `NFR-10`, `NFR-2`. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: re-measured 2026-09-25; the Minitest trigger's numbers corrected in `docs/first-release.md`; the pin stays phase 0's. Row 32 of the inbound table in phase 10's checklist.] - **The process tooling is outside the RuboCop baseline.** Found by phase 0's implementation on 2026-09-14, the first time `rubocop --fail-level=convention` ran against the whole tree: 303 of the 310 findings were in the four pre-existing files under `scripts/` — the knowledge CLI, its structure and drift verifiers and their @@ -2357,7 +2366,7 @@ design. `Dexpace/NoKeywordSplat` hits — reviewed as such, after which the two `Exclude` lines are deleted. **Code half: the two lines and the four files.** Touches `NFR-7`, `NFR-13`. Added after phase 10's planning pass, so it is the thirty-third bullet and is not yet in its design's disposition table; phase 10 - dispositions it at execution. + dispositions it at execution. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: `docs/first-release.md` § Post-release triggers, the residues line: `.rubocop.yml` and `scripts/` stayed outside phase 10's file list. Row 33 of the inbound table in phase 10's checklist.] - **`rake rubocop` passes vacuously in a worktree nested under the parent checkout's `.claude/`.** Found by phase 1's implementation on 2026-09-14, the first time the gate ran from such a worktree: RuboCop takes `AllCops/Exclude` from the topmost `.rubocop.yml` on the path — here the parent checkout's, whose @@ -2369,7 +2378,7 @@ design. with `test/gates/rubocop_config_test.rb` asserting it — and it is a change to a phase-0 gate, so it is phase 10's. **Code half: the one flag and its assertion.** Touches `NFR-7`, `NFR-17`. Added after phase 10's planning pass, so it is the thirty-fourth bullet and is not yet in its design's disposition table; - phase 10 dispositions it at execution. + phase 10 dispositions it at execution. [2026-09-25, phase 10 -- repaired: `--ignore-parent-exclusion` on the gate's command, asserted by `rubocop_config_test.rb`. Row 34 of the inbound table in phase 10's checklist.] - **Nine later-phase design documents cite a retired corpus key, `data-modeling/5bc538ba`, as narrowing the wire model's Ractor-shareability claim.** The phase 3, 4, 5 and 7 segmentation designs and the 4a, 4b, 5a, 5b and 8a designs each lean on that note — "`data-modeling/5bc538ba` narrows the shareability claim" — and @@ -2381,7 +2390,7 @@ design. documents, which is audit work against planned phases and therefore phase 10's. **Documentation half only: repoint each citation and reword each sentence to the as-built claim.** Touches `HTTP-1`, `XCUT-15`. Added after phase 10's planning pass, so it is the thirty-fifth bullet and is not yet in its design's disposition - table; phase 10 dispositions it at execution. + table; phase 10 dispositions it at execution. [2026-09-25, phase 10 -- repaired: each of the eight designs carries a dated bracket naming the retired key and `P1-13`. Row 35 of the inbound table in phase 10's checklist.] - **`Dexpace::URL.parse!` accepts a host-less `http:` and any absolute non-HTTP URI, so a `Request` can carry a URL no transport can dispatch, and nothing owns the rejection.** Found by the round-3 review of phase 1's stack on 2026-09-15: `URI::Generic#absolute?` is only "a scheme is present", so `URL.parse!("http:")` @@ -2398,7 +2407,7 @@ design. **Code half: one check in one of those two places, with its negative tests; documentation half: the choice, in the owning phase's checklist row.** Touches `HTTP-47`. Added after phase 10's planning pass, so it is the thirty-sixth bullet and is not yet in its design's disposition table; phase 10 - dispositions it at execution. + dispositions it at execution. [2026-09-25, phase 10 -- repaired: `URL.parse!` refuses a host-less http-family URL and wraps every `URI::Error` (`HTTP-47`); which other schemes dispatch stays the transport's call. Row 36 of the inbound table in phase 10's checklist.] - **`MultipartBody` refuses a non-ASCII part name or filename, because its part-header sweep is the outbound header grammar.** Found by phase 3b's implementation on 2026-09-16. Phase 3b's design has every assembled part-header line swept by phase 1's `HeaderSyntax.valid_outbound_value?` "as a second, whole-line sweep", @@ -2412,7 +2421,7 @@ design. `MultipartBody#sweep!` with a non-ASCII filename test; documentation half: `docs/sdk-documentation/body.md`'s multipart section and 3b's checklist row.** Touches `HTTP-51`. Added after phase 10's planning pass, so it is the thirty-seventh bullet and is not yet in its design's disposition table; phase 10 dispositions it at - execution. + execution. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: not taken: `docs/first-release.md` § Behavioural asymmetries, the port-readings entry (a). Row 37 of the inbound table in phase 10's checklist.] - **Phase 0's plan says the keyword-splat cop "carries no ordinal" and that phase 4a's cop is "the seventh"; every as-built document counts it and calls phase 4a's the eighth.** Found 2026-09-16 by phase 4a's implementation. Phase 0's plan, Task 4 amendment of 2026-09-13 numbers the original five, calls phase 2's @@ -2425,7 +2434,7 @@ design. rewrites them; the one true count is eight, and phase 0's two sentences should say so or say why the ordinal is retired. Touches no requirement ID. Added after phase 10's planning pass, so it is the thirty-eighth bullet and is not yet in its design's disposition table; phase 10 dispositions it at - execution. + execution. [2026-09-25, phase 10 -- repaired: phase 0's plan carries a dated bracket with the as-built count (eight). Row 38 of the inbound table in phase 10's checklist.] - **`Fiber[:k] = nil` retains the key with a `nil` value on Ruby 3.2, and `Fiber["k"]` raises `TypeError` on 3.2 and 3.3 — two carrier facts the phase-5 documents and phase 8b's plan state as facts of the range that are facts of 3.3+ and 3.4+ respectively.** Found 2026-09-17 by phase 5c's implementation, @@ -2445,7 +2454,7 @@ design. misled at execution. **Documentation half only: the two plans' sentences and the charter's fact 1, to the as-built range.** Touches `OBS-10`, `OBS-23`, `OBS-24`, `ASYNC-9`, `ASYNC-11`. Added after phase 10's planning pass, so it is the thirty-ninth bullet and is not yet in its design's disposition table; - phase 10 dispositions it at execution. + phase 10 dispositions it at execution. [2026-09-25, phase 10 -- repaired: 5b's plan, 8b's plan and the phase-5 charter carry a dated bracket naming the floor facts. Row 39 of the inbound table in phase 10's checklist.] - **Phase 5b's design states two verified facts that the as-built code no longer rests on: fact 6's rebuild route (`u.userinfo = "***:***"` and `#to_s`) and fact 13's "`Async::Future` offers `#on_settle` and no combinator".** Found 2026-09-17 by phase 5b's implementation. Measured on 3.2.11, 3.3.12, 3.4.10 @@ -2461,7 +2470,7 @@ design. addendum records both beside the facts they correct. **Documentation half only: the two facts and `P5-27`'s mechanism sentence, to the as-built route.** Touches `OBS-14`, `OBS-15`, `OBS-36`. Added after phase 10's planning pass, so it is the fortieth bullet and is not yet in its design's disposition - table; phase 10 dispositions it at execution. + table; phase 10 dispositions it at execution. [2026-09-25, phase 10 -- verified already fixed: already corrected in 5b's design As-built addendum; a dated bracket at the fact now points there. Row 40 of the inbound table in phase 10's checklist.] - **The warnings-fatal gate runs every gem suite in one process, and Ruby 3.4+'s unused-block warning is suppressed process-wide once any same-named method that takes a block has been compiled — so the gate cannot see the warning on a method that declares no block and is called with one.** Found @@ -2477,7 +2486,7 @@ design. `-W:strict_unused_block` added to `RUBYOPT`, or run each suite file in its own process, and either is a change to phase 0's gate that `test/gates/` would have to prove against a fixture. Touches `NFR-6`, `OBS-1`. Added after phase 10's planning pass, so it is the forty-first bullet and is not - yet in its design's disposition table; phase 10 dispositions it at execution. + yet in its design's disposition table; phase 10 dispositions it at execution. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: `docs/first-release.md` § Post-release triggers, the residues line (`-W:strict_unused_block`). Row 41 of the inbound table in phase 10's checklist.] - **Four phase-3b and phase-4b tests assert the default materialisation ceiling and read the live one, so an exported `MAX_MATERIALIZED_BYTES` fails them.** Found 2026-09-17 by phase 5b's implementation, running the whole core suite with the configuration environment exported to hostile values @@ -2492,7 +2501,7 @@ design. it, or pin the slot with `Dexpace.configure` inside the test — and is audit work against phases 3b and 4b. Every phase-5b suite is hermetic under the same environment. Touches `IO-9`, `BODY-19`, `BODY-22`, `BODY-30`, `RECOV-16`, `CFG-11`. Added after phase 10's planning pass, so it is the forty-second - bullet and is not yet in its design's disposition table; phase 10 dispositions it at execution. + bullet and is not yet in its design's disposition table; phase 10 dispositions it at execution. [2026-09-25, phase 10 -- repaired: the four tests pin the default ceiling at the override tier (`test/support/pinned_ceiling.rb`); red under `MAX_MATERIALIZED_BYTES=4096` before, green after. Row 42 of the inbound table in phase 10's checklist.] - **Phase 6a's design describes an async retry pump that recurses, and states the opposite.** Found 2026-09-18 by phase 6a's implementation. The design's `AsyncRetryStep` sketch re-arms by calling `pump.call(attempt + 1)` from inside the delay future's `on_settle` callback and its `RETRY-30` @@ -2506,7 +2515,7 @@ design. design's `R2` paragraph right for the wrong reason: `Async.delay`'s `SeamError` is raised synchronously and is caught by the fence around the *caller*, never by a callback's rescue. Touches `RETRY-30`, `RETRY-31`, `RETRY-33`. Added after phase 10's planning pass, so it is the forty-third bullet and is not - yet in its design's disposition table; phase 10 dispositions it at execution. + yet in its design's disposition table; phase 10 dispositions it at execution. [2026-09-25, phase 10 -- verified already fixed: already corrected in 6a's design As-built addendum (`P6-54`); a dated bracket at the sketch now points there. Row 43 of the inbound table in phase 10's checklist.] - **After phase 6 the body-replayability predicate has three spellings, two of them public and `NFR-4`-locked.** Found 2026-09-18 by phase 6c's implementation, reading the three sub-phase designs side by side: 6a's plan writes `Dexpace::Resilience::Resend.eligible?(request)` for `RETRY-5`'s @@ -2521,7 +2530,7 @@ design. Touches `RETRY-5`, `REDIR-6`, `AUTH-31`, `NFR-4`. Added after phase 10's planning pass by phase 6c and referred to by date and content, never by ordinal, because phase 6a's lane is adding bullets to this list at the same time; it is not yet in phase 10's design's disposition table, and phase 10 - dispositions it at execution. + dispositions it at execution. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: not unified: the one-time surface-choices blocker in `docs/first-release.md` names the three spellings. Row 44 of the inbound table in phase 10's checklist.] - **An explicit scheme-default port is elided at phase 1's model boundary, and a redirect target inherits it.** Found 2026-09-19 by phase 6b's implementation, re-running its `Location` facts on every interpreter: `URI#to_s` drops an explicit `:443` on `https` and `:80` on `http` on every supported @@ -2537,7 +2546,7 @@ design. since the answer changes what a caller's request renders and not only a redirect's. Touches `REDIR-13`, `HTTP-46`, `HTTP-47`, `XCUT-15`. Recorded as 6b's P6-96; added after phase 10's planning pass and referred to by date and content, never by ordinal; not yet in phase 10's design's disposition - table, which dispositions it at execution. + table, which dispositions it at execution. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: recorded as a port reading: the one-time surface-choices blocker names the default-port elision (`HTTP-46`). Row 45 of the inbound table in phase 10's checklist.] - **After a mid-stream failure, `BufferedSource.over`'s enumerator restarts `#each` from its first chunk, so a further read re-delivers the body's first bytes rather than nil or a second failure.** Found 2026-09-20 by phase 7b's implementation, re-running its facts on every interpreter against a @@ -2554,7 +2563,7 @@ design. exhausted" read literally, is phase 10's `IO` audit to decide, since it changes what every `.over` consumer sees and not only the SSE reader's. Touches `IO-1`, `IO-16`, `IO-22`, `SSE-27`, `SSE-29`. Routed by 7b's checklist; referred to by date and content, never by ordinal; not in phase 10's design's - disposition table, which dispositions it at execution. + disposition table, which dispositions it at execution. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: `docs/first-release.md` § Behavioural asymmetries, port-readings entry (d). Row 46 of the inbound table in phase 10's checklist.] - **The per-byte read path through `BufferedSource#getbyte` costs about 0.9 µs a byte, so the SSE line machine parses at roughly 1 MiB/s on 4.0.6.** Found 2026-09-20 by phase 7b's implementation, timing its at-scale cap battery: every `#getbyte` pays a `Closeable#closed?` mutex acquisition (`IO-38`, @@ -2568,7 +2577,7 @@ design. a performance decision across 3a and 7b, for phase 10's `IO` audit, with the numbers above as the baseline. Touches `IO-11`, `IO-38`, `SSE-2`, `SSE-39`. Routed by 7b's checklist; referred to by date and content, never by ordinal; not in phase 10's design's disposition table, which dispositions it at - execution. + execution. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: `docs/first-release.md` § Post-release triggers, the residues line. Row 47 of the inbound table in phase 10's checklist.] - **`gates:clean_bundle` installs into the interpreter's own gem directory, and the first adapter with a third-party dependency makes that a network fetch on two matrix rows.** Found 2026-09-20 by phase 7a's implementation, the first to run the gate against a gem whose gemspec declares a third-party gem @@ -2587,7 +2596,7 @@ design. and content, never by ordinal. [closed by 8a's Task 23 on this tree, 2026-09-21: `clean_bundle_check` sets `BUNDLE_PATH` to a `vendor/` directory beside the scratch `Gemfile`, so the fetch lands inside the run's own `Dir.mktmpdir` and nothing outside it changes — run green for all six gems on 3.2.11, 3.3.12, - 3.4.10 and 4.0.6 by the reconciliation that landed 8a after phase 7] + 3.4.10 and 4.0.6 by the reconciliation that landed 8a after phase 7] [2026-09-25, phase 10 -- verified already fixed: closed by 8a's Task 23 on 2026-09-21 (its own bracket). Row 48 of the inbound table in phase 10's checklist.] - **`rbs_collection.yaml`'s header comment is stale: "json arrives with dexpace-serde-json's codec in phase 7", and phase 7a added no row.** Found 2026-09-20 by phase 7a's implementation and routed here by its review round 0 (R0-1): `json`'s signatures are rbs's own stdlib set — `rbs collection install` @@ -2597,7 +2606,7 @@ design. first row (`net-http`), so 7a's correction of the sentence was dropped rather than merged ahead of that row; whichever lane adds the first row rewrites the sentence and closes this bullet, and if none does before phase 10, the repair is a one-comment edit. No gate reads the comment. Touches nothing - normative. Recorded by 7a on its docs branch; referred to by date and content, never by ordinal. + normative. Recorded by 7a on its docs branch; referred to by date and content, never by ordinal. [2026-09-25, phase 10 -- repaired: `rbs_collection.yaml`'s header rewritten to what the file holds. Row 49 of the inbound table in phase 10's checklist.] - **`net-http`'s connect-phase `Timeout.timeout` has an observable: the first connection in a process starts Ruby's process-wide `Timeout` thread, on the 3.2, 3.3 and 3.4 rows and not on 4.0.** Found 2026-09-20 by phase 8a's implementation, running its adapter suite on every interpreter: below @@ -2615,7 +2624,7 @@ design. thread behind, which a host that counts threads will see once and never again. Touches `NFR-6`, `XCUT-11`, `XCUT-13`. Added after phase 10's planning pass by phase 8a and referred to by date and content, never by ordinal, because phases 7a, 7b and 7c are adding bullets to this list at the same - time; not yet in phase 10's design's disposition table, which dispositions it at execution. + time; not yet in phase 10's design's disposition table, which dispositions it at execution. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: amendment `C8` carries the clause; the warm-up stays. Row 50 of the inbound table in phase 10's checklist.] - **rbs 4.2.0 types `TCPServer#initialize` as `(?String host, Integer port)` — an optional positional before a required one — and Steep 2.1.0 refuses the two-argument call Ruby accepts.** Found 2026-09-20 by phase 8a's implementation, running `steep check` over the conformance target for the first time: @@ -2628,7 +2637,7 @@ design. tcp_server.rbs`, and removes the local when it has, or records the workaround in `docs/knowledge/notes/type-system.md` when it has not. Touches `NFR-3`. Added after phase 10's planning pass by phase 8a and referred to by date and content, never by ordinal; not yet in phase 10's design's disposition table, which - dispositions it at execution. + dispositions it at execution. [2026-09-25, phase 10 -- verified already fixed: the bundle's rbs is still 4.2.0 on every row, so the `untyped` local in `WireServer#initialize` stays; nothing to remove. Row 51 of the inbound table in phase 10's checklist.] - **Phase 6b's `REDIR-23` proof carries a ten-second wall-clock bound over 5,000 hops, and the bound fails under machine load while the stack-flatness half still passes.** Found 2026-09-20 by phase 8a's implementation, running `test:gems` on 4.0.6 while three other lanes ran their own gates on the same @@ -2645,7 +2654,7 @@ design. a per-hop budget measured against a warm-up drive, and apply the same reading to any wall-clock assertion its `NFR-6` audit finds. Touches `REDIR-23`, `NFR-6`. Added after phase 10's planning pass by phase 8a and referred to by date and content, never by ordinal; not yet in phase 10's design's - disposition table, which dispositions it at execution. + disposition table, which dispositions it at execution. [2026-09-25, phase 10 -- repaired: the ten-second bound is gone; the depth comparison is the proof (`step_test.rb`). Row 52 of the inbound table in phase 10's checklist.] - **Phase 6a's `RETRY-42` / `RECOV-28` eight-thread budget test is interleaving-dependent, and errors under the whole-repository process on a loaded machine.** Found 2026-09-20 by phase 8a's review round 1, on the same machine and under the same three concurrent lanes as the `REDIR-23` bullet above: @@ -2664,7 +2673,7 @@ design. draws, and read this together with the `REDIR-23` bullet as the same `NFR-6` class. Touches `RETRY-42`, `RECOV-28`, `NFR-6`. Added after phase 10's planning pass by phase 8a and referred to by date and content, never by ordinal; not yet in phase 10's design's disposition table, which - dispositions it at execution. + dispositions it at execution. [2026-09-25, phase 10 -- repaired: each thread draws from its own script and the invariant is asserted per thread (`recovery_retry_test.rb`). Row 53 of the inbound table in phase 10's checklist.] - **`Dexpace::Status` guards `100`–`599` while `Net::HTTP` parses any three-digit code, so a `999` or a `600` head raises `Dexpace::InvalidArgumentError` out of the net-http adapter — a phase-1 model question in the shape of the `"http/1.0"` bullet above.** Found 2026-09-20 by phase 8a's review round 2 (R2-2), @@ -2687,7 +2696,7 @@ design. `ResponseMapper`'s YARD beside its `HTTP/1.0` sentence, and in the as-built page. Touches `HTTP-10`, `TRANSPORT-24`, `TRANSPORT-22`, `NFR-4`. Added after phase 10's planning pass by phase 8a and referred to by date and content, never by ordinal; not yet in phase 10's design's disposition table, which - dispositions it at execution. + dispositions it at execution. [2026-09-25, phase 10 -- repaired: a MUST repair (`HTTP-10`): `Status` maps every Integer code and the protocol range is `Status#standard?` (a first repair to 0..999 was widened by the phase's review round 0, R0-5). Row 54 of the inbound table in phase 10's checklist.] - **Two child-process idioms prove one property — `seam_surface_test.rb`'s private `bare_require_report` and `test/support/bare_require.rb`'s `BareRequire#bare_require` each spawn a `ruby` that requires `dexpace` alone and read a seam registry back — and the tree should carry one.** @@ -2710,7 +2719,7 @@ design. property has one spelling in `test/support/` and the next adapter's pins (8b's and 8c's, on `AsyncTransport`) inherit it. Touches `SEAM-1`, `SEAM-2`, `SEAM-6` (the IDs both spellings assert) and nothing normative in the code. Recorded by the 8a reconciliation on its docs branch; - referred to by date and content, never by ordinal. + referred to by date and content, never by ordinal. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: `docs/first-release.md` § Post-release triggers, the residues line. Row 55 of the inbound table in phase 10's checklist.] - **Under the one-process `rake test:gems` the main fiber's storage carries 5c's no-op span (`dexpace.current_span => Instrumentation::NO_SPAN`) into suites that never activated one, and the shared test case asserts nothing about fiber storage at `teardown`.** Found 2026-09-21 by phase 8b's @@ -2726,7 +2735,7 @@ design. beside its thread count, which is the same class of cross-suite leak `NFR-6` already guards for threads. Touches `OBS-23`, `OBS-24` and the 5c-owned `dexpace.current_span` carrier, and nothing normative in the code. Recorded by phase 8b on its docs branch; referred to by date and content, - never by ordinal. + never by ordinal. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: `docs/first-release.md` § Post-release triggers, the residues line. Row 56 of the inbound table in phase 10's checklist.] - **Core's two duration guards, `Dexpace::Async.validate_delay` and `Dexpace::Clock::Guard.duration`, admit a `NaN` and a `Complex`.** Found 2026-09-21 by phase 8b's review round 1 (R1-1), which found the same shape — `is_a?(::Numeric)` and `negative?` — in `Pool#delay`, where a `NaN` duration killed the @@ -2740,7 +2749,7 @@ design. did, with the same three tests, and decide whether `Future#value(deadline:)` and `Completer#await`'s deadline arithmetic want the same screen. Touches `CFG-15`, `CFG-17`, `CFG-18` and `XCUT-11`, and nothing normative beyond the messages. Recorded by phase 8b's review round 1 on its docs branch; - referred to by date and content, never by ordinal. + referred to by date and content, never by ordinal. [2026-09-25, phase 10 -- repaired: `Async.validate_delay` and `Clock::Guard.duration` refuse NaN and Complex (P8-77's shape), tested in `delay_test.rb` and `clock_test.rb`. Row 57 of the inbound table in phase 10's checklist.] - **Two of phase 8c's design facts are stale on `async` 2.46.0, and `async-http`'s server lets a peer's mid-head `EOFError` reach Console.** Found 2026-09-21 by phase 8c's implementation against the bundle's `async` 2.46.0 / `async-http` 0.105.0, where the design measured 0.104.0. Verified fact 8 @@ -2761,7 +2770,7 @@ design. worked around in the test fixture (`AsyncHTTPServerFixture::QuietServer`), and worth an upstream report rather than an SDK change. Touches `TRANSPORT-8`, `ASYNC-6` and nothing normative in the code. Recorded by phase 8c on its docs branch; referred to by date and content, never by ordinal, - because 8b's lane is writing to the same list. + because 8b's lane is writing to the same list. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: read in 8c's As-built addendum; the Console report is an upstream matter -- the residues line. Row 58 of the inbound table in phase 10's checklist.] - **The portable `TRANSPORT-7` row proves its delivered-body clause by chance against a streaming adapter.** Found 2026-09-21 by phase 8c's review round 1 (R1-1), measured by the fix round: the row cancels the token on a second thread the moment the server has written the head, and against a @@ -2780,7 +2789,7 @@ design. through a short transport timeout — changes what the row asserts on 8a's driver and 8a's own `RawWireTransport` proof, so it is conformance-gem work for the phase that next touches the suite, not a fix round's. Touches `TRANSPORT-7` and nothing normative in the code. Recorded by phase 8c's - review round 1 on its docs branch; referred to by date and content, never by ordinal. + review round 1 on its docs branch; referred to by date and content, never by ordinal. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: `docs/first-release.md` § Post-release triggers, the residues line. Row 59 of the inbound table in phase 10's checklist.] - **`Dexpace::Registry#resolve`'s discovery loop has no bound.** Found 2026-09-23 by phase 9's implementation while cutting a mutation for `XCUT-23`: `#resolve` is `loop do … end` over `take_or_join_claim`/`wait_on`, and a change that makes the `return hand_out(resolved)` guard @@ -2788,21 +2797,21 @@ design. six minutes and was killed. **The shipped code is correct**; what is missing is a bound or a structural argument that the loop terminates, which phase 2 states nowhere. **Code half: a bound on the loop, or a comment that argues the termination and a test that would catch its loss.** Touches - `SEAM-5`, `XCUT-23`. Referred to by date and content, never by ordinal. + `SEAM-5`, `XCUT-23`. Referred to by date and content, never by ordinal. [2026-09-25, phase 10 -- repaired: a termination argument at `Registry#resolve` and `registry_test.rb`'s Termination suite, which phase 9's spin mutation turns red. Row 60 of the inbound table in phase 10's checklist.] - **`XCUT-12`'s fiber-scheduler form is unasserted, and the checklist row says so.** Phase 9's design already routed this (its "Work phase 9 postponed" table names the thread form only), and the built assertion repeats it in `invariant_suite/resolution.rb`: the single-flight refresh is proven for sixteen THREADS and never for two fibers under a reactor, because a credential path running under a scheduler is a composition no first-party suite assembles. **Code half: the fiber form of the assertion, or a stated decision that it stays out of v1.** Touches `XCUT-12`, `AUTH-34`, `AUTH-37`. - Verified still owned on 2026-09-23 rather than re-filed. + Verified still owned on 2026-09-23 rather than re-filed. [2026-09-25, phase 10 -- verified already fixed: verified as `XCUT-12`'s fiber form (`fiber_single_flight_test.rb`). Row 61 of the inbound table in phase 10's checklist.] - **`XCUT-9`'s cycle assertion has two stated residues, and `gates:cause_walk` proves neither absent.** A collect-then-yield cause walk never returns a step to count, so a step-bounded drive cannot end it; and a depth cap of exactly three passes a three-node cycle, because a black-box test over one finite input cannot tell a counter from a visited set. Both are written into `invariant_suite/classification.rb` above the assertion. **Code half: a second input of a different length, or a source-shape check over the walk's own file.** Touches `XCUT-9`. Found 2026-09-23 by - phase 9's implementation. + phase 9's implementation. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: `docs/first-release.md` § Post-release triggers, the residues line. Row 62 of the inbound table in phase 10's checklist.] - **7a's `SEAM-21` evidence lives inside another assertion's body and is declared in no ID-keyed map.** Phase 9's `P9-22` re-derived `CodecSuite`'s two portable assertions rather than lifting 7a's, exactly so `assert_closes_nothing`'s `SEAM-21` type-token clause would keep its home — but that clause is @@ -2810,7 +2819,7 @@ design. `Aggregate.by_requirement_id` cannot see it and `APPENDIX_B.md`'s `B.3` rows point at a file rather than at a check. **Code half: split the clause into its own assertion in `dexpace-serde-json`'s suite, or state in the map that the row is a file reference.** Touches `SEAM-21`, `SERDE-3`. Found - 2026-09-23 by phase 9's implementation. + 2026-09-23 by phase 9's implementation. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: `docs/first-release.md` § Post-release triggers, the residues line. Row 63 of the inbound table in phase 10's checklist.] - **`tools/surface.rb` cannot see a `private_constant` module, so a group that became public would be invisible to `gates:surface_snapshot` until one of its methods leaked.** The walk is `Module#constants(false)`, which omits a private constant entirely — which is what lets phase 8a's @@ -2818,7 +2827,7 @@ design. found two real public-surface leaks (`Aggregate#add_rows` and `CodecSuite`'s two assertion factories) by reading the regenerated manifest row by row, not by a gate. **Code half: have the walk record a private constant's NAME with a marker, so the manifest shows the transition without exporting the - contents.** Touches `NFR-4`. Found 2026-09-23 by phase 9's implementation. + contents.** Touches `NFR-4`. Found 2026-09-23 by phase 9's implementation. [2026-09-25, phase 10 -- moved to `docs/first-release.md`: `docs/first-release.md` § Post-release triggers, the residues line. Row 64 of the inbound table in phase 10's checklist.] - **`Dexpace::Conformance::PackagingCase`'s default gem-name → constant-path rule covers none of this port's exceptional four.** The rule is segment-for-segment (`dexpace-transport-net_http` → `Dexpace::Transport::NetHttp`), and the port's own names need `Dexpace` for `dexpace-core` and @@ -2826,7 +2835,7 @@ design. override a driver supplies. A driver that omits it gets a `:vacuous` result with an actionable message and never a wrong answer, which is why this is a repair and not a defect. **Code half: a default that covers a root-namespaced gem and a known-acronym leaf, with the override kept for - everything else.** Touches `NFR-15`. Found 2026-09-23 by phase 9's aggregate run. + everything else.** Touches `NFR-15`. Found 2026-09-23 by phase 9's aggregate run. [2026-09-25, phase 10 -- repaired: `PackagingCase`'s default rule covers all six gems (`-core` is the root, `http`/`json` are acronyms), tested in `packaging_suite_test.rb`. Row 65 of the inbound table in phase 10's checklist.] **2026-09-13** — **Execution order amended by the roadmap-level generator-fitness review, which read the plan end to end against one question: will a generated OpenAPI client be able to use this?** No cell of @@ -5281,3 +5290,48 @@ driver declares **twenty** shared instances, not fifteen, in the checklist's `XC `docs/sdk-documentation/conformance.md`; design `R8`'s nine is a different list and stands. `main` is `582e33a` before and after this pass; nothing is pushed, and issue #33 stays open. + +**2026-09-25** — **Phase 10 implemented**, as three stacked branches against issue #34: code +(`10-phase-10-deviation-reconciliation-and-release-readiness`), tests (`…-tests`) and documentation +(`…-docs`), each targeting the one below and the first targeting `main` at `b242de6`, which holds every +phase through 9 and was green under all twenty-one gates before a line changed. Nothing is pushed, and +nothing is published: every gem stays at `0.0.0` and no tag is cut. **This is the last phase of the +roadmap**, so nothing it found could be routed to a later one: what it did not repair is a dated +`docs/first-release.md` line. + +**`docs/deviations.md` is flipped.** All nineteen rows now carry an as-built verdict — every one +*confirmed* or *confirmed-with-a-narrowing*, none contradicted — each citing a `gems/…` path and +defined `Dexpace::` constants, re-derived from source and never from another document (`P10-2`). The +narrowings are the amendment set, **`C1`–`C19`**: the design's thirteen, `C14` reconciled from +`docs/first-release.md` into the register, and five the as-built audit found. The trees they amend are +frozen, so applying them — and consolidating every phase's as-built ledger rows into §10 — stays a +human's act, named inside the existing `docs/first-release.md` blocker. + +**Three blocking gates and a ninth probe check.** `gates:ledger_audit` keeps the register tied to +§10 (row subject, the EQUAL ID set, and resolvable evidence for a verdict), `gates:spdx_rbs` keeps the +SPDX header on every shipped `.rbs` (phase 9's one `:failed` audit, `NFR-13`, repaired on all 307) and +`gates:sole_parse` keeps `AstScan.parse` the one `parse_file` — **twenty-four** in all, in +`DEFAULT_GATES` and in `ci.yml`'s `gates` job. The probe's `chapters` check found two live +misattributions in the phase-8 segmentation design and fixed them in place. + +**Repairs, every one test-first, each test run red on the untouched base.** The proxy warning's +credential leak (5b's review R3-1, eight of eight spellings on 4.0.6 and 3.2.11, and the password +prefix a `/`, `?`, `#` or second `@` still left, found by the phase's review round 0); `Status.of` total +over every Integer with `#standard?` (`HTTP-10`, a MUST phase 1 recorded as a reading in no ledger +row); `Protocol`'s HTTP/1.0 (`HTTP-33`); `URL.parse!`'s wrapping (`HTTP-47`); `Model.own` over a +default-proc Hash; `HeaderSyntax`'s total predicates; a closed stream refused at `Body.stream`; the NaN +and Complex duration guards 8b's P8-77 left to core; the configuration chain's per-configure growth +(`CFG-13`); the async logging step counting the caller's continuation (`OBS-24`); the require +allowlist's dot segments; RuboCop's parent-exclusion hole; `PackagingCase`'s gem-name rule; +`TransportSuite.run` onto `Runner`; and five flaky or environment-reading tests made deterministic. `R8` +was not carried out — its defect was absent (`P10-22`) — and `R9` became a verification: two fibers of +one reactor on one mutex park rather than deadlock, and the fiber race test proves single-flight under a +real reactor (`P10-21`). + +**This list is empty by disposition.** Every one of its sixty-five bullets carries a dated bracket — +**18 repaired, 14 verified already fixed, 32 moved to `docs/first-release.md`, 1 withdrawn**, none +carried forward — and the checklist, +`docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-checklist.md`, +holds the same sixty-five beside 205 requirement rows, the repairs table and the guards run red. + +`main` is `b242de6` before and after this pass; nothing is pushed, and issue #34 stays open. diff --git a/docs/work/mvp/phase0/2026-09-05-phase0-scaffold-and-quality-gates.md b/docs/work/mvp/phase0/2026-09-05-phase0-scaffold-and-quality-gates.md index 1049f9d..08c0f3f 100644 --- a/docs/work/mvp/phase0/2026-09-05-phase0-scaffold-and-quality-gates.md +++ b/docs/work/mvp/phase0/2026-09-05-phase0-scaffold-and-quality-gates.md @@ -906,7 +906,7 @@ Minitest method per row, so a failure still names exactly one case (`testing/fc3 `Hash` per call even when nothing is passed.** It carries **no ordinal**, deliberately. This task's cops were numbered one to five when they were written, phase 2's `Dexpace/QualifiedCoreConstant` is "the sixth" and phase 4a's `Dexpace/NoWeakReferences` "the seventh", and renumbering a later phase's -cop to make room for one added here would falsify every document that already cites those ordinals. +cop to make room for one added here would falsify every document that already cites those ordinals. [2026-09-25, phase 10: the as-built count is eight -- every as-built document counts `Dexpace/NoKeywordSplat`, `Dexpace/QualifiedCoreConstant` is the seventh and phase 4a's `Dexpace/NoWeakReferences` the eighth (`.rubocop/test/cops_test.rb`'s head comment); this sentence's ordinals are the planning-time count.] So this one is named and never counted, every ordinal elsewhere in the repository stands as written, and where this task's own prose counts it says "the original five and the keyword-splat cop". `api-design/1d9e6e0b` requires keyword arguments on every public method and gives its reason as diff --git a/docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-checklist.md b/docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-checklist.md new file mode 100644 index 0000000..59a39d5 --- /dev/null +++ b/docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-checklist.md @@ -0,0 +1,728 @@ +# Phase 10 — Deviation Reconciliation and Release Readiness: Checklist + +**Written at execution time, 2026-09-25, from what was built** — not from the plan. The design and the +plan were written on 2026-09-13 against the DESIGNS of phases 1–9, before phase 3 existed as code; every +earlier phase has since been built, reviewed and merged, and phase 9 audited them. Phase 10 was cut from +`main` at `b242de6`, which holds all of it and was green under all twenty-one gates before this phase +changed a line (measured, "Gate runs" below). Where the plan's text and the built tree disagree the tree +wins, and "Deviations from the plan" states each departure with its reason. + +**Phase 10 is the last phase.** There is no later phase to route anything to: every finding this phase +does not repair is a dated `docs/first-release.md` line, and phase 10's inbound list on the roadmap is +**emptied by disposition** — every one of its sixty-five bullets carries a dated bracket naming what +happened to it, and the table below holds the same sixty-five. Nothing is published: every gem stays at +`0.0.0`, no tag is cut and no version is bumped. + +Legend, verbatim from the roadmap's cross-cutting constraint 3: ✅ implemented and tested · +🚫 not built (permanent simplification, named reason) · ⏳ deferred (naming the plan task — phase, task +number and path — that will do it, or the `docs/first-release.md` entry that owns it) · N/A not +applicable in this port. + +Plan: `docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness.md`, nineteen +tasks plus the "Task 10b" the maintainer added at execution (small repairs). Design: +`docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-design.md`, whose +Deviation Ledger carries `P10-1`–`P10-11` and whose As-built addendum, written with this checklist, adds +**`P10-21`–`P10-38`** (the band starts at `P10-21`; `P10-12`–`P10-20` are left free and nothing is +renumbered). The design's `R1` file list was widened by a dated addendum (see Deviations 1). + +**Verdict vocabulary** (the design's `R1`): *confirmed*, *confirmed-with-a-narrowing* (true, and narrower +than the sentence — the narrowing is an amendment in `docs/deviations.md`'s set), *contradicted* (none +found), *unverifiable* (none). The per-entry verdict and its evidence is `docs/deviations.md`'s Status +column, which `gates:ledger_audit` checks: every verdict row cites a `gems/…` path that exists and names +only `Dexpace::` constants that are defined. + +Test files are named with their gem: `core/…` is `gems/dexpace-core/test/dexpace/`, `gates/…` is +`test/gates/`, `conformance/…` is `gems/dexpace-conformance/test/dexpace/conformance/`, `async_http/…` is +`gems/dexpace-transport-async_http/test/dexpace/transport/async_http/`, `net_http/…` likewise, and +`housekeeping/…` is `.claude/skills/housekeeping/test/`. + +--- + +## Requirement rows + +**205 rows: 124 own, 64 cross-reference and 17 added cross-reference.** The 124 own rows are every ID +design §10's nineteen entries name plus `RETRY-28` from its closing note — extracted mechanically by +`LedgerAudit.ids_in` from the frozen chapter (123 distinct IDs, the same number `gates:ledger_audit` +asserts row by row) — **108 MUST, 15 SHOULD, 1 MAY**, each level read from appendix C. The 64 are the +design's cross-reference set (44 MUST, 16 SHOULD, 4 MAY). The 17 added are IDs a phase-10 repair changed +that neither set carried: `HTTP-10`, `HTTP-19`, `HTTP-33`, `HTTP-47`, `BODY-9`, `TRANSPORT-22`, `TRANSPORT-24`, `OBS-11`, +`CFG-13`, `NFR-7`, `NFR-15`, `REDIR-23`, `RETRY-42`, `RECOV-28`, `AUTH-34`, `AUTH-37` and `PIPE-39`. An ID +named by more than one §10 entry has one row naming every entry; the *Task* column is the audit task +(11–15) for an own row. The *Owner* column is the earlier phase whose checklist first carries the ID; no +earlier row moves, except the ten levels corrected in place (Deviations 10). + +**Own rows by mark:** ✅ 107 · 🚫 11 (the retired provider apparatus: `SEAM-3`, `SEAM-4`, `IO-30`–`IO-36`, +`IO-39`; and `SEAM-22`'s mechanism, retired by §10.14, its surviving clause ✅ as phase 2 marks it) · N/A 4 (`SEAM-10`, `NFR-8`, `NFR-9`, `ASYNC-4`) · ⏳ 2 (`ASYNC-3`, `PIPE-33`, both +`docs/first-release.md` § Unsatisfied MUSTs) — `CFG-20` is counted ✅ with its fourth clause ⏳ on the same +entry. **By verdict:** every row's §10 entry is *confirmed* or *confirmed-with-a-narrowing*; nothing is +*contradicted* and nothing *unverifiable*. + +### Own rows + +| ID | Level | Status | Task | §10 entry | Owner | Verdict and as-built evidence | +|---|---|---|---|---|---|---| +| `SEAM-1` | MUST | ✅ | 12 | §10.3, §10.7 | 0 | confirmed (`docs/deviations.md` row 3); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SEAM-3` | MUST | 🚫 | 11 | §10.1, §10.2, §10.12 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 1); retired apparatus (§10.1); its ownership-on-wrap rule lives on as `IO-6`; also named by §10.2 (the duck type) and §10.12 (C3's misattribution) | +| `SEAM-4` | MUST | 🚫 | 11 | §10.1 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 1); retired apparatus: provider resolution for a seam that does not exist | +| `SEAM-5` | MUST | ✅ | 11 | §10.1, §10.8 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 1); the loud zero/many-candidate branches (`registry.rb`); §10.8's substrate; phase 10 added the termination argument at `#resolve` and `registry_test.rb`'s Termination pins | +| `SEAM-6` | MUST | ✅ | 11 | §10.1, §10.8 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 1); phase 2's `Dexpace::Registry#install` (`gems/dexpace-core/lib/dexpace/registry.rb`): an `equal?` re-install is a silent no-op and a different instance over an explicit install raises `Dexpace::InvalidArgumentError` naming both; phase 2's row and its `registry_test.rb` cases stand. §10.1 names it only as part of the retired I/O seam's apparatus; the rule itself survives on the three registries that remain (§10.8) | +| `SEAM-7` | MUST | ✅ | 11 | §10.1, §10.8 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 1); phase 2's `Dexpace::Registry#resolve` (`gems/dexpace-core/lib/dexpace/registry.rb`): a successful resolution memoised process-wide, a failed one memoising nothing; phase 2's row and its `registry_test.rb` cases stand. §10.1 names it only as part of the retired I/O seam's apparatus; the rule survives on the three registries that remain (§10.8) | +| `SEAM-8` | SHOULD | ✅ | 11 | §10.1, §10.8 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 1); phase 2's `Dexpace::Registry` (`gems/dexpace-core/lib/dexpace/registry.rb`): replacing an already handed-out auto-resolved provider emits one `Kernel#warn`; phase 2's row and its `registry_test.rb` cases stand. §10.1 names it only as part of the retired I/O seam's apparatus; the rule survives on the three registries that remain (§10.8) | +| `SEAM-9` | MUST | ✅ | 11 | §10.1, §10.8 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 1); phase 2's `Dexpace::Registry` (`gems/dexpace-core/lib/dexpace/registry.rb`): one frozen `Registry::State` snapshot swapped under a `::Thread::Mutex`, lock-free reads, one factory call across 32 concurrent first accesses; phase 2's row and its `registry_test.rb` cases stand. §10.1 names it only as part of the retired I/O seam's apparatus; the rule survives on the three registries that remain (§10.8) | +| `SEAM-10` | SHOULD | N/A | 11 | §10.1, §10.9 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 1); vacuous (one constant namespace), replaced by the `core:` version-skew guard (§10.9). **Level corrected** in phase 2's checklist: appendix C says SHOULD | +| `SEAM-13` | SHOULD | ✅ | 12 | §10.4 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 4); cooperative cancellation through `Dexpace::Cancellation`; §10.4 | +| `SEAM-16` | MUST | ✅ | 12 | §10.3 | 2 | confirmed (`docs/deviations.md` row 3); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SEAM-17` | SHOULD | ✅ | 12 | §10.3 | 2 | confirmed (`docs/deviations.md` row 3); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SEAM-20` | MUST | ✅ | 14 | §10.12, §10.13 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 12); four profiles (§10.13) and closes-nothing (§10.12) | +| `SEAM-21` | MUST | ✅ | 14 | §10.12 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 12); residue: 7a's evidence is a branch inside another assertion, declared in no ID-keyed map -- `docs/first-release.md` § Post-release triggers | +| `SEAM-22` | MUST | 🚫 mechanism (§10.14); surviving clause ✅ | 14 | §10.14 | 2 | confirmed (`docs/deviations.md` row 14); appendix-C only, read verbatim; the reflective type capture is retired and the witness is the substitution, so the mark is phase 2's own -- the surviving clause, an explicit witness with no witness-less `#load` overload, is phase 2's `serde_test.rb` case | +| `SEAM-23` | MUST | ✅ | 14 | §10.14 | 2 | confirmed (`docs/deviations.md` row 14); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SEAM-29` | MUST | ✅ | 14 | §10.10 | 1 | confirmed-with-a-narrowing (`docs/deviations.md` row 10; amendment `C19`); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SEAM-30` | MUST | ✅ | 12 | §10.4 | 2 | confirmed-with-a-narrowing (`docs/deviations.md` row 4); `Completer#fulfil`'s close of an undelivered response | +| `HTTP-2` | MUST | ✅ | 14 | §10.10 | 1 | confirmed-with-a-narrowing (`docs/deviations.md` row 10; amendment `C19`); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `HTTP-4` | MUST | ✅ | 14 | §10.10 | 1 | confirmed-with-a-narrowing (`docs/deviations.md` row 10; amendment `C19`); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `HTTP-5` | MUST | ✅ | 14 | §10.11 | 1 | confirmed (`docs/deviations.md` row 11); phase 10 repaired `Model.own`'s default-proc escape (Task 10b) | +| `HTTP-7` | MUST | ✅ | 14 | §10.10 | 1 | confirmed-with-a-narrowing (`docs/deviations.md` row 10; amendment `C19`); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `HTTP-17` | MUST | ✅ | 14 | §10.10 | 1 | confirmed-with-a-narrowing (`docs/deviations.md` row 10; amendment `C19`); re-validated at both wires; phase 10 made every `HeaderSyntax` predicate total over a non-String (Task 10b) | +| `HTTP-18` | MUST | ✅ | 14 | §10.10 | 1 | confirmed-with-a-narrowing (`docs/deviations.md` row 10; amendment `C19`); as `HTTP-17` | +| `IO-1` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); residue, not repaired: after a mid-stream failure `.over`'s enumerator restarts `#each` (7b's finding) -- `docs/first-release.md` § Post-release triggers | +| `IO-2` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-3` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-4` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-5` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-6` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); ownership-on-wrap, read from `IO-6`'s own text: `BufferedSource.wrapping(StringIO).close` closes the StringIO (measured 2026-09-25) | +| `IO-7` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-8` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-9` | SHOULD | ✅ | 11 | §10.1, §10.18 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); the materialisation ceiling, read per call; also §10.18. **Level corrected** in 5a's checklist: SHOULD | +| `IO-10` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-11` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); residue: the per-byte `#getbyte` cost (~0.9 µs/byte) -- `docs/first-release.md` § Post-release triggers | +| `IO-12` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-13` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-14` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-15` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-16` | SHOULD | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); the bridge half; C3 names it beside `IO-6` | +| `IO-17` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-18` | SHOULD | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-19` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-20` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-21` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-22` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-23` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-24` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-25` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-26` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-27` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-28` | MUST | ✅ | 11 | §10.1, §10.10 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); also §10.10's claim: a duck-typed stream passes the builder, and the wire boundary re-validates headers, not streams | +| `IO-29` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-30` | MUST | 🚫 | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); retired apparatus: the provider seam's factory operations are removed with the seam (§10.1); the one behavioural clause survives as a property of `Dexpace::IO::BufferedSource.of_bytes` -- the source is an independent copy -- as 3a's row records | +| `IO-31` | MUST | 🚫 | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); retired apparatus: provider resolution rules for a seam that does not exist (§10.1); asserted negatively by `Dexpace::IO.constants` holding no registry, factory or installation call (3a's `io_test.rb`), as 3a's row records | +| `IO-32` | MUST | 🚫 | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); appendix-C only; the install-idempotence clause whose subject is removed | +| `IO-33` | MUST | 🚫 | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); appendix-C only; explicit-install-wins, subject removed | +| `IO-34` | SHOULD | 🚫 | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); appendix-C only; the resolution cache, subject removed. **Level corrected** in 3a's checklist: SHOULD | +| `IO-35` | SHOULD | 🚫 | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); appendix-C only; the replaced-provider warning, subject removed | +| `IO-36` | MAY | 🚫 | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); the provider's own obligations, subject removed. **Level corrected** in 3a's checklist: MAY | +| `IO-37` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-38` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); tested on CRuby only; the GVL hides a missing lock -- the standing non-CRuby post-release trigger | +| `IO-39` | SHOULD | 🚫 | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); retired apparatus: lock-free reads of a provider registry that does not exist (§10.1); the property survives in phase 2's `Dexpace::Registry` (`SEAM-9`), as 3a's row records | +| `IO-40` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-41` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `IO-42` | MUST | ✅ | 11 | §10.1 | 3a | confirmed-with-a-narrowing (`docs/deviations.md` row 1); implemented in full by 3a under `gems/dexpace-core/lib/dexpace/io/` -- only the pluggability apparatus is removed; 3a's row stands | +| `BODY-1` | MUST | ✅ | 12 | §10.2 | 3b | confirmed (`docs/deviations.md` row 2); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `BODY-8` | MUST | ✅ | 14 | §10.12 | 3b | confirmed-with-a-narrowing (`docs/deviations.md` row 12); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `BODY-32` | MUST | ✅ | 15 | §10.18 | 3b | confirmed-with-a-narrowing (`docs/deviations.md` row 18); the ceiling BODY-32 shares with `IO-9` | +| `BODY-35` | MUST | ✅ | 12 | §10.2 | 3b | confirmed (`docs/deviations.md` row 2); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `BODY-37` | MUST | ✅ | 14 | §10.10 | 3b | confirmed-with-a-narrowing (`docs/deviations.md` row 10; amendment `C19`); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `PIPE-16` | MUST | ✅ | 15 | §10.15 | 4c | confirmed (`docs/deviations.md` row 15); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `PIPE-33` | MUST | ⏳ | 12 | §10.5 | 4c | confirmed as admitted, and not re-opened (roadmap cross-cutting constraint 8) (`docs/deviations.md` row 5); the interrupt clause not satisfied; the non-interrupting half met exactly -- `docs/first-release.md` § Unsatisfied MUSTs | +| `RECOV-12` | MUST | ✅ | 13 | §10.6 | 4b | confirmed-with-a-narrowing (`docs/deviations.md` row 6); the trail on `Suppressible`; C14 is the §10.6/§5.2 sentence correction | +| `RECOV-34` | MUST | ✅ | 15 | §10.18 | 4b | confirmed-with-a-narrowing (`docs/deviations.md` row 18); the ~292-year bound, refused above | +| `RETRY-23` | MUST | ✅ | 12 | §10.4 | 6a | confirmed-with-a-narrowing (`docs/deviations.md` row 4); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `RETRY-26` | MUST | ✅ | 15 | §10.17 | 6a | confirmed (`docs/deviations.md` row 17); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `RETRY-28` | MUST | ✅ | 15 | closing note | 6a | confirmed (`docs/deviations.md` the closing note); the closing note's own row: nothing unified, both sanctions un-invoked | +| `RETRY-34` | MUST | ✅ | 13 | §10.6 | 4b | confirmed-with-a-narrowing (`docs/deviations.md` row 6); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `REDIR-11` | MUST | ✅ | 15 | §10.15 | 4c | confirmed (`docs/deviations.md` row 15); the porter trap cannot occur: no header carries the marker | +| `AUTH-14` | MUST | ✅ | 13 | §10.7 | 6c | confirmed (`docs/deviations.md` row 7); `["u:p"].pack("m0")` (`auth/basic_handler.rb:45`), never `Base64` | +| `AUTH-29` | MUST | ✅ | 15 | §10.15 | 4c | confirmed (`docs/deviations.md` row 15); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `PAGE-13` | MUST | ✅ | 13 | §10.6 | 7c | confirmed-with-a-narrowing (`docs/deviations.md` row 6); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `PAGE-15` | MUST | ✅ | 13 | §10.6 | 7c | confirmed-with-a-narrowing (`docs/deviations.md` row 6); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SSE-11` | MUST | ✅ | 15 | §10.18 | 7b | confirmed-with-a-narrowing (`docs/deviations.md` row 18); documented and public; C11's appendix-C `SSE-19` note stands beside it | +| `SSE-29` | MUST | ✅ | 13 | §10.6 | 7b | confirmed-with-a-narrowing (`docs/deviations.md` row 6); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SSE-30` | MUST | ✅ | 13 | §10.6 | 7b | confirmed-with-a-narrowing (`docs/deviations.md` row 6); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SSE-36` | MUST | ✅ | 13 | §10.6 | 7b | confirmed-with-a-narrowing (`docs/deviations.md` row 6); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SERDE-5` | MUST | ✅ | 14 | §10.14 | 7a | confirmed (`docs/deviations.md` row 14); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SERDE-6` | MUST | ✅ | 14 | §10.14 | 7a | confirmed (`docs/deviations.md` row 14); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SERDE-7` | MUST | ✅ | 14 | §10.14 | 7a | confirmed (`docs/deviations.md` row 14); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SERDE-8` | MUST | ✅ | 14 | §10.14 | 7a | confirmed (`docs/deviations.md` row 14); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SERDE-16` | MUST | ✅ | 14 | §10.14 | 7a | confirmed (`docs/deviations.md` row 14); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `SERDE-17` | MUST | ✅ | 14 | §10.14 | 7a | confirmed (`docs/deviations.md` row 14); filed under Constraints in the corpus, not Rules | +| `OBS-2` | MUST | ✅ | 13 | §10.7 | 5b | confirmed (`docs/deviations.md` row 7); the duck-typed sink; no `require "logger"` anywhere in core's `lib/` | +| `OBS-35` | SHOULD | ✅ | 15 | §10.16 | 5b | confirmed (`docs/deviations.md` row 16); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `CFG-1` | MUST | ✅ | 15 | §10.16 | 5a | confirmed (`docs/deviations.md` row 16); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `CFG-3` | MUST | ✅ | 15 | §10.16 | 5a | confirmed (`docs/deviations.md` row 16); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `CFG-4` | MUST | ✅ | 15 | §10.16 | 5a | confirmed (`docs/deviations.md` row 16); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `CFG-15` | MUST | ✅ | 15 | §10.17 | 5a | confirmed (`docs/deviations.md` row 17); phase 10: NaN and Complex refused (Task 10b) | +| `CFG-17` | MUST | ✅ | 12 | §10.4, §10.17 | 5a | confirmed-with-a-narrowing (`docs/deviations.md` row 4); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `CFG-18` | SHOULD | ✅ | 15 | §10.17 | 5a | confirmed (`docs/deviations.md` row 17); phase 10: `Async.delay` refuses NaN and Complex (Task 10b) | +| `CFG-20` | SHOULD | ✅ (three clauses) / ⏳ (the interrupt clause) | 12 | §10.4 | 5a | confirmed-with-a-narrowing (`docs/deviations.md` row 4); three clauses met; the cancel-with-interrupt clause is §10.5's same unmet clause under a second ID -- `docs/first-release.md` § Unsatisfied MUSTs | +| `CFG-21` | MUST | ✅ | 12 | §10.4 | 5a | confirmed-with-a-narrowing (`docs/deviations.md` row 4); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `CFG-24` | MUST | ✅ | 15 | §10.16 | 5a | confirmed (`docs/deviations.md` row 16); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `CFG-26` | MUST | ✅ | 15 | §10.16 | 5a | confirmed (`docs/deviations.md` row 16); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `TRANSPORT-3` | MUST | ✅ | 12 | §10.4 | 8a | confirmed-with-a-narrowing (`docs/deviations.md` row 4); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `ASYNC-1` | MUST | ✅ | 12 | §10.3 | 8b | confirmed (`docs/deviations.md` row 3); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `ASYNC-2` | MUST | ✅ | 12 | §10.3 | 8b | confirmed (`docs/deviations.md` row 3); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `ASYNC-3` | MUST | ⏳ | 12 | §10.5 | 8b | confirmed as admitted, and not re-opened (roadmap cross-cutting constraint 8) (`docs/deviations.md` row 5); not satisfied, admitted: `ExecutorSuite` asserts it so it FAILS and the pool's driver waives it by ID, printed `waived (would fail): ASYNC-3` (`gems/dexpace-async-thread/test/dexpace/async/thread/conformance_test.rb`) -- `docs/first-release.md` § What v1 ships without › Unsatisfied MUSTs | +| `ASYNC-4` | MUST | N/A | 12 | §10.5 | 8b | confirmed as admitted, and not re-opened (roadmap cross-cutting constraint 8) (`docs/deviations.md` row 5); vacuous: a port that never delivers an interrupt cannot produce the poisoning hazard | +| `ASYNC-5` | MUST | ✅ | 12 | §10.4 | 8b | confirmed-with-a-narrowing (`docs/deviations.md` row 4); the producer-side orphan close, both transports | +| `XCUT-1` | MUST | ✅ | 12 | §10.4 | 9 | confirmed-with-a-narrowing (`docs/deviations.md` row 4); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `XCUT-2` | MUST | ✅ | 12 | §10.4 | 9 | confirmed-with-a-narrowing (`docs/deviations.md` row 4); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `XCUT-3` | MUST | ✅ | 12 | §10.4, §10.17 | 9 | confirmed-with-a-narrowing (`docs/deviations.md` row 4); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `XCUT-9` | MUST | ✅ | 13 | §10.6 | 4b | confirmed-with-a-narrowing (`docs/deviations.md` row 6); one walk, cycle-safe; residues stated, not repaired -- a collect-then-yield walk and a depth cap equal to the cycle length pass the black-box assertion (`docs/first-release.md` § Post-release triggers) | +| `XCUT-13` | MUST | ✅ | 15 | §10.17 | 9 | confirmed (`docs/deviations.md` row 17); the requirement stands as its owner's row records it; the entry's verdict and evidence are that row of `docs/deviations.md` | +| `XCUT-15` | MUST | ✅ | 14 | §10.11 | 9 | confirmed (`docs/deviations.md` row 11); as `HTTP-5` | +| `XCUT-18` | MUST | ✅ | 14 | §10.10 | 9 | confirmed-with-a-narrowing (`docs/deviations.md` row 10; amendment `C19`); the call-site assertion runs for real in the aggregate and is accepted-vacuous in core's own driver | +| `XCUT-23` | MUST | ✅ | 11 | §10.1, §10.8 | 9 | confirmed-with-a-narrowing (`docs/deviations.md` row 1); three surviving instances -- `Transport::REGISTRY`, `AsyncTransport::REGISTRY`, `Serde::REGISTRY` -- not "transport, serde, executor" as §11.8 says: there is no executor registry (8b's direct version assertion); re-read for §10.8 | +| `NFR-1` | MUST | ✅ | 13 | §10.7 | 0 | confirmed (`docs/deviations.md` row 7); phase 9's row ✅; the same claim's four mechanisms green | +| `NFR-8` | MUST | N/A | 13 | §10.19 | 0 | confirmed (`docs/deviations.md` row 19); N/A by its own text ("does not apply"). Phase 0's checklist marks it `🚫 retargeted`, phase 9's N/A; this checklist takes **N/A** as the convention for a retargeted vacuous requirement and records the departure from phase 0's mark (Deviations from the plan, 11) | +| `NFR-9` | SHOULD | N/A | 13 | §10.19 | 0 | confirmed (`docs/deviations.md` row 19); as `NFR-8`; its content that applies -- the allowlist still being the right list -- is the require-allowlist regeneration post-release trigger | +| `NFR-11` | SHOULD | ✅ | 12 | §10.3 | 0 | confirmed (`docs/deviations.md` row 3); a SHOULD (**level corrected** in 4c's, 5a's, 5b's and 5c's checklists, which said MUST) | + +### Cross-reference rows + +The 64 of the design plus the 17 added. Each names what phase 10 did and the owning phase whose row it +cross-references; the owning row is not re-marked. *Status* is the requirement's standing after phase 10, +with "unchanged" where phase 10 audited or documented and touched no code. + +| ID | Level | Status | Task | Owner | What phase 10 did | +|---|---|---|---|---|---| +| `SEAM-11` | MUST | ✅ unchanged | 17 | 2 | amendment C5 (§3.2's `read_body` sentence); 8a's producer thread satisfies it | +| `SEAM-15` | MAY | ✅ unchanged | 7 | 2 | the `chapters` check's subject: appendix C is its only normative statement; the phase-8 segmentation design's misattribution fixed in place | +| `SEAM-28` | MAY | ⏳ unchanged (a MAY, deferred in §12) | 16 | 2 | `R7`: no carrier is built; the worked-example blocker gains the correlation chain | +| `HTTP-3` | MUST | ✅ unchanged | 17 | 1 | amendment C2 (§4's builder list omits the multipart body) | +| `HTTP-24` | MUST | ✅ unchanged | 10 | 1 | named by the plan for the `http/1.0` repair in error: `HTTP-24` is `MediaType`'s charset lookup and the repair is `HTTP-33`'s (see that row); also C1's | +| `HTTP-42` | MUST | ✅ unchanged | 17 | 3b | amendment C1 (§3.1's decode recipe) | +| `HTTP-43` | MUST | ✅ unchanged | 10 | 3b | named by the plan for the `http/1.0` repair in error: `HTTP-43` is `Response#close`; C5 touches it | +| `HTTP-51` | SHOULD | ✅ unchanged | 10b | 3b | MultipartBody's non-ASCII part names not widened -- `docs/first-release.md` port-readings entry (a) | +| `BODY-2` | MUST | ✅ unchanged | 17 | 3b | amendment C2 | +| `BODY-15` | MUST | ✅ unchanged | 17 | 3b | amendment C5 | +| `BODY-16` | MUST | ✅ unchanged | 17 | 3b | amendment C1 | +| `CTX-14` | MUST | ✅ unchanged | 16 | 4a | `Bundle#tracer_factory`'s YARD: CTX-14's span-tracer factory, not OBS-29's; pinned by `http_tracer_test.rb` | +| `CTX-16` | SHOULD | ✅ unchanged (4a's) | 16 | 4a | `R7`: conforming by the modal clauses; the correlation chain is the worked-example blocker's and a *Behavioural asymmetries* entry | +| `CTX-20` | SHOULD | ✅ unchanged | 16 | 4a | as `CTX-14` | +| `PIPE-2` | MUST | ✅ unchanged | 17 | 4c | amendment C6 (`Net::HTTP` retries by default; 8a sets `max_retries = 0`) | +| `PIPE-11` | MUST | ✅ unchanged | 16 | 4c | no ambient carrier added (`R6`, `R7`) | +| `PIPE-37` | MUST | ✅ unchanged | 16 | 4b | no `PRE_REDIRECT` step added (`R6`) | +| `RETRY-13` | MUST | ✅ unchanged | 17 | 6a | amendment C6 | +| `AUTH-35` | MUST | ✅ unchanged | 9, 10b | 6c | asserted under a reactor (`fiber_single_flight_test.rb`) and on the REFRESH path from a warm cache (`bearer_stamper_test.rb`, 6c's review R4-1) | +| `PAGE-14` | MUST | ✅ verified | 8 | 7c | `Page::PageStateError < ::StandardError` (`page/page_state_error.rb:17`), raised at `page/pages.rb:96`; `page_state_error_test.rb` refutes `ArgumentError`. `R8` not carried out (`P10-22`) | +| `SSE-19` | MAY | ✅ unchanged | 17 | 7b | amendment C11 (appendix C's row drops the chapter's port sanction) | +| `SSE-26` | MUST | ✅ verified | 8 | 7b | `SSE::StreamStateError` unchanged; one family per subsystem, neither in the argument family (`P10-22`) | +| `SSE-40` | SHOULD | ✅ verified | 8 | 7b | as `SSE-26` | +| `SERDE-2` | MUST | ✅ unchanged | 5 | 7a | `gates:spdx_rbs` finds no shipped `.rbs` empty of declarations (0 of 307); bullet 26 withdrawn by 7a | +| `OBS-4` | MUST | ✅ unchanged | 17 | 5b | amendment C4 (`Event#tag`) | +| `OBS-5` | MUST | ✅ unchanged | 17 | 5b | amendment C4; the Redactor's YARD now names where the three sources meet redaction | +| `OBS-8` | MUST | ✅ unchanged | 10b | 5b | `event_test.rb`'s race now asserts every racer parked before the gate opens (5b's review R3-3); also C4 | +| `OBS-19` | SHOULD | ✅ unchanged | 17 | 5b | amendment C7's header-drop scoping | +| `OBS-21` | MUST | ✅ unchanged | 16 | 5c | the span-tracer factory, distinguished from OBS-29's | +| `OBS-22` | MUST | ✅ unchanged | 16 | 5c | as `OBS-21` | +| `OBS-23` | MUST | ✅ unchanged | 16 | 5b | as `OBS-21` | +| `OBS-24` | MUST | ✅ repaired share | 10b | 5b | `AsyncStep` at BODY no longer counts the caller's continuation in the duration (5b's review R3-2), `async_step_test.rb` | +| `OBS-25` | MUST | ✅ unchanged | 16 | 5c | the no-op factory answers one tracer for every library (pinned) | +| `OBS-28` | SHOULD | ✅ unchanged | 16 | 5c | `HTTPTracer`'s YARD already states the contract | +| `OBS-29` | MUST | ✅ unchanged (5c's) | 16 | 5c | `R6`: documented contract, wiring a follow-up by its own text; the *Behavioural asymmetries* entry | +| `OBS-34` | MUST | ✅ unchanged | 16 | 5b | as `OBS-29` | +| `CFG-22` | MUST | ✅ repaired share | 10b | 5a | the proxy warning's credential belt unanchored: eight spellings of eight leaked on 4.0.6 and 3.2.11 before (5b's review R3-1); review round 0 (R0-4) found a password holding `/`, `?`, `#` or a second `@` still leaking its prefix past the unanchored belt, and the raw value is now scrubbed from the authority's start through the LAST `@` first -- fifteen spellings (the eight and seven more), none leaking, on 4.0.6 and 3.2.11; review round 1 (R1-1) added three whose password holds an `@` ahead of a reserved character, which only the through-the-LAST-`@` rule redacts -- eighteen | +| `CFG-23` | MUST | ✅ unchanged | 17 | 5a | bullet 20's proxy-ID substitution, corrected 2026-09-13 | +| `CFG-25` | MUST | ✅ unchanged | 17 | 5a | as `CFG-23` | +| `CFG-27` | MUST | ✅ unchanged | 17 | 5a | as `CFG-23` | +| `CFG-28` | MAY | ✅ unchanged | 17 | 5a | as `CFG-23` | +| `TRANSPORT-2` | MUST | ✅ unchanged | 17 | 8a | amendment C6 | +| `TRANSPORT-4` | MUST | ✅ unchanged | 17 | 8a | amendment C8; the conformance caveat blocker | +| `TRANSPORT-8` | MUST | ✅ unchanged | 17 | 8c | amendment C7 (second direction) | +| `TRANSPORT-13` | SHOULD | ✅ unchanged | 4 | 8c | `DropPolicy::MAX_TRACKED_NAMES`; beside the verified `Clients` cap | +| `TRANSPORT-14` | MUST | ✅ unchanged / waived on async_http | 3, 17 | 8a | the waiver re-measured green (`conformance_test.rb`'s "the two waived clauses are unreachable here"); amendment C7 | +| `TRANSPORT-17` | MUST | ✅ unchanged | 17 | 8a | amendment C6 | +| `TRANSPORT-18` | MUST | ✅ unchanged | 17 | 8a | amendment C6 | +| `TRANSPORT-19` | SHOULD | ✅ unchanged | 17 | 8a | amendment C5 | +| `TRANSPORT-25` | MUST | ✅ unchanged | 17 | 8a | amendment C5 | +| `TRANSPORT-28` | SHOULD | ⏳ unchanged (zero-copy clause) | 18 | 8a | `docs/first-release.md` § SHOULD- and MAY-level, unchanged | +| `TRANSPORT-29` | MUST | ✅ unchanged | 17 | 8a | amendment C5 | +| `TRANSPORT-30` | SHOULD | ✅ unchanged | 17 | 8a | bullet 28, fixed 2026-09-13 | +| `ASYNC-6` | MUST | ✅ unchanged | 9 | 8b | the fiber race runs inside the reactor the async transport uses | +| `XCUT-4` | MUST | ✅ unchanged | 17 | 9 | amendment C6 | +| `XCUT-11` | MUST | ✅ unchanged | 9 | 4c | fibers of one thread, asserted by thread identity in the fiber race | +| `XCUT-12` | SHOULD | ✅ (fiber form added) | 9 | 9 | `fiber_single_flight_test.rb`: one fetch for eight fibers against the real `BearerStamper` and `AsyncBearerStamper`; the trigger is closed | +| `XCUT-14` | MUST | ✅ verified | 4 | 9 | `AsyncHTTP::MAX_ORIGINS` 32, `Clients#drain`'s loop, eviction close, `#close` clear; `gates:bounded_map` green | +| `NFR-2` | SHOULD | ✅ unchanged | 17 | 0 | amendments C8 and C9 | +| `NFR-3` | SHOULD | ✅ unchanged | 5 | 0 | `gates:spdx_rbs` asserts every shipped signature declares something; `rbs validate` and `steep` green with the header | +| `NFR-4` | SHOULD | ✅ unchanged (phase 9's) | 18 | 0 | kept ✅: appendix C's `NFR-4` is a checked-in snapshot with drift failing the build, which `gates:surface_snapshot` is; the manifests regenerated once (two rows); `gates:sig_diff` stays vacuous without a tag (`P10-35`) | +| `NFR-6` | SHOULD | ✅ strengthened | 6 | 0 | `gates:sole_parse` asserts `AstScan.parse` is the one `RubyVM::AbstractSyntaxTree.parse_file` | +| `NFR-13` | SHOULD | ✅ (was ✅ / ⏳) | 5 | 0 | all 307 shipped `.rbs` carry the header; `gates:spdx_rbs` blocking; phase 9's `PackagingSuite` assertion now passes | +| `NFR-17` | MUST | ✅ strengthened | 2, 5, 6 | 0 | twenty-four blocking gates; `default_task_test.rb` and `ci_workflow_test.rb` assert the three new names | +| `HTTP-10` | MUST | ✅ repaired (MUST) | 10b | 1 | `Status` is total over every Integer (only a non-Integer is refused); the protocol range is `Status#standard?`. A first repair stopped at 0..999 and still threw on 1000 and -1, narrower than the MUST; review round 0 (R0-5) widened it | +| `HTTP-19` | MUST | ✅ repaired share | 10b | 1 | `HeaderSyntax`'s predicates, the lenient inbound one included, answer `false` for a non-String instead of raising `NoMethodError` (phase 1's review R3-4), `core/http/header_syntax_test.rb` "every predicate is total…" (repairs 6, Guards x3) | +| `HTTP-33` | MUST | ✅ repaired | 10 | 1 | `Protocol` gains `http/1.0` (WIRE_FORMS, ALIASES, `HTTP_1_0`); the negative case holds | +| `HTTP-47` | SHOULD | ✅ repaired (SHOULD) | 10b | 1 | `URL.parse!` refuses a host-less http-family URL and an unrenderable FTP typecode URI, and wraps every `URI::Error` | +| `BODY-9` | SHOULD | ✅ repaired share | 10b | 3b | `Body.stream` over an already-closed stream refuses it with `InvalidArgumentError` instead of letting a raw `IOError` escape (phase 3b's review R2-1), `core/http/body/stream_body_test.rb` "rejects an already-closed stream…" (repairs 7, Guards x4) | +| `TRANSPORT-22` | MUST | ✅ unchanged | 10 | 8a | the adaptation-failure tests now use an `HTTP/1.2` head | +| `TRANSPORT-24` | MUST | ✅ strengthened | 10b | 8a | a 600 or 999 head maps on both transports | +| `OBS-11` | MUST | ✅ repaired share | 10b | 5b | as `CFG-22` | +| `CFG-13` | SHOULD | ✅ repaired share | 10b | 5a | the property chain a repeated `Dexpace.configure` grew is flattened (5a's review R1-2), `config_test.rb` | +| `NFR-7` | SHOULD | ✅ repaired share | 10b | 0 | `rake rubocop` passes `--ignore-parent-exclusion` | +| `NFR-15` | SHOULD | ✅ repaired share | 10b | 0 | `PackagingCase`'s default name rule reaches all six gems | +| `REDIR-23` | SHOULD | ✅ unchanged | 10b | 6b | the wall-clock bound dropped; flatness is the proof | +| `RETRY-42` | MUST | ✅ unchanged | 10b | 6a | per-thread scripts; the interleaving dependence removed | +| `RECOV-28` | MUST | ✅ unchanged | 10b | 4b | as `RETRY-42` | +| `AUTH-34` | MUST | ✅ unchanged | 9 | 6c | single-flight under a reactor, asserted | +| `AUTH-37` | MUST | ✅ unchanged | 9 | 6c | the async stamper's coalescing under a reactor, asserted | +| `PIPE-39` | SHOULD | ✅ unchanged | 10b | 4c | the async preset's no-tracer-factory branch now asserted to thread `settings:` (6b's review R3-1) | + +**Three IDs are named by phase 10's text and deliberately get no row** — `NFR-10`, `NFR-12`, `NFR-16` — +as the design's *64 cross-reference rows* section argues; phase 9 owns all three and phase 10 only walked +the `docs/first-release.md` lines that name them. + +--- + +## What was built + +- **Three blocking gates, twenty-four in all**, every body in `tools/` and every path joined to + `gate_root` (`P9-27`'s lesson): `gates:ledger_audit` (`tools/ledger_audit.rb`: register row N's subject + a word subsequence of §10 entry N's, its IDs EQUAL to the entry's, and a verdict row citing a real + `gems/…` path and only defined `Dexpace::` constants — the rake task requires core and every adapter the + interpreter's floor admits), `gates:spdx_rbs` (`tools/spdx_rbs.rb`: line 1 of every shipped `.rbs` is the + SPDX header, and every shipped signature declares something) and `gates:sole_parse` + (`tools/sole_parse.rb`: `AstScan.parse` is the one `RubyVM::AbstractSyntaxTree.parse_file` in `tools/`, + `tasks/`, `test/gates/` and `.claude/skills/`, the exemption the owner METHOD, three test contrasts + allowlisted with reasons). Each is in `DEFAULT_GATES` in the root `Rakefile` and in CI's `gates` job, and + each is driven red through its rake task against a fixture workspace (`test/fixtures/gates/{ledger, + spdx_rbs,sole_parse}/workspace`). +- **The probe's ninth check, `chapters`** (`.claude/skills/housekeeping/chapters.rb`), clause-scoped, with + a backtick-tolerant range vocabulary, a negation vocabulary written as a `Regexp.union` of phrases, two + stated gaps in `Chapters::GAPS`, and the two phase-8 pre-correction lines as committed regression + fixtures. It found two live true positives, fixed in place. +- **The SPDX header on all 307 shipped `.rbs` files**, one line each; `rbs validate`, `steep`, + `gates:rbs_surface` and `gates:sig_diff` unchanged by it, and phase 9's `PackagingSuite` `NFR-13` + assertion now passes against the real tree. +- **`TransportSuite.run` on `Runner`**, the teardown supplied through the `around` wrapper so `Runner` is + not widened. +- **The repairs in the table below**, every one test-first: Task 10's `Protocol` repair, Task 5's header, Task 3's fold and + twenty of Task 10b's twenty-six items (its other six are the comment, YARD and document corrections listed + after the table). +- **`docs/deviations.md`'s nineteen rows flipped**, each with as-built evidence, and **the amendment set + written out as `C1`–`C19`**, with `docs/first-release.md`'s blocker naming all nineteen and the + ledger-consolidation act beside them. +- **The fiber single-flight verification** (`async_http/fiber_single_flight_test.rb`) — a verification, + not the repair `R9` planned (`P10-21`). + +## Repairs, each with the test that is its audit + +Every test below was run against the untouched `main` tree (the repair's own test files copied onto a +clean `git archive` of `b242de6`) before a line of `lib/` changed; the third column is what it printed +there. A pin whose test PASSED on `main` is a verification, and says so — its evidence is the mutation in +"Guards run red" that turns it red. + +| # | Task | IDs | The failing test | What it printed on the untouched base | +|---|---|---|---|---| +| 1 | 10b | `OBS-11`, `CFG-22` | `core/instrumentation/downstream_wirings_test.rb` "R3-1, OBS-11, CFG-22: a credential after a slash reaches neither channel" | `Expected "[dexpace] proxy URL \"http:/user:secret@proxy.corp:3128\" has no explicit port; …" to not include "secret"` — 8 of 8 spellings leak, on 4.0.6 and 3.2.11 (a scratch probe over `Kernel#warn`) | +| 2 | 10 | `HTTP-33`, `TRANSPORT-24` | `core/http/protocol_test.rb` "parses HTTP/1.0 to its own canonical form…"; `net_http/response_mapper_test.rb` "an HTTP/1.0 head maps…" | `Dexpace::InvalidArgumentError: unrecognised protocol "HTTP/1.0"` (both) | +| 3 | 10b | `HTTP-10`, `TRANSPORT-24` | `core/http/status_test.rb` "maps every three-place code…", "standard? is the protocol's own range…"; `async_http/response_mapper_test.rb` "a vendor 999 status and an HTTP/1.0 head both map" | `InvalidArgumentError: code must be an integer status code between 100 and 599`; `NoMethodError: undefined method 'standard?'` | +| 4 | 10b | `HTTP-47` | `core/http/url_test.rb` "every malformed, unrenderable or host-less http URL fails with the SDK's error" | a failure: `mailto://host` raised `URI::InvalidComponentError`, `http:` parsed | +| 5 | 10b | `HTTP-5`, `XCUT-15` | `core/model_test.rb` "own takes a Hash with a default proc…", "own refuses a value it cannot make shareable…" | `TypeError: allocator undefined for Proc` | +| 6 | 10b | `HTTP-17`, `HTTP-18`, `HTTP-19` | `core/http/header_syntax_test.rb` "every predicate is total…" | `NoMethodError: undefined method 'b' for an instance of Integer` | +| 7 | 10b | `BODY-9`, `SEAM-29` | `core/http/body/stream_body_test.rb` "rejects an already-closed stream…" | a failure: a bare `IOError: closed stream` escaped | +| 8 | 10b | `CFG-15`, `CFG-17`, `CFG-18` | `core/clock_test.rb` "a NaN or a Complex duration is refused…"; `core/async/delay_test.rb` "a NaN or a Complex delay is refused…" | `Dexpace::InvalidArgumentError expected but nothing was raised` (NaN); `NoMethodError` (Complex) | +| 9 | 10b | `CFG-13` | `core/config_test.rb` "a lookup's depth does not grow with the number of configures" | `Expected: 22 / Actual: 222` | +| 10 | 10b | `OBS-24`, `OBS-34` | `core/instrumentation/async_step_test.rb` "at BODY the recorded duration excludes the caller's continuation" | `-[{amount: 250.0}] +[{amount: 5250.0}]` | +| 11 | 10b | `SEAM-1` | `gates/require_allowlist_test.rb` "refuses a dexpace/ path carrying a dot segment" | a failure: `dexpace/../json` and `dexpace/./../base64` accepted | +| 12 | 10b | `NFR-7` | `gates/rubocop_config_test.rb` "the gate ignores a parent checkout's exclusions…" | `Expected "task :rubocop do …" to include "--ignore-parent-exclusion"` | +| 13 | 10b | `NFR-15` | `conformance/packaging_suite_test.rb` "the default gem-name rule reaches all six…" | `Expected: "Dexpace" / Actual: "Dexpace::Core"` | +| 14 | 5 | `NFR-13`, `NFR-3` | `gates/spdx_rbs_test.rb` "every shipped signature in the six gems is clean"; `conformance/packaging_suite_test.rb` "NFR-13 against this repository's own shipped signatures passes" | 307 offences, 0 of them "declares nothing"; `Expected: :passed / Actual: :failed` | +| 15 | 2, 5, 6 | `NFR-17` | `gates/{ledger_audit,spdx_rbs,sole_parse}_test.rb`, `gates/default_task_test.rb` | `LoadError` (no tool file) and a twenty-one-gate count | +| 16 | 10b | `SEAM-5`, `XCUT-23` | `core/registry_test.rb` Termination (2 tests) | **passed** — a pin; phase 9's spin mutation turns it red | +| 17 | 10b | `AUTH-35` | `core/auth/bearer_stamper_test.rb` "every rejection holds on the REFRESH path too…" | **passed** — a pin for 6c's review R4-1; a validate-skipped-on-refresh mutant turns it red | +| 18 | 10b | `PIPE-39` | `core/pipeline/standard_test.rb` "settings: reaches the async retry step on the no-tracer-factory branch too" | **passed** — a pin for 6b's review R3-1 | +| 19 | 3 | `NFR-17` | `conformance/transport_suite_test.rb` "one run yields all five statuses in order…" | **passed** — the invariant of the fold, green before and after | +| 20 | 10b | `OBS-8` | `core/instrumentation/event_test.rb` "the terminal emit … happens once" gains a parking barrier | test quality (5b's review R3-3): without the barrier the "race" could be a sequential second call on 3.2 | +| 21 | 10b | `REDIR-23` | `core/redirect/step_test.rb` "a chain of 5,000 hops is followed iteratively" | test quality: the ten-second wall clock measured 10.6 s and 12.6 s under load (8a's record); the depth comparison is kept as the proof | +| 22 | 10b | `RETRY-42`, `RECOV-28` | `core/resilience/recovery_retry_test.rb` "eight concurrent calls … keep their budgets apart" | test quality: interleaving-dependent, red two runs in five under load on 3.2.11 (8a's review R1); now one script per thread | +| 23 | 10b | `IO-9`, `BODY-19`, `BODY-22`, `BODY-30`, `RECOV-16` | the four ceiling tests in `core/http/body_test.rb`, `core/http/body/response_body_test.rb`, `core/recovery_test.rb` | red under `MAX_MATERIALIZED_BYTES=4096` exported (4 failures), green after `test/support/pinned_ceiling.rb` | +| 24 | 10b | `CFG-11` | `core/configuration/builder_test.rb`'s seeded-builder case | red under `K=from-the-host` exported: `Expected "from-the-host" to be nil` | +| 25 | 16 | `CTX-14`, `OBS-25`, `OBS-29` | `core/instrumentation/http_tracer_test.rb` "the bundle's factory makes shared span tracers, never an HTTPTracer" | **passed** — a documentation repair; no test can go red for a YARD sentence, and this one pins the distinction it states | +| 26 | 9 | `XCUT-12`, `AUTH-34`, `AUTH-37` | `async_http/fiber_single_flight_test.rb` | **passed** — a verification (`P10-21`); the thread-identity guard mutant turns it red | +| 27 | review R0-4 | `OBS-11`, `CFG-22` | `core/instrumentation/downstream_wirings_test.rb` "R0-4, OBS-11, CFG-22: a password holding a reserved character leaks no prefix" | against repair 1's tree: `http:/user:pa/ss@h:1` warned `http:/user:pa/***:***@h:1` -- the username and the prefix before the reserved character, in `Kernel#warn` and the config sink, on 4.0.6 and 3.2.11 (the reviewer's measurement; the test's own red run is Guards 32) | +| 28 | review R0-5 | `HTTP-10`, `TRANSPORT-24` | `core/http/status_test.rb` "maps every Integer code …" and "construction is total over the Integer line"; `core/http/response_test.rb`'s two coercion cases | against the 0..999 tree: `InvalidArgumentError: code must be an integer status code between 0 and 999` for `-1`, `1000` and `2**70` | +| 29 | review R0-8 | — (process tooling) | `housekeeping/chapters_test.rb` "an appears-in attribution binds forward and fires when wrong", "a second appears-in run does not bind backward" | against the round-0 check: the wrong attribution is silent (`Expected: ["SEAM-15"] Actual: []`) | + +Beside these, comment and YARD repairs that no test can observe: `Redactor`'s class YARD names where +redaction is applied (5b's review R3-4); `Cancellation::Source#off_cancel`'s comment no longer says +`Proc#==` is identity (measured false on all four rows; phase 2's review R3-3); `dexpace_test.rb`'s +"five public Resilience constants" is six (6a's review R2-1); `rbs_collection.yaml`'s header; the +`@decode_content` and `@owned`/`@dexpace_owns_upstream` YARD unknown-tag warnings; the `ResponseMapper` +YARD in both transports; `Bundle#tracer_factory`'s YARD (Task 16); and +`InvariantGates::BOUNDED_MAP_ALLOWED`'s `clients.rb` reason now citing its three tests. + +## Inbound list, dispositioned + +All sixty-five bullets of the roadmap's phase-10 inbound list, by position, date and content (the +first thirty-two are those of 2026-09-13; thirty-three were added after the design). Each bullet carries +the same disposition as a dated bracket in the roadmap. **18 repaired · 14 verified already fixed · +32 moved to `docs/first-release.md` (thirteen of them the amendment set) · 1 withdrawn** — none carried +forward. + +| # | Date | Bullet | Disposition | Evidence | +|---|---|---|---|---| +| 1 | 2026-09-13 | The audit of the §10.5 ledger | `docs/first-release.md` | §10.5 audited, not re-opened: `docs/deviations.md` row 5 carries the verdict with `ASYNC-3` waived-would-fail in the pool's driver; the gap stays `docs/first-release.md` § Unsatisfied MUSTs | +| 2 | 2026-09-13 | `OBS-29`'s surface decision, which is three findings and one decision. | `docs/first-release.md` | decided with no surface (`R6`): `OBS-29`'s own follow-up clause; `Bundle#tracer_factory`'s YARD now says it is CTX-14's span-tracer factory and not OBS-29's, pinned by `http_tracer_test.rb`; the unwired groups are a *Behavioural asymmetries* entry | +| 3 | 2026-09-13 | The probe's "this ID is stated in chapter X" claim check, which is not built. | repaired | built as the probe's ninth check, `chapters` (`.claude/skills/housekeeping/chapters.rb`, `test/chapters_test.rb`); two live true positives fixed in the phase-8 segmentation design | +| 4 | 2026-09-13 | The judgement whether the thread-only `XCUT-12` form phase 9 ships in Tasks 7–8 suffices | verified already fixed | judged and verified: the fiber form passes on the untouched tree (no deadlock), now asserted in `fiber_single_flight_test.rb`; the fallback trigger is closed | +| 5 | 2026-09-13 | The repairs of every audit phase 9 reports `:failed` | verified already fixed | the one `:failed` (`NFR-13`) is repaired by Task 5; `ASYNC-3`'s waiver stands per §10.5; no other `:failed` exists | +| 6 | 2026-09-13 | `dexpace-transport-async_http`'s `Clients#@by_origin` is an uncapped per-origin client cache, which `XCUT-14` (MUST) forbids. | verified already fixed | already closed by phase 9 (green `gates:bounded_map`, 2026-09-23); the allowlist reason now cites `clients_test.rb`'s three `XCUT-14` cases | +| 7 | 2026-09-13 | 8a's `Adapter#dispatch` leaves its rescue variable unused — first half CLOSED 2026-09-13 — and every repository tool that parses a filed s… | repaired | the general half built as `gates:sole_parse` (`tools/sole_parse.rb`, `test/gates/sole_parse_test.rb`) | +| 8 | 2026-09-13 | `NFR-13`'s SPDX gate is a RuboCop cop, so it cannot reach `sig/ | repaired | the SPDX header on all 307 shipped `.rbs` files plus `gates:spdx_rbs`; `packaging_suite_test.rb`'s `NFR-13` pin flipped to `:passed` | +| 9 | 2026-09-13 | The `PAGE-15`/`P7-1` and §10.5 attribution notes in `docs/deviations.md` | `docs/first-release.md` | written out as amendments `C12` and `C13` in `docs/deviations.md`; applying them is the frozen-chapter blocker's | +| 10 | 2026-09-13 | Any `gates:bounded_map` that runs red. | verified already fixed | every run green (`gates:bounded_map`, phase 10's tips) | +| 11 | 2026-09-13 | §3.1's decode recipe destroys every non-ASCII byte, and its target-less `#encode` follows a process global. | `docs/first-release.md` | amendment `C1`, written out; the blocker | +| 12 | 2026-09-13 | §4's builder list drops the multipart body, which `HTTP-3` names. | `docs/first-release.md` | amendment `C2`, written out; the blocker | +| 13 | 2026-09-13 | §3.1's ownership sentence and §10.12 attribute `IO-6`'s rule to the retired `SEAM-3`. | `docs/first-release.md` | amendment `C3`, written out; `IO-6` audited on its own row (ownership-on-wrap measured) | +| 14 | 2026-09-13 | §8.1 names `Event#tag(key, value)` and no requirement in chapter 15 does. | `docs/first-release.md` | amendment `C4`, written out; the blocker | +| 15 | 2026-09-13 | §3.2's block-scoped `read_body` construction cannot satisfy the two requirements it says it satisfies "literally". | `docs/first-release.md` | amendment `C5`, written out; the blocker | +| 16 | 2026-09-13 | `Net::HTTP` has a built-in automatic retry that is on by default, and §3.2, §11.18 and §12 all record that it has none. | `docs/first-release.md` | amendment `C6`, written out; the blocker | +| 17 | 2026-09-13 | `TRANSPORT-14`'s malformed-inbound-header-*name* clause is unreachable on `dexpace-transport-async_http`, and §12 records `TRANSPORT-14` a… | `docs/first-release.md` | amendment `C7` (first direction), written out; the waiver re-measured green by `conformance_test.rb` | +| 18 | 2026-09-13 | `TRANSPORT-8` is satisfiable on `dexpace-transport-async_http`, and §12 counts it among the eight MUSTs that hold vacuously — the inverse … | `docs/first-release.md` | amendment `C7` (second direction), written out | +| 19 | 2026-09-13 | §8.3's prohibition is absolute and `net-http`'s connect phase uses `Timeout.timeout`, which the cop that enforces the ban cannot see. | `docs/first-release.md` | amendment `C8`, written out with the per-row measurement (net-http 0.9.1 carries both the `Timeout.timeout` connect and `TCPSocket.open(open_timeout:)`) | +| 20 | 2026-09-13 | Phase 5a's exclusions table hands proxy use to phase 8 under two requirement IDs that are about something else, and it is the second docum… | verified already fixed | fixed in place on 2026-09-13 by phase 10's planning; re-read 2026-09-25 | +| 21 | 2026-09-13 | §9.3 calls Minitest a default gem; it is a bundled gem, and §2.4 is built on exactly that distinction. | `docs/first-release.md` | amendment `C9`, written out | +| 22 | 2026-09-13 | §9.3 fixes the requirement ID as the *waiver's* unit and leaves the *report's* unit unstated, and an appendix-B item cannot be it. | `docs/first-release.md` | amendment `C10`, written out | +| 23 | 2026-09-13 | Nothing in the MVP constructs or promotes an execution context, so `CTX-16`'s operation name never reaches the tracing seam it is defined … | `docs/first-release.md` | decided with no surface (`R7`); the correlation chain joins the worked-example blocker and the *Behavioural asymmetries* entry | +| 24 | 2026-09-13 | `PAGE-14` and `SSE-26`/`SSE-40` guard the identical single-use latch with two different error families, and the divergence has to be settl… | verified already fixed | the premise is gone: 7c raises `Page::PageStateError < ::StandardError` (`page/page_state_error.rb:17`), refuted as an ArgumentError by `page_state_error_test.rb`; `R8` not carried out (`P10-22`) | +| 25 | 2026-09-13 | Appendix C's `SSE-19` row drops the port sanction the chapter carries, and `docs/product-spec/` is frozen. | `docs/first-release.md` | amendment `C11`, written out; also a recommendation to the specification author | +| 26 | 2026-09-13 | Phase 2's three other declared-and-unwritten `sig/` files, which no gate can see. | withdrawn | withdrawn by 7a on 2026-09-20 (premise false on the tree); `gates:spdx_rbs`'s empty-declarations check finds 0 of 307 | +| 27 | 2026-09-13 | `Dexpace::Protocol.parse` has no alias for `"http/1.0"`, so a real `HTTP/1.0` response makes `ResponseMapper` raise. | repaired | `Protocol` gains `http/1.0` in `WIRE_FORMS` and `ALIASES` and `Protocol::HTTP_1_0` (`HTTP-33`); both adapters map an HTTP/1.0 head | +| 28 | 2026-09-13 | 8a's forward-obligations row for the two declined `TRANSPORT` SHOULDs is half stale. | verified already fixed | fixed in place on 2026-09-13 by phase 10's planning | +| 29 | 2026-09-13 | `APPENDIX_B.md`'s check 2 is weaker than the prose that specifies it. | verified already fixed | already done by phase 9: `tools/appendix_b.rb` and `test/gates/appendix_b_test.rb`'s set-equality test | +| 30 | 2026-09-13 | `TransportSuite` is not on `Runner`, leaving two status-deciding paths in one gem. | repaired | `TransportSuite.run` delegates to `Runner.run` with the teardown supplied through `around:`; the five-status invariant test in `transport_suite_test.rb` | +| 31 | 2026-09-13 | A design document that promised a shape the code did not take is phase 10's doc repair. | verified already fixed | a method, applied: each audit row names its as-built evidence (`docs/deviations.md`), and every design found wrong is bracket-corrected in place | +| 32 | 2026-09-13 | The `minitest` pin's ownership. | `docs/first-release.md` | re-measured 2026-09-25; the Minitest trigger's numbers corrected in `docs/first-release.md`; the pin stays phase 0's | +| 33 | 2026-09-14 | The process tooling is outside the RuboCop baseline. | `docs/first-release.md` | `docs/first-release.md` § Post-release triggers, the residues line: `.rubocop.yml` and `scripts/` stayed outside phase 10's file list | +| 34 | 2026-09-14 | `rake rubocop` passes vacuously in a worktree nested under the parent checkout's `.claude/`. | repaired | `--ignore-parent-exclusion` on the gate's command, asserted by `rubocop_config_test.rb` | +| 35 | 2026-09-15 | Nine later-phase design documents cite a retired corpus key, `data-modeling/5bc538ba`, as narrowing the wire model's Ractor-shareability c… | repaired | each of the eight designs carries a dated bracket naming the retired key and `P1-13` | +| 36 | 2026-09-15 | `Dexpace::URL.parse!` accepts a host-less `http:` and any absolute non-HTTP URI, so a `Request` can carry a URL no transport can dispatch,… | repaired | `URL.parse!` refuses a host-less http-family URL and wraps every `URI::Error` (`HTTP-47`); which other schemes dispatch stays the transport's call | +| 37 | 2026-09-16 | `MultipartBody` refuses a non-ASCII part name or filename, because its part-header sweep is the outbound header grammar. | `docs/first-release.md` | not taken: `docs/first-release.md` § Behavioural asymmetries, the port-readings entry (a) | +| 38 | 2026-09-16 | Phase 0's plan says the keyword-splat cop "carries no ordinal" and that phase 4a's cop is "the seventh"; every as-built document counts it… | repaired | phase 0's plan carries a dated bracket with the as-built count (eight) | +| 39 | 2026-09-17 | `Fiber[:k] = nil` retains the key with a `nil` value on Ruby 3.2, and `Fiber["k"]` raises `TypeError` on 3.2 and 3.3 — two carrier facts t… | repaired | 5b's plan, 8b's plan and the phase-5 charter carry a dated bracket naming the floor facts | +| 40 | 2026-09-17 | Phase 5b's design states two verified facts that the as-built code no longer rests on: fact 6's rebuild route (`u.userinfo = " | verified already fixed | already corrected in 5b's design As-built addendum; a dated bracket at the fact now points there | +| 41 | 2026-09-17 | The warnings-fatal gate runs every gem suite in one process, and Ruby 3.4+'s unused-block warning is suppressed process-wide once any same… | `docs/first-release.md` | `docs/first-release.md` § Post-release triggers, the residues line (`-W:strict_unused_block`) | +| 42 | 2026-09-17 | Four phase-3b and phase-4b tests assert the default materialisation ceiling and read the live one, so an exported `MAX_MATERIALIZED_BYTES`… | repaired | the four tests pin the default ceiling at the override tier (`test/support/pinned_ceiling.rb`); red under `MAX_MATERIALIZED_BYTES=4096` before, green after | +| 43 | 2026-09-18 | Phase 6a's design describes an async retry pump that recurses, and states the opposite. | verified already fixed | already corrected in 6a's design As-built addendum (`P6-54`); a dated bracket at the sketch now points there | +| 44 | 2026-09-18 | After phase 6 the body-replayability predicate has three spellings, two of them public and `NFR-4`-locked. | `docs/first-release.md` | not unified: the one-time surface-choices blocker in `docs/first-release.md` names the three spellings | +| 45 | 2026-09-19 | An explicit scheme-default port is elided at phase 1's model boundary, and a redirect target inherits it. | `docs/first-release.md` | recorded as a port reading: the one-time surface-choices blocker names the default-port elision (`HTTP-46`) | +| 46 | 2026-09-20 | After a mid-stream failure, `BufferedSource.over`'s enumerator restarts `#each` from its first chunk, so a further read re-delivers the bo… | `docs/first-release.md` | `docs/first-release.md` § Behavioural asymmetries, port-readings entry (d) | +| 47 | 2026-09-20 | The per-byte read path through `BufferedSource#getbyte` costs about 0.9 µs a byte, so the SSE line machine parses at roughly 1 MiB/s on 4.… | `docs/first-release.md` | `docs/first-release.md` § Post-release triggers, the residues line | +| 48 | 2026-09-20 | `gates:clean_bundle` installs into the interpreter's own gem directory, and the first adapter with a third-party dependency makes that a n… | verified already fixed | closed by 8a's Task 23 on 2026-09-21 (its own bracket) | +| 49 | 2026-09-20 | `rbs_collection.yaml`'s header comment is stale: "json arrives with dexpace-serde-json's codec in phase 7", and phase 7a added no row. | repaired | `rbs_collection.yaml`'s header rewritten to what the file holds | +| 50 | 2026-09-20 | `net-http`'s connect-phase `Timeout.timeout` has an observable: the first connection in a process starts Ruby's process-wide `Timeout` thr… | `docs/first-release.md` | amendment `C8` carries the clause; the warm-up stays | +| 51 | 2026-09-20 | rbs 4.2.0 types `TCPServer#initialize` as `(?String host, Integer port)` — an optional positional before a required one — and Steep 2.1.0 … | verified already fixed | the bundle's rbs is still 4.2.0 on every row, so the `untyped` local in `WireServer#initialize` stays; nothing to remove | +| 52 | 2026-09-20 | Phase 6b's `REDIR-23` proof carries a ten-second wall-clock bound over 5,000 hops, and the bound fails under machine load while the stack-… | repaired | the ten-second bound is gone; the depth comparison is the proof (`step_test.rb`) | +| 53 | 2026-09-20 | Phase 6a's `RETRY-42` / `RECOV-28` eight-thread budget test is interleaving-dependent, and errors under the whole-repository process on a … | repaired | each thread draws from its own script and the invariant is asserted per thread (`recovery_retry_test.rb`) | +| 54 | 2026-09-20 | `Dexpace::Status` guards `100`–`599` while `Net::HTTP` parses any three-digit code, so a `999` or a `600` head raises `Dexpace::InvalidArg… | repaired | a MUST repair (`HTTP-10`): `Status` maps every Integer code and the protocol range is `Status#standard?` | +| 55 | 2026-09-21 | Two child-process idioms prove one property — `seam_surface_test.rb`'s private `bare_require_report` and `test/support/bare_require.rb`'s … | `docs/first-release.md` | `docs/first-release.md` § Post-release triggers, the residues line | +| 56 | 2026-09-21 | Under the one-process `rake test:gems` the main fiber's storage carries 5c's no-op span (`dexpace.current_span => Instrumentation::NO_SPAN… | `docs/first-release.md` | `docs/first-release.md` § Post-release triggers, the residues line | +| 57 | 2026-09-21 | Core's two duration guards, `Dexpace::Async.validate_delay` and `Dexpace::Clock::Guard.duration`, admit a `NaN` and a `Complex`. | repaired | `Async.validate_delay` and `Clock::Guard.duration` refuse NaN and Complex (P8-77's shape), tested in `delay_test.rb` and `clock_test.rb` | +| 58 | 2026-09-21 | Two of phase 8c's design facts are stale on `async` 2.46.0, and `async-http`'s server lets a peer's mid-head `EOFError` reach Console. | `docs/first-release.md` | read in 8c's As-built addendum; the Console report is an upstream matter -- the residues line | +| 59 | 2026-09-21 | The portable `TRANSPORT-7` row proves its delivered-body clause by chance against a streaming adapter. | `docs/first-release.md` | `docs/first-release.md` § Post-release triggers, the residues line | +| 60 | 2026-09-23 | `Dexpace::Registry#resolve`'s discovery loop has no bound. | repaired | a termination argument at `Registry#resolve` and `registry_test.rb`'s Termination suite, which phase 9's spin mutation turns red | +| 61 | 2026-09-23 | `XCUT-12`'s fiber-scheduler form is unasserted, and the checklist row says so. | verified already fixed | verified as `XCUT-12`'s fiber form (`fiber_single_flight_test.rb`) | +| 62 | 2026-09-23 | `XCUT-9`'s cycle assertion has two stated residues, and `gates:cause_walk` proves neither absent. | `docs/first-release.md` | `docs/first-release.md` § Post-release triggers, the residues line | +| 63 | 2026-09-23 | 7a's `SEAM-21` evidence lives inside another assertion's body and is declared in no ID-keyed map. | `docs/first-release.md` | `docs/first-release.md` § Post-release triggers, the residues line | +| 64 | 2026-09-23 | `tools/surface.rb` cannot see a `private_constant` module, so a group that became public would be invisible to `gates:surface_snapshot` un… | `docs/first-release.md` | `docs/first-release.md` § Post-release triggers, the residues line | +| 65 | 2026-09-23 | `Dexpace::Conformance::PackagingCase`'s default gem-name → constant-path rule covers none of this port's exceptional four. | repaired | `PackagingCase`'s default rule covers all six gems (`-core` is the root, `http`/`json` are acronyms), tested in `packaging_suite_test.rb` | + +## Leftover review items, dispositioned + +The review nits and stated gaps the earlier phases' final reviews left open and routed nowhere — found in +the maintainer's per-phase leftover notes by the cross-check that opened this phase, not on the inbound +list. Each is re-measured on the tree before it is dispositioned. + +| Source | Item | Still present on `main`? | Disposition | +|---|---|---|---| +| 5b R3-1 | the proxy warning's credential belt, anchored | yes, 8 of 8 spellings on 4.0.6 and 3.2.11 | **repaired** (repairs 1) | +| 5b R3-2 | `AsyncStep` at BODY counts the caller's continuation | yes (5250 ms against 250) | **repaired** (repairs 10) | +| 5b R3-3 | `OBS-8`'s race test probabilistic on 3.2 | yes | **repaired** (repairs 20) | +| 5b R3-4 | `Redactor`'s YARD omits its application point | yes | **repaired** (YARD) | +| 5a R1-2 | the property chain grows per configure | yes (depth 22 → 222) | **repaired** (repairs 9) | +| 5a R1-1 | `builder_test.rb` reads the host's `K` | yes | **repaired** (repairs 24) | +| 5c R0-1 | "thirteen cross-reference rows" against eleven rows / fourteen IDs | yes | **repaired**: a dated bracket in 5c's checklist | +| 5c R0-2 | `Fiber#storage=` in a fact-pin test | yes | not taken: judged no action by 5c; a fact pin, which warns by design | +| 5c | the probe's `claims` check does not read CLAUDE.md's spelled lib-file count | yes | not taken; counted by hand for this phase's CLAUDE.md edit -- `docs/first-release.md`'s post-release residues line, with its pick-up condition | +| 0 R3-2 | `require_allowlist.rb` accepts `dexpace/../json` | yes | **repaired** (repairs 11) | +| 0 R3-3 | CLAUDE.md's "32 inbound bullets" | yes | **repaired** in CLAUDE.md | +| 0 R3-4, R3-6, R3-7 | gate tests resolve `ruby`/`bundle` from PATH; a category assertion run once; a third-party sub-feature refused | yes | `docs/first-release.md` residues line | +| 1 R3-1 | `Model.own` leaks TypeError for a default-proc Hash | yes | **repaired** (repairs 5) | +| 1 R3-3 | `Query.parse` strips a trailing NUL | yes | `docs/first-release.md` port-readings entry (b) | +| 1 R3-4 | `HeaderSyntax` predicates raise on a non-String | yes | **repaired** (repairs 6) | +| 2 R3-1, R3-2 | `mailto://host` and `ftp…;type=x` leak URI errors | yes (`InvalidComponentError`; a `FrozenError` from `#to_s`) | **repaired** (repairs 4) | +| 2 R3-3 | the comment "Proc#== is identity" | yes, and false on all four rows (`a == a.dup` is true) | **repaired** (comment) | +| 3a | three `untyped` signature params; `TeeSink`'s Float `tap_limit` refusal; an ASCII-only tee test | yes | `docs/first-release.md` residues line | +| 3b R2-1 | `Body.stream(closed)` raises a raw IOError | yes | **repaired** (repairs 7) | +| 4a-8c | the one-time public-surface choices "decide before the first tag" | yes | **one `docs/first-release.md` blocker** enumerating them | +| 6a R2-1 | `dexpace_test.rb`'s "five" Resilience constants (six) | yes | **repaired** (comment) | +| 6a R2-2 | the async pump's close-before-schedule order unpinned | yes (an equivalent mutation) | `docs/first-release.md` residues line | +| 6b R3-1 | `AsyncPipeline.standard`'s `settings:` on the no-factory branch unasserted | yes | **repaired** (repairs 18) | +| 6c R4-1 | the sync stamper's refresh-path rejections unpinned | yes | **repaired** (repairs 17) | +| 7a R2-1 | `serde.md`'s fence lacks `require "stringio"` | yes | **repaired** in `docs/sdk-documentation/serde.md`, the fence run on 4.0.6 and 3.2.11 | +| 7a R2-2, R2-3 | a CLAUDE.md overstatement; a roadmap count | superseded by this phase's CLAUDE.md and roadmap edits | re-derived, not patched | +| 7b R1-2 | an 11+-digit zero-padded `retry:` is ignored | yes | `docs/first-release.md` port-readings entry (c) | +| 7b R1-1 | `sse.md`'s `#with` sentence and fence names | not re-measured | not taken -- `docs/first-release.md`'s post-release residues line, picked up at the next edit to `sse.md` | +| 7c R2-1 | a non-`Response` duck never closed after a parse | yes | `docs/first-release.md` residues line | +| 9 | the YARD unknown-tag warning `@decode_content` | yes | **repaired** (YARD), with 3a's `@owned` pair | + +## Guards run red + +Every mutation below was applied to the tree at the end of this phase, the owning test or gate run, the +first failure captured and the file restored — through one harness, on **4.0.6** unless the row says +otherwise. The list is the brief's thirty-one-item minimum where it applies (items 17 and 18 do not: +`R8` was not carried out) plus the repairs' own mutants. + +| # | Mutation | Caught by (first failure) | IDs | +|---|---|---|---| +| 1 | Delete the header from one shipped `.rbs` (`dexpace-core/sig/dexpace/http/status.rbs`) | `gates:spdx_rbs`: "…/status.rbs: line 1 is not `# SPDX-License-Identifier: MIT` (NFR-13)"; `conformance/packaging_suite_test.rb` "NFR-13 against this repository's own shipped signatures passes": `Expected: :passed / Actual: :failed` | `NFR-13` | +| 2 | Put that header on line 3, two blank lines first | `gates:spdx_rbs` red on "line 1 is not …". **No asymmetry**: the brief expected `PackagingCase` to stay green (it reads two lines); measured, its `NFR-13` assertion goes red too (`Expected: :passed / Actual: :failed`) | `NFR-13` | +| 3 | `declarations.empty?` → the plan's `parse_signature(…).flatten.compact.empty?` | `gates/spdx_rbs_test.rb` "a signature with no declarations is an offence" (the Buffer survives the flatten, so the empty file is clean) | `NFR-3` | +| 4 | A direct `RubyVM::AbstractSyntaxTree.parse_file(path)` added to `tools/sole_parse.rb` itself | `gates:sole_parse`: "tools/sole_parse.rb:29: RubyVM::AbstractSyntaxTree.parse_file outside AstScan.parse (NFR-6)" | `NFR-6` | +| 5 | The same through `.send(:parse_file, path)`, and through `.public_send("parse_file", path)` | `gates:sole_parse`, the same message for each | `NFR-6` | +| 6 | Drop the `RubyVM::AbstractSyntaxTree` receiver scope | `gates/sole_parse_test.rb` "the repository has exactly one …": `tools/gemspec_audit.rb:78` and the other `Prism.parse_file` sites reported — the scope is load-bearing | `NFR-6` | +| 7 | Each new gate's rake task pointed at its fixture workspace (`DEXPACE_GATE_ROOT=test/fixtures/gates/{ledger,spdx_rbs,sole_parse}/workspace`) | exit 1 each: "row 1 ID set differs: chapter-only IO-6"; "…/probe.rbs: line 1 is not …"; "tools/x.rb:… parse_file outside AstScan.parse" — driven by `ledger_audit_test.rb`, `spdx_rbs_test.rb` and `sole_parse_test.rb` through `rake`, and re-run by hand at the code tip | `NFR-17` | +| 8 | `docs/deviations.md` row 8 loses `XCUT-23` | `gates:ledger_audit`: "row 8 ID set differs: chapter-only XCUT-23" | `NFR-17` | +| 9 | Rows 13 and 14 swapped | `gates:ledger_audit`: "row 13 subject drifted from entry 13", "row 14 subject drifted from entry 14", and both rows' ID sets | `NFR-17` | +| 10 | Row 2's verdict cites only `docs/sdk-design-ruby/10-…md` | `gates:ledger_audit`: "row 2 carries a verdict and no resolvable as-built evidence (P10-2)" | `NFR-17` | +| 11 | Row 7's verdict names `Dexpace::Completer` | `gates:ledger_audit`: "row 7 names Dexpace::Completer, which is not defined" — the task loaded the gems | `NFR-17` | +| 11a | Row 7 loses `OBS-2`; row 2 carries row 1's title; row 1 names `Dexpace::IO::Bufferz`; row 7 cites `basic_handlerz.rb` | `gates:ledger_audit`, each with its own line ("chapter-only OBS-2", "row 2 subject drifted", "not defined", "which does not exist") | `NFR-17` | +| 11b | `LedgerAudit#drift` stops reporting row-only IDs (a subset test, not equality) | **survived the first run** — no test gave a row a WIDER set. `ledger_audit_test.rb` gained "a row whose ID set is WIDER … equality, not a subset" over `register_extra_ids.md`; now red: `-["row 2 ID set differs: row-only BODY-9"]` | `NFR-17` | +| 11c | `LedgerAudit#evidence` never checks (`return []`) | `ledger_audit_test.rb`: the docs-only and the undefined-constant cases | `NFR-17` | +| 11d | `SpdxRbs` accepts the header anywhere in the first three lines | `spdx_rbs_test.rb` "a header that is present but not on line 1 is an offence" | `NFR-13` | +| 12 | `gates:ledger_audit` removed from `DEFAULT_GATES`; `gates:spdx_rbs` removed from `ci.yml` | `gates/default_task_test.rb`: "the default task's prerequisite count drifted. Expected: 24 Actual: 23"; `gates/ci_workflow_test.rb` red | `NFR-17` | +| 13 | `resolution.rb`'s belt back to the anchored `sub(/\A[^\/?#]*@/, …)` | `core/instrumentation/downstream_wirings_test.rb` "R3-1 …": `Expected "[dexpace] proxy URL \"http:/user:secret@proxy.corp:3128\" has no explicit port; …" to not include "secret"` — 4.0.6 and 3.2.11 | `OBS-11`, `CFG-22` | +| 14 | `"http/1.0"` dropped from `ALIASES` only; then from `WIRE_FORMS` only | `core/http/protocol_test.rb`: `InvalidArgumentError: unrecognised protocol "HTTP/1.0"`; the second fails at load: `Protocol#initialize: wire must be one of: http/1.1, http/2` from `HTTP_1_0 = build(…)` | `HTTP-33` | +| 15 | `"http/0.9" => "http/1.0"` added to `ALIASES` | `core/http/protocol_test.rb` "raises on an unrecognised identifier, naming it": `http/0.9. Dexpace::InvalidArgumentError expected but nothing was raised` | `HTTP-33` | +| 16 | `net_http`'s `ResponseMapper` over an `HTTP/1.0` head, the alias removed | `net_http/response_mapper_test.rb`: `InvalidArgumentError: unrecognised protocol "HTTP/1.0"` | `HTTP-33`, `TRANSPORT-24` | +| 17, 18 | (Task 8 mutants) | **not run**: `R8` was not carried out (`P10-22`), so there is no re-parenting to mutate | — | +| 19 | `BearerStamper#refresh!`'s `synchronize` replaced by a thread-identity guard (re-entry on the owning thread passes through) | `async_http/fiber_single_flight_test.rb`: "a refresh under one reactor was not single-flight. Expected: 1 Actual: 8" — 4.0.6 and 3.3.12 | `XCUT-12`, `AUTH-34` | +| 20 | The racers run as `::Thread.new` instead of `task.async` | the same file: `NoMethodError: undefined method 'wait' for an instance of Thread` before the one-thread assertion (`threads.uniq.size == 1`) is reached, and the base case's thread-count teardown (`Expected: 9 / Actual: 17`) — red, though not on the identity assertion the brief named | `XCUT-12` | +| 21 | `Model.own` without the default-proc strip | `core/model_test.rb`: `InvalidArgumentError: a model cannot own this collection: allocator undefined for Proc` | `HTTP-5`, `XCUT-15` | +| 22 | `Clock::Guard`'s NaN screen removed | `core/clock_test.rb` "CFG-15, CFG-17: a NaN or a Complex duration is refused …": `NaN. Dexpace::InvalidArgumentError expected but nothing was raised` | `CFG-15`, `CFG-17` | +| 23 | `Async.validate_delay`'s `real?` screen removed | `core/async/delay_test.rb`: `(1+1i). [Dexpace::InvalidArgumentError] exception expected, not Class: ` | `CFG-18` | +| 24 | `Configuration::Builder#composed_source` back to nesting per configure | `core/config_test.rb` "CFG-13: a lookup's depth does not grow …": `Expected: 26 / Actual: 626` | `CFG-13` | +| 25 | `AsyncStep`'s end-time `on_settle` removed (the pre-repair attach order) | `core/instrumentation/async_step_test.rb` "at BODY the recorded duration excludes the caller's continuation": `-[{amount: 250.0}] +[{amount: 5250.0}]` | `OBS-24`, `OBS-34` | +| 26 | `reachable?` back to `start_with?("dexpace/")` | `gates/require_allowlist_test.rb` "refuses a dexpace/ path carrying a dot segment": `"" to include "dot_segments.rb:6"` | `SEAM-1` | +| 27 | `--ignore-parent-exclusion` removed from the rubocop task | `gates/rubocop_config_test.rb`: `Expected "task :rubocop do …" to include "--ignore-parent-exclusion"` | `NFR-7` | +| 28 | `REDIR-23`: the ten-second wall-clock bound re-added; then the step made recursive | the bound: **nothing red** (as intended: it was never the proof); the recursion: `core/redirect/step_test.rb` "a chain of 5,000 hops is followed iteratively": `Expected: 27 / Actual: 5027` | `REDIR-23` | +| 29 | The `chapters` check's range pattern disabled | `housekeeping/chapters_test.rb` "the phase8a pre-correction line fires on all three wrong ids" and one more | — (process tooling) | +| 30 | `Checks::Chapters` dropped from `Probe::ALL` | `housekeeping/probe_test.rb` "check names are the nine the documentation states" | — | +| 31 | `Bundle#tracer_factory`'s YARD | **no test can go red for a YARD sentence**; `http_tracer_test.rb` pins the distinction it states (repairs 25) | `CTX-14`, `OBS-29` | +| 32 | `ProxyResolution#scrub_userinfo` returns the raw value (review round 0's R0-4 repair disabled) | `core/instrumentation/downstream_wirings_test.rb` "R0-4 …": the first spelling's warning includes `pa/` -- red on 4.0.6 | `OBS-11`, `CFG-22` | +| 32a | `AUTHORITY_START` admits a bare `user:` as a scheme (`/+` → `/*`) | **survived, equivalent**: `user:***:***@…` is then reduced to `***:***@…` by the unanchored belt that runs after the redactor, so no channel ever carries the username; recorded, not a gap | `OBS-11` | +| 33 | `Status#initialize` back to the 0..999 range | `core/http/status_test.rb` "maps every Integer code …": `code must be an integer status code between 0 and 999` for `-1` | `HTTP-10` | +| 34 | The `chapters` check's forward binding removed (`run_target` always the preceding chapter) | `housekeeping/chapters_test.rb`: the phase-5 two-run shape fires `SEAM-13` against chapter 03, and the wrong `appears in` attribution goes silent | — (process tooling) | +| 35 | `'appears in'` restored to `Chapters::NEGATION` | `housekeeping/chapters_test.rb` "an appears-in attribution binds forward and fires when wrong": `Expected: ["SEAM-15"]` | — (process tooling) | +| 36 | `ProxyResolution#scrub_userinfo` scrubs through the FIRST `@` (`index` for `rindex`) | `core/instrumentation/downstream_wirings_test.rb` "R0-4 …": `http://user:se@c/ret@proxy.corp`'s warning keeps `c/` -- red on 4.0.6 and 3.2.11 (it survived round 0's spellings; review round 1, R1-1) | `OBS-11`, `CFG-22` | +| 37 | `"not appear in"`, `"does not appear"` and `"do not appear"` dropped from `Chapters::NEGATION` | `housekeeping/chapters_test.rb` "a negated appears-in is not an attribution": `SEAM-15` fires against chapter 03 -- red on 4.0.6 (review round 1, R1-4) | — (process tooling) | +| 38 | `'never appear'` and then `"n't appear"` dropped from `Chapters::NEGATION`, one at a time | `housekeeping/chapters_test.rb` "a negated appears-in is not an attribution": red on `never appears in`, then on `doesn't appear in` -- 4.0.6 (review round 2, R2-2) | — (process tooling) | +| x1 | `Status` guard back to `100..599` (as first cut; the guard is now a type check, R0-5) | `core/http/status_test.rb`: three errors, `code must be an integer status code between 0 and 999` raised from the mutated guard's own message path | `HTTP-10` | +| x2 | `URL.parse!` rescues `URI::InvalidURIError` only | `core/http/url_test.rb`: `mailto://host. [Dexpace::InvalidArgumentError] exception expected, not Class: ` | `HTTP-47` | +| x3 | `HeaderSyntax.valid_name?`'s String screen removed | `core/http/header_syntax_test.rb`: `NoMethodError: undefined method 'b' for an instance of Integer` | `HTTP-17` | +| x4 | `StreamBody#refuse_closed!` no longer reads `closed?` | `core/http/body/stream_body_test.rb` "rejects an already-closed stream …" | `BODY-9` | +| x5 | `Registry#resolve`'s installed-provider return made conditional on an empty factory map (phase 9's spin) | `core/registry_test.rb` Termination: "#resolve made no progress within 2.0s" | `SEAM-5` | +| x6 | `PackagingCase`'s default rule back to segment-for-segment | `conformance/packaging_suite_test.rb` "the default gem-name rule reaches all six …": `dexpace-core. Expected: "Dexpace" / Actual: "Dexpace::Core"` | `NFR-15` | +| x7 | `BearerStamper#refresh!` validates only on a cold cache | `core/auth/bearer_stamper_test.rb` "AUTH-35: every rejection holds on the REFRESH path too …": `[Auth::ProviderError] exception expected, not NoMethodError` | `AUTH-35` | +| x8 | `AsyncPipeline.standard` drops `settings:` on the no-tracer-factory branch | `core/pipeline/standard_test.rb` "settings: reaches the async retry step on the no-tracer-factory branch too": `SeamError: Async.delay needs a registered Fiber.scheduler …` (the default settings' positive delay) | `PIPE-39` | +| x9 | `TransportSuite`'s teardown lambda clears instead of tearing down | `conformance/transport_suite_test.rb` "one run yields all five statuses …": `Expected: 3 / Actual: 0` teardowns | `NFR-17` | + +Every mutated file was restored by the harness's `ensure`, and `git status` was clean after each batch. + +## Facts re-measured (Task 1) + +Every fact the plan rests on was re-run on **3.2.11, 3.3.12, 3.4.10 and 4.0.6**, outside the bundle +where the fact is about the interpreter's own install, with `PATH` pointed at each interpreter. + +| Fact | 3.2.11 | 3.3.12 | 3.4.10 | 4.0.6 | +|---|---|---|---|---| +| `rbs` installed with the interpreter | 4.1.3, 2.8.2 | 3.4.0 | 3.8.0 | 4.2.0, 3.10.0 | +| active `net-http`, outside the bundle | 0.9.1 (an installed gem above the default) | 0.4.1 | 0.6.0 | 0.9.1 | +| `Timeout.timeout(@open_timeout` present in `net/http.rb` (the connect phase) | yes (:1791) | yes (:1601) | yes (:1657) | yes (:1791) | +| `minitest` installed; version `require "minitest/mock"` loads | 5.27.0, 5.25.1; 5.27.0 | 5.20.0; 5.20.0 | 6.0.6, 5.25.4; 5.25.4 | 6.0.0, 5.27.0; 5.27.0 | +| `a = proc {}; a == a.dup` (the "`Proc#==` is identity" comment) | true | true | true | true | +| Two fibers of one reactor on one `Thread::Mutex` park rather than deadlock (async 2.46.0) | — (async-http needs 3.3) | park | not measured | park | + +The `net-http` rows correct the plan's assumption that 0.9.1 dropped the `Timeout.timeout` connect path: +it did not: the `Timeout.timeout` line is still there at :1791, beside the `TCPSocket.open(open_timeout:)` +path 0.9.1 added — which is why `C8` narrows §8.3's sentence to code this repository writes. The +`minitest` row corrects `docs/first-release.md`'s 2026-09-13 trigger text (fact 7): `minitest/mock` loads +on every row because the bundle's 5.x is what activates, and the pin in the root `Gemfile` is right — +the trigger's measurement was rewritten, the pin was not. + +## Intake from phase 9 (Task 3) + +Phase 9's aggregate over its four suites was **43 passed, 1 failed, 0 vacuous, 1 waived, 0 errored**. +Every non-pass was dispositioned here: + +- **`NFR-13` `:failed`** → **repaired** (repairs 14): the header on all 307 shipped `.rbs` files, the + `packaging_suite_test.rb` pin flipped to `:passed`, and `gates:spdx_rbs` so it stays that way. +- **`ASYNC-3` waived (would fail)** → unchanged, by design: design §10.5's unsatisfied MUST; its line in + `docs/first-release.md` § Unsatisfied MUSTs stands, and the second test that runs it unwaived and + requires the failure still passes. +- **`XCUT-18`'s wire-boundary assertion** accepted as phase 9 left it — real against `net_http` as the + aggregate's `transport:`, and vacuous only in the core driver that has no transport to aim at. +- **The async transport's two waivers** (`TRANSPORT-14`, `TRANSPORT-27`) were re-measured through + `async_http/conformance_test.rb:173`, which runs each unwaived and requires the failure; both still + fail unwaived, so both waivers stand. + +## Audit groups run + +The phase-start pair first: **71 notes** (`--origin note --brief`) and **6 conflicts** +(`--section conflicts --brief`), every one of the six printed `[overridden by notes/…]` — none open. +Then one group per audit task, each query from the knowledge-lookup skill's table, the entry count as +printed: + +| Task | Group | Entries read | +|---|---|---| +| 11 | Encoding and binary strings, plus `--prefix SEAM,IO --section rules` (§10.1, §10.2, §10.12) | 71 | +| 12 | Gem layout, zero-dependency core; public API surface (§10.3, §10.4, §10.7, §10.8, §10.9) | 327 | +| 13 | Pipeline composition and execution context; resilience (§10.5, §10.6, §10.10, §10.11, §10.15) | 297 | +| 14 | Serialization, SSE and pagination (§10.13, §10.14, §10.16) | 192 | +| 15 | Observability, configuration and redaction; transport and async-runtime adapters; cross-cutting (§10.17, §10.18, §10.19, closing note) | 397 | + +No rule in any group was found broken by the as-built tree that a note does not already record, so +**no note was added** to `docs/knowledge/notes/`; `ruby scripts/verify_knowledge_structure.rb` exits 0. + +## Gate runs + +Measured on the untouched base (`b242de6`, a `git archive` export so the phase's own edits could not +leak in) and on the finished tree, **4.0.6**. The three tips are proven separately, gate by gate, and +the report that hands the stack over carries those runs. + +| Tree | Gate | Result | +|---|---|---| +| base | all twenty-one | green; `test:gems` 4206 runs, 74618 assertions, 0 failures, 0 errors, 9 skips, coverage 99.82%; `test:gates` 193 runs, 877 assertions | +| finished | all twenty-four | green; `test:gems` 4231 runs, 74843 assertions, 0 failures, 0 errors, 9 skips (the base's nine: no `skip` was added or removed), coverage 99.83%; `test:gates` 219 runs, 963 assertions; YARD 100.00% documented; `bundler_audit` no vulnerabilities | +| finished | `gates:ledger_audit` | "every register row matches its design section 10 entry, and every verdict cites resolvable as-built evidence." | +| finished | `gates:spdx_rbs` | "307 shipped signatures carry the header and declare something." | +| finished | `gates:sole_parse` | "AstScan.parse is the one parse_file; 3 deliberate test contrasts allowlisted with reasons." | +| finished | `ruby .claude/skills/housekeeping/probe.rb` | nine checks, exit 0 | + +## Deviations from the plan + +1. **`R1`'s file list was widened** by a dated addendum in the design (the maintainer's decision 4): + `docs/sdk-documentation/**`, the root `README.md`, `gems/dexpace-transport-net_http/lib/**` (YARD + only), `rbs_collection.yaml`'s header comment and `gems/dexpace-conformance/**` in full. + `.rubocop.yml` and `scripts/` stayed out, and the RuboCop exclusion became a `docs/first-release.md` + residue line. The addendum was written during execution rather than strictly before the first file + outside the original list was touched; its content is what the decision said. +2. **Task 8 was not carried out** (`P10-22`); `PAGE-14`, `SSE-26` and `SSE-40` are verification rows. +3. **Task 9 is a verification** (`P10-21`): one fiber race test, and the thread-identity mutant is its + discriminating red (Guards 19). +4. **Task 4 is a verification** (`P10-23`), and **Task 3 Step 4 was already phase 9's** (`P10-24`). +5. **`TransportSuite.run` folds onto `Runner` without widening it** (`P10-25`); two detail pins moved. +6. **The plan named `HTTP-24`/`HTTP-43` for the `Protocol` repair**; it is `HTTP-33` (`P10-28`). +7. **Three MUST/SHOULD repairs the plan did not carry**, all under the maintainer's decision 3: + `Status.of` total over every Integer with `#standard?` (`HTTP-10`, `P10-27`), `URL.parse!`'s wrapping and host + screen (`HTTP-47`, `P10-29`) and `Model.own`'s default proc (`HTTP-5`, `P10-30`). The runtime + manifest gains exactly two rows, `Dexpace::Protocol::HTTP_1_0` and `Dexpace::Status#standard?`. +8. **Task 10b** (the maintainer's decision 7) took twenty-six items and stopped at the bound + (`P10-34`). +9. **`NFR-8`, `NFR-9` and `SEAM-10` are N/A** here where phase 0 wrote `🚫 retargeted` (`P10-32`). +10. **Ten requirement levels were corrected in place** in earlier checklists, each with a dated bracket + naming appendix C's level: `SEAM-10` (phase 2), `IO-34`, `IO-36` (3a), `RECOV-25` (4b), `NFR-11` + (4c, 5a, 5b, 5c), `IO-9` (5a), `BODY-20` (5b). The rows' marks are unchanged. +11. **`NFR-4` stays ✅** (`P10-35`) and **the RBS baseline line stays open** (`P10-36`). +12. **The amendment set is `C1`–`C19`**, not `C1`–`C14`: `C14` was reconciled into + `docs/deviations.md` (it was filed only in `docs/first-release.md`), and the as-built audit added + `C15`–`C19` (`C19`, §4's and §10.10's `send(:new)` sentence, from the review's R0-6). The consolidation of every phase's as-built ledger rows into §10 is named inside the + existing frozen-chapter amendment blocker with its alternative, not as a new blocker. +13. **`docs/deviations.md` rides the code branch** (`P10-33`). +14. **The probe's `chapters` check exempts phase 10's own documents** and writes its negation vocabulary + as a union of phrases (`P10-31`). + +## Findings routed + +This is the last phase: a finding it does not repair has no phase to go to and becomes a dated +`docs/first-release.md` line. + +| Finding | Owner | +|---|---| +| The thirteen-plus-six frozen-chapter amendments (`C1`–`C19`) and the ledger consolidation | `docs/first-release.md`, the frozen-chapter amendment blocker | +| The one-time public-surface choices phases 4–8 said to "decide before the first tag" | `docs/first-release.md`, a new blocker enumerating them | +| `Query.parse`'s trailing NUL; the default-port elision at the model boundary; 7b's zero-padded `retry:` | `docs/first-release.md`, *Behavioural asymmetries* → "Port readings" (a)–(d) | +| The process-tooling RuboCop exclusion; the per-byte `#getbyte` cost; the main fiber's leftover span; `.over`'s restart; the phase-0, phase-3a, 6a and 7c residues in the leftovers table above | `docs/first-release.md`, the post-release residues line | +| The continued-clause blind spot of the `chapters` check | `docs/first-release.md`'s existing trigger, re-measured | +| The RBS sig-diff baseline | `docs/first-release.md` § Release path, left open (`P10-36`) | +| A `ledger_audit` survivor found by the mutation battery (row-only IDs unasserted) | fixed in place: `register_extra_ids.md` and its test | + +## Work phase 10 postponed + +Three things, each owned by a `docs/first-release.md` entry and none by a phase: applying the nineteen +frozen-chapter amendments (a human's act; the trees are frozen to every tool), the `chapters` check's +continued-clause blind spot (a sentence-spanning parser is not justified at two fires), and an `NFR-4` +diff against a release tag that does not exist. The design's section "Work phase 10 postponed, and who +owns it now" carries each with its pick-up condition. + +## Review round 0, repaired 2026-09-25 + +The maintainer's review of the three tips (code `e9ddaa9`, tests `2d59890`, docs `2f1451a`) returned one +blocking finding, eight to fix and three nits; every one is repaired on the branch that owns its file. + +- **R0-1** (blocking): seven own rows carried a copied verdict false for their ID -- `SEAM-6`–`SEAM-9` + now cite phase 2's `Dexpace::Registry`, and `IO-30`, `IO-31`, `IO-39` say *retired apparatus*, each + from its owner's row. A mechanical pass over all 124 own rows against their owners' marks found no + other contradiction beyond the deliberate departures each row states (`PIPE-33`, `RECOV-34`, `NFR-8`, + `NFR-9`). +- **R0-2**: `docs/sdk-documentation/{transport-net_http,http,transport-async_http}.md` describe the + as-built `Protocol` (`http/1.0` admitted) and `Status` (total, `#standard?`). +- **R0-3**, **R0-12**: `CLAUDE.md` names net_http's two YARD comments and two tests, and `C14`'s origin. +- **R0-4**: the proxy warning's residual prefix leak is repaired (repairs 27, Guards 32). +- **R0-5**: `Status` is total over every Integer (repairs 28, Guards 33); `P10-27` amended. +- **R0-6**: `docs/deviations.md` row 10 is *confirmed, narrower* and the set gains `C19` (`P10-37`). +- **R0-7**: 5c's probe-claims item and 7b's `sse.md` item are on `docs/first-release.md`'s residues + line, each with its pick-up. +- **R0-8**: `"appears in"` binds forward rather than negating (repairs 29, Guards 34–35); `P10-31` + amended. Live fires over `docs/` stay the two phase-8 lines the phase fixed, now zero. +- **R0-9**: `SEAM-22` carries phase 2's `🚫 mechanism; surviving clause ✅`; the tally is ✅ 107 · 🚫 11. +- **R0-10**, **R0-11**: the misplaced brackets moved to the end of the sentence or item citing the key, + their repeated tail reworded; the struck `XCUT-12` trigger's old body struck too. + +## Review round 1, repaired 2026-09-25 + +The review of the round-0 tips (code `0d830c2`, tests `cb69353`, docs `91f0d51`) returned no blocking +finding, three to fix and two nits; every one is repaired on the branch that owns its file. + +- **R1-1**: the R0-4 proxy case gains three spellings whose password holds an `@` ahead of a reserved + character, so the through-the-LAST-`@` rule is pinned: a first-`@` scrub now fails it on 4.0.6 and + 3.2.11 (Guards 36). The shipped code was already right; the gap was the test's. +- **R1-2**: `HTTP-19` and `BODY-9`, both carried by lib repairs 6 and 7, gain cross-reference rows; the + added set is 17 and the total 205. +- **R1-3**: the one `rubocop:disable` phase 10 added (`tools/ledger_audit.rb`, `Metrics/ParameterLists`) + carries its reason. +- **R1-4**: `Chapters::NEGATION` gains the negated verb (`not appear in`, `does not appear`, `do not + appear`), so "`X` does not appear in " is no longer read as an attribution (Guards 37). +- **R1-5**: `docs/first-release.md`'s `dexpace-async-async` entry no longer points at the closed + `XCUT-12` trigger as live. + +## Review round 2, repaired 2026-09-25 + +The review of the round-1 tips (code `b21031c`, tests `8ec8f16`, docs `795d30a`) returned no blocking +finding, one to fix and one nit; both are repaired on the branch that owns the file. + +- **R2-1**: `docs/first-release.md`'s RBS-baseline entry still read `NFR-4` as a diff against the + previous release tag that "stays ⏳ until a `v*` tag exists", against `P10-35` and this checklist's + `NFR-4` ✅. The sentence now says the entry is design §9's release-tag mechanism, that `NFR-4` is ✅ + through `gates:surface_snapshot`, and that what waits for a tag is `gates:sig_diff`. It carries a + dated correction note. The entry stays open for `P10-36`'s reason. +- **R2-2**: `Chapters::NEGATION` gains `never appear` and `n't appear`, so "`X` never appears in + " and "`X` doesn't appear in " are no longer read as attributions. The negated-verb + case drives both, plus `don't appear in` and `appears nowhere in` (Guards 38). The probe is clean at + the documentation tip. + +--- + +**Reconciled 2026-09-25.** Written on the working tree that the three stacked branches were cut from, +before the cut; the cut was proven exact (`git diff` between the documentation tip and the working +branch empty) and the working branch deleted. `main` is `b242de6` before and after; nothing is pushed, +and issue #34 stays open. diff --git a/docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-design.md b/docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-design.md index aca45f3..cce3b0c 100644 --- a/docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-design.md +++ b/docs/work/mvp/phase10/2026-09-13-phase10-deviation-reconciliation-and-release-readiness-design.md @@ -546,6 +546,18 @@ inherits it: `docs/sdk-design-ruby.md`, `docs/knowledge/harvested/`. Five trees, frozen, and `R5` is the whole consequence. +**Addendum, 2026-09-25 — `R1`'s file list widened, by the maintainer's decision at execution.** +Four trees join the list above, each for a named reason and nothing wider: `docs/sdk-documentation/**` +and the root `README.md`, where a repair makes a page's prose stale; `gems/dexpace-transport-net_http/lib/**` +for **YARD comments only** — the `@decode_content` unknown-tag warning and the `ResponseMapper` sentence +that said an HTTP/1.0 response raises — and no code; `rbs_collection.yaml`'s header comment and nothing +else in that file; and `gems/dexpace-conformance/**` in full, because Task 5's repair invalidates +`packaging_suite_test.rb`'s pinned `NFR-13` `:failed` and the `PackagingCase` name-rule repair lives in its +`lib/`. `.rubocop.yml` and `scripts/` stay **out**: the process-tooling RuboCop exclusion becomes a +`docs/first-release.md` line instead. **Recorded honestly:** this addendum was written in the documentation +pass, after the code that relied on it was committed on the working branch, not before it as the decision +asked; every file it admits is named above and in the checklist's "Deviations from the plan". + --- ## `R2` — the re-derivation method, stated so it can be failed @@ -1292,6 +1304,33 @@ Each row is consolidated into design §10 and audited by `docs/deviations.md`. | P10-10 | The chapter-attribution check is clause-scoped, not verb-scoped, and it gates | `R10`; the requirement-ID conventions; `NFR-17` | A claim-verb grammar matches 0 of 38 live candidates; clause scoping takes the naive 23-pair fire set to 3 fires with 0 false positives. Two blind spots are written into the check's own gap, as phase 9 did for its three | | P10-11 | Phase 10 defines no release process and marks `NFR-4` ⏳ while establishing its baseline | `R11`; `NFR-4`, `NFR-16`, `NFR-12`; `docs/first-release.md` | The release path's conditions are outside this repository. Establishing a baseline is not satisfying a requirement about diffing against one, and marking it ✅ would be the kind of claim this phase exists to catch | +### As built, 2026-09-25 + +The rows above stand and nothing is renumbered. What execution changed takes the band from **`P10-21`**; +`P10-12`–`P10-20` are left free as a visible gap, the convention 4a/4b/4c and 5a/5b/5c used. Every row is +re-derived from the tree, and each names the evidence. + +| # | Deviation | Requirement / document | Why | +|---|---|---|---| +| P10-21 | **`R9` is carried out as a verification, not a repair.** The deadlock premise is false: two fibers of one reactor blocking on one `Thread::Mutex` do not deadlock -- the second parks on the scheduler (measured on 3.3.12 and 4.0.6, async 2.46.0), and the real `Auth::BearerStamper` under `Sync` with eight fibers makes exactly one fetch. The fiber race ships as `gems/dexpace-transport-async_http/test/dexpace/transport/async_http/fiber_single_flight_test.rb` against the real `BearerStamper` and `AsyncBearerStamper`, discriminating a thread-identity single-flight guard (Guards run red, 19) | `XCUT-12`, `XCUT-11`, `AUTH-34`, `AUTH-35`, `AUTH-37`; `R9` | `R9` justified the driver by a hazard the tree disproves; a repair whose failing test passes on `main` is a verification (`P10-1`). What a thread-only race truly cannot see is a guard keyed by thread identity, and that is what the fiber race catches. Not through `InvariantSuite`'s `around:`: its `XCUT-12` assertion races sixteen THREADS it builds itself, so wrapping the run in `Sync` runs threads inside a reactor and proves nothing new | +| P10-22 | **`R8` is not carried out.** 7c already raises `Dexpace::Page::PageStateError < ::StandardError` (`page/page_state_error.rb:17`, raised at `page/pages.rb:96` and `page/walk.rb:94`), and `page_state_error_test.rb` refutes `ArgumentError` -- `R8`'s defect is absent. A shared supertype no requirement asks for is not a repair, and `SSE::StreamStateError` is also `SSE-27`'s closed-stream refusal, so `SingleUseError` would misname one of its two raise sites. `PAGE-14`, `SSE-26` and `SSE-40` are verification rows | `PAGE-14`, `SSE-26`, `SSE-40`; `R8`, `P10-8` | `P10-1` | +| P10-23 | Task 4 is a verification: `Transport::AsyncHTTP::MAX_ORIGINS` (32), `Clients#drain`'s loop under the insert's mutex, the eviction close and `#close`'s clear all shipped with 8c, pinned by `clients_test.rb`'s three `XCUT-14` cases; `gates:bounded_map` green. The allowlist reason for `clients.rb` now cites those three tests as its mechanism | `XCUT-14`, `TRANSPORT-13` | No defect; phase 9 closed the blocker on 2026-09-23 | +| P10-24 | Task 3 Step 4 was already done by phase 9 in files the plan does not name: `tools/appendix_b.rb` and `test/gates/appendix_b_test.rb`, whose "every row's ID column equals the set parsed from that item's own text" is set equality. No accessor, no fixture | `NFR-17`; bullet 29 | Verification row | +| P10-25 | `TransportSuite.run` folds onto `Runner` **without widening `Runner`**: the fresh `TransportCase` is built inside the invocation and the `around` handed to `Runner` tears it down in an `ensure` after the driver's own wrapper returns. One observable change: a `:failed` detail now carries `Runner`'s `(expected …, got …)` suffix; the two pins that spelled the old form moved with the change | `NFR-17`; `P9-10` | Five statuses decided in one place; phase 9's `R3` interface untouched | +| P10-26 | The three new gates join every path to `gate_root` (P9-27's lesson from the first line); `gates:ledger_audit` requires core and every adapter whose Ruby floor the interpreter meets before resolving a constant; `gates:spdx_rbs` reads the declarations as the third element of `RBS::Parser.parse_signature` (`.flatten.compact.empty?` is never true on rbs 4.2.0); `gates:sole_parse` is scoped to a `RubyVM::AbstractSyntaxTree` receiver (Prism's `parse_file` routes no diagnostic through `Warning.warn`) with three reasoned test-contrast entries in `SoleParse::ALLOWED`, each asserted still to report | `NFR-3`, `NFR-6`, `NFR-13`, `NFR-17`; addenda A8, A9, A11 | The plan's fences were CWD-relative, called three private or absent `AstScan` helpers, and would have reported six offences on a clean tree | +| P10-27 | **`HTTP-10` repaired, a MUST.** `Dexpace::Status` is total over every Integer -- only a non-Integer, which is not a code, is refused -- and the protocol's 100-599 range moved to `Status#standard?`. (The first cut stopped at 0..999, "every code a status line can carry", and still threw on 1000 and -1: a port reading narrower than the MUST, recorded in no ledger row; review round 0's R0-5 widened it.) Phase 1 recorded the old guard as "the port's reading" in its plan and checklist and in no ledger row, so by the maintainer's rule it was a defect: a 600 or LinkedIn's 999 made both transports raise where `TRANSPORT-24` says to surface it. Two phase-1 pins and two phase-8 adaptation-failure tests changed with it (the latter now use an `HTTP/1.2` head) | `HTTP-10`, `HTTP-11`, `TRANSPORT-24`, `TRANSPORT-22` | Additive: `NFR-4` permits the widening; the manifest gains `Dexpace::Status#standard?` | +| P10-28 | `Protocol` gains `http/1.0` in BOTH `WIRE_FORMS` and `ALIASES`, and the constant `Protocol::HTTP_1_0`, under `HTTP-33` -- not `HTTP-24`/`HTTP-43`, which the plan named and which are `MediaType`'s charset lookup and `Response#close` | `HTTP-33`; bullet 27 | The manifest gains one row | +| P10-29 | `URL.parse!` refuses (i) an http-family URL with no host, (ii) a URI whose own `#to_s` mutates it -- `URI::FTP` with `;type=` -- and wraps (iii) every `URI::Error`, not only `InvalidURIError`, as `InvalidArgumentError` naming the input. Non-http schemes are still admitted: which schemes dispatch stays a transport's call (phase 1's reading) | `HTTP-47` | The rationale clause -- fail at construction rather than later -- was unmet for three shapes | +| P10-30 | `Model.own` drops a caller's Hash default proc from the model's copy and turns any other un-shareable value into `InvalidArgumentError` | `HTTP-5`, `XCUT-15` | A default proc is behaviour, not data; `Ractor.make_shareable(copy: true)` raised a raw TypeError | +| P10-31 | The `chapters` probe check exempts phase 10's own design, plan and checklist (they quote the pre-correction attributions on purpose) and spells its negation vocabulary as a `Regexp.union` of phrases, never an `/x` pattern -- under `/x` the phrases' spaces vanish, which the first live run showed as sixty false fires. Measured on the live tree: two fires, both true positives, in `phase8/2026-09-11-phase8-segmentation-design.md:60` (`SEAM-13` and `SEAM-15` inside a range attributed to chapter 03), fixed in place with a dated bracket **Amended by review round 0 (R0-8):** the first cut also listed `appears in` as a negation to hide the phase-5 segmentation design's :538-539, whose second ID run bound backward to the first run's chapter (40 false fires) -- which silenced the usual positive attribution, `X appears in `. The phrase is now a FORWARD binding instead: an ID run followed by it pairs with the chapter it introduces, or with nothing when that chapter is on the next line (the continued-clause gap). Live fires are unchanged. | `SEAM-13`, `SEAM-15`; `R10`, addendum A10 | The design measured the population before phase 10's documents joined it | +| P10-32 | A retargeted vacuous requirement is marked **N/A** (`NFR-8`, `NFR-9`, `SEAM-10`), phase 9's convention; phase 0's checklist's `🚫 retargeted` stands as its own record and the departure is stated here | `NFR-8`, `NFR-9` | Two conventions for one disposition was a reader's trap | +| P10-33 | `docs/deviations.md` rides the **code** branch: `gates:ledger_audit` reads it at run time, and a gate's input lives where the gate runs | `NFR-17` | The layering rule | +| P10-34 | "Task 10b", the small-repairs task the maintainer added, took twenty-six items -- twenty test-first repairs (the checklist's repairs-table rows marked `10b`) and six comment, YARD or document corrections no test can observe -- and stopped there, at the decision's bound; every other inbound bullet or leftover it could have taken is a dated `docs/first-release.md` line | Scope | The decision's own bound (~25 items, one `lib/` file each) | +| P10-35 | `NFR-4` stays **✅**, phase 9's mark. `P10-11` and Task 18 Step 6 read the requirement as "a diff against the previous release tag"; appendix C's `NFR-4` is "captured in a checked-in, machine-comparable snapshot, and the build SHOULD fail on any drift", which `gates:surface_snapshot` over the six manifests does. The release-tag diff is design §9's mechanism, and its vacuity is stated in the row | `NFR-4`; `P10-11` superseded | Following `P10-11` would have overturned a correct row | +| P10-36 | The "RBS sig-diff baseline" line in `docs/first-release.md` stays **open**: `tools/sig_diff.rb` takes its baseline from `git describe --tags --match v*`, there is no committed baseline file, and this phase may not tag. The runtime manifests were regenerated once, deliberately | `NFR-4`; `R11` | Establishing a baseline without a tag is not possible in this repository's design | +| P10-37 | **`docs/deviations.md` row 10 is *confirmed, narrower*, and the amendment set gains `C19`** (review round 0's R0-6). §4 and §10 item 10 say `send(:new, …)` reaches the generated constructor; as built it bypasses `.build` but runs the validating `#initialize` (`Request.send(:new, …)` with a GET and a body raises `HTTP-7`), and the residual holes are `Request.allocate`, which yields all-nil members, and duck typing -- measured on 4.0.6 and 3.2.11. The mitigation is unchanged: both transports re-validate at the wire | `HTTP-2`, `SEAM-29`, `HTTP-4`, `HTTP-7`, `XCUT-18`; `docs/deviations.md` row 10 | the register must say what the tree does (`P10-2`), and a frozen sentence is corrected by an amendment written out for a human, never in place | +| P10-38 | **The proxy warning's credential scrub runs on the RAW value first** (review round 0's R0-4). The unanchored belt of the first repair runs on the redactor's output and stops at the first `/`, `?` or `#`, so a password holding one left its prefix in `Kernel#warn` and the config sink; `ProxyResolution#scrub_userinfo` now replaces everything from the authority's start (after `scheme:/…` or a leading `/` run, never after a bare `user:`) through the LAST `@`, and the belt stays behind it. Over-redacting an `@` in a proxy URL's path is the safe direction, as the belt already accepted | `OBS-11`, `CFG-22` | `OBS-11`'s userinfo rule is "unconditional", and a belt that stops at a delimiter leaves whatever precedes it | + --- ## Work phase 10 postponed, and who owns it now diff --git a/docs/work/mvp/phase2/2026-09-07-phase2-seam-foundations-checklist.md b/docs/work/mvp/phase2/2026-09-07-phase2-seam-foundations-checklist.md index 609e95e..138faab 100644 --- a/docs/work/mvp/phase2/2026-09-07-phase2-seam-foundations-checklist.md +++ b/docs/work/mvp/phase2/2026-09-07-phase2-seam-foundations-checklist.md @@ -29,7 +29,7 @@ Thirty: `SEAM-1`–`SEAM-30`. | `SEAM-7` | MUST | ✅ | 7 | A successful resolution is memoised process-wide (one factory call across 32 concurrent resolvers); a failed one memoises nothing, so a later `#register` or `#install` takes effect on the next `#resolve` — including after a `LoadError` from the factory, because the single-flight claim is released in an `ensure` and not on a `rescue StandardError` path (`dexpace/registry_test.rb`, the top-level class, `Concurrency` and `Reentrancy`) | | `SEAM-8` | SHOULD | ✅ | 7 | Replacing an auto-resolved provider that was already handed out emits one `Kernel#warn` naming the replacement and saying objects built against the previous one may still be in use (P2-6, observed through `WarningCapture`, P2-12). Replacing one that was never handed out is silent — the negative clause, reachable only through the unchecked swap seam and asserted there; the test says in so many words that no production path reaches it, because `#resolve` hands out in the same call that resolves (`dexpace/registry_test.rb`, `Installation`) | | `SEAM-9` | MUST | ✅ | 7 | State is one frozen `Registry::State` snapshot in one instance variable, swapped under a `Thread::Mutex` held across the swap and nothing else; reads are one unsynchronised reference read. Proven: 32 concurrent first accesses run the factory exactly once and receive one `equal?` instance; 16 concurrent installs admit exactly one; a second resolver arriving mid-build parks on the claim's `Thread::Queue` and receives the winner's instance; a factory that takes the registry's own write lock, or a `.conforms?` predicate that does, cannot deadlock because neither is called under it (`dexpace/registry_test.rb`, `Concurrency`). `IO-39`'s lock-free read survives here as a property, not as a row | -| `SEAM-10` | MUST | N/A | 7, 8 | Vacuous in Ruby (design §10.9): one process-global constant namespace, no classloader, `require` de-duplicates by resolved feature path. Replaced by the version-skew guard phase 0 postponed to this phase: `#register(key, factory, core:)` requires the adapter's `~> MAJOR.MINOR` and compares it against `Dexpace::VERSION` by hand (P2-7), because `Gem` is undefined under `ruby --disable-gems` (re-verified on 3.2.11 and 4.0.6 during the build, and the guard exercised under `--disable-gems` on 4.0.6). Any other requirement form — `>= 0.1`, `~> 0.1.2`, `""`, `nil`, a `Symbol` — is refused as `Dexpace::InvalidArgumentError`, never reinterpreted (`dexpace/registry_version_test.rb`, which cross-checks the comparison against `Gem::Requirement#satisfied_by?` over a seeded grid and an exhaustive 6 × 12 square of running versions and requirements) | +| `SEAM-10` | SHOULD | N/A | 7, 8 | Vacuous in Ruby (design §10.9): one process-global constant namespace, no classloader, `require` de-duplicates by resolved feature path. Replaced by the version-skew guard phase 0 postponed to this phase: `#register(key, factory, core:)` requires the adapter's `~> MAJOR.MINOR` and compares it against `Dexpace::VERSION` by hand (P2-7), because `Gem` is undefined under `ruby --disable-gems` (re-verified on 3.2.11 and 4.0.6 during the build, and the guard exercised under `--disable-gems` on 4.0.6). Any other requirement form — `>= 0.1`, `~> 0.1.2`, `""`, `nil`, a `Symbol` — is refused as `Dexpace::InvalidArgumentError`, never reinterpreted (`dexpace/registry_version_test.rb`, which cross-checks the comparison against `Gem::Requirement#satisfied_by?` over a seeded grid and an exhaustive 6 × 12 square of running versions and requirements) [level corrected 2026-09-25 by phase 10: appendix C gives `SEAM-10` as SHOULD; this row said MUST] | | `SEAM-11` | MUST | ✅ | 9 | `Dexpace::Transport`: any object responding to `#call(request, options, cancellation)`; a bare lambda, a non-lambda proc, a `->(*)` and any object with a `#call` of that arity conform; the wrong arity, a non-callable and — since review round 1 — a callable with a required keyword (`->(r, o, c, must:) {}`, which would pass registration and raise `ArgumentError` at the first send) do not, the last asserted on both transport seams and on `Registry.callable?` itself. Single operation: one `#call`, one response. Options may be ignored: an options-ignoring transport returns the same thing for `RequestOptions::EMPTY` and a populated `RequestOptions`, because options are inert immutable data. No pre-buffering is stated at the seam (the module's YARD block) and is phase 8a's to prove over a socket (`dexpace/transport_test.rb`) | | `SEAM-12` | MUST | ⏳ | 9 | Phase 8a, Tasks 4–8 and 20 (`dexpace-conformance`'s `TransportSuite`, `docs/work/mvp/phase8/phase8a/2026-09-11-phase8a-synchronous-transport-and-conformance.md`). What the seam owes is that nothing forces per-request state onto shared storage — `#call` takes everything it needs and returns everything it produces — and the 32-thread concurrent-call test exercises that harness against the fake. Concurrency safety is a property of an implementation, and this phase ships none | | `SEAM-13` | SHOULD | ✅ | 4, 9 | Cancellation reaches a transport as the third argument, an ordinary `Dexpace::Cancellation` value and never an ambient interrupt; a transport honours it by re-checking `#cancelled?` at every resume point, and `#check!` is the one-call form of that check. The token, `.none`, `.source`, `.any` and `#on_cancel` are built and tested (`dexpace/cancellation_test.rb`, `dexpace/cancellation/source_test.rb`); `FakeTransport` records the token it was handed (`dexpace/bridge/async_over_test.rb`). Phase 8's adapters honour it; `dexpace-conformance` asserts it | diff --git a/docs/work/mvp/phase3/2026-09-08-phase3-segmentation-design.md b/docs/work/mvp/phase3/2026-09-08-phase3-segmentation-design.md index 975290c..96aec19 100644 --- a/docs/work/mvp/phase3/2026-09-08-phase3-segmentation-design.md +++ b/docs/work/mvp/phase3/2026-09-08-phase3-segmentation-design.md @@ -38,7 +38,7 @@ unresolved conflict and owns no conflict decision of its own. Two note entries bind this phase directly and are cited rather than restated: `concurrency-and-async/f414b864` (core's shared mutable state is one frozen `Data` snapshot swapped under a `Thread::Mutex`; `Mutex` is per-fiber-owned and non-reentrant, both re-verified) and `data-modeling/5bc538ba` -(the wire model is Ractor-shareable except where a member holds a `URI::Generic`). +(the wire model is Ractor-shareable except where a member holds a `URI::Generic`). [2026-09-25, phase 10: `data-modeling/5bc538ba` is retired and resolves to nothing -- phase 1's build found `Request` and `Response` Ractor-shareable as built (`P1-13`, retiring `P1-9`), and the note is rewritten as built under `## Conflicts` in `docs/knowledge/notes/data-modeling.md`. Only the key's citation is stale; the passage stands as its phase's record.] One audit group from the skill's table was run in full here, **Encoding and binary strings** — `--prefix IO --section rules --brief`, then `--topic io-and-byte-streams,serde --section rules --brief` narrowed by diff --git a/docs/work/mvp/phase3/phase3a/2026-09-08-phase3a-io-contracts-checklist.md b/docs/work/mvp/phase3/phase3a/2026-09-08-phase3a-io-contracts-checklist.md index a37fe44..d90f8eb 100644 --- a/docs/work/mvp/phase3/phase3a/2026-09-08-phase3a-io-contracts-checklist.md +++ b/docs/work/mvp/phase3/phase3a/2026-09-08-phase3a-io-contracts-checklist.md @@ -55,9 +55,9 @@ Forty-two: `IO-1`–`IO-42`. | `IO-31` | MUST | 🚫 | — | Provider resolution rules for a seam that does not exist (design §10.1). No fourth registry, no factory, no installation call, no discovery — asserted negatively by `Dexpace::IO.constants` being exactly the six streaming constants plus the ceiling (`dexpace/io_test.rb`) | | `IO-32` | MUST | 🚫 | — | Install idempotence for the retired seam (design §10.1; one of the roadmap's five gap IDs, appendix-C only, read for nothing) | | `IO-33` | MUST | 🚫 | — | Install-wins-after-discovery for the retired seam (design §10.1; appendix-C only) | -| `IO-34` | MUST | 🚫 | — | Discovery caching for the retired seam (design §10.1; appendix-C only) | +| `IO-34` | SHOULD | 🚫 | — | Discovery caching for the retired seam (design §10.1; appendix-C only) [level corrected 2026-09-25 by phase 10: appendix C gives `IO-34` as SHOULD; this row said MUST] | | `IO-35` | SHOULD | 🚫 | — | The late-replacement warning for the retired seam (design §10.1; appendix-C only) | -| `IO-36` | MUST | 🚫 | — | The provider's own conformance obligations for the retired seam (design §10.1) | +| `IO-36` | MAY | 🚫 | — | The provider's own conformance obligations for the retired seam (design §10.1) [level corrected 2026-09-25 by phase 10: appendix C gives `IO-36` as MAY; this row said MUST] | | `IO-37` | MUST | ✅ | 10 | No instance carries a lock over its read or write path; the only synchronised state is `Closeable`'s latch, read once per public entry point. The proof is the lock's absence: two fibers of one thread interleave `#read_exactly` calls on one `BufferedSource` whose upstream yields mid-read, and no `ThreadError` is raised — which a mutex held across a fill would raise (verified fact 7) (`dexpace/io/buffered_source_test.rb`, `Threads`) | | `IO-38` | MUST | ✅ | 2, 10 | `Closeable#closed?` now reads the latch under the close mutex (P3-6), the one change to a phase-2 constant; the assertion that distinguishes it from phase 2's unsynchronised reader — a fiber holding the latch mutex across a `Fiber.yield`, a second fiber calling `#closed?`, `ThreadError` — was run red against phase 2's version first (guards below). Behaviourally: a `#read_exactly` blocked on a pipe on thread B fails with Ruby's own `IOError` when thread A closes the source, forwarded unchanged (`IO-40`), and every later read is `Dexpace::ClosedError`; a view being read on thread B raises `Dexpace::ClosedError` after thread A closes the parent — both sequenced through `Thread::Queue`s, never raced. On every CRuby row the GVL would hide a missing lock, which is the postponement the design records and `docs/first-release.md` § Post-release triggers owns (`dexpace/closeable_test.rb`; `dexpace/io/buffered_source_test.rb`, `Threads`) | | `IO-39` | SHOULD | 🚫 | — | Lock-free reads of a provider registry that does not exist (design §10.1). The property survives in phase 2's `Dexpace::Registry`, as its checklist's `SEAM-9` row records | diff --git a/docs/work/mvp/phase4/2026-09-08-phase4-segmentation-design.md b/docs/work/mvp/phase4/2026-09-08-phase4-segmentation-design.md index 79b0dff..a6c9174 100644 --- a/docs/work/mvp/phase4/2026-09-08-phase4-segmentation-design.md +++ b/docs/work/mvp/phase4/2026-09-08-phase4-segmentation-design.md @@ -650,7 +650,7 @@ them; it is recorded here so a `4b` or `4c` designer meeting either does not re- LOGGING pillar — that stage is a slot, and the sink is a duck type (§8.1). 7. **`Ractor` is never load-bearing.** `data-modeling/5bc538ba` already narrows the shareability claim; a context holds a `Response` holding a body holding an `IO`, so no shareability claim is made for the - promotion chain at all, despite every link being a frozen `Data`. + promotion chain at all, despite every link being a frozen `Data`. [2026-09-25, phase 10: `data-modeling/5bc538ba` is retired and resolves to nothing -- phase 1's build found `Request` and `Response` Ractor-shareable as built (`P1-13`, retiring `P1-9`), and the note is rewritten as built under `## Conflicts` in `docs/knowledge/notes/data-modeling.md`. Only the key's citation is stale; the passage stands as its phase's record.] --- diff --git a/docs/work/mvp/phase4/phase4a/2026-09-08-phase4a-execution-context-design.md b/docs/work/mvp/phase4/phase4a/2026-09-08-phase4a-execution-context-design.md index 0bd8283..3f4aa2a 100644 --- a/docs/work/mvp/phase4/phase4a/2026-09-08-phase4a-execution-context-design.md +++ b/docs/work/mvp/phase4/phase4a/2026-09-08-phase4a-execution-context-design.md @@ -188,6 +188,8 @@ Two IDs outside `CTX` fix values 4a must carry, and are quoted because 4a is whe | `AUTH-19`'s per-nonce counter store | 6. It reuses 4a's map and adds one operation to it (R4) | | Any `Ractor` shareability claim for a context | none. `data-modeling/5bc538ba` already narrows it and a context transitively holds a body holding an `IO` | +[2026-09-25, phase 10: `data-modeling/5bc538ba` is retired and resolves to nothing -- phase 1's build found `Request` and `Response` Ractor-shareable as built (`P1-13`, retiring `P1-9`), and the note is rewritten as built under `## Conflicts` in `docs/knowledge/notes/data-modeling.md`. Only the key's citation is stale; the passage stands as its phase's record.] + **No segmentation design of its own.** 4a is one spec chapter, one gem, 20 IDs, under a segmentation design that already exists at the `phase4/` level. diff --git a/docs/work/mvp/phase4/phase4b/2026-09-08-phase4b-recovery-primitives-design.md b/docs/work/mvp/phase4/phase4b/2026-09-08-phase4b-recovery-primitives-design.md index 726a288..c1a5fe6 100644 --- a/docs/work/mvp/phase4/phase4b/2026-09-08-phase4b-recovery-primitives-design.md +++ b/docs/work/mvp/phase4/phase4b/2026-09-08-phase4b-recovery-primitives-design.md @@ -1429,7 +1429,7 @@ Boundaries 2, 3, 4, 11, 12, 13, 14, 15 and 16 are 4a's or 4c's and 4b touches no a step list is an `Array` of caller-supplied *callables*, and `Ractor.make_shareable(…, copy: true)` would attempt to deep-copy a lambda's closure. The chains `dup` the array and `freeze` the copy — a shallow copy is what `RECOV-14` asks for, since the requirement is about later mutation - of the *list* and not about the steps. **P4-22**, because a reader who knows `Model.own` will ask. + of the *list* and not about the steps. **P4-22**, because a reader who knows `Model.own` will ask. [2026-09-25, phase 10: `data-modeling/5bc538ba` is retired and resolves to nothing -- phase 1's build found `Request` and `Response` Ractor-shareable as built (`P1-13`, retiring `P1-9`), and the note is rewritten as built under `## Conflicts` in `docs/knowledge/notes/data-modeling.md`. Only the key's citation is stale; the passage stands as its phase's record.] ## Testing strategy diff --git a/docs/work/mvp/phase4/phase4b/2026-09-09-phase4b-recovery-primitives-checklist.md b/docs/work/mvp/phase4/phase4b/2026-09-09-phase4b-recovery-primitives-checklist.md index 518a080..a4d5dcd 100644 --- a/docs/work/mvp/phase4/phase4b/2026-09-09-phase4b-recovery-primitives-checklist.md +++ b/docs/work/mvp/phase4/phase4b/2026-09-09-phase4b-recovery-primitives-checklist.md @@ -54,7 +54,7 @@ fifteen ⏳ to phase 6a's recovery-stack retry engine with the charter's twin `R | `RECOV-22` | MUST | ⏳ | — | A pacing hint replaces, is still clamped, gains no jitter: phase 6a, Tasks 3 and 4; twin `RETRY-20`, `RETRY-21` | | `RECOV-23` | MUST | ⏳ | — | The parser is total, malformed → no hint, past → zero: phase 6a, Task 4; twin `RETRY-16`, `RETRY-17` | | `RECOV-24` | MUST | ⏳ | — | The four recognised pacing forms and their precedence: phase 6a, Task 4; twin `RETRY-15`, `RETRY-19`, `RETRY-21` | -| `RECOV-25` | MUST | ⏳ | — | `X-RateLimit-Reset` positive jitter to [100%, 120%]: phase 6a, Task 4; twin `RETRY-15`'s own last clause | +| `RECOV-25` | SHOULD | ⏳ | — | `X-RateLimit-Reset` positive jitter to [100%, 120%]: phase 6a, Task 4; twin `RETRY-15`'s own last clause [level corrected 2026-09-25 by phase 10: appendix C gives `RECOV-25` as SHOULD; this row said MUST] | | `RECOV-26` | MUST | ⏳ | — | Overflow-safe duration arithmetic and the 365-day clamp: phase 6a, Task 3; twin `RETRY-11`, `RETRY-18` | | `RECOV-27` | MUST | ⏳ | — | The cancellable, non-pinning inter-attempt wait: phase 6a, Task 5, behind `CFG-15`'s seam, which is the charter's second forcing argument; twin `RETRY-23`, `RETRY-26`, `XCUT-3`. 4b builds no wait of any kind | | `RECOV-28` | MUST | ⏳ | — | The engine is stateless across calls: phase 6a, Task 11; twin `RETRY-42` | diff --git a/docs/work/mvp/phase4/phase4c/2026-09-09-phase4c-stage-pipeline-checklist.md b/docs/work/mvp/phase4/phase4c/2026-09-09-phase4c-stage-pipeline-checklist.md index 0ace255..c432cc7 100644 --- a/docs/work/mvp/phase4/phase4c/2026-09-09-phase4c-stage-pipeline-checklist.md +++ b/docs/work/mvp/phase4/phase4c/2026-09-09-phase4c-stage-pipeline-checklist.md @@ -81,7 +81,7 @@ nothing 🚫, nothing N/A. | `REDIR-11` | MUST | ✅ mechanism | 6 | Cross-reference, phase 6b's ID: the mechanism its "no step downstream of AUTH can set the marker" rests on is built and asserted by name — cursor-scoped state keyed by `(stage, key)` (P4-28), written only through `#fork(state:)` into the forking pillar's own slot (P4-29), a `RETRY` pillar's fork carrying `cross_origin: false` visible under `Stages::RETRY` and never under `Stages::REDIRECT`, and a second fork from the same parent starting from the parent's slot so hop 2 never sees hop 1's write. The marker itself and its expiry are phase 6b's (`pipeline/cursor_test.rb`, `StateTest`) | | `AUTH-29` | MUST | ✅ mechanism | 6 | Cross-reference, phase 6c's ID: `Cursor#state(Stages::REDIRECT)` is the read side — a frozen hash, one shared empty for an unwritten slot, a `FrozenError` on any write, a non-`Stage` refused — and there is no state-setting method on the cursor at all, pinned from the manifest's side. The auth step's reading of the marker is phase 6c's (`pipeline/cursor_test.rb`, `StateTest`) | | `XCUT-11` | MUST | ✅ share | 8, 10 | Cross-reference, phase 9's ID: `Dexpace::Pipeline` and `AsyncPipeline` are audited shared-instance state with no lock — a frozen entry table, a frozen step view, a write-once transport, no writer, no `#with` — and the cursor is the only per-call state, allocated per send and never published. Sixteen concurrent sends share nothing (`dexpace/pipeline_test.rb`) | -| `NFR-11` | MUST | ✅ share | 4 | Cross-reference, phase 9's ID: the step protocol's static half has a named home, `_Step` and `_AsyncStep` in `sig/dexpace/pipeline/step.rbs`, every name in them a `Dexpace::` constant; `gates:rbs_surface` is green over the twelve new mirrors | +| `NFR-11` | SHOULD | ✅ share | 4 | Cross-reference, phase 9's ID: the step protocol's static half has a named home, `_Step` and `_AsyncStep` in `sig/dexpace/pipeline/step.rbs`, every name in them a `Dexpace::` constant; `gates:rbs_surface` is green over the twelve new mirrors [level corrected 2026-09-25 by phase 10: appendix C gives `NFR-11` as SHOULD; this row said MUST] | | `SEAM-18` | MUST | ✅ composed | 8 | Cross-reference, phase 2's ID: both bridges are used over a built pipeline and neither is reimplemented — 4c ships no executor duck type, no orphan close and no normalisation of its own at the bridge (charter boundary 15, P4-35). The design's finding that `Transport.async_over` accepted an async transport silently was found already repaired on this base by phase 2's Task 11: `Bridge::AsyncOver#deliver` raises `Dexpace::SeamError` when the wrapped transport delivers a `Future` (`dexpace/pipeline_test.rb`) | | `TRANSPORT-1`, `TRANSPORT-2` | MUST | ✅ premise | 2 | Cross-reference, phase 8's IDs: the premise they presuppose — `PIPE` is the single authority on redirect and retry, at `Stages::REDIRECT` and `Stages::RETRY` — is what this phase makes true; disabling a native client's own is phase 8a's (`pipeline/stages_test.rb`) | diff --git a/docs/work/mvp/phase5/2026-09-09-phase5-segmentation-design.md b/docs/work/mvp/phase5/2026-09-09-phase5-segmentation-design.md index fd39b52..b338bb5 100644 --- a/docs/work/mvp/phase5/2026-09-09-phase5-segmentation-design.md +++ b/docs/work/mvp/phase5/2026-09-09-phase5-segmentation-design.md @@ -706,7 +706,7 @@ mitigation that row itself carries. claim for the same reason. 9. **An `Enumerator` abandoned mid-`#next` never runs its `ensure`.** `OBS-22`'s scope handle and `OBS-24`'s snapshot bridge are both `begin/ensure` around a block, never an enumerator, and no resource is acquired - inside any block phase 5 yields from. + inside any block phase 5 yields from. [2026-09-25, phase 10: `data-modeling/5bc538ba` is retired and resolves to nothing -- phase 1's build found `Request` and `Response` Ractor-shareable as built (`P1-13`, retiring `P1-9`), and the note is rewritten as built under `## Conflicts` in `docs/knowledge/notes/data-modeling.md`. Only the key's citation is stale; the passage stands as its phase's record.] --- @@ -770,7 +770,7 @@ claimed across a range that was not run. 1. **`Fiber[:k] = v` emits no warning; `Fiber#storage=` warns on every call.** Two `Fiber[]=` writes and one read-back produce zero warnings; two `Fiber#storage=` calls produce two, category `:experimental`, text `Fiber#storage= is experimental and may be removed in the future!`. **And `Fiber[:k] = nil` deletes the - key** — `Fiber.current.storage.key?(:k)` is `false` afterwards, not `true` with a `nil` value. *What it + key** — `Fiber.current.storage.key?(:k)` is `false` afterwards, not `true` with a `nil` value. [2026-09-25, phase 10: a fact of 3.3 and later, not of the range -- on the 3.2 floor `Fiber[:k] = nil` RETAINS the key with a nil value, and `Fiber["k"]` raises TypeError on 3.2 and 3.3 (5c's `P5-72`, `docs/knowledge/notes/observability.md`); every reader that skips nulls reads the two as the same.] *What it licenses:* `OBS-23`'s whole contract — "push the trace id and span id … for the scope's lifetime, and MUST restore each key to its prior value (**or remove it if previously unset**) on close" — is expressible with `Fiber[]=` alone, per key, with no warning and no gate problem. *What it does not license:* any claim about diff --git a/docs/work/mvp/phase5/phase5a/2026-09-09-phase5a-configuration-checklist.md b/docs/work/mvp/phase5/phase5a/2026-09-09-phase5a-configuration-checklist.md index da90999..bea7048 100644 --- a/docs/work/mvp/phase5/phase5a/2026-09-09-phase5a-configuration-checklist.md +++ b/docs/work/mvp/phase5/phase5a/2026-09-09-phase5a-configuration-checklist.md @@ -75,11 +75,11 @@ theirs: `XCUT-5`, `CTX-11`, `IO-9`, `BODY-32`, `SEAM-18`, `XCUT-11` and `NFR-11` | `CFG-38` | MUST | ✅ | 11 | `#integer`, `#boolean` and `#duration` resolve through `#string` — the whole chain — before parsing: supplied ONLY through the environment seam and through a normalised property they still resolve, which is the case an implementation reading the override map alone fails, and the typed default applies only when the chain is absent or unparseable (`configuration_test.rb`, `AccessorsTest`, "CFG-38" ×2) | | `XCUT-5` | MUST | ✅ share | 4 | Cross-reference, phase 9's ID: the SINGLE shared status classifier now has one home, `Dexpace::Retryability.retryable_status?`; phase 6a computes `Dexpace::ProtocolError`'s baked flag FROM it (its plan Task 6) and builds no second, and `XCUT-7`'s configurable default set `{408, 429, 500, 502, 503, 504}` is asserted to be a subset of this classification and a different object (`retryability_test.rb`, "XCUT-5 / XCUT-7") | | `CTX-11` | MUST | ✅ share | 13 | Cross-reference, phase 4a's ID and the cap's configuration source it postponed here: `ContextStore.default` is built on its FIRST call, under one `::Thread::Mutex`, with `Configuration::Keys::MAX_TRACKED_CONTEXTS` read from the chain and `MAX_TRACKED_CONTEXTS = 1024` as the fallback for an absent, unparseable or non-positive value (P5-55). A `Dexpace.configure` — or the environment variable — before the first call sets the process-wide store's cap, driven end to end in a fresh process by filling past the cap; a configure after the first call does not resize it and neither does `reset_config!`; the chain is read OUTSIDE the mutex and only the `||=` runs under it — the rule `Dexpace.configure` follows, no caller-supplied code under a non-reentrant lock, applied to the store since review round 0 (R0-4; deviation 26 below) — with a lock-free read of the published reference in front, so a promotion after the first runs neither seam and takes no lock; 16 threads reaching `.default` first under a `new` slowed to 50 ms get ONE store, which the unsynchronised `||=` guard fails below, a seam observes the mutex unowned while it runs, and a later `.default` runs no seam; no signature changed. Phase 4a's load-time assignment and its fresh-process case changed to say so (`context_store_config_test.rb`; `context_store_test.rb`, `ReachabilityTest`) | -| `IO-9` | MUST | ✅ share | 13 | Cross-reference, phase 3a's ID and the ceiling half of the body-logging caps phase 3b postponed here: `Dexpace::IO.max_materialized_bytes(configuration = Dexpace.configuration)` reads `Keys::MAX_MATERIALIZED_BYTES` on EVERY call, `MAX_MATERIALIZED_BYTES` the default and the fallback for an unparseable or non-positive value, so `Dexpace.configure` and `.reset_config!` govern the live ceiling; the five value-readers — `TypedReads#guard_materialization!` (whose refusal now names the limit that applied and the key), `Buffer#snapshot`, `Body.clamp_cap`, `StreamBody#replayable?` and `BufferBody#==` — read the function rather than the constant (P5-56), each driven through a configured ceiling of 4 or 8 bytes, with the slot's environment seam replaced by an empty fake in every setup and after each mid-case reset, so a host exporting `MAX_MATERIALIZED_BYTES` reaches no read (review round 0, R0-7). Phase 3a's and 3b's own materialisation suites read the ceiling through the slot too and install no fake: they assume, as this suite did before, that the host defines no such variable — stated here rather than fixed, because those suites are their phases' and the assumption held on every row this stack ran. No `ceiling:` keyword exists anywhere (`io_ceiling_test.rb`) | +| `IO-9` | SHOULD | ✅ share | 13 | Cross-reference, phase 3a's ID and the ceiling half of the body-logging caps phase 3b postponed here: `Dexpace::IO.max_materialized_bytes(configuration = Dexpace.configuration)` reads `Keys::MAX_MATERIALIZED_BYTES` on EVERY call, `MAX_MATERIALIZED_BYTES` the default and the fallback for an unparseable or non-positive value, so `Dexpace.configure` and `.reset_config!` govern the live ceiling; the five value-readers — `TypedReads#guard_materialization!` (whose refusal now names the limit that applied and the key), `Buffer#snapshot`, `Body.clamp_cap`, `StreamBody#replayable?` and `BufferBody#==` — read the function rather than the constant (P5-56), each driven through a configured ceiling of 4 or 8 bytes, with the slot's environment seam replaced by an empty fake in every setup and after each mid-case reset, so a host exporting `MAX_MATERIALIZED_BYTES` reaches no read (review round 0, R0-7). Phase 3a's and 3b's own materialisation suites read the ceiling through the slot too and install no fake: they assume, as this suite did before, that the host defines no such variable — stated here rather than fixed, because those suites are their phases' and the assumption held on every row this stack ran. No `ceiling:` keyword exists anywhere (`io_ceiling_test.rb`) [level corrected 2026-09-25 by phase 10: appendix C gives `IO-9` as SHOULD; this row said MUST] | | `BODY-32` | MUST | ✅ share | 13 | Cross-reference, phase 3b's ID: `Body.buffer_bounded` clamps a cap down to the CONFIGURED ceiling, never up and never to the constant — `Float::INFINITY` and a cap of 1000 both clamp to a configured 4, a cap of 3 stays 3 (`io_ceiling_test.rb`, `ReadersTest`, "BODY-32") | | `SEAM-18` | MUST | ✅ share | 6, 8 | Cross-reference, phase 2's ID and the `deadline:` keyword its P2-5 postponed here: `Future#value`, `#wait` and `Completer#await` gain `deadline:` (a monotonic INSTANT, never a duration) and `clock:` — widenings only, `#await`'s token positional as phase 2 shipped it; `Clock.deadline_in` names the scale. Expiry is a timed gate pop with `remaining` recomputed from the clock on every iteration, ending in phase 2's own `request_cancel(:deadline_expired)` — one settlement, the producer's `on_cancel` hook fired, a late `fulfil` closed through SEAM-30 — so `#wait` still returns `self`, `#await` still returns `self` and never raises, and `#value` raises the `CancelledError` whose `#reason` is the Symbol `:deadline_expired` (XCUT-2: never a string match). A deadline exactly now is expired; an already-settled future ignores one; a real producer beats a real deadline; a real deadline passing cancels; a cancellation token wins with its own reason; no deadline is phase 2's wait unchanged; a non-numeric deadline or a clock without `#monotonic` is refused — settled or not, since review round 0 moved the validation above the settled short-circuit, because a settled future ignores an expired deadline and not an invalid one (R0-5; the validate-after-return guard runs red below). Not a token composed through `Cancellation.any`, as phase 2 anticipated (`async/future_deadline_test.rb`, `ExpiryTest` and `NonExpiryTest`) | | `XCUT-11` | MUST | ✅ share | 12, 13 | Cross-reference, phase 9's ID: the two pieces of shared mutable state 5a adds are one frozen `Configuration` reference swapped under a mutex and read without one (16 writers × 50 against 16 readers × 200, and configure racing reset, every reader seeing a whole frozen snapshot), and `ContextStore.default`'s first-call construction under its own mutex (16 first callers, one store, under a `new` slowed to 50 ms so an unsynchronised `||=` publishes sixteen; the seams run outside the lock and a seam asserts `Mutex#owned?` false while it runs); `Clock::SYSTEM` and every `Configuration` are frozen and stateless, no 5a code path holds two locks (`config_test.rb`, `RaceTest`; `context_store_config_test.rb`, `RaceTest`) | -| `NFR-11` | MUST | ✅ share | 4, 6, 9 | Cross-reference, phase 9's ID: the three new RBS interfaces — `_Clock` (`sig/dexpace/clock.rbs`), `_ConfigSource` (`sig/dexpace/configuration/sources.rbs`) and `_RetryableStatus` (`sig/dexpace/retryability.rbs`) — name only `Dexpace::` constants and allowlisted stdlib (`::Time`, `Numeric`); `gates:rbs_surface` is green over the twenty-one `sig/` files 5a adds or changes | +| `NFR-11` | SHOULD | ✅ share | 4, 6, 9 | Cross-reference, phase 9's ID: the three new RBS interfaces — `_Clock` (`sig/dexpace/clock.rbs`), `_ConfigSource` (`sig/dexpace/configuration/sources.rbs`) and `_RetryableStatus` (`sig/dexpace/retryability.rbs`) — name only `Dexpace::` constants and allowlisted stdlib (`::Time`, `Numeric`); `gates:rbs_surface` is green over the twenty-one `sig/` files 5a adds or changes [level corrected 2026-09-25 by phase 10: appendix C gives `NFR-11` as SHOULD; this row said MUST] | ## What was built diff --git a/docs/work/mvp/phase5/phase5a/2026-09-09-phase5a-configuration-design.md b/docs/work/mvp/phase5/phase5a/2026-09-09-phase5a-configuration-design.md index b32a603..c7ab9a2 100644 --- a/docs/work/mvp/phase5/phase5a/2026-09-09-phase5a-configuration-design.md +++ b/docs/work/mvp/phase5/phase5a/2026-09-09-phase5a-configuration-design.md @@ -60,7 +60,7 @@ than by taste. - **A `Data` holding a callable is not Ractor-shareable, so `Configuration` makes no shareability claim.** Verified `Ractor.make_shareable` on a frozen `Data` holding a lambda raises `Ractor::IsolationError`. `CFG-11` requires the two sources to be callables, so the claim `data-modeling/5bc538ba` already narrows is - narrowed once more, deliberately. + narrowed once more, deliberately. [2026-09-25, phase 10: `data-modeling/5bc538ba` is retired and resolves to nothing -- phase 1's build found `Request` and `Response` Ractor-shareable as built (`P1-13`, retiring `P1-9`), and the note is rewritten as built under `## Conflicts` in `docs/knowledge/notes/data-modeling.md`. Only the key's citation is stale; the passage stands as its phase's record.] 5a ships no logger, no event, no redactor, no span and no meter. Its whole test surface is value objects, one process-wide slot behind a mutex, one bounded wait, and six parsers. diff --git a/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction-checklist.md b/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction-checklist.md index eb5c38c..1e73c62 100644 --- a/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction-checklist.md +++ b/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction-checklist.md @@ -79,10 +79,10 @@ identity on both runtimes) and `NFR-11` (the two interfaces' home). **Twenty-six | `CFG-16` | MUST | ✅ first consumer | 15 | Cross-reference, 5a's ID: `http.response.duration_ms` and the histogram's measurement are `(clock.monotonic - started) * 1000.0` and never `Clock#now`; `Step.build(clock:)` defaults to `Clock::SYSTEM` and the suites inject 5a's `FakeClock` for exact durations (`step_test.rb`, `async_step_test.rb`) | | `SEAM-25` | MUST | ✅ event SHAPE only | 3 | Cross-reference, phase 2's ID: `Events::INSTRUMENTATION_SHUTDOWN` (`"http.instrumentation.shutdown"`) and the shape `Instrumentation.diagnostic(logger, event:, cause:, message:)` emits are the half this phase supplies; the emission is phase 8b's (Tasks 6 and 10 of `docs/work/mvp/phase8/phase8b/2026-09-11-phase8b-async-runtime-adapter.md`), where the first owned executor exists, and the harness phase 9's (Task 11). Not claimed done. It is a log-event name and not an `OBS-28` tracer callback, as the design settled | | `BODY-19`, `BODY-22`, `BODY-34` | MUST | ✅ gate landed | 14, 15 | Cross-reference, phase 3b's IDs: the enablement gate phase 3b postponed — the two wrappers are built ONLY in `Step#wrap_request`/`#wrap_response`, ONLY at `HTTPLogging::BODY`, with the one shared cap, and below the body level neither is constructed even when a cap is supplied (the body reaches the caller as the transport produced it, `assert_same`); the two 3b pins that read "nothing in core constructs a wrapper" now read "exactly one file does, `step.rb`". `BODY-22`'s first-access drain is what `#snapshot` triggers on the response event (`step_test.rb`, `BodyLevelTest`; 3b's two body suites) | -| `BODY-20` | MUST | ✅ placement | 15 | Cross-reference, phase 3b's ID: the request preview rides on the RESPONSE and FAILURE events, never the request event — `RequestLoggingBody` mirrors on WRITE and the write happens inside `cursor.call`, after the request event, so a preview read there is `""` on every request; the failure event carries "the bytes mirrored up to the failure point" (`step_test.rb`, `BodyLevelTest`) | +| `BODY-20` | SHOULD | ✅ placement | 15 | Cross-reference, phase 3b's ID: the request preview rides on the RESPONSE and FAILURE events, never the request event — `RequestLoggingBody` mirrors on WRITE and the write happens inside `cursor.call`, after the request event, so a preview read there is `""` on every request; the failure event carries "the bytes mirrored up to the failure point" (`step_test.rb`, `BodyLevelTest`) [level corrected 2026-09-25 by phase 10: appendix C gives `BODY-20` as SHOULD; this row said MUST] | | `OBS-23` | MUST | ✅ keys, 5c's push | 6 | Cross-reference, 5c's ID: `Diagnostics::TRACE_ID` and `::SPAN_ID` are the Symbols 5c's `Tracing.correlate` pushes and `Scope#close` restores; this phase extended the file 5c created without adding a require, and 5c's `independence_test.rb` — the subprocess loading the ten 5c files by name and asserting `Event`, `Logger`, `Keys`, `Events`, `HTTPLogging`, `Step` and `Configuration` undefined — is unchanged and green, which is the structural proof that the logging half did not leak in behind `Tracing` (R11) | | `PIPE-28` | MUST | ✅ share | 15 | Cross-reference, phase 4c's ID: `Step#stage` and `AsyncStep#stage` are both `Stages::LOGGING`, `AsyncStep < Step` inherits `.build` unchanged, both are installed with `builder.append(step)` and no `stage:`, both drive the cursor exactly once through `#call` and never `#fork` (asserted with a recording cursor whose `#fork` raises), and `Pipeline::Step.conforms?` accepts both | -| `NFR-11` | MUST | ✅ share | 4, 6 | Cross-reference, phase 9's ID: the two new interfaces have a home — `_Sink` in `null_sink.rbs` (the duck type, so no bundled-gem `Logger` constant ever appears in a public signature) and `_DiagnosticSnapshot` in `diagnostics.rbs` — every name in them a `Dexpace::` constant or stdlib; the step types its slots against 4a's `_TracerFactory` and 5c's `_Meter` by bare name and declares neither; `gates:rbs_surface` reports no foreign constant over the 142 mirrors | +| `NFR-11` | SHOULD | ✅ share | 4, 6 | Cross-reference, phase 9's ID: the two new interfaces have a home — `_Sink` in `null_sink.rbs` (the duck type, so no bundled-gem `Logger` constant ever appears in a public signature) and `_DiagnosticSnapshot` in `diagnostics.rbs` — every name in them a `Dexpace::` constant or stdlib; the step types its slots against 4a's `_TracerFactory` and 5c's `_Meter` by bare name and declares neither; `gates:rbs_surface` reports no foreign constant over the 142 mirrors [level corrected 2026-09-25 by phase 10: appendix C gives `NFR-11` as SHOULD; this row said MUST] | ## What was built diff --git a/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction-design.md b/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction-design.md index 943dde3..f068ed1 100644 --- a/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction-design.md +++ b/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction-design.md @@ -540,6 +540,8 @@ this document, and the substantive entry was located beside the roll-up in every | **RuboCop and formatting** | `--topic tooling-and-quality-gates --section rules --brief` | Clean. **5b adds no cop.** The one it would have wanted — a `Dexpace/QualifiedCoreConstant` entry for `Logger` — cannot be written in the cop's current shape, and the plan's Task 10 owns the scoped watch that would replace it rather than leaving a silent omission | | **Styleguide-vs-design conflicts** | `--section conflicts --brief` | All six resolved; none open | +[2026-09-25, phase 10: `data-modeling/5bc538ba` is retired and resolves to nothing -- phase 1's build found `Request` and `Response` Ractor-shareable as built (`P1-13`, retiring `P1-9`), and the note is rewritten as built under `## Conflicts` in `docs/knowledge/notes/data-modeling.md`. Only the key's citation is stale; the passage stands as its phase's record.] + ### The notes filed against the corpus by this phase **One, filed at reconciliation rather than by this document, and one declined.** @@ -837,7 +839,7 @@ is an asymmetry with `query=` worth knowing and irrelevant to a redactor that wr `api-version`. What **does** raise is assigning any component to an **opaque** URI — `mailto:`, `urn:`, `data:` — with `URI::InvalidURIError: cannot set user with opaque` and `query conflicts with opaque`. Those parse cleanly with a non-`nil` scheme, so they reach `OBS-16`'s "parseable absolute" branch, and a -`Location: mailto:support@example.com` header is not hypothetical. +`Location: mailto:support@example.com` header is not hypothetical. [2026-09-25, phase 10: the as-built redactor takes a different route -- it reassembles from `RFC3986_PARSER.split` and assigns no component (`P5-91`), because `URI#to_s` drops a default port; this fact and fact 13 are corrected in this design's As-built addendum, and read there.] **The rule that makes it unreachable is `P5-27`: the redactor never assigns a component that was absent.** `userinfo=` runs only when `#userinfo` is non-`nil`; `query=` runs only when `#query` is non-`nil`; diff --git a/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction.md b/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction.md index bdcf5f9..b9e27f9 100644 --- a/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction.md +++ b/docs/work/mvp/phase5/phase5b/2026-09-09-phase5b-logging-and-redaction.md @@ -99,7 +99,7 @@ confirms the matrix, and the five floor-straddling facts carry the consequence o The following were re-run on 3.4.10 while writing this plan, because each decides a code fence: 1. `Fiber[]=` writes and reads without a warning; `Fiber#storage=` warns on every call with `:experimental` category and - rejects `String` keys with `TypeError`. `Fiber[:a] = nil` deletes the key (`Fiber.current.storage.key?(:a)` is `false`). + rejects `String` keys with `TypeError`. `Fiber[:a] = nil` deletes the key (`Fiber.current.storage.key?(:a)` is `false`). [2026-09-25, phase 10: a fact of 3.3 and later, not of the range -- on the 3.2 floor `Fiber[:k] = nil` RETAINS the key with a nil value, and `Fiber["k"]` raises TypeError on 3.2 and 3.3 (5c's `P5-72`, `docs/knowledge/notes/observability.md`); every reader that skips nulls reads the two as the same.] 2. `Fiber.current.storage` returns a fresh, unfrozen `Hash` on every call. In a fiber created with `Fiber.new(storage: nil)`, `Fiber.current.storage` is `nil`, not `{}`. 3. The per-key union restore `(prior.keys | snapshot.keys).each { |k| Fiber[k] = prior[k] }` is exact. A prior key holding diff --git a/docs/work/mvp/phase5/phase5c/2026-09-09-phase5c-tracing-and-metrics-checklist.md b/docs/work/mvp/phase5/phase5c/2026-09-09-phase5c-tracing-and-metrics-checklist.md index bd1b79b..9f527cf 100644 --- a/docs/work/mvp/phase5/phase5c/2026-09-09-phase5c-tracing-and-metrics-checklist.md +++ b/docs/work/mvp/phase5/phase5c/2026-09-09-phase5c-tracing-and-metrics-checklist.md @@ -36,7 +36,7 @@ honours from its side), `XCUT-11` and `XCUT-20` (phase 9's audits), `NFR-11` (th `ASYNC-9`/`ASYNC-11` (the floor finding routed forward). **Eleven ✅** — `OBS-29` ✅ with its unwired half named in the row, `OBS-30` ✅ by construction — **one ⏳** (`OBS-32`, post-v1), nothing 🚫, nothing N/A. `OBS-24` and `OBS-34` are `5b`'s and appear only as cross-reference rows, so a reader who -arrives looking for either finds the answer here. +arrives looking for either finds the answer here. [2026-09-25, phase 10, counting the table below: eleven cross-reference ROWS naming fourteen IDs -- `CTX-14`, `CTX-15`, `CTX-20`, `SEAM-28`, `XCUT-21`, `OBS-10`, `OBS-24`, `OBS-34`, `OBS-20`, `XCUT-11`, `XCUT-20`, `NFR-11`, `ASYNC-9`, `ASYNC-11`; "thirteen" is neither count (5c's review R0-1).] | ID | Level | Status | Task(s) | What was built, and where it is proven | |---|---|---|---|---| @@ -61,7 +61,7 @@ arrives looking for either finds the answer here. | `OBS-20` | MUST | ✅ boundary | 6, 9 | Cross-reference, 5b's ID: "the runtime does NOT defensively wrap tracer or metrics calls" is 5c's side of boundary 2 — no `rescue` on any tracer or meter path, and a raise propagates with the slots restored (the `OBS-30` row). 5b may not stop wrapping and 5c did not start | | `XCUT-11` | MUST | ✅ share | 3–9 | Cross-reference, phase 9's ID: eight frozen stateless singletons — `NO_SPAN`, `NO_TRACER`, `NO_TRACER_FACTORY`, `NO_SCOPE`, `NULL`, `NO_METER` and its two instruments — are the audited shared instances, with no lock and no state; per-call state lives on the call's own stack in `Scope`'s three ivars, and the one `Fiber[]` slot holds only an immutable span reference. Identity is asserted across sixteen threads for four of them | | `XCUT-20` | MUST | ✅ scoped | 3–9 | Cross-reference, phase 9's ID: no method 5c writes can raise on any input it owns — `.current_span` never returns `nil`, `NULL` defines all eleven, `#finish` accepts a second call, the histogram takes NaN — and the guarantee is deliberately not extended to a foreign callback, which `OBS-20` forbids. Phase 9 audits that sentence | -| `NFR-11` | MUST | ✅ share | 3–9 | Cross-reference, phase 9's ID: seven interfaces have a named home — `_Span` and `_Tracer` widened in place, `_TracerFactory` unchanged, `_Scope`, `_Meter`, `_Counter` and `_Histogram` new — every name in them a `Dexpace::` constant or stdlib; `gates:rbs_surface` reports no foreign constant over the 110 mirrors, and `_HTTPTracer` is deliberately not declared | +| `NFR-11` | SHOULD | ✅ share | 3–9 | Cross-reference, phase 9's ID: seven interfaces have a named home — `_Span` and `_Tracer` widened in place, `_TracerFactory` unchanged, `_Scope`, `_Meter`, `_Counter` and `_Histogram` new — every name in them a `Dexpace::` constant or stdlib; `gates:rbs_surface` reports no foreign constant over the 110 mirrors, and `_HTTPTracer` is deliberately not declared [level corrected 2026-09-25 by phase 10: appendix C gives `NFR-11` as SHOULD; this row said MUST] | | `ASYNC-9`, `ASYNC-11` | MUST | ⏳ finding routed | 1 | Cross-reference, phase 8b's IDs, touched by no code here: the floor finding — `Fiber[:k] = nil` retains a nil-valued key on 3.2 — reaches 8b's pooled-worker restore and 5b's union restore, both planned on the 3.4.10 fact. Routed as the roadmap's thirty-ninth inbound bullet and the corpus note (marker `sha:manual-phase5c-fiber-nil-and-string-key-floor`); neither plan is edited from 5c | ## What was built diff --git a/docs/work/mvp/phase6/phase6a/2026-09-09-phase6a-retry-design.md b/docs/work/mvp/phase6/phase6a/2026-09-09-phase6a-retry-design.md index 3b64127..98fe3e4 100644 --- a/docs/work/mvp/phase6/phase6a/2026-09-09-phase6a-retry-design.md +++ b/docs/work/mvp/phase6/phase6a/2026-09-09-phase6a-retry-design.md @@ -1262,7 +1262,7 @@ fires on whatever thread or fiber settles that future — not nested inside the `pump.call(attempt)` — so `RETRY-30`'s "N retries MUST NOT build an N-deep chain of future continuations or stack frames" holds by the same mechanism phase 2's `Completer#fulfil`/`#on_settle` pair already relies on (§3.3's "callbacks run outside the settling mutex, on the calling fiber, and -a late registration is never lost"). +a late registration is never lost"). [2026-09-25, phase 10: false as written -- `Future#on_settle` runs inline on a settled future, so this sketch overflows at about 1,500 attempts; the built driver is a re-arm trampoline (`P6-54`), recorded in this design's As-built addendum.] ### `Dexpace::ProtocolError#retryable_by_status?` — `XCUT-5`'s baked flag, postponed by phase 4b diff --git a/docs/work/mvp/phase7/2026-09-10-phase7-segmentation-design.md b/docs/work/mvp/phase7/2026-09-10-phase7-segmentation-design.md index 64aa916..611274d 100644 --- a/docs/work/mvp/phase7/2026-09-10-phase7-segmentation-design.md +++ b/docs/work/mvp/phase7/2026-09-10-phase7-segmentation-design.md @@ -646,7 +646,7 @@ decide. Item 5 is the one exception and is labelled as such. 23. **`Ractor` is never load-bearing, and `Data`-based value types are frozen at construction.** `SSE-20`/`SSE-21` and `PAGE-2` want immutable values with structural equality; they get them from `Data` plus `Dexpace::Model`, with Ractor-shareability a free side effect and no part of the claim - (`data-modeling/5bc538ba` narrows exactly which models are shareable and why). + (`data-modeling/5bc538ba` narrows exactly which models are shareable and why). [2026-09-25, phase 10: `data-modeling/5bc538ba` is retired and resolves to nothing -- phase 1's build found `Request` and `Response` Ractor-shareable as built (`P1-13`, retiring `P1-9`), and the note is rewritten as built under `## Conflicts` in `docs/knowledge/notes/data-modeling.md`. Only the key's citation is stale; the passage stands as its phase's record.] 24. **Phases 1 through 7 test against an in-memory fake transport.** Roadmap cross-cutting constraint 4 (`:52-53`). `7c` is the first phase-7 sub-phase with a transport-shaped dependency at all, and it uses phase 2's `SEAM-11`/`SEAM-16` fakes. No `TCPServer` fixture, no socket. diff --git a/docs/work/mvp/phase8/2026-09-11-phase8-segmentation-design.md b/docs/work/mvp/phase8/2026-09-11-phase8-segmentation-design.md index e4e21b2..99be8d6 100644 --- a/docs/work/mvp/phase8/2026-09-11-phase8-segmentation-design.md +++ b/docs/work/mvp/phase8/2026-09-11-phase8-segmentation-design.md @@ -58,7 +58,7 @@ no substantive corpus entry. Both chapters were read in full anyway (51 and 46 l 52 appears in its own prose chapter with a `*Conformance:*` clause appendix C drops, and several of those clauses are load-bearing (named in the scope tables below). - `docs/product-spec/03-pluggable-seams-and-extension-model.md` for `SEAM-11`–`SEAM-18`, `SEAM-24`, - `SEAM-25` and `SEAM-30` — the seams phase 8's gems implement and phase 2 built. + `SEAM-25` and `SEAM-30` — the seams phase 8's gems implement and phase 2 built. [2026-09-25, phase 10's `chapters` probe check: that chapter does not carry `SEAM-13` or `SEAM-15` of the range; `SEAM-13`'s prose home is `docs/product-spec/02-architectural-principles.md` and appendix C is `SEAM-15`'s only normative statement. The other eight IDs check out.] - `docs/sdk-design-ruby/03-seam-by-seam-idiomatic-mapping.md` §3.2 (`:155-188`, the sync transport seam as a duck-typed `#call`), §3.3 (`:189-294`, the async seam, the core-owned pivot, the check-after-resume rule, cancellation and deadlines end to end) and §3.7 (`:452-518`, the close diff --git a/docs/work/mvp/phase8/phase8b/2026-09-11-phase8b-async-runtime-adapter.md b/docs/work/mvp/phase8/phase8b/2026-09-11-phase8b-async-runtime-adapter.md index 90eb070..a603db7 100644 --- a/docs/work/mvp/phase8/phase8b/2026-09-11-phase8b-async-runtime-adapter.md +++ b/docs/work/mvp/phase8/phase8b/2026-09-11-phase8b-async-runtime-adapter.md @@ -335,7 +335,7 @@ The design's six open questions, resolved with a concrete decision each. 6. **The three-interpreter re-run.** *Decision:* Task 1 installs `ruby@3.2.11` and `ruby@4.0.6` and re-runs every fact this plan's `R8`/`R9` decisions are conditional on — specifically, that `Fiber[:k] = nil` deletes the key and that a pooled worker created before a key was set sees - nothing set afterward — on both, before any implementation task begins. A failure on either is a + nothing set afterward — on both, before any implementation task begins. [2026-09-25, phase 10: a fact of 3.3 and later, not of the range -- on the 3.2 floor `Fiber[:k] = nil` RETAINS the key with a nil value, and `Fiber["k"]` raises TypeError on 3.2 and 3.3 (5c's `P5-72`, `docs/knowledge/notes/observability.md`); every reader that skips nulls reads the two as the same.] A failure on either is a **finding, not a silent blocker**: it is recorded in Task 1's own step rather than causing the plan to improvise a different mechanism, because `observability/65191069`'s single-interpreter caveat is a phase-8 obligation this plan is the one place that can close it, and closing it means diff --git a/gems/dexpace-async-thread/sig/dexpace/async/thread.rbs b/gems/dexpace-async-thread/sig/dexpace/async/thread.rbs index 75d6cc2..f4e4538 100644 --- a/gems/dexpace-async-thread/sig/dexpace/async/thread.rbs +++ b/gems/dexpace-async-thread/sig/dexpace/async/thread.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Async module Thread diff --git a/gems/dexpace-async-thread/sig/dexpace/async/thread/pool.rbs b/gems/dexpace-async-thread/sig/dexpace/async/thread/pool.rbs index 5d6eab6..ef16696 100644 --- a/gems/dexpace-async-thread/sig/dexpace/async/thread/pool.rbs +++ b/gems/dexpace-async-thread/sig/dexpace/async/thread/pool.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Async module Thread diff --git a/gems/dexpace-async-thread/sig/dexpace/async/thread/rejected_error.rbs b/gems/dexpace-async-thread/sig/dexpace/async/thread/rejected_error.rbs index dbb81d8..efe39de 100644 --- a/gems/dexpace-async-thread/sig/dexpace/async/thread/rejected_error.rbs +++ b/gems/dexpace-async-thread/sig/dexpace/async/thread/rejected_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Async module Thread diff --git a/gems/dexpace-async-thread/sig/dexpace/async/thread/timer.rbs b/gems/dexpace-async-thread/sig/dexpace/async/thread/timer.rbs index 27435e6..eebb032 100644 --- a/gems/dexpace-async-thread/sig/dexpace/async/thread/timer.rbs +++ b/gems/dexpace-async-thread/sig/dexpace/async/thread/timer.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Async::Thread::Timer is a private_constant and not public API: this declaration exists # because the `async_thread` Steep target checks every file under lib/ and needs the class # declared to type Pool#delay's and #release's call sites. RBS has no visibility for a constant, diff --git a/gems/dexpace-async-thread/sig/dexpace/async/thread/version.rbs b/gems/dexpace-async-thread/sig/dexpace/async/thread/version.rbs index fbd9d20..d952774 100644 --- a/gems/dexpace-async-thread/sig/dexpace/async/thread/version.rbs +++ b/gems/dexpace-async-thread/sig/dexpace/async/thread/version.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Async module Thread diff --git a/gems/dexpace-conformance/lib/dexpace/conformance/packaging_case.rb b/gems/dexpace-conformance/lib/dexpace/conformance/packaging_case.rb index 5178ea9..3e1ffe2 100644 --- a/gems/dexpace-conformance/lib/dexpace/conformance/packaging_case.rb +++ b/gems/dexpace-conformance/lib/dexpace/conformance/packaging_case.rb @@ -25,6 +25,11 @@ module Conformance # available without one -- which is what keeps this gem's declared dependency set at # dexpace-core and nothing else. class PackagingCase + # Words a gem name spells lower-case that its constant path spells as an acronym, the two + # this port's own gems use (NetHTTP, AsyncHTTP, Serde::JSON). + ACRONYMS = %w[http json].freeze + private_constant :ACRONYMS + # Resolves a gem name to its installed specification, or nil when it is not installed. # # `Gem::LoadError` is named explicitly and is the WHOLE point of the rescue: RubyGems raises @@ -146,9 +151,19 @@ def resolve_constant(path) end end + # The gem-name -> constant-path rule, segment for segment, with the two exceptions this + # port's own six gems need (phase 10, phase 9's aggregate-run finding): a `-core` gem is its + # root namespace (`dexpace-core` -> `Dexpace`), and a word in ACRONYMS is upper-cased + # (`net_http` -> `NetHTTP`, `json` -> `JSON`). Before it, four of the six needed the + # `constants:` override; a gem this rule still misses keeps that override. def default_constant_path(name) - name.split("-").map { |segment| segment.split("_").map(&:capitalize).join }.join("::") + segments = name.split("-") + segments = segments.first(1) if segments.size == 2 && segments.last == "core" + segments.map { |segment| segment.split("_").map { |word| constant_word(word) }.join } + .join("::") end + + def constant_word(word) = ACRONYMS.include?(word) ? word.upcase : word.capitalize end end end diff --git a/gems/dexpace-conformance/lib/dexpace/conformance/runner.rb b/gems/dexpace-conformance/lib/dexpace/conformance/runner.rb index fe47817..36ef6c5 100644 --- a/gems/dexpace-conformance/lib/dexpace/conformance/runner.rb +++ b/gems/dexpace-conformance/lib/dexpace/conformance/runner.rb @@ -13,9 +13,9 @@ module Conformance # one place. Four suites would otherwise carry four copies of one status loop, which is # §11.12's "four reference sync/async drifts" reappearing inside the port's own suite. # - # TransportSuite (phase 8a) predates this and is deliberately NOT refactored onto it: phase 8 - # owns that file and phase 9 reports rather than refactors (design R6, P9-10). The residue is - # real -- two status-deciding paths in one gem -- and is stated rather than hidden. + # TransportSuite (phase 8a) predates this and phase 9 deliberately left it on its own loop + # (design R6, P9-10); phase 10, the first phase owning every gem, folded it onto this one, so + # every suite in the gem now decides the five statuses here. module Runner extend self diff --git a/gems/dexpace-conformance/lib/dexpace/conformance/transport_suite.rb b/gems/dexpace-conformance/lib/dexpace/conformance/transport_suite.rb index 7b600dd..c2593c6 100644 --- a/gems/dexpace-conformance/lib/dexpace/conformance/transport_suite.rb +++ b/gems/dexpace-conformance/lib/dexpace/conformance/transport_suite.rb @@ -6,6 +6,7 @@ require_relative "assertion" require_relative "result" require_relative "report" +require_relative "runner" require_relative "transport_case" require_relative "transport_suite/checks" require_relative "transport_suite/outbound" @@ -72,42 +73,36 @@ def assertions # the native client refuses the head before a response exists -- so the default is empty # and 8a's plain `waived:` rendering stands. # @return [Report] + # + # Phase 10 folded this onto Runner (P9-10's residue: "a future change to the five statuses + # must be made twice"), so the five statuses are decided in one place for every suite in + # the gem. What Runner had no seam for is the per-assertion teardown, and it is supplied + # here without widening Runner: the fresh TransportCase is built inside the invocation, and + # the `around` Runner is handed tears it down in an `ensure` after the driver's own wrapper + # returns -- clause 9 unchanged, the reactor an async driver opens still enclosing the whole + # assertion. One observable change, recorded: a :failed detail now carries Runner's + # "(expected …, got …)" suffix after the message. def run(build:, borrow: nil, waive: [], around: nil, settle: nil, wire: nil, assertions: self.assertions, accepted_vacuous: {}, would_fail: []) - results = assertions.map do |assertion| - if assertion.ids.intersect?(waive) - Result.build(assertion: assertion, status: :waived) - else - run_one(assertion, build: build, borrow: borrow, around: around, settle: settle, - wire: wire,) - end + cases = [] #: Array[TransportCase] + subject = lambda do + TransportCase.new(build: build, borrow: borrow, + settle: settle || TransportCase::DEFAULT_SETTLE, + wire: wire || TransportCase::DEFAULT_WIRE,).tap { |kase| cases << kase } end - Report.new(results, preamble: PREAMBLE, accepted_vacuous: accepted_vacuous, - would_fail: would_fail,) + Runner.run(assertions, waive: waive, around: tearing_down(around, cases), + preamble: PREAMBLE, accepted_vacuous: accepted_vacuous, + would_fail: would_fail, &subject) end private - def run_one(assertion, build:, borrow:, around:, settle:, wire:) - kase = TransportCase.new( - build: build, borrow: borrow, - settle: settle || TransportCase::DEFAULT_SETTLE, wire: wire || TransportCase::DEFAULT_WIRE, - ) - begin - # Clause 9: the runner INVOKES the assertion, so `around` can put a block around the - # whole of it -- the reactor an async driver opens here is the one the body reads a - # streamed response inside. - around ? around.call { assertion.call(kase) } : assertion.call(kase) - Result.build(assertion: assertion, status: :passed) - rescue Vacuous => error - Result.build(assertion: assertion, status: :vacuous, detail: error.reason) - rescue Failure => error - Result.build(assertion: assertion, status: :failed, detail: error.message) - rescue ::StandardError => error - Result.build(assertion: assertion, status: :error, - detail: "#{error.class}: #{error.message}",) + # Clause 9's wrapper, with every case built inside it torn down afterwards whatever happened. + def tearing_down(around, cases) + lambda do |&invocation| + around.nil? ? invocation.call : around.call(&invocation) ensure - kase.teardown + cases.pop&.teardown until cases.empty? end end diff --git a/gems/dexpace-conformance/sig/dexpace/conformance.rbs b/gems/dexpace-conformance/sig/dexpace/conformance.rbs index 37af212..f159e98 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance end diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/aggregate.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/aggregate.rbs index 9b6530a..c716326 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/aggregate.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/aggregate.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # One report over every suite in a run; the caller 8a deferred Report#to_h for. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/allocations.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/allocations.rbs index 87612f8..30f06af 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/allocations.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/allocations.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # OBS-25's per-call allocation measurement, two loops agreeing twice (5b's R8). diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/assertion.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/assertion.rbs index 85d9799..5ee3c3a 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/assertion.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/assertion.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # One conformance check as a value; the body is typed against no named interface because a diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/borrowed_pair.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/borrowed_pair.rbs index d052432..9681e30 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/borrowed_pair.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/borrowed_pair.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # Suite contract 4a: the borrowing transport and the adapter's own "still usable?" probe. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/check.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/check.rbs index 8f9ef6c..0d744c2 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/check.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/check.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # The one assertion primitive every phase-9 suite body uses. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/codec_case.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/codec_case.rbs index 643baad..f07eb2c 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/codec_case.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/codec_case.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # What a CodecSuite assertion receives: a codec factory, the driver's witness and its source diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/codec_suite.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/codec_suite.rbs index 117a185..a700d53 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/codec_suite.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/codec_suite.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # Appendix B.3's seam half: SEAM-20 with SERDE-3, and SERDE-9. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/codec_suite/seam.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/codec_suite/seam.rbs index def9413..652485f 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/codec_suite/seam.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/codec_suite/seam.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::CodecSuite::Seam is a private_constant (see seam.rb); declared because the # conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/executor_case.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/executor_case.rbs index 3c8c74c..52e3256 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/executor_case.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/executor_case.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # What an ExecutorSuite assertion receives: an executor factory, an optional borrowing entry diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite.rbs index 5e92f7f..5c5eddd 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # Appendix B.7's lifecycle half, and the harness half of SEAM-25's lifecycle event. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite/lifecycle.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite/lifecycle.rbs index 9b9efbf..7e35e15 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite/lifecycle.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite/lifecycle.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::ExecutorSuite::Lifecycle is a private_constant (see lifecycle.rb); # declared because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite/shutdown.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite/shutdown.rbs index 7bc614c..7b9c7df 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite/shutdown.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/executor_suite/shutdown.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::ExecutorSuite::Shutdown is a private_constant (see shutdown.rb); declared # because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/failure.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/failure.rbs index e45a0c4..efbfe4e 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/failure.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/failure.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # Design §9.3's assertion failure: a test result, never a Dexpace::Error (P8-8). diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_case.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_case.rbs index 2af60df..957a6fd 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_case.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_case.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # What an InvariantSuite assertion receives: the loaded core module and the driver's factories diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite.rbs index d2bcbfd..118b1a5 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # Appendix B.8: twenty-eight assertions over all twenty-four XCUT IDs (design P9-8). diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/classification.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/classification.rbs index af1ed1e..cce9c0e 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/classification.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/classification.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::InvariantSuite::Classification is a private_constant (see # classification.rb); declared because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/concurrency.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/concurrency.rbs index 961f83e..be81554 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/concurrency.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/concurrency.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::InvariantSuite::Concurrency is a private_constant (see concurrency.rb); # declared because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/credentials.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/credentials.rbs index e2c27cf..d087cb5 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/credentials.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/credentials.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::InvariantSuite::Credentials is a private_constant (see credentials.rb); # declared because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/header_syntax.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/header_syntax.rbs index 8d37808..8f7b389 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/header_syntax.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/header_syntax.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::InvariantSuite::HeaderSyntax is a private_constant (see header_syntax.rb); # declared because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/memory.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/memory.rbs index 571d954..170b3ed 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/memory.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/memory.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::InvariantSuite::Memory is a private_constant (see memory.rb); declared # because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/models.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/models.rbs index 0b033e9..3643dda 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/models.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/models.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::InvariantSuite::Models is a private_constant and not public API: this # declaration exists because the conformance Steep target checks every file under lib/. RBS has no # visibility for a constant, so the privacy lives in models.rb alone. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/outcomes.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/outcomes.rbs index 88cd619..8f4d15c 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/outcomes.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/outcomes.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::InvariantSuite::Outcomes is a private_constant (see outcomes.rb); declared # because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/resolution.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/resolution.rbs index a7d698d..422c4a4 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/resolution.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/resolution.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::InvariantSuite::Resolution is a private_constant (see resolution.rb); # declared because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/security.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/security.rbs index 96a1eed..4a4f00b 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/security.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/security.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::InvariantSuite::Security is a private_constant (see security.rb); declared # because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/sharing.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/sharing.rbs index e32a7ac..602bf01 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/sharing.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/sharing.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::InvariantSuite::Sharing is a private_constant (see sharing.rb); declared # because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/taxonomy.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/taxonomy.rbs index 88eae9d..8d3d286 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/taxonomy.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/invariant_suite/taxonomy.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::InvariantSuite::Taxonomy is a private_constant (see taxonomy.rb); declared # because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/levels.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/levels.rbs index 4b77032..3c41c44 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/levels.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/levels.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # Generated from appendix C by tools/requirement_levels.rb; what lets a Report tell a diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/minitest_driver.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/minitest_driver.rbs index a36d3b9..37163b8 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/minitest_driver.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/minitest_driver.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # The thin Minitest driver: extended into a consumer's test class, whose `define_method`, diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/packaging_case.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/packaging_case.rbs index c9410e6..c6ed01e 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/packaging_case.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/packaging_case.rbs @@ -1,8 +1,11 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # What a PackagingSuite assertion receives: gem NAMES resolved through RubyGems, which is # what NFR-1's "published dependency metadata" names (design P9-2). class PackagingCase + ACRONYMS: Array[String] DEFAULT_RESOLVE: ^(String name) -> untyped @core_name: String @@ -30,6 +33,7 @@ module Dexpace def unheadered: (String? root) -> Array[String] def resolve_constant: (String path) -> Module def default_constant_path: (String name) -> String + def constant_word: (String word) -> String end end end diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite.rbs index 1533a04..4f49e19 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # Appendix B.9's portable half: the eight NFRs whose subject is a UNIT (design R2). diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite/dependencies.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite/dependencies.rbs index 08dfed9..79956a1 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite/dependencies.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite/dependencies.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::PackagingSuite::Dependencies is a private_constant (see dependencies.rb); # declared because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite/surface.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite/surface.rbs index ce231c5..6452e55 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite/surface.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/packaging_suite/surface.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::PackagingSuite::Surface is a private_constant (see surface.rb); declared # because the conformance Steep target checks every file under lib/. module Dexpace diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/recording_span.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/recording_span.rbs index 12e2f0c..482e987 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/recording_span.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/recording_span.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # OBS-21's recording span double, over phase 5c's _Span protocol. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/report.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/report.rbs index 428dbf2..496cd85 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/report.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/report.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # A frozen list of Results with the renderer 8a shipped and the structured form it deferred; diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/result.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/result.rbs index 2616464..10372cf 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/result.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/result.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # One assertion's outcome, in exactly five statuses (design §12). diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/rspec_driver.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/rspec_driver.rbs index 36b3480..88e42f6 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/rspec_driver.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/rspec_driver.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # The thin RSpec driver, opted into by requiring its file; `::RSpec` is resolved at call time diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/runner.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/runner.rbs index ab13167..b0a35d6 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/runner.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/runner.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # The one loop every phase-9 suite's `.run` delegates to; the five statuses decided in one diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/scripts.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/scripts.rbs index b7c6641..9881c1c 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/scripts.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/scripts.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # The named response scripts a WireServer runs; each returns a callable taking the accepted diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/shared_instance.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/shared_instance.rbs index d534029..362e0ce 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/shared_instance.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/shared_instance.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # XCUT-11's structural predicate (design R8, P9-9): frozen, or every ivar declared by the diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/transport_case.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/transport_case.rbs index e7c4c5a..60d4528 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/transport_case.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/transport_case.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # Suite contract 11: a fixture is whatever answers this -- WireServer's own surface -- so 8c's diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite.rbs index 4a079df..81112c1 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # The transport suite's runner (8a's R7, R16). `build:` and `borrow:` are untyped callables: @@ -16,8 +18,7 @@ module Dexpace private - def run_one: (Assertion assertion, build: untyped, borrow: untyped, around: untyped, - settle: untyped, wire: untyped) -> Result + def tearing_down: (untyped around, Array[TransportCase] cases) -> untyped end end end diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/asynchronous.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/asynchronous.rbs index 6248e17..f6e0693 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/asynchronous.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/asynchronous.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::TransportSuite::Asynchronous is a private_constant and not public API: # this declaration exists because the conformance Steep target checks every file under lib/. RBS # has no visibility for a constant, so the privacy lives in asynchronous.rb alone. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/checks.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/checks.rbs index cad8c7e..ea6708c 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/checks.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/checks.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::TransportSuite::Checks is a private_constant and not public API: this # declaration exists because the conformance Steep target checks every file under lib/ and needs # the helpers declared to type the five assertion groups. RBS has no visibility for a constant, so diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/header_drops.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/header_drops.rbs index 6208712..b682021 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/header_drops.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/header_drops.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::TransportSuite::HeaderDrops is a private_constant and not public API: # this declaration exists because the conformance Steep target checks every file under lib/. RBS # has no visibility for a constant, so the privacy lives in header_drops.rb alone. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/inbound.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/inbound.rbs index 4598dc2..5e1fdd8 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/inbound.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/inbound.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::TransportSuite::Inbound is a private_constant and not public API: this # declaration exists because the conformance Steep target checks every file under lib/. RBS has no # visibility for a constant, so the privacy lives in inbound.rb alone. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/lifecycle.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/lifecycle.rbs index 9350b45..84e7eb3 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/lifecycle.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/lifecycle.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::TransportSuite::Lifecycle is a private_constant and not public API: this # declaration exists because the conformance Steep target checks every file under lib/. RBS has no # visibility for a constant, so the privacy lives in lifecycle.rb alone. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/outbound.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/outbound.rbs index 6060d68..fa58167 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/outbound.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/outbound.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::TransportSuite::Outbound is a private_constant and not public API: this # declaration exists because the conformance Steep target checks every file under lib/. RBS has no # visibility for a constant, so the privacy lives in outbound.rb alone. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/resilience.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/resilience.rbs index ad22dc0..06c0f54 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/resilience.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/resilience.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::TransportSuite::Resilience is a private_constant and not public API: this # declaration exists because the conformance Steep target checks every file under lib/. RBS has no # visibility for a constant, so the privacy lives in resilience.rb alone. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/streaming.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/streaming.rbs index 3490a2d..f87ebfc 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/streaming.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/transport_suite/streaming.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::TransportSuite::Streaming is a private_constant and not public API: this # declaration exists because the conformance Steep target checks every file under lib/. RBS has no # visibility for a constant, so the privacy lives in streaming.rb alone. diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/vacuous.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/vacuous.rbs index 253cfca..8ad59c3 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/vacuous.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/vacuous.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # Raised from inside an assertion whose antecedent is absent on the subject (design §12). diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/version.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/version.rbs index 6c9ab0d..c5ebe2b 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/version.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/version.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance VERSION: String diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/wire_server.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/wire_server.rbs index 5a6f7d3..146a21a 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/wire_server.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/wire_server.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance # Design §9.3's plaintext TCPServer fixture. The listener, the sockets and the accepted diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/wire_server/recorded_request.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/wire_server/recorded_request.rbs index d3aad5a..f2848b8 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/wire_server/recorded_request.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/wire_server/recorded_request.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Conformance class WireServer diff --git a/gems/dexpace-conformance/sig/dexpace/conformance/wire_server/request_reader.rbs b/gems/dexpace-conformance/sig/dexpace/conformance/wire_server/request_reader.rbs index 9c65486..588638f 100644 --- a/gems/dexpace-conformance/sig/dexpace/conformance/wire_server/request_reader.rbs +++ b/gems/dexpace-conformance/sig/dexpace/conformance/wire_server/request_reader.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Conformance::WireServer::RequestReader is a private_constant and not public API: this # declaration exists because the conformance Steep target checks every file under lib/. RBS has no # visibility for a constant, so the privacy lives in request_reader.rb alone. diff --git a/gems/dexpace-conformance/test/dexpace/conformance/packaging_suite_test.rb b/gems/dexpace-conformance/test/dexpace/conformance/packaging_suite_test.rb index d82b70a..bfd4b26 100644 --- a/gems/dexpace-conformance/test/dexpace/conformance/packaging_suite_test.rb +++ b/gems/dexpace-conformance/test/dexpace/conformance/packaging_suite_test.rb @@ -264,14 +264,33 @@ def shipped(lib:, sig:) assert_equal(:passed, statuses(report)["NFR-13"]) end - # The assertion must discriminate against the REAL tree too, not only against a fixture: 278 - # shipped .rbs files carry no header today, which is the gap phase 10's inbound list owns. - test "NFR-13 against this repository's own shipped signatures reports the real gap" do + # The assertion runs against the REAL tree too, not only against a fixture. Phase 9 pinned it + # :failed -- 307 of 307 shipped .rbs files carried no header on 2026-09-23 -- and phase 10's + # NFR-13 repair put the header on every one and flipped this pin, which is "the direction a gate + # should move under its own repair". The fixture cases above keep the :failed branch proven. + test "NFR-13 against this repository's own shipped signatures passes" do root = File.expand_path("../../../..", __dir__) report = run_suite(conforming, adapters: [], sig_roots: { CORE => File.join(root, "dexpace-core", "sig") },) - assert_equal(:failed, statuses(report)["NFR-13"], + assert_equal(:passed, statuses(report)["NFR-13"], "an unconditional vacuity here would check nothing for anyone",) end + + # NFR-15 (phase 10, phase 9's aggregate-run finding): the default name rule was segment for + # segment, so four of this port's six gems needed the `constants:` override -- `Dexpace::Core` + # is not loaded, and NetHttp, AsyncHttp and Json are not the constants. The rule now covers all + # six; the override stays for a name it still misses. + test "the default gem-name rule reaches all six of this port's namespaces" do + kase = Dexpace::Conformance::PackagingCase.new(core_name: CORE, adapter_names: []) + expected = { + "dexpace-core" => "Dexpace", "dexpace-transport-net_http" => "Dexpace::Transport::NetHTTP", + "dexpace-transport-async_http" => "Dexpace::Transport::AsyncHTTP", + "dexpace-serde-json" => "Dexpace::Serde::JSON", "dexpace-async-thread" => "Dexpace::Async::Thread", + "dexpace-conformance" => "Dexpace::Conformance", + } + + expected.each { |name, path| assert_equal(path, kase.send(:default_constant_path, name), name) } + assert_equal(Dexpace::VERSION, kase.runtime_version(CORE), "no override was supplied") + end end diff --git a/gems/dexpace-conformance/test/dexpace/conformance/transport_suite_test.rb b/gems/dexpace-conformance/test/dexpace/conformance/transport_suite_test.rb index d75b46b..3f0a2bc 100644 --- a/gems/dexpace-conformance/test/dexpace/conformance/transport_suite_test.rb +++ b/gems/dexpace-conformance/test/dexpace/conformance/transport_suite_test.rb @@ -53,7 +53,9 @@ class OutcomesTest < DexpaceTestCase report = run_suite(failing, vacuous, erroring) assert_equal(%i[failed vacuous error], report.results.map(&:status)) - assert_equal(["x", "no antecedent", "RuntimeError: boom"], report.results.map(&:detail)) + # Since phase 10 the statuses are Runner's, whose :failed detail carries the expectation. + assert_equal(["x (expected 1, got 2)", "no antecedent", "RuntimeError: boom"], + report.results.map(&:detail),) refute_predicate(report, :passed?) end @@ -96,7 +98,7 @@ class OutcomesTest < DexpaceTestCase }, assertions: [checks_close],) assert_equal(1, report.failures.size, "the suite must DETECT the defect, not merely run") - assert_equal("closed? did not flip", report.failures.first.detail) + assert_equal("closed? did not flip (expected true, got false)", report.failures.first.detail) end test "every assertion gets a FRESH case, torn down after it, whatever it raised" do @@ -181,4 +183,36 @@ class DriverKeywordsTest < DexpaceTestCase assert_raises(ArgumentError) { kase_seen.transport.call(:r, :o, :c) } end end + + # Phase 10's invariant for the fold onto Runner, in a class of its own under + # Metrics/ClassLength. + class RunnerFoldTest < DexpaceTestCase + include Running + + # Phase 10's invariant for folding TransportSuite.run onto Runner (P9-10's residue, inbound + # bullet 30): one run over five assertions yields exactly the five statuses, in order, and + # every case that was built is torn down -- the one behaviour Runner lacked a seam for. + test "one run yields all five statuses in order, and every built case is torn down" do + torn = [] + build = lambda do |**_| + transport = Object.new + transport.define_singleton_method(:close) { torn << :closed } + transport + end + assertions = [ + assertion("P", &:transport), + assertion("F") do |_| + raise Failure.new("f", expected: 1, actual: 2, requirement_ids: ["F"]) + end, + assertion("V") { |kase| kase.transport && raise(Vacuous, "v") }, + assertion("W") { |_| raise "never runs" }, + assertion("E") { |kase| kase.transport && raise("e") }, + ] + + report = TransportSuite.run(build: build, assertions: assertions, waive: ["W"]) + + assert_equal(%i[passed failed vacuous waived error], report.results.map(&:status)) + assert_equal(3, torn.size, "each case that built a transport closed it at teardown") + end + end end diff --git a/gems/dexpace-core/lib/dexpace/async/delay.rb b/gems/dexpace-core/lib/dexpace/async/delay.rb index 803d836..35bd0c3 100644 --- a/gems/dexpace-core/lib/dexpace/async/delay.rb +++ b/gems/dexpace-core/lib/dexpace/async/delay.rb @@ -73,11 +73,22 @@ def self.validate_delay(duration) unless duration.is_a?(::Numeric) raise InvalidArgumentError, "duration must be a number of seconds, got #{duration.class}" end + # Phase 10, P8-77's shape: `real?` first (a Complex has no `negative?`), and NaN refused + # (it answers false to both `negative?` and `zero?`, and the scheduler's wait it reached + # never ends). The same screen as Clock::Guard.duration. + unless duration.real? + raise InvalidArgumentError, "duration must be a real number, got #{duration.inspect}" + end if duration.negative? raise InvalidArgumentError, "duration must be non-negative, got #{duration}" end seconds = duration #: untyped + if seconds.respond_to?(:nan?) && seconds.nan? + raise InvalidArgumentError, + "duration must be a number, got NaN" + end + seconds.to_f end private_class_method :validate_delay diff --git a/gems/dexpace-core/lib/dexpace/cancellation/source.rb b/gems/dexpace-core/lib/dexpace/cancellation/source.rb index 0d6a768..7b8cb4e 100644 --- a/gems/dexpace-core/lib/dexpace/cancellation/source.rb +++ b/gems/dexpace-core/lib/dexpace/cancellation/source.rb @@ -85,8 +85,12 @@ def on_cancel(&block) # Withdraws a hook a token registered, so a bounded wait retains nothing on an unbounded # source. Called by Cancellation::Subscription#detach and by nothing else. Idempotent, and a - # no-op once #cancel has taken the list. Array#delete compares with ==, which for a Proc is - # object identity, so one registration is removed and an identical-looking one is not. + # no-op once #cancel has taken the list. Array#delete compares with ==, and Proc#== is NOT + # object identity: a Proc's #dup answers == to it on 3.2.11 through 4.0.6 (measured by phase + # 10, correcting this comment -- phase 2's review R3-3), while two Procs made separately from + # one literal do not. The hooks this list holds are made fresh per registration and never + # duplicated, so removing one never removes another; a caller that registered a Proc and its + # own #dup would see both go. # # @return [self] def off_cancel(hook) diff --git a/gems/dexpace-core/lib/dexpace/clock.rb b/gems/dexpace-core/lib/dexpace/clock.rb index 3185aa8..a5b56f9 100644 --- a/gems/dexpace-core/lib/dexpace/clock.rb +++ b/gems/dexpace-core/lib/dexpace/clock.rb @@ -34,7 +34,17 @@ def duration(value) unless value.is_a?(::Numeric) raise InvalidArgumentError, "duration must be a number of seconds, got #{value.class}" end + # Phase 10, P8-77's shape (8b's review R1-1): `real?` before `negative?`, because a Complex + # is a Numeric with no order and `negative?` was a bare NoMethodError; and a NaN answers + # false to `negative?` AND `zero?`, so it reached `Queue#pop(timeout: NaN)`, which parks + # forever on every supported row -- a sleep no elapsed time ends. Infinity stays admitted: + # an unbounded wait only the token ends is a caller's legitimate choice here. + unless value.real? + raise InvalidArgumentError, + "duration must be a real number, got #{value.inspect}" + end raise InvalidArgumentError, "duration must be non-negative, got #{value}" if value.negative? + raise InvalidArgumentError, "duration must be a number, got NaN" if Guard.nan?(value) # rbs's Numeric declares no #to_f (its subclasses do), so the conversion reads the value # untyped after the check above has done the typing. @@ -42,6 +52,9 @@ def duration(value) seconds.to_f end + # Numeric's own protocol: Float and BigDecimal answer #nan?, Integer and Rational do not. + def nan?(value) = value.respond_to?(:nan?) && value.nan? + # @return [Dexpace::Cancellation, nil] nil for no token or the never-cancelled one def token(cancellation) return nil if cancellation.nil? diff --git a/gems/dexpace-core/lib/dexpace/configuration/builder.rb b/gems/dexpace-core/lib/dexpace/configuration/builder.rb index ab14188..a32c405 100644 --- a/gems/dexpace-core/lib/dexpace/configuration/builder.rb +++ b/gems/dexpace-core/lib/dexpace/configuration/builder.rb @@ -121,17 +121,47 @@ def build property_source: composed_source,) end + # A property source this builder composed: its own entries shadowing one base source. + # Frozen, and never nested -- composing over a Layered merges into a new one over the same + # base (phase 10). + class Layered + attr_reader :entries, :base + + def initialize(entries, base) + @entries = entries.to_h { |key, value| [key.to_s, value.to_s] }.freeze + @base = base + freeze + end + + # This layer's own entry for the key, else the base source's answer (CFG-13's + # last-write-wins at the key level). + # + # @param key [String] + # @return [String, nil] + def call(key) = @entries.fetch(key.to_s) { @base.call(key) } + end + private_constant :Layered + private + # Phase 10, repairing 5a's review R1-2: each `Dexpace.configure` that added a property over + # an inherited source wrapped it in one more closure, so N configures built an N-deep chain + # every property lookup walked, and none was ever released. A layer this builder made is now + # FLATTENED -- its entries merged under the new ones, over the same base -- so the chain is + # at most one layer over the base whatever the configure count. Last-write-wins per key + # (CFG-13) and an untouched inherited seam passed through by reference (CFG-9) both hold. def composed_source - inherited = @property_source + inherited = @property_source #: untyped return inherited || Sources::NONE if @property_source_explicit || @properties.empty? added = Sources.from_hash(@properties) - return added unless inherited - return added if Sources::NONE.equal?(inherited) + return added if inherited.nil? || Sources::NONE.equal?(inherited) + if inherited.is_a?(Layered) + return Layered.new(inherited.entries.merge(@properties), + inherited.base,) + end - ->(key) { added.call(key) || inherited.call(key) }.freeze + Layered.new(@properties, inherited) end def key!(key) diff --git a/gems/dexpace-core/lib/dexpace/http/body/stream_body.rb b/gems/dexpace-core/lib/dexpace/http/body/stream_body.rb index f588260..3eeeb0d 100644 --- a/gems/dexpace-core/lib/dexpace/http/body/stream_body.rb +++ b/gems/dexpace-core/lib/dexpace/http/body/stream_body.rb @@ -25,6 +25,7 @@ def initialize(io, media_type: nil, content_length: -1, close: false) raise Dexpace::InvalidArgumentError, "a stream body's io must respond to #readpartial or #read, got #{io.class}" end + refuse_closed!(io) unless content_length.is_a?(::Integer) && content_length >= -1 raise Dexpace::InvalidArgumentError, "content_length must be an Integer of -1 or more, got #{content_length.inspect}" @@ -85,6 +86,14 @@ def write_to(sink) # Errno::ESPIPE is a SystemCallError and NOT an IOError, so a bare `rescue IOError` would not # catch it. Guarded by respond_to? on both methods first, because the factory accepts any # #read-shaped object and a bare probe would raise NoMethodError out of a question. + # A stream already closed can never be written, and its #pos raised a bare IOError out of the + # probe below, outside `rescue Dexpace::Error` (phase 10, phase 3b's review R2-1). + def refuse_closed!(io) + return unless io.respond_to?(:closed?) && io.closed? + + raise Dexpace::InvalidArgumentError, "a stream body's io is already closed" + end + def probe_rewindability(io) return [0, false] unless io.respond_to?(:pos) && io.respond_to?(:seek) diff --git a/gems/dexpace-core/lib/dexpace/http/header_syntax.rb b/gems/dexpace-core/lib/dexpace/http/header_syntax.rb index da72ee4..1f95753 100644 --- a/gems/dexpace-core/lib/dexpace/http/header_syntax.rb +++ b/gems/dexpace-core/lib/dexpace/http/header_syntax.rb @@ -61,7 +61,12 @@ def trim(name) # HTTP-17: no C0 control, no DEL, no byte >= 0x80, not blank. Every byte below 0x21 is # rejected, which covers the controls, SP and HTAB -- a header name has no interior space. + # + # Every predicate here is TOTAL: a non-String answers false rather than raising NoMethodError + # from `#b`, which escaped `rescue Dexpace::Error` (phase 10, phase 1's review R3-4). def valid_name?(name) + return false unless name.is_a?(::String) + trimmed = trim(name) return false if trimmed.empty? @@ -70,6 +75,8 @@ def valid_name?(name) # HTTP-18: HTAB plus printable ASCII 0x20-0x7E, and nothing else. def valid_outbound_value?(value) + return false unless value.is_a?(::String) + value.b.each_byte.all? { |byte| byte == HTAB || (byte >= SPACE && byte < DEL) } end @@ -77,6 +84,8 @@ def valid_outbound_value?(value) # in a field value and applying the outbound grammar to a response would silently drop a # legitimate Latin-1 Content-Disposition. def valid_inbound_value?(value) + return false unless value.is_a?(::String) + value.b.each_byte.all? { |byte| byte == HTAB || (byte >= SPACE && byte != DEL) } end @@ -132,6 +141,8 @@ def validate_inbound_value!(value, name:) # String carrying invalid UTF-8, and an ArgumentError raised from inside Ruby escapes # `rescue Dexpace::Error` -- a validator that crashes has not rejected its input. def token?(text) + return false unless text.is_a?(::String) + bytes = text.b !bytes.empty? && bytes.each_byte.all? { |byte| TCHAR.include?(byte) } end diff --git a/gems/dexpace-core/lib/dexpace/http/protocol.rb b/gems/dexpace-core/lib/dexpace/http/protocol.rb index 828bd01..8e14104 100644 --- a/gems/dexpace-core/lib/dexpace/http/protocol.rb +++ b/gems/dexpace-core/lib/dexpace/http/protocol.rb @@ -17,10 +17,16 @@ class Protocol < Data.define(:wire) private_class_method :new - # The canonical lower-case wire forms a Protocol can hold. - WIRE_FORMS = %w[http/1.1 http/2].freeze + # The canonical lower-case wire forms a Protocol can hold. HTTP-33 names http/1.1 and http/2 as + # EXAMPLES; http/1.0 joined in phase 10, because a real HTTP/1.0 status line is one any server + # may send and both transports feed their native version straight to .parse -- before it, an + # HTTP/1.0 response made both ResponseMappers raise. Additive, so NFR-4 permits it. + WIRE_FORMS = %w[http/1.0 http/1.1 http/2].freeze # Every accepted spelling, folded, mapped to its canonical form; HTTP/2.0 is HTTP-33's alias. - ALIASES = { "http/1.1" => "http/1.1", "http/2" => "http/2", "http/2.0" => "http/2" }.freeze + ALIASES = { + "http/1.0" => "http/1.0", "http/1.1" => "http/1.1", + "http/2" => "http/2", "http/2.0" => "http/2", + }.freeze # The validating factory: accepts a canonical wire form and nothing looser, because #with # routes through it and a derived protocol must be exactly as canonical as a parsed one. @@ -68,6 +74,8 @@ def to_s wire end + # The HTTP/1.0 protocol, canonical instance (phase 10). + HTTP_1_0 = build(wire: "http/1.0") # The HTTP/1.1 protocol, canonical instance. HTTP_1_1 = build(wire: "http/1.1") # The HTTP/2 protocol, canonical instance; `HTTP/2` and `HTTP/2.0` both parse to it. diff --git a/gems/dexpace-core/lib/dexpace/http/status.rb b/gems/dexpace-core/lib/dexpace/http/status.rb index e307c93..8bfb34e 100644 --- a/gems/dexpace-core/lib/dexpace/http/status.rb +++ b/gems/dexpace-core/lib/dexpace/http/status.rb @@ -4,7 +4,7 @@ require_relative "../model" module Dexpace - # An HTTP status code, total over the protocol's range (HTTP-10 - HTTP-12). + # An HTTP status code, total over every Integer code (HTTP-10 - HTTP-12). # # ONE member, and that is HTTP-12 rather than minimalism: two Status values must be equal iff # their codes are equal, with the name taking no part. A Data.define(:code, :name) would generate @@ -16,7 +16,7 @@ class Status < Data.define(:code) private_class_method :new - # The recognised codes and their reason phrases; every other code in range is a vendor code. + # The recognised codes and their reason phrases; every other code is a vendor code. CANONICAL_NAMES = { 100 => "Continue", 101 => "Switching Protocols", 200 => "OK", 201 => "Created", 202 => "Accepted", 204 => "No Content", @@ -54,19 +54,26 @@ def self.canonical_name(code) CANONICAL_NAMES[code] end - # HTTP-10 says construction is total over "any code"; the 100-599 guard is the port's reading - # and not a narrowing of it. A vendor code such as nginx's 499 or Cloudflare's 520-526 and 530 - # is inside the range and constructs cleanly with no name, which is the requirement's own - # rationale; an Integer outside the protocol's range is a caller mistake, not a vendor code. + # HTTP-10 says construction is total over "any code" -- "mapping any code MUST return a + # Status (never throw)" -- and TRANSPORT-24 that any code a server returns is surfaced rather + # than rejected. Phase 1's 100-599 guard refused a 600 or LinkedIn's 999, which Net::HTTP + # delivers as an HTTPUnknownResponse, and made both transports raise on a head the + # requirement says to surface (phase 10, 8a's R2-2); a first repair to 0..999 -- "every code a + # status line can carry" -- still threw on 1000 and -1, a port reading narrower than the + # MUST (phase 10's review round 0, R0-5). So every Integer is a code, and the protocol's own + # range is the #standard? predicate; only a non-Integer is refused, because it is not a code + # at all (HTTP-4's type check, not a range). def initialize(code:) Model.required!("code", code) - unless code.is_a?(Integer) && code.between?(100, 599) - raise InvalidArgumentError, "code must be an integer status code between 100 and 599" - end + raise InvalidArgumentError, "code must be an Integer status code" unless code.is_a?(Integer) super end + # 100-599: the classes HTTP-11 names. A vendor code outside them (a 999) is a Status all the + # same and answers false to every class predicate below. + def standard? = code.between?(100, 599) + # The reason phrase for a recognised code; nil for a vendor code (HTTP-10). def canonical_name CANONICAL_NAMES[code] diff --git a/gems/dexpace-core/lib/dexpace/http/url.rb b/gems/dexpace-core/lib/dexpace/http/url.rb index e0ceb91..e3e2ada 100644 --- a/gems/dexpace-core/lib/dexpace/http/url.rb +++ b/gems/dexpace-core/lib/dexpace/http/url.rb @@ -27,10 +27,7 @@ module URL # still holds is externally-mutable state XCUT-15 forbids a model from aliasing. Parsing # yields components this method owns, which it can then freeze without touching the caller's. def parse!(input) - Model.required!("url", input) - unless input.is_a?(String) || input.is_a?(::URI::Generic) - raise InvalidArgumentError, "url must be a String or a URI, got #{input.class}" - end + string_or_uri!(input) # rbs's stdlib signatures declare URI::RFC3986_Parser as an empty class, so its #parse is # invisible to Steep; the annotation is local to this one call rather than a reopened @@ -41,10 +38,15 @@ def parse!(input) raise InvalidArgumentError, "url #{input.to_s.inspect} is not an absolute URI (HTTP-47)" end - own(uri) - rescue ::URI::InvalidURIError => error + refuse_hostless!(uri, input) + + renderable!(own(uri), input) + rescue ::URI::Error => error # The Task 1 rule: a stdlib exception raised because of the argument is re-raised as the - # SDK's argument error, naming the input, with the original left as the cause. + # SDK's argument error, naming the input, with the original left as the cause. URI::Error, + # not only InvalidURIError: `mailto://host` raises URI::InvalidComponentError from the + # scheme's own constructor, which escaped `rescue Dexpace::Error` until phase 10 (phase 2's + # review R3-1). raise InvalidArgumentError, "url #{input.to_s.inspect} is malformed: #{error.message}" end @@ -93,6 +95,37 @@ def resolve(base, reference) private + def string_or_uri!(input) + Model.required!("url", input) + return if input.is_a?(String) || input.is_a?(::URI::Generic) + + raise InvalidArgumentError, "url must be a String or a URI, got #{input.class}" + end + + # HTTP-47's rationale is failing at construction "rather than surfacing a lower-level or + # transport-specific error later", and `URI::Generic#absolute?` is only "a scheme is present": + # `http:` and `http:///p` parse as absolute URI::HTTPs with no host, which surfaced as whatever + # the transport raised on a nil host. The http family is this SDK's own; which OTHER schemes a + # transport dispatches stays the transport's knowledge (phase 1's reading), so only the http + # family is screened for a host here (phase 10, phase 1's review round 3). + def refuse_hostless!(uri, input) + return unless uri.is_a?(::URI::HTTP) && (uri.host.nil? || uri.host.empty?) + + raise InvalidArgumentError, "url #{input.to_s.inspect} has no host (HTTP-47)" + end + + # A URI whose own #to_s mutates it cannot be held frozen: URI::FTP with a `;type=` code + # rewrites @path inside #to_s, so the frozen result raised FrozenError from every later + # rendering -- a lower-level error, later, which is what HTTP-47 exists to prevent (phase 2's + # review R3-2). Refused here, at construction, with the SDK's own error. + def renderable!(uri, input) + uri.to_s + uri + rescue ::FrozenError + raise InvalidArgumentError, + "url #{input.to_s.inspect} cannot be held as an immutable URI (HTTP-47)", cause: nil + end + # Freezes the URI's String components, then the URI. Only Strings: a URI also references # its parser, a process-global object this method leaves alone -- the uri gem freezes # URI::RFC3986_PARSER at definition on every supported Ruby, so nothing here needs to touch diff --git a/gems/dexpace-core/lib/dexpace/instrumentation/async_step.rb b/gems/dexpace-core/lib/dexpace/instrumentation/async_step.rb index 77e0081..da4d64a 100644 --- a/gems/dexpace-core/lib/dexpace/instrumentation/async_step.rb +++ b/gems/dexpace-core/lib/dexpace/instrumentation/async_step.rb @@ -103,10 +103,17 @@ def head(request, cursor) # response event and the derived future's fulfilment, then the finish; a meter that raises # there is collected by Hooks.notify, which still runs every later callback, and re-raised # into the producer once the list has run. + # + # The clock is read in a callback registered BEFORE the derivation (phase 10, 5b's review + # R3-2): the derived future's fulfilment runs every callback the CALLER put on it, and with + # the clock read in the finish that follows, the recorded duration included the caller's own + # continuation. The sync path records before it returns; this records at settlement. def attach(future, pending) if body? + ended = nil #: Float? + future.on_settle { |_settlement| ended = @clock.monotonic } derived = future.then { |response| settle_response(pending, wrap_response(response)) } - future.on_settle { |settlement| settle(pending, settlement) } + future.on_settle { |settlement| settle(pending, settlement, ended) } derived else future.on_settle do |settlement| @@ -123,12 +130,12 @@ def settle_response(pending, response) end # The failure event, bridged, then the tracer and meter work. - def settle(pending, settlement) + def settle(pending, settlement, ended = nil) error = settlement.error unless error.nil? bridged(pending.snapshot) { log_failure(pending.request, error, pending.started) } end - finish(pending.span, pending.started) + finish(pending.span, pending.started, ended) end # OBS-24's bridge around a log emission; nothing to bridge when nothing is logged. diff --git a/gems/dexpace-core/lib/dexpace/instrumentation/bundle.rb b/gems/dexpace-core/lib/dexpace/instrumentation/bundle.rb index c1620a6..dfd3ae1 100644 --- a/gems/dexpace-core/lib/dexpace/instrumentation/bundle.rb +++ b/gems/dexpace-core/lib/dexpace/instrumentation/bundle.rb @@ -25,6 +25,11 @@ module Instrumentation # made a bundle reachable by a pipeline step through Pipeline::Cursor#bundle, seeded per # call by the `bundle:` keyword on Pipeline#call and AsyncPipeline#call (its Task 8). # + # Two factories, not one (phase 10, R6): the member `tracer_factory` makes SPAN tracers + # (OBS-21 - OBS-25) and is legitimately shared and cached; OBS-29's HTTP-tracer factory makes + # one Instrumentation::HTTPTracer per operation and lives on the retry steps' + # `http_tracer_factory:`, never here. + # # trace_state is a list of pairs, not a Hash: OBS-26 says "a vendor trace-state list" and W3C # tracestate is ordered, most recent vendor first, which a Hash would lose. trace_flags stays # the two-hex-char wire form OBS-26 fixes; phase 5c added the #sampled? predicate over it, @@ -60,8 +65,13 @@ class Bundle < Data.define( # @param trace_state [Array] the vendor trace-state list, in order # @param remote [Boolean] whether the trace context arrived from another service # @param span [Object] the active span; NO_SPAN when tracing is off - # @param tracer_factory [#tracer] the per-operation tracer factory; NO_TRACER_FACTORY when - # tracing is off + # @param tracer_factory [#tracer] CTX-14's SPAN-tracer factory, keyed by instrumentation + # library name and version (opentelemetry-api's TracerProvider shape, 4a's P4-8) and shared + # across operations; NO_TRACER_FACTORY when tracing is off. It is NOT OBS-29's + # per-operation HTTP-tracer factory, which the retry steps take as `http_tracer_factory:` + # and call once per operation (6a's P6-7): OBS-25's no-op factory must answer the same + # object on every call, the opposite of one instance per operation, so the two are two + # objects (phase 10, R6) # @return [Bundle] def self.build(trace_id:, span_id:, flavour:, trace_flags: "00", trace_state: [], remote: false, span: NO_SPAN, tracer_factory: NO_TRACER_FACTORY) diff --git a/gems/dexpace-core/lib/dexpace/instrumentation/redactor.rb b/gems/dexpace-core/lib/dexpace/instrumentation/redactor.rb index 9b795ab..b70e14c 100644 --- a/gems/dexpace-core/lib/dexpace/instrumentation/redactor.rb +++ b/gems/dexpace-core/lib/dexpace/instrumentation/redactor.rb @@ -40,6 +40,13 @@ module Instrumentation # common case never reaches the rescue, because the name is folded through `#scrub` first; # the rescue is the totality backstop XCUT-20 asks for. # + # WHERE it is applied, which a caller wiring a sink needs to know (phase 10, 5b's review R3-4): + # on the way INTO the record, by the field's name, and never at the sink. Event#field routes + # every reserved URL and header key through here; Logger.build scrubs the global context once; + # Event#emit scrubs the folded diagnostic context -- OBS-5's three sources, one table + # (ReservedKeys). A sink therefore receives already-redacted values and is never trusted to + # redact; a value written under an unreserved key is not a URL or a header and passes as given. + # # Frozen, holding a frozen policy, with every intermediate a method local: XCUT-11's shared # instance by construction. Redactor::DEFAULT is the one over RedactionPolicy::DEFAULT. # diff --git a/gems/dexpace-core/lib/dexpace/instrumentation/step.rb b/gems/dexpace-core/lib/dexpace/instrumentation/step.rb index f27cce9..44cb033 100644 --- a/gems/dexpace-core/lib/dexpace/instrumentation/step.rb +++ b/gems/dexpace-core/lib/dexpace/instrumentation/step.rb @@ -164,13 +164,15 @@ def open_span(request, bundle) # two, OBS-32's conventions are post-v1, and an inline Hash literal here would allocate # per request to carry a vocabulary nothing has chosen. The histogram takes milliseconds, # the same figure the log field carries. - def finish(span, started) + # `ended` is the settlement's own reading when the async step took one (phase 10); nil reads + # the clock now, which is the sync path's moment. + def finish(span, started, ended = nil) span.finish @counter.add(1) - @histogram.record(elapsed_ms(started)) + @histogram.record(elapsed_ms(started, ended)) end - def elapsed_ms(started) = (@clock.monotonic - started) * 1000.0 + def elapsed_ms(started, ended = nil) = ((ended || @clock.monotonic) - started) * 1000.0 def logged? = @level.at_least?(HTTPLogging::HEADERS) def body? = @level.at_least?(HTTPLogging::BODY) diff --git a/gems/dexpace-core/lib/dexpace/io/buffered_source.rb b/gems/dexpace-core/lib/dexpace/io/buffered_source.rb index c8bee09..a31c321 100644 --- a/gems/dexpace-core/lib/dexpace/io/buffered_source.rb +++ b/gems/dexpace-core/lib/dexpace/io/buffered_source.rb @@ -12,8 +12,8 @@ module IO # representation, because it responds to #each yielding BINARY chunks. # # Ownership is two frozen construction-time facts, never a close-time judgement. Closeable's - # @owned is always true -- a source always owns its own buffer -- and a separate frozen - # @dexpace_owns_upstream decides whether #release also closes the upstream. The factory names + # `@owned` is always true -- a source always owns its own buffer -- and a separate frozen + # `@dexpace_owns_upstream` decides whether #release also closes the upstream. The factory names # carry it: .wrapping owns (IO-6), .of_bytes and .over own nothing. IO-6 is read out of # appendix C, and cited with design §10.12; SEAM-3, which the older statements of the rule # cite, is retired (docs/knowledge/notes/message-bodies.md). diff --git a/gems/dexpace-core/lib/dexpace/model.rb b/gems/dexpace-core/lib/dexpace/model.rb index 602cc80..d0365fd 100644 --- a/gems/dexpace-core/lib/dexpace/model.rb +++ b/gems/dexpace-core/lib/dexpace/model.rb @@ -25,8 +25,23 @@ def self.required!(name, value) # copy: true the caller's object graph is untouched and the returned copy is deep-frozen, # which is what lets every accessor return the same frozen reference with no per-access # wrapper (design §4, §10.11). + # + # Two things make_shareable cannot copy, both answered here rather than escaping as Ruby's own + # error outside `rescue Dexpace::Error` (phase 10, phase 1's review R3-1). A Hash's DEFAULT PROC + # is behaviour, not data -- `Hash.new { ... }` made make_shareable raise TypeError ("allocator + # undefined for Proc") on every supported Ruby -- so a model's copy drops it: what the model + # owns is the entries the caller handed it, and a lookup miss on the model answers nil, as on + # any other model collection. Anything else that cannot be made shareable (a Proc or a Mutex + # among the values) is the SDK's argument error naming what was refused. def self.own(collection) - Ractor.make_shareable(collection, copy: true) + owned = collection #: untyped + if owned.is_a?(::Hash) && owned.default_proc + owned = owned.dup + owned.default_proc = nil + end + Ractor.make_shareable(owned, copy: true) + rescue ::TypeError, ::Ractor::Error => error + raise InvalidArgumentError, "a model cannot own this collection: #{error.message}" end # A String a caller still holds a reference to is externally-mutable state XCUT-15 forbids a diff --git a/gems/dexpace-core/lib/dexpace/proxy/resolution.rb b/gems/dexpace-core/lib/dexpace/proxy/resolution.rb index ecbd931..10b2938 100644 --- a/gems/dexpace-core/lib/dexpace/proxy/resolution.rb +++ b/gems/dexpace-core/lib/dexpace/proxy/resolution.rb @@ -58,11 +58,27 @@ module ProxyResolution # rubocop:disable Metrics/ModuleLength # a value with no `//`, which RFC 3986 reads as an opaque URI with no userinfo and the # redactor therefore writes back as given (P5-100). Applied to the redactor's form of the # URL before it is shown, so `user:secret@proxy.corp:3128` -- the scheme forgotten -- shows - # as `***:***@proxy.corp:3128`. Anchored, one bounded class and one literal; the per-pattern - # timeout is the port's rule. - CREDENTIAL_BEFORE_AT = ::Regexp.new('\A[^/?#]*@', timeout: 1.0) + # as `***:***@proxy.corp:3128`. One bounded class and one literal; the per-pattern timeout is + # the port's rule. + # + # UNANCHORED, and applied with `gsub` to every `@` the shown form carries (phase 10, repairing + # phase 5b's R3-1). Anchored at `\A`, the belt reached only a credential in the value's FIRST + # slash-free run, and a spelling that puts a slash before the userinfo -- `http:/u:p@h:1`, + # `http:///u:p@h`, `/http://u:p@h:1`, `socks5:/u:p@h:1` -- is read by the parser as a path + # the redactor keeps verbatim (OBS-14), so the password reached Kernel#warn and the config sink + # on eight spellings of eight on 4.0.6 and 3.2.11. Over-redacting an `@` that sits in a path is + # the safe direction in a warning: OBS-11's userinfo rule is "unconditional". + CREDENTIAL_BEFORE_AT = ::Regexp.new("[^/?#]*@", timeout: 1.0) private_constant :CREDENTIAL_BEFORE_AT + # Where a proxy URL's authority starts: after a `scheme:` that is followed by at least one + # `/`, or after any leading run of `/` -- never after a bare `user:`, which is the forgotten- + # scheme spelling's username and part of the credential. Anchored, and no class in it admits + # the `:` or `/` that ends it, so it cannot backtrack; the per-pattern timeout is the port's + # rule all the same. + AUTHORITY_START = ::Regexp.new("\\A(?:[A-Za-z][A-Za-z0-9+.\\-]*:/+|/*)", timeout: 1.0) + private_constant :AUTHORITY_START + # The header name #shown renders the URL under: `location`, the first of # RedactionPolicy::DEFAULT's URL-valued names, which is what makes Redactor#header_value take # the URL route rather than pass the value through (OBS-17). @@ -174,8 +190,26 @@ def split_url(url, logger) # URL-valued header name, since that entry point is keyed by header name (OBS-16, OBS-17). # Then CFG-24's grammar rule, for the credential the redactor cannot know is one. def shown(url) - redacted = Instrumentation::Redactor::DEFAULT.header_value(SHOWN_AS, url) - redacted.sub(CREDENTIAL_BEFORE_AT, "#{Instrumentation::Redactor::REDACTED_USERINFO}@") + scrubbed = scrub_userinfo(url) + redacted = Instrumentation::Redactor::DEFAULT.header_value(SHOWN_AS, scrubbed) + redacted.gsub(CREDENTIAL_BEFORE_AT, "#{Instrumentation::Redactor::REDACTED_USERINFO}@") + end + + # CFG-24's grammar applied to the RAW value, before the parser or the redactor can split a + # password at a `/`, `?` or `#` it contains: in a proxy URL everything from the authority's + # start through the LAST `@` is userinfo, so it is replaced whole. The belt above runs on the + # redactor's output and stops at the first reserved character, which left the prefix of a + # password such as `pa/ss`, `p#ss` or `se#cret` in the warning (phase 10's review round 0, + # R0-4); this pass runs first and has no delimiter to stop at. A value with no `@` is + # returned unchanged. + def scrub_userinfo(url) + last = url.rindex("@") + return url if last.nil? + + start = AUTHORITY_START.match(url)&.end(0) || 0 + return url if start > last + + "#{url[0, start]}#{Instrumentation::Redactor::REDACTED_USERINFO}@#{url[(last + 1)..]}" end def url_problem(host, raw_port) diff --git a/gems/dexpace-core/lib/dexpace/registry.rb b/gems/dexpace-core/lib/dexpace/registry.rb index 2c33c14..c118bf2 100644 --- a/gems/dexpace-core/lib/dexpace/registry.rb +++ b/gems/dexpace-core/lib/dexpace/registry.rb @@ -177,6 +177,19 @@ def install(provider) # scenario as the motivation for replacing it, so trading a loud error for a silent wedge would # be a straight regression. The claim is released by #complete_resolution's ensure on this path # exactly as on any other, so the registry stays re-evaluable for everyone else. + # + # **Why the loop terminates** (phase 10, answering phase 9's finding that nothing stated it). + # Every pass ends in one of four ways, and only the last repeats. (1) A resolved slot returns + # at once. (2) A fresh claim runs #complete_resolution, which returns or raises -- it never + # loops. (3) The claim swap answers nil only when a provider was resolved between the two + # reads, so the next pass is (1). (4) A claim owned by another fiber is waited on, and its gate + # is closed in that owner's `ensure` whether the build succeeded or raised, and a pop on a + # closed queue returns at once; the owner's slot is cleared in the same `ensure`, so the next + # pass meets (1) when the owner succeeded or takes a fresh claim, (2), when it failed. So a + # caller repeats at most once per concurrent claim that FAILED ahead of it, each of which is a + # real exception raised to that claim's owner. The argument rests on the guard in (1) being + # unconditional: phase 9 made it conditional on anything else and the loop span forever with + # no progress. `registry_test.rb`'s Termination suite pins every state under a bounded join. def resolve loop do resolved = @state.resolved diff --git a/gems/dexpace-core/sig/dexpace.rbs b/gems/dexpace-core/sig/dexpace.rbs index b14f1e5..3cf24c6 100644 --- a/gems/dexpace-core/sig/dexpace.rbs +++ b/gems/dexpace-core/sig/dexpace.rbs @@ -1,2 +1,4 @@ +# SPDX-License-Identifier: MIT + module Dexpace end diff --git a/gems/dexpace-core/sig/dexpace/async/completer.rbs b/gems/dexpace-core/sig/dexpace/async/completer.rbs index 347e104..0b27349 100644 --- a/gems/dexpace-core/sig/dexpace/async/completer.rbs +++ b/gems/dexpace-core/sig/dexpace/async/completer.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Async # Async::Deadline is a private_constant and not public API: declared because the strict diff --git a/gems/dexpace-core/sig/dexpace/async/delay.rbs b/gems/dexpace-core/sig/dexpace/async/delay.rbs index db7ceb4..317a460 100644 --- a/gems/dexpace-core/sig/dexpace/async/delay.rbs +++ b/gems/dexpace-core/sig/dexpace/async/delay.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Async # Both private_constants; declared because the strict `core` Steep target types their uses. diff --git a/gems/dexpace-core/sig/dexpace/async/future.rbs b/gems/dexpace-core/sig/dexpace/async/future.rbs index 5767eff..855ab1f 100644 --- a/gems/dexpace-core/sig/dexpace/async/future.rbs +++ b/gems/dexpace-core/sig/dexpace/async/future.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Async class Future diff --git a/gems/dexpace-core/sig/dexpace/async/settlement.rbs b/gems/dexpace-core/sig/dexpace/async/settlement.rbs index 6e42724..9f25347 100644 --- a/gems/dexpace-core/sig/dexpace/async/settlement.rbs +++ b/gems/dexpace-core/sig/dexpace/async/settlement.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Async class Settlement < Data diff --git a/gems/dexpace-core/sig/dexpace/async_pipeline.rbs b/gems/dexpace-core/sig/dexpace/async_pipeline.rbs index a644c47..9a02534 100644 --- a/gems/dexpace-core/sig/dexpace/async_pipeline.rbs +++ b/gems/dexpace-core/sig/dexpace/async_pipeline.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class AsyncPipeline include Dexpace::Closeable diff --git a/gems/dexpace-core/sig/dexpace/async_transport.rbs b/gems/dexpace-core/sig/dexpace/async_transport.rbs index 0ab8626..54789b2 100644 --- a/gems/dexpace-core/sig/dexpace/async_transport.rbs +++ b/gems/dexpace-core/sig/dexpace/async_transport.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # Differs from _Transport in exactly one position, the return type -- the whole of what # .conforms? cannot check and the reason both interfaces are declared at all. diff --git a/gems/dexpace-core/sig/dexpace/auth.rbs b/gems/dexpace-core/sig/dexpace/auth.rbs index bfa3cc8..840f2d3 100644 --- a/gems/dexpace-core/sig/dexpace/auth.rbs +++ b/gems/dexpace-core/sig/dexpace/auth.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # Phase 6c: the authentication layer's namespace and its one shared redaction marker. module Auth diff --git a/gems/dexpace-core/sig/dexpace/auth/async_bearer_stamper.rbs b/gems/dexpace-core/sig/dexpace/auth/async_bearer_stamper.rbs index 1131a75..5af1220 100644 --- a/gems/dexpace-core/sig/dexpace/auth/async_bearer_stamper.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/async_bearer_stamper.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-37, AUTH-36 (async half), AUTH-11: the three-zone async bearer stamper. diff --git a/gems/dexpace-core/sig/dexpace/auth/async_step.rbs b/gems/dexpace-core/sig/dexpace/auth/async_step.rbs index 4dfd529..2b001d9 100644 --- a/gems/dexpace-core/sig/dexpace/auth/async_step.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/async_step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-27 through AUTH-38 on the async runtime: Step over 4c's _AsyncStep. Exchange is a diff --git a/gems/dexpace-core/sig/dexpace/auth/basic_handler.rbs b/gems/dexpace-core/sig/dexpace/auth/basic_handler.rbs index 2bdf721..c7e9d13 100644 --- a/gems/dexpace-core/sig/dexpace/auth/basic_handler.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/basic_handler.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-14: Basic, preemptive through #call and challenge-answered through diff --git a/gems/dexpace-core/sig/dexpace/auth/bearer_provider.rbs b/gems/dexpace-core/sig/dexpace/auth/bearer_provider.rbs index 6a8064e..dc81a46 100644 --- a/gems/dexpace-core/sig/dexpace/auth/bearer_provider.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/bearer_provider.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-11: the provider duck type -- #fetch, and optionally #fetch_async. diff --git a/gems/dexpace-core/sig/dexpace/auth/bearer_stamper.rbs b/gems/dexpace-core/sig/dexpace/auth/bearer_stamper.rbs index 8ae8486..6358773 100644 --- a/gems/dexpace-core/sig/dexpace/auth/bearer_stamper.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/bearer_stamper.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-11, AUTH-34, AUTH-35, AUTH-36: the sync bearer stamper. diff --git a/gems/dexpace-core/sig/dexpace/auth/bearer_token.rbs b/gems/dexpace-core/sig/dexpace/auth/bearer_token.rbs index 32a23f8..aa16c73 100644 --- a/gems/dexpace-core/sig/dexpace/auth/bearer_token.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/bearer_token.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-8, AUTH-9, AUTH-10: the bearer token, redacted in every rendering. diff --git a/gems/dexpace-core/sig/dexpace/auth/challenge.rbs b/gems/dexpace-core/sig/dexpace/auth/challenge.rbs index c542ae0..cfd88cd 100644 --- a/gems/dexpace-core/sig/dexpace/auth/challenge.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/challenge.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-12: one parsed challenge, folded once at construction. diff --git a/gems/dexpace-core/sig/dexpace/auth/challenge_handler_chain.rbs b/gems/dexpace-core/sig/dexpace/auth/challenge_handler_chain.rbs index 14297c5..4706cf2 100644 --- a/gems/dexpace-core/sig/dexpace/auth/challenge_handler_chain.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/challenge_handler_chain.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-23's one-method handler protocol (6c's P6-2). diff --git a/gems/dexpace-core/sig/dexpace/auth/challenges.rbs b/gems/dexpace-core/sig/dexpace/auth/challenges.rbs index d7d958e..b624255 100644 --- a/gems/dexpace-core/sig/dexpace/auth/challenges.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/challenges.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-12, AUTH-13: the RFC 7235 challenge-list parser. Its patterns and its Parser class are diff --git a/gems/dexpace-core/sig/dexpace/auth/descriptor.rbs b/gems/dexpace-core/sig/dexpace/auth/descriptor.rbs index 47d2537..6cd0945 100644 --- a/gems/dexpace-core/sig/dexpace/auth/descriptor.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/descriptor.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-3: a non-empty ordered requirement list, immutable in and out. diff --git a/gems/dexpace-core/sig/dexpace/auth/digest_handler.rbs b/gems/dexpace-core/sig/dexpace/auth/digest_handler.rbs index 7f9224c..1f53703 100644 --- a/gems/dexpace-core/sig/dexpace/auth/digest_handler.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/digest_handler.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # A hash function the handler drives: what ::Digest::MD5 and ::Digest::SHA256 answer. diff --git a/gems/dexpace-core/sig/dexpace/auth/https_required_error.rbs b/gems/dexpace-core/sig/dexpace/auth/https_required_error.rbs index 0672b97..d0bd3f5 100644 --- a/gems/dexpace-core/sig/dexpace/auth/https_required_error.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/https_required_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-28: the HTTPS guard's refusal, naming the step and the scheme. diff --git a/gems/dexpace-core/sig/dexpace/auth/key_credential.rbs b/gems/dexpace-core/sig/dexpace/auth/key_credential.rbs index 1e07c02..2610d7b 100644 --- a/gems/dexpace-core/sig/dexpace/auth/key_credential.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/key_credential.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-8, AUTH-9, AUTH-26: an API key with reference identity. diff --git a/gems/dexpace-core/sig/dexpace/auth/key_stamper.rbs b/gems/dexpace-core/sig/dexpace/auth/key_stamper.rbs index d14cfa1..0747b06 100644 --- a/gems/dexpace-core/sig/dexpace/auth/key_stamper.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/key_stamper.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # What KeyStamper is written against: KeyCredential and NamedKeyCredential both answer it. diff --git a/gems/dexpace-core/sig/dexpace/auth/named_key_credential.rbs b/gems/dexpace-core/sig/dexpace/auth/named_key_credential.rbs index e65e0d9..8f41b04 100644 --- a/gems/dexpace-core/sig/dexpace/auth/named_key_credential.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/named_key_credential.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-8, AUTH-9, AUTH-26: a named key with reference identity. diff --git a/gems/dexpace-core/sig/dexpace/auth/password_credential.rbs b/gems/dexpace-core/sig/dexpace/auth/password_credential.rbs index 5ac3872..c8d9dd1 100644 --- a/gems/dexpace-core/sig/dexpace/auth/password_credential.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/password_credential.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-8, AUTH-14: the username/password pair, both fields redacted. diff --git a/gems/dexpace-core/sig/dexpace/auth/provider_error.rbs b/gems/dexpace-core/sig/dexpace/auth/provider_error.rbs index 5d1e4b5..9f25062 100644 --- a/gems/dexpace-core/sig/dexpace/auth/provider_error.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/provider_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-35, AUTH-11: a misbehaving bearer provider result. diff --git a/gems/dexpace-core/sig/dexpace/auth/requirement.rbs b/gems/dexpace-core/sig/dexpace/auth/requirement.rbs index 11731b9..ff5b05a 100644 --- a/gems/dexpace-core/sig/dexpace/auth/requirement.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/requirement.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-2: one scheme bound to its own scopes and params, deep-frozen once. diff --git a/gems/dexpace-core/sig/dexpace/auth/resolver.rbs b/gems/dexpace-core/sig/dexpace/auth/resolver.rbs index a6a5665..d7d458c 100644 --- a/gems/dexpace-core/sig/dexpace/auth/resolver.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/resolver.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-4 through AUTH-7: tier resolution as a pure module function. diff --git a/gems/dexpace-core/sig/dexpace/auth/scheme.rbs b/gems/dexpace-core/sig/dexpace/auth/scheme.rbs index c7796ee..74b15b8 100644 --- a/gems/dexpace-core/sig/dexpace/auth/scheme.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/scheme.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-1: the closed five-member set, in Pipeline::Stage's shape (P4-32, P4-56): both diff --git a/gems/dexpace-core/sig/dexpace/auth/step.rbs b/gems/dexpace-core/sig/dexpace/auth/step.rbs index 24bd214..9ed2549 100644 --- a/gems/dexpace-core/sig/dexpace/auth/step.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # A synchronous stamper: KeyStamper, BasicHandler, BearerStamper, or Step::NO_STAMP. diff --git a/gems/dexpace-core/sig/dexpace/auth/unencodable_credential_error.rbs b/gems/dexpace-core/sig/dexpace/auth/unencodable_credential_error.rbs index e255bdb..61cd2f6 100644 --- a/gems/dexpace-core/sig/dexpace/auth/unencodable_credential_error.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/unencodable_credential_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Auth # AUTH-21 (R10): the typed failure of either encoding branch, naming its target and the diff --git a/gems/dexpace-core/sig/dexpace/auth/validation.rbs b/gems/dexpace-core/sig/dexpace/auth/validation.rbs index 8407a9d..8ce38d5 100644 --- a/gems/dexpace-core/sig/dexpace/auth/validation.rbs +++ b/gems/dexpace-core/sig/dexpace/auth/validation.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Auth::Validation is a private_constant and not public API: this declaration exists # because the strict `core` Steep target checks every file under lib/ and needs the module and # its one method declared to type the credential constructors, exactly as hooks.rbs does for diff --git a/gems/dexpace-core/sig/dexpace/bounded_map.rbs b/gems/dexpace-core/sig/dexpace/bounded_map.rbs index 4e67bab..c80b017 100644 --- a/gems/dexpace-core/sig/dexpace/bounded_map.rbs +++ b/gems/dexpace-core/sig/dexpace/bounded_map.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::BoundedMap is a private_constant and not public API (design P4-3): this declaration # exists because the strict `core` Steep target checks every file under lib/ and needs the class # declared to type ContextStore's call sites, exactly as hooks.rbs does for Dexpace::Hooks. RBS diff --git a/gems/dexpace-core/sig/dexpace/bridge/async_over.rbs b/gems/dexpace-core/sig/dexpace/bridge/async_over.rbs index 87470d7..2aa2103 100644 --- a/gems/dexpace-core/sig/dexpace/bridge/async_over.rbs +++ b/gems/dexpace-core/sig/dexpace/bridge/async_over.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Bridge class AsyncOver diff --git a/gems/dexpace-core/sig/dexpace/bridge/sync_over.rbs b/gems/dexpace-core/sig/dexpace/bridge/sync_over.rbs index 372ccd3..d959134 100644 --- a/gems/dexpace-core/sig/dexpace/bridge/sync_over.rbs +++ b/gems/dexpace-core/sig/dexpace/bridge/sync_over.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Bridge class SyncOver diff --git a/gems/dexpace-core/sig/dexpace/build_info.rbs b/gems/dexpace-core/sig/dexpace/build_info.rbs index 22edd56..76a3a47 100644 --- a/gems/dexpace-core/sig/dexpace/build_info.rbs +++ b/gems/dexpace-core/sig/dexpace/build_info.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module BuildInfo UNKNOWN: String diff --git a/gems/dexpace-core/sig/dexpace/builder.rbs b/gems/dexpace-core/sig/dexpace/builder.rbs index 949307e..9bb1541 100644 --- a/gems/dexpace-core/sig/dexpace/builder.rbs +++ b/gems/dexpace-core/sig/dexpace/builder.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace interface _Builder[T] def build: () -> T diff --git a/gems/dexpace-core/sig/dexpace/cancellation.rbs b/gems/dexpace-core/sig/dexpace/cancellation.rbs index f34865f..0201912 100644 --- a/gems/dexpace-core/sig/dexpace/cancellation.rbs +++ b/gems/dexpace-core/sig/dexpace/cancellation.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Cancellation # A private_constant with no visibility in RBS; declared so Steep can type .none. diff --git a/gems/dexpace-core/sig/dexpace/cancellation/source.rbs b/gems/dexpace-core/sig/dexpace/cancellation/source.rbs index 4f20a9b..93a1a46 100644 --- a/gems/dexpace-core/sig/dexpace/cancellation/source.rbs +++ b/gems/dexpace-core/sig/dexpace/cancellation/source.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Cancellation class Source diff --git a/gems/dexpace-core/sig/dexpace/clock.rbs b/gems/dexpace-core/sig/dexpace/clock.rbs index d16e9ae..23a08de 100644 --- a/gems/dexpace-core/sig/dexpace/clock.rbs +++ b/gems/dexpace-core/sig/dexpace/clock.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # The time seam a fake satisfies and what Future#value(clock:) types against: exactly the three # operations CFG-15 declares. @@ -13,6 +15,7 @@ module Dexpace module Guard def self?.duration: (untyped value) -> Float def self?.token: (untyped cancellation) -> Dexpace::Cancellation? + def self?.nan?: (untyped value) -> bool end SYSTEM: Clock diff --git a/gems/dexpace-core/sig/dexpace/closeable.rbs b/gems/dexpace-core/sig/dexpace/closeable.rbs index 0a8229a..dd12cd4 100644 --- a/gems/dexpace-core/sig/dexpace/closeable.rbs +++ b/gems/dexpace-core/sig/dexpace/closeable.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Closeable def initialize_closeable: (?owned: bool) -> nil diff --git a/gems/dexpace-core/sig/dexpace/config.rbs b/gems/dexpace-core/sig/dexpace/config.rbs index 5109242..eb4d984 100644 --- a/gems/dexpace-core/sig/dexpace/config.rbs +++ b/gems/dexpace-core/sig/dexpace/config.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace self.@config_mutex: Thread::Mutex self.@configuration: Configuration diff --git a/gems/dexpace-core/sig/dexpace/configuration.rbs b/gems/dexpace-core/sig/dexpace/configuration.rbs index 2ed40d1..557c651 100644 --- a/gems/dexpace-core/sig/dexpace/configuration.rbs +++ b/gems/dexpace-core/sig/dexpace/configuration.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Configuration < Data include Model diff --git a/gems/dexpace-core/sig/dexpace/configuration/builder.rbs b/gems/dexpace-core/sig/dexpace/configuration/builder.rbs index 4dd230d..051e5b2 100644 --- a/gems/dexpace-core/sig/dexpace/configuration/builder.rbs +++ b/gems/dexpace-core/sig/dexpace/configuration/builder.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Configuration class Builder @@ -12,6 +14,16 @@ module Dexpace def env_source=: (_ConfigSource source) -> void def build: () -> Configuration + # Builder::Layered is a private_constant and not public API: declared because the strict + # `core` Steep target types #composed_source. + class Layered + attr_reader entries: Hash[String, String] + attr_reader base: _ConfigSource + + def initialize: (Hash[untyped, untyped] entries, _ConfigSource base) -> void + def call: (String key) -> String? + end + private @overrides: Hash[String, String] diff --git a/gems/dexpace-core/sig/dexpace/configuration/keys.rbs b/gems/dexpace-core/sig/dexpace/configuration/keys.rbs index 4f7c7e3..b38667e 100644 --- a/gems/dexpace-core/sig/dexpace/configuration/keys.rbs +++ b/gems/dexpace-core/sig/dexpace/configuration/keys.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Configuration module Keys diff --git a/gems/dexpace-core/sig/dexpace/configuration/parsers.rbs b/gems/dexpace-core/sig/dexpace/configuration/parsers.rbs index f63e83d..78650ba 100644 --- a/gems/dexpace-core/sig/dexpace/configuration/parsers.rbs +++ b/gems/dexpace-core/sig/dexpace/configuration/parsers.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::ConfigParsers is a private_constant and not public API: this declaration exists # because the strict `core` Steep target checks every file under lib/ and needs the module and # its three functions declared to type their call sites in configuration.rb. RBS has no diff --git a/gems/dexpace-core/sig/dexpace/configuration/sources.rbs b/gems/dexpace-core/sig/dexpace/configuration/sources.rbs index c18e325..c7959cd 100644 --- a/gems/dexpace-core/sig/dexpace/configuration/sources.rbs +++ b/gems/dexpace-core/sig/dexpace/configuration/sources.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # CFG-11's seam: a callable from key name to String?. What a fake satisfies and what every # Configuration member and builder writer types against. diff --git a/gems/dexpace-core/sig/dexpace/context.rbs b/gems/dexpace-core/sig/dexpace/context.rbs index 315910f..9b21ef9 100644 --- a/gems/dexpace-core/sig/dexpace/context.rbs +++ b/gems/dexpace-core/sig/dexpace/context.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # What an includer of Context must be beyond a Model: a Data whose members include the store # #close releases through and the key it is registered under. The store is `untyped` because diff --git a/gems/dexpace-core/sig/dexpace/context/call_key.rbs b/gems/dexpace-core/sig/dexpace/context/call_key.rbs index e2f66c2..6061cee 100644 --- a/gems/dexpace-core/sig/dexpace/context/call_key.rbs +++ b/gems/dexpace-core/sig/dexpace/context/call_key.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::CallKey is a private_constant and not public API (design P4-3): this declaration # exists because the strict `core` Steep target checks every file under lib/ and needs the module # and its one method declared to type the three .build call sites, exactly as hooks.rbs does for diff --git a/gems/dexpace-core/sig/dexpace/context/dispatch_context.rbs b/gems/dexpace-core/sig/dexpace/context/dispatch_context.rbs index dc62215..9454eb8 100644 --- a/gems/dexpace-core/sig/dexpace/context/dispatch_context.rbs +++ b/gems/dexpace-core/sig/dexpace/context/dispatch_context.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class DispatchContext < Data include Context diff --git a/gems/dexpace-core/sig/dexpace/context/exchange_context.rbs b/gems/dexpace-core/sig/dexpace/context/exchange_context.rbs index c905d81..9388118 100644 --- a/gems/dexpace-core/sig/dexpace/context/exchange_context.rbs +++ b/gems/dexpace-core/sig/dexpace/context/exchange_context.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class ExchangeContext < Data include Context diff --git a/gems/dexpace-core/sig/dexpace/context/request_context.rbs b/gems/dexpace-core/sig/dexpace/context/request_context.rbs index 73a175e..2f64844 100644 --- a/gems/dexpace-core/sig/dexpace/context/request_context.rbs +++ b/gems/dexpace-core/sig/dexpace/context/request_context.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class RequestContext < Data include Context diff --git a/gems/dexpace-core/sig/dexpace/context_store.rbs b/gems/dexpace-core/sig/dexpace/context_store.rbs index 71cfbf9..e538d3f 100644 --- a/gems/dexpace-core/sig/dexpace/context_store.rbs +++ b/gems/dexpace-core/sig/dexpace/context_store.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class ContextStore MAX_TRACKED_CONTEXTS: Integer diff --git a/gems/dexpace-core/sig/dexpace/deep_value.rbs b/gems/dexpace-core/sig/dexpace/deep_value.rbs index c999c96..e143d0d 100644 --- a/gems/dexpace-core/sig/dexpace/deep_value.rbs +++ b/gems/dexpace-core/sig/dexpace/deep_value.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::DeepValue is a private_constant and not public API (R4): this declaration exists # because the strict `core` Steep target checks every file under lib/, and a later # dexpace-conformance suite reaches it by bare name from inside `module Dexpace`. RBS has no diff --git a/gems/dexpace-core/sig/dexpace/each_cause.rbs b/gems/dexpace-core/sig/dexpace/each_cause.rbs index 1dace63..4034068 100644 --- a/gems/dexpace-core/sig/dexpace/each_cause.rbs +++ b/gems/dexpace-core/sig/dexpace/each_cause.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace def self.each_cause: (::Exception error) -> Enumerator[::Exception, nil] | (::Exception error) { (::Exception) -> void } -> nil diff --git a/gems/dexpace-core/sig/dexpace/error.rbs b/gems/dexpace-core/sig/dexpace/error.rbs index 97cf37f..8a9abc4 100644 --- a/gems/dexpace-core/sig/dexpace/error.rbs +++ b/gems/dexpace-core/sig/dexpace/error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # The same self-type as Suppressible's: a module including a self-typed module must satisfy # that self-type, and every including class is an Exception by phase 2's shape. diff --git a/gems/dexpace-core/sig/dexpace/error/auth_resolution_error.rbs b/gems/dexpace-core/sig/dexpace/error/auth_resolution_error.rbs index 470076f..fa6939f 100644 --- a/gems/dexpace-core/sig/dexpace/error/auth_resolution_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/auth_resolution_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # AUTH-6's resolution failure, flat under Dexpace in phase 2's error shape. class AuthResolutionError < ::StandardError diff --git a/gems/dexpace-core/sig/dexpace/error/cancelled_error.rbs b/gems/dexpace-core/sig/dexpace/error/cancelled_error.rbs index 7fd1484..e7f57b8 100644 --- a/gems/dexpace-core/sig/dexpace/error/cancelled_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/cancelled_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class CancelledError < ::StandardError include Dexpace::Error diff --git a/gems/dexpace-core/sig/dexpace/error/closed_error.rbs b/gems/dexpace-core/sig/dexpace/error/closed_error.rbs index 817502d..e4a4c95 100644 --- a/gems/dexpace-core/sig/dexpace/error/closed_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/closed_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class ClosedError < ::StandardError include Dexpace::Error diff --git a/gems/dexpace-core/sig/dexpace/error/context_conflict_error.rbs b/gems/dexpace-core/sig/dexpace/error/context_conflict_error.rbs index 0d61dc8..301dc24 100644 --- a/gems/dexpace-core/sig/dexpace/error/context_conflict_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/context_conflict_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class ContextConflictError < ::StandardError include Dexpace::Error diff --git a/gems/dexpace-core/sig/dexpace/error/end_of_stream_error.rbs b/gems/dexpace-core/sig/dexpace/error/end_of_stream_error.rbs index b0daf2c..32ac40d 100644 --- a/gems/dexpace-core/sig/dexpace/error/end_of_stream_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/end_of_stream_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class EndOfStreamError < ::EOFError include Dexpace::Error diff --git a/gems/dexpace-core/sig/dexpace/error/invalid_argument_error.rbs b/gems/dexpace-core/sig/dexpace/error/invalid_argument_error.rbs index c1e062b..b6389f5 100644 --- a/gems/dexpace-core/sig/dexpace/error/invalid_argument_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/invalid_argument_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class InvalidArgumentError < ::ArgumentError include Dexpace::Error diff --git a/gems/dexpace-core/sig/dexpace/error/not_replayable_error.rbs b/gems/dexpace-core/sig/dexpace/error/not_replayable_error.rbs index 6d56027..98afbfa 100644 --- a/gems/dexpace-core/sig/dexpace/error/not_replayable_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/not_replayable_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # REDIR-6: the clear error for a present, non-replayable body at a re-send site. class NotReplayableError < ::StandardError diff --git a/gems/dexpace-core/sig/dexpace/error/outcome_error.rbs b/gems/dexpace-core/sig/dexpace/error/outcome_error.rbs index 4b3ad52..0a67207 100644 --- a/gems/dexpace-core/sig/dexpace/error/outcome_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/outcome_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class OutcomeError < ::StandardError include Dexpace::Error diff --git a/gems/dexpace-core/sig/dexpace/error/pipeline_error.rbs b/gems/dexpace-core/sig/dexpace/error/pipeline_error.rbs index cb60560..5173cc2 100644 --- a/gems/dexpace-core/sig/dexpace/error/pipeline_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/pipeline_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class PipelineError < ::StandardError include Dexpace::Error diff --git a/gems/dexpace-core/sig/dexpace/error/protocol_error.rbs b/gems/dexpace-core/sig/dexpace/error/protocol_error.rbs index 33b512f..e64a1d0 100644 --- a/gems/dexpace-core/sig/dexpace/error/protocol_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/protocol_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class ProtocolError < ::StandardError include Dexpace::Error diff --git a/gems/dexpace-core/sig/dexpace/error/retry_predicate_error.rbs b/gems/dexpace-core/sig/dexpace/error/retry_predicate_error.rbs index 1e33088..daceec0 100644 --- a/gems/dexpace-core/sig/dexpace/error/retry_predicate_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/retry_predicate_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class RetryPredicateError < ::StandardError include Dexpace::Error diff --git a/gems/dexpace-core/sig/dexpace/error/seam_error.rbs b/gems/dexpace-core/sig/dexpace/error/seam_error.rbs index fe6c0ab..3a8b2a5 100644 --- a/gems/dexpace-core/sig/dexpace/error/seam_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/seam_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class SeamError < ::StandardError include Dexpace::Error diff --git a/gems/dexpace-core/sig/dexpace/error/stream_error.rbs b/gems/dexpace-core/sig/dexpace/error/stream_error.rbs index 1d726ea..127829c 100644 --- a/gems/dexpace-core/sig/dexpace/error/stream_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/stream_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class StreamError < ::IOError include Dexpace::Error diff --git a/gems/dexpace-core/sig/dexpace/error/transport_error.rbs b/gems/dexpace-core/sig/dexpace/error/transport_error.rbs index f7036fa..fe85864 100644 --- a/gems/dexpace-core/sig/dexpace/error/transport_error.rbs +++ b/gems/dexpace-core/sig/dexpace/error/transport_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # TRANSPORT-20 and XCUT-4 branch (b): the always-retryable transport failure, a sibling of # StreamError inside ::IOError (P3-3). Phase 8's phase-level task, landed by 8a. diff --git a/gems/dexpace-core/sig/dexpace/hooks.rbs b/gems/dexpace-core/sig/dexpace/hooks.rbs index 7c930c7..40ead7c 100644 --- a/gems/dexpace-core/sig/dexpace/hooks.rbs +++ b/gems/dexpace-core/sig/dexpace/hooks.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Hooks is a private_constant and not public API (design P2-15): this declaration exists # because the strict `core` Steep target checks every file under lib/ and needs the module and # its one method declared to type the three call sites. RBS has no visibility for a constant, so diff --git a/gems/dexpace-core/sig/dexpace/http/body.rbs b/gems/dexpace-core/sig/dexpace/http/body.rbs index 7073456..4133403 100644 --- a/gems/dexpace-core/sig/dexpace/http/body.rbs +++ b/gems/dexpace-core/sig/dexpace/http/body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # HTTP-44/HTTP-45's handler contract (R7): any object responding to #call(response). It names # Dexpace::Response and nothing of phase 7's, so NFR-11's scan stays clean and phase 7 supplies diff --git a/gems/dexpace-core/sig/dexpace/http/body/buffer_body.rbs b/gems/dexpace-core/sig/dexpace/http/body/buffer_body.rbs index c91fa3e..d9b4c1d 100644 --- a/gems/dexpace-core/sig/dexpace/http/body/buffer_body.rbs +++ b/gems/dexpace-core/sig/dexpace/http/body/buffer_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class BufferBody include Dexpace::Body diff --git a/gems/dexpace-core/sig/dexpace/http/body/bytes_body.rbs b/gems/dexpace-core/sig/dexpace/http/body/bytes_body.rbs index 3e1dfca..37a91f7 100644 --- a/gems/dexpace-core/sig/dexpace/http/body/bytes_body.rbs +++ b/gems/dexpace-core/sig/dexpace/http/body/bytes_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class BytesBody include Dexpace::Body diff --git a/gems/dexpace-core/sig/dexpace/http/body/chunked_body.rbs b/gems/dexpace-core/sig/dexpace/http/body/chunked_body.rbs index a4bdb1c..6ca6b9c 100644 --- a/gems/dexpace-core/sig/dexpace/http/body/chunked_body.rbs +++ b/gems/dexpace-core/sig/dexpace/http/body/chunked_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class ChunkedBody include Dexpace::Body diff --git a/gems/dexpace-core/sig/dexpace/http/body/file_body.rbs b/gems/dexpace-core/sig/dexpace/http/body/file_body.rbs index 06135f8..79546af 100644 --- a/gems/dexpace-core/sig/dexpace/http/body/file_body.rbs +++ b/gems/dexpace-core/sig/dexpace/http/body/file_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class FileBody include Dexpace::Body diff --git a/gems/dexpace-core/sig/dexpace/http/body/form_body.rbs b/gems/dexpace-core/sig/dexpace/http/body/form_body.rbs index a726a32..9d86b5e 100644 --- a/gems/dexpace-core/sig/dexpace/http/body/form_body.rbs +++ b/gems/dexpace-core/sig/dexpace/http/body/form_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class FormBody include Dexpace::Body diff --git a/gems/dexpace-core/sig/dexpace/http/body/multipart_body.rbs b/gems/dexpace-core/sig/dexpace/http/body/multipart_body.rbs index 14cbd37..297c249 100644 --- a/gems/dexpace-core/sig/dexpace/http/body/multipart_body.rbs +++ b/gems/dexpace-core/sig/dexpace/http/body/multipart_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class MultipartBody include Dexpace::Body diff --git a/gems/dexpace-core/sig/dexpace/http/body/request_logging_body.rbs b/gems/dexpace-core/sig/dexpace/http/body/request_logging_body.rbs index ab0839c..0629cc1 100644 --- a/gems/dexpace-core/sig/dexpace/http/body/request_logging_body.rbs +++ b/gems/dexpace-core/sig/dexpace/http/body/request_logging_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class RequestLoggingBody include Dexpace::Body diff --git a/gems/dexpace-core/sig/dexpace/http/body/response_body.rbs b/gems/dexpace-core/sig/dexpace/http/body/response_body.rbs index f3e361f..2d0eb76 100644 --- a/gems/dexpace-core/sig/dexpace/http/body/response_body.rbs +++ b/gems/dexpace-core/sig/dexpace/http/body/response_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class ResponseBody include Dexpace::Body diff --git a/gems/dexpace-core/sig/dexpace/http/body/response_logging_body.rbs b/gems/dexpace-core/sig/dexpace/http/body/response_logging_body.rbs index 09679ee..86693d0 100644 --- a/gems/dexpace-core/sig/dexpace/http/body/response_logging_body.rbs +++ b/gems/dexpace-core/sig/dexpace/http/body/response_logging_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class ResponseLoggingBody include Dexpace::Body diff --git a/gems/dexpace-core/sig/dexpace/http/body/stream_body.rbs b/gems/dexpace-core/sig/dexpace/http/body/stream_body.rbs index ef29cd0..acf3959 100644 --- a/gems/dexpace-core/sig/dexpace/http/body/stream_body.rbs +++ b/gems/dexpace-core/sig/dexpace/http/body/stream_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class StreamBody include Dexpace::Body @@ -21,6 +23,7 @@ module Dexpace @close: bool @rewindable: bool + def refuse_closed!: (untyped io) -> void def probe_rewindability: (untyped io) -> [Integer, bool] def write_once: (Dexpace::IO::_Sink sink) -> Integer def write_replayable: (Dexpace::IO::_Sink sink) -> Integer diff --git a/gems/dexpace-core/sig/dexpace/http/header_name.rbs b/gems/dexpace-core/sig/dexpace/http/header_name.rbs index 5a0a729..d2acd68 100644 --- a/gems/dexpace-core/sig/dexpace/http/header_name.rbs +++ b/gems/dexpace-core/sig/dexpace/http/header_name.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class HeaderName < Data include Model diff --git a/gems/dexpace-core/sig/dexpace/http/header_syntax.rbs b/gems/dexpace-core/sig/dexpace/http/header_syntax.rbs index 670e0c8..6247891 100644 --- a/gems/dexpace-core/sig/dexpace/http/header_syntax.rbs +++ b/gems/dexpace-core/sig/dexpace/http/header_syntax.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module HeaderSyntax HTAB: Integer diff --git a/gems/dexpace-core/sig/dexpace/http/headers.rbs b/gems/dexpace-core/sig/dexpace/http/headers.rbs index 9296fab..5d3e7f3 100644 --- a/gems/dexpace-core/sig/dexpace/http/headers.rbs +++ b/gems/dexpace-core/sig/dexpace/http/headers.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Headers < Data include Model diff --git a/gems/dexpace-core/sig/dexpace/http/headers/builder.rbs b/gems/dexpace-core/sig/dexpace/http/headers/builder.rbs index 40b2b50..3701396 100644 --- a/gems/dexpace-core/sig/dexpace/http/headers/builder.rbs +++ b/gems/dexpace-core/sig/dexpace/http/headers/builder.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Headers class Builder diff --git a/gems/dexpace-core/sig/dexpace/http/media_type.rbs b/gems/dexpace-core/sig/dexpace/http/media_type.rbs index 5ade8fb..91020fc 100644 --- a/gems/dexpace-core/sig/dexpace/http/media_type.rbs +++ b/gems/dexpace-core/sig/dexpace/http/media_type.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class MediaType < Data include Model diff --git a/gems/dexpace-core/sig/dexpace/http/method.rbs b/gems/dexpace-core/sig/dexpace/http/method.rbs index 533f859..ef5a00b 100644 --- a/gems/dexpace-core/sig/dexpace/http/method.rbs +++ b/gems/dexpace-core/sig/dexpace/http/method.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Method < Data include Model diff --git a/gems/dexpace-core/sig/dexpace/http/percent_encoding.rbs b/gems/dexpace-core/sig/dexpace/http/percent_encoding.rbs index 32ff317..17b6053 100644 --- a/gems/dexpace-core/sig/dexpace/http/percent_encoding.rbs +++ b/gems/dexpace-core/sig/dexpace/http/percent_encoding.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module PercentEncoding UNRESERVED: Array[Integer] diff --git a/gems/dexpace-core/sig/dexpace/http/protocol.rbs b/gems/dexpace-core/sig/dexpace/http/protocol.rbs index e4f6bda..881cbd3 100644 --- a/gems/dexpace-core/sig/dexpace/http/protocol.rbs +++ b/gems/dexpace-core/sig/dexpace/http/protocol.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Protocol < Data include Model @@ -5,6 +7,7 @@ module Dexpace WIRE_FORMS: Array[String] ALIASES: Hash[String, String] + HTTP_1_0: Protocol HTTP_1_1: Protocol HTTP_2: Protocol diff --git a/gems/dexpace-core/sig/dexpace/http/query.rbs b/gems/dexpace-core/sig/dexpace/http/query.rbs index fe373f3..75fec8f 100644 --- a/gems/dexpace-core/sig/dexpace/http/query.rbs +++ b/gems/dexpace-core/sig/dexpace/http/query.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Query < Data include Model diff --git a/gems/dexpace-core/sig/dexpace/http/query/builder.rbs b/gems/dexpace-core/sig/dexpace/http/query/builder.rbs index cd49dfb..af67fde 100644 --- a/gems/dexpace-core/sig/dexpace/http/query/builder.rbs +++ b/gems/dexpace-core/sig/dexpace/http/query/builder.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Query class Builder diff --git a/gems/dexpace-core/sig/dexpace/http/request.rbs b/gems/dexpace-core/sig/dexpace/http/request.rbs index 04dada4..7c15898 100644 --- a/gems/dexpace-core/sig/dexpace/http/request.rbs +++ b/gems/dexpace-core/sig/dexpace/http/request.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Request < Data include Model diff --git a/gems/dexpace-core/sig/dexpace/http/request/builder.rbs b/gems/dexpace-core/sig/dexpace/http/request/builder.rbs index 3fbffa8..9654573 100644 --- a/gems/dexpace-core/sig/dexpace/http/request/builder.rbs +++ b/gems/dexpace-core/sig/dexpace/http/request/builder.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Request class Builder diff --git a/gems/dexpace-core/sig/dexpace/http/request_options.rbs b/gems/dexpace-core/sig/dexpace/http/request_options.rbs index d979f2c..8c9b4e6 100644 --- a/gems/dexpace-core/sig/dexpace/http/request_options.rbs +++ b/gems/dexpace-core/sig/dexpace/http/request_options.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class RequestOptions < Data include Model diff --git a/gems/dexpace-core/sig/dexpace/http/request_options/builder.rbs b/gems/dexpace-core/sig/dexpace/http/request_options/builder.rbs index c098d8f..442212f 100644 --- a/gems/dexpace-core/sig/dexpace/http/request_options/builder.rbs +++ b/gems/dexpace-core/sig/dexpace/http/request_options/builder.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class RequestOptions class Builder diff --git a/gems/dexpace-core/sig/dexpace/http/response.rbs b/gems/dexpace-core/sig/dexpace/http/response.rbs index ccd18e8..fb9d6de 100644 --- a/gems/dexpace-core/sig/dexpace/http/response.rbs +++ b/gems/dexpace-core/sig/dexpace/http/response.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Response < Data include Model diff --git a/gems/dexpace-core/sig/dexpace/http/response/builder.rbs b/gems/dexpace-core/sig/dexpace/http/response/builder.rbs index 7c329d4..17f60b7 100644 --- a/gems/dexpace-core/sig/dexpace/http/response/builder.rbs +++ b/gems/dexpace-core/sig/dexpace/http/response/builder.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Response class Builder diff --git a/gems/dexpace-core/sig/dexpace/http/status.rbs b/gems/dexpace-core/sig/dexpace/http/status.rbs index f1a2ae5..dbe8919 100644 --- a/gems/dexpace-core/sig/dexpace/http/status.rbs +++ b/gems/dexpace-core/sig/dexpace/http/status.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Status < Data include Model @@ -36,5 +38,6 @@ module Dexpace def client_error?: () -> bool def server_error?: () -> bool def error?: () -> bool + def standard?: () -> bool end end diff --git a/gems/dexpace-core/sig/dexpace/http/typed_response.rbs b/gems/dexpace-core/sig/dexpace/http/typed_response.rbs index b48a7a2..55f6ca7 100644 --- a/gems/dexpace-core/sig/dexpace/http/typed_response.rbs +++ b/gems/dexpace-core/sig/dexpace/http/typed_response.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class TypedResponse attr_reader response: Dexpace::Response diff --git a/gems/dexpace-core/sig/dexpace/http/url.rbs b/gems/dexpace-core/sig/dexpace/http/url.rbs index 11d80e3..1edd4e2 100644 --- a/gems/dexpace-core/sig/dexpace/http/url.rbs +++ b/gems/dexpace-core/sig/dexpace/http/url.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module URL def self?.parse!: (String | URI::Generic input) -> URI::Generic @@ -7,5 +9,8 @@ module Dexpace private def own: (URI::Generic uri) -> URI::Generic + def string_or_uri!: (untyped input) -> void + def refuse_hostless!: (URI::Generic uri, String | URI::Generic input) -> void + def renderable!: (URI::Generic uri, String | URI::Generic input) -> URI::Generic end end diff --git a/gems/dexpace-core/sig/dexpace/http_date.rbs b/gems/dexpace-core/sig/dexpace/http_date.rbs index 9712ae7..2bfeda9 100644 --- a/gems/dexpace-core/sig/dexpace/http_date.rbs +++ b/gems/dexpace-core/sig/dexpace/http_date.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module HTTPDate # Both private_constant; declared because the strict `core` Steep target types their uses. diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/async_step.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/async_step.rbs index 499de25..911a98d 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/async_step.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/async_step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # The async runtime's step over 4c's _AsyncStep: the same .build, the same Emitter (P5-34). @@ -24,7 +26,7 @@ module Dexpace def head: (Dexpace::Request request, Pipeline::Cursor cursor) -> [Dexpace::Async::Future, Pending] def attach: (Dexpace::Async::Future future, Pending pending) -> Dexpace::Async::Future def settle_response: (Pending pending, Dexpace::Response response) -> Dexpace::Response - def settle: (Pending pending, Dexpace::Async::Settlement settlement) -> void + def settle: (Pending pending, Dexpace::Async::Settlement settlement, ?Float? ended) -> void def bridged: [T] (Hash[Symbol, untyped]? snapshot) { () -> T } -> T end end diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/bundle.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/bundle.rbs index 9df22aa..6374057 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/bundle.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/bundle.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation class Bundle < Data diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/callable_adapter.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/callable_adapter.rbs index 9206eb1..0631bcc 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/callable_adapter.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/callable_adapter.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # Design §8.1's bus adapter over one `#call(name, payload)` object; the eleven overrides diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/contain.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/contain.rbs index 5fa6150..9646876 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/contain.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/contain.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-20, XCUT-20: the one containment primitive, a module function (P5-37). Returns nil diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/diagnostics.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/diagnostics.rbs index 9e025f3..14ef544 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/diagnostics.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/diagnostics.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # OBS-24's "immutable snapshot": what Diagnostics.with installs -- a frozen Hash from # Diagnostics.capture, or anything else that yields Symbol-keyed pairs and names its keys. diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/emitter.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/emitter.rbs index e7cbda8..1139583 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/emitter.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/emitter.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Instrumentation::Emitter is a private_constant and not public API (P2-15, P4-3): this # declaration exists because the strict `core` Steep target checks every file under lib/ and # needs the class and its three writers declared to type Step and AsyncStep. RBS has no diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/event.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/event.rbs index 5cc27ff..5953458 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/event.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/event.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # A private_constant with no visibility in RBS; declared because the strict `core` Steep diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/http_logging.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/http_logging.rbs index 52c4eb1..6de0658 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/http_logging.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/http_logging.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-34, OBS-35: a closed set of three. .new and .[] are both private and #with always diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/http_tracer.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/http_tracer.rbs index 7d41b43..bb78051 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/http_tracer.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/http_tracer.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-28's eleven-method vocabulary: the module an implementer includes, and -- since phase 6a diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/keys.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/keys.rbs index e175f59..5285dc1 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/keys.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/keys.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-39: the emitted field-key vocabulary, sixteen frozen Strings. diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/logger.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/logger.rbs index bdbedc1..63c6bdc 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/logger.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/logger.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-1, OBS-2, OBS-9, OBS-10, OBS-40: the facade. Frozen after .build; NULL is the one over diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/meter.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/meter.rbs index 105b48b..d6402a0 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/meter.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/meter.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-31's monotonic counter. OBS-33: only non-negative increments are valid; a negative diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/no_span.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/no_span.rbs index 8abe01c..f21dbab 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/no_span.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/no_span.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-21's span protocol, as phase 5c fixed it (phase 4a declared this empty on purpose and diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/no_tracer.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/no_tracer.rbs index 651347c..c571a80 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/no_tracer.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/no_tracer.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-25's tracer protocol, as phase 5c fixed it (phase 4a declared this empty on purpose and diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/null_sink.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/null_sink.rbs index b9dc571..58049c4 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/null_sink.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/null_sink.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # OBS-1, OBS-2, design §8.1: the sink duck type -- the stdlib Logger surface as a structural # subset, so a consumer's Logger drops in and core never requires `logger`. A signature typed diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/preview.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/preview.rbs index c334f22..fe5dbe2 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/preview.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/preview.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-38: charset-aware text and binary-safe preview rendering, total. diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/redaction_policy.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/redaction_policy.rbs index 30026a8..67f7e6e 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/redaction_policy.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/redaction_policy.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-12, OBS-17, OBS-18: three folded Sets and one boolean, no member for userinfo (OBS-11 is diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/redactor.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/redactor.rbs index 6da361f..784613b 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/redactor.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/redactor.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-11..OBS-18, XCUT-19, XCUT-20: two total entry points over one frozen policy (P5-25). diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/render.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/render.rbs index aadbacf..dea158f 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/render.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/render.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Instrumentation::Render is a private_constant and not public API (P2-15, P4-3): this # declaration exists because the strict `core` Steep target checks every file under lib/ and # needs the module and its one entry point declared to type Event#emit. RBS has no visibility diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/scope.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/scope.rbs index 61ae3aa..b28f653 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/scope.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/scope.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # The current-span slot key: a private_constant shared by Scope and Tracing, declared here diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/severity.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/severity.rbs index 3929d8e..613b092 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/severity.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/severity.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-2: a closed set of four. .new and .[] are both private, and #with always raises, so the diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/step.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/step.rbs index 35bbda3..3329af8 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/step.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-34, OBS-36, OBS-39, OBS-20: the instrumentation step at Stages::LOGGING. The two slots @@ -34,8 +36,8 @@ module Dexpace # untyped: a _Span is what the tracer returns and `_Span & Object` is what Tracing takes # (5c's identity device); the step passes one straight to the other. def open_span: (Dexpace::Request request, Bundle bundle) -> untyped - def finish: (untyped span, Float started) -> void - def elapsed_ms: (Float started) -> Float + def finish: (untyped span, Float started, ?Float? ended) -> void + def elapsed_ms: (Float started, ?Float? ended) -> Float def logged?: () -> bool def body?: () -> bool def prepare: (Dexpace::Request request) -> Dexpace::Request diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/trace_id_flavour.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/trace_id_flavour.rbs index 1b073b9..fb23485 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/trace_id_flavour.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/trace_id_flavour.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation class TraceIdFlavour < Data diff --git a/gems/dexpace-core/sig/dexpace/instrumentation/tracing.rbs b/gems/dexpace-core/sig/dexpace/instrumentation/tracing.rbs index 74277d9..3b2725e 100644 --- a/gems/dexpace-core/sig/dexpace/instrumentation/tracing.rbs +++ b/gems/dexpace-core/sig/dexpace/instrumentation/tracing.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Instrumentation # OBS-22, OBS-23: module functions over one Fiber[] slot. The block forms are primary and diff --git a/gems/dexpace-core/sig/dexpace/io.rbs b/gems/dexpace-core/sig/dexpace/io.rbs index 9a43fb7..f16babe 100644 --- a/gems/dexpace-core/sig/dexpace/io.rbs +++ b/gems/dexpace-core/sig/dexpace/io.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module IO MAX_MATERIALIZED_BYTES: Integer diff --git a/gems/dexpace-core/sig/dexpace/io/buffer.rbs b/gems/dexpace-core/sig/dexpace/io/buffer.rbs index f048f07..2905940 100644 --- a/gems/dexpace-core/sig/dexpace/io/buffer.rbs +++ b/gems/dexpace-core/sig/dexpace/io/buffer.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module IO class Buffer diff --git a/gems/dexpace-core/sig/dexpace/io/buffered_sink.rbs b/gems/dexpace-core/sig/dexpace/io/buffered_sink.rbs index a480a75..e13673b 100644 --- a/gems/dexpace-core/sig/dexpace/io/buffered_sink.rbs +++ b/gems/dexpace-core/sig/dexpace/io/buffered_sink.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module IO class BufferedSink diff --git a/gems/dexpace-core/sig/dexpace/io/buffered_source.rbs b/gems/dexpace-core/sig/dexpace/io/buffered_source.rbs index 2032dfa..b7233d8 100644 --- a/gems/dexpace-core/sig/dexpace/io/buffered_source.rbs +++ b/gems/dexpace-core/sig/dexpace/io/buffered_source.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module IO class BufferedSource diff --git a/gems/dexpace-core/sig/dexpace/io/tee_sink.rbs b/gems/dexpace-core/sig/dexpace/io/tee_sink.rbs index 2461333..d4dcecd 100644 --- a/gems/dexpace-core/sig/dexpace/io/tee_sink.rbs +++ b/gems/dexpace-core/sig/dexpace/io/tee_sink.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module IO class TeeSink diff --git a/gems/dexpace-core/sig/dexpace/io/typed_reads.rbs b/gems/dexpace-core/sig/dexpace/io/typed_reads.rbs index b20e13a..e836712 100644 --- a/gems/dexpace-core/sig/dexpace/io/typed_reads.rbs +++ b/gems/dexpace-core/sig/dexpace/io/typed_reads.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module IO module TypedReads diff --git a/gems/dexpace-core/sig/dexpace/io/typed_writes.rbs b/gems/dexpace-core/sig/dexpace/io/typed_writes.rbs index e868335..97e9ff3 100644 --- a/gems/dexpace-core/sig/dexpace/io/typed_writes.rbs +++ b/gems/dexpace-core/sig/dexpace/io/typed_writes.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module IO module TypedWrites diff --git a/gems/dexpace-core/sig/dexpace/model.rbs b/gems/dexpace-core/sig/dexpace/model.rbs index c1ff827..4e56459 100644 --- a/gems/dexpace-core/sig/dexpace/model.rbs +++ b/gems/dexpace-core/sig/dexpace/model.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # What an object that includes Model must be: a Data, so `to_h` yields its members, whose class # carries a validating `.build` taking those members as keywords. That factory's keyword list diff --git a/gems/dexpace-core/sig/dexpace/operation.rbs b/gems/dexpace-core/sig/dexpace/operation.rbs index 2b73c84..62dd39f 100644 --- a/gems/dexpace-core/sig/dexpace/operation.rbs +++ b/gems/dexpace-core/sig/dexpace/operation.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Operation < Data include Dexpace::Model diff --git a/gems/dexpace-core/sig/dexpace/outcome.rbs b/gems/dexpace-core/sig/dexpace/outcome.rbs index 282ecdf..9c1efea 100644 --- a/gems/dexpace-core/sig/dexpace/outcome.rbs +++ b/gems/dexpace-core/sig/dexpace/outcome.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # The closed sum type both variants include; `t` is the union every fold site is checked # against. diff --git a/gems/dexpace-core/sig/dexpace/outcome/failure.rbs b/gems/dexpace-core/sig/dexpace/outcome/failure.rbs index 78261f0..79614f0 100644 --- a/gems/dexpace-core/sig/dexpace/outcome/failure.rbs +++ b/gems/dexpace-core/sig/dexpace/outcome/failure.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Outcome class Failure < Data diff --git a/gems/dexpace-core/sig/dexpace/outcome/success.rbs b/gems/dexpace-core/sig/dexpace/outcome/success.rbs index cf8384b..cddc1c5 100644 --- a/gems/dexpace-core/sig/dexpace/outcome/success.rbs +++ b/gems/dexpace-core/sig/dexpace/outcome/success.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Outcome class Success < Data diff --git a/gems/dexpace-core/sig/dexpace/page.rbs b/gems/dexpace-core/sig/dexpace/page.rbs index 0508a2d..9126e7e 100644 --- a/gems/dexpace-core/sig/dexpace/page.rbs +++ b/gems/dexpace-core/sig/dexpace/page.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # PAGE-2 / PAGE-3: one page of a paginated walk, owning exactly one live Response behind # Dexpace::Closeable's latch; also the namespace of the pagination subsystem (P7-2). The three diff --git a/gems/dexpace-core/sig/dexpace/page/async_paginator.rbs b/gems/dexpace-core/sig/dexpace/page/async_paginator.rbs index 640e8d9..178d232 100644 --- a/gems/dexpace-core/sig/dexpace/page/async_paginator.rbs +++ b/gems/dexpace-core/sig/dexpace/page/async_paginator.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Page # PAGE-25..PAGE-33: the non-blocking engine, a frozen Data of immutable configuration plus an diff --git a/gems/dexpace-core/sig/dexpace/page/closing.rbs b/gems/dexpace-core/sig/dexpace/page/closing.rbs index 2d6083e..036b1a7 100644 --- a/gems/dexpace-core/sig/dexpace/page/closing.rbs +++ b/gems/dexpace-core/sig/dexpace/page/closing.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Page::Closing is a private_constant and not public API: this declaration exists # because the strict `core` Steep target checks every file under lib/ and needs the module and # its two functions declared to type the views', the engines' and the async pump's call sites. diff --git a/gems/dexpace-core/sig/dexpace/page/cursor_strategy.rbs b/gems/dexpace-core/sig/dexpace/page/cursor_strategy.rbs index 1acbc02..56ef093 100644 --- a/gems/dexpace-core/sig/dexpace/page/cursor_strategy.rbs +++ b/gems/dexpace-core/sig/dexpace/page/cursor_strategy.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Page # PAGE-16: the cursor strategy over a caller-supplied `#call(response) -> [items, cursor]` diff --git a/gems/dexpace-core/sig/dexpace/page/fetchers.rbs b/gems/dexpace-core/sig/dexpace/page/fetchers.rbs index 48387e9..13f1d4e 100644 --- a/gems/dexpace-core/sig/dexpace/page/fetchers.rbs +++ b/gems/dexpace-core/sig/dexpace/page/fetchers.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Page # PAGE-34 / PAGE-35: the fetcher-based front-end, a frozen Data over two caller callables and diff --git a/gems/dexpace-core/sig/dexpace/page/info.rbs b/gems/dexpace-core/sig/dexpace/page/info.rbs index a4f19ca..3daa59e 100644 --- a/gems/dexpace-core/sig/dexpace/page/info.rbs +++ b/gems/dexpace-core/sig/dexpace/page/info.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Page # PAGE-4's PageInfo: items plus the next request, whose nil is the one end-of-stream signal. diff --git a/gems/dexpace-core/sig/dexpace/page/items.rbs b/gems/dexpace-core/sig/dexpace/page/items.rbs index de68a92..c5aebb5 100644 --- a/gems/dexpace-core/sig/dexpace/page/items.rbs +++ b/gems/dexpace-core/sig/dexpace/page/items.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Page # PAGE-1 / PAGE-8 / PAGE-11: the re-iterable item-level view, one fresh walk per #each, every diff --git a/gems/dexpace-core/sig/dexpace/page/link_header.rbs b/gems/dexpace-core/sig/dexpace/page/link_header.rbs index c1c1397..d11e1da 100644 --- a/gems/dexpace-core/sig/dexpace/page/link_header.rbs +++ b/gems/dexpace-core/sig/dexpace/page/link_header.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Page::LinkHeader is a private_constant and not public API: this declaration exists # because the strict `core` Steep target checks every file under lib/ and needs the module and # its one function declared to type LinkStrategy's call site. RBS has no visibility for a diff --git a/gems/dexpace-core/sig/dexpace/page/link_strategy.rbs b/gems/dexpace-core/sig/dexpace/page/link_strategy.rbs index 28df566..536eab8 100644 --- a/gems/dexpace-core/sig/dexpace/page/link_strategy.rbs +++ b/gems/dexpace-core/sig/dexpace/page/link_strategy.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Page # PAGE-18..PAGE-20: the Link-header strategy over a caller-supplied `#call(response) -> items` diff --git a/gems/dexpace-core/sig/dexpace/page/page_number_strategy.rbs b/gems/dexpace-core/sig/dexpace/page/page_number_strategy.rbs index 8cd76dc..88e062a 100644 --- a/gems/dexpace-core/sig/dexpace/page/page_number_strategy.rbs +++ b/gems/dexpace-core/sig/dexpace/page/page_number_strategy.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Page # PAGE-17: the page-number strategy over a caller-supplied `#call(response) -> items` extractor. diff --git a/gems/dexpace-core/sig/dexpace/page/page_state_error.rbs b/gems/dexpace-core/sig/dexpace/page/page_state_error.rbs index ace0b8e..3f3c796 100644 --- a/gems/dexpace-core/sig/dexpace/page/page_state_error.rbs +++ b/gems/dexpace-core/sig/dexpace/page/page_state_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Page # PAGE-14: a second #each on a single-use page view; a state error, never an ArgumentError. diff --git a/gems/dexpace-core/sig/dexpace/page/pages.rbs b/gems/dexpace-core/sig/dexpace/page/pages.rbs index ffb97aa..d0e73f8 100644 --- a/gems/dexpace-core/sig/dexpace/page/pages.rbs +++ b/gems/dexpace-core/sig/dexpace/page/pages.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Page # PAGE-1 / PAGE-12 / PAGE-14 / PAGE-15: the single-use, auto-closing page-level view over one diff --git a/gems/dexpace-core/sig/dexpace/page/paginator.rbs b/gems/dexpace-core/sig/dexpace/page/paginator.rbs index bdb9776..6785fd5 100644 --- a/gems/dexpace-core/sig/dexpace/page/paginator.rbs +++ b/gems/dexpace-core/sig/dexpace/page/paginator.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Page # PAGE-6..PAGE-10, PAGE-36: the blocking engine, a frozen Data of immutable configuration. diff --git a/gems/dexpace-core/sig/dexpace/page/query_rewriter.rbs b/gems/dexpace-core/sig/dexpace/page/query_rewriter.rbs index 279fe27..2db3f1e 100644 --- a/gems/dexpace-core/sig/dexpace/page/query_rewriter.rbs +++ b/gems/dexpace-core/sig/dexpace/page/query_rewriter.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Page # PAGE-21..PAGE-24: the verbatim query splice over the raw query substring. diff --git a/gems/dexpace-core/sig/dexpace/page/walk.rbs b/gems/dexpace-core/sig/dexpace/page/walk.rbs index 485bdb1..4deac04 100644 --- a/gems/dexpace-core/sig/dexpace/page/walk.rbs +++ b/gems/dexpace-core/sig/dexpace/page/walk.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Page::Walk is a private_constant and not public API: this declaration exists because # the strict `core` Steep target checks every file under lib/ and needs the class, its ivars and # its readers declared to type the two views and the two engines. RBS has no visibility for a diff --git a/gems/dexpace-core/sig/dexpace/pipeline.rbs b/gems/dexpace-core/sig/dexpace/pipeline.rbs index 6abb2a1..f6f78da 100644 --- a/gems/dexpace-core/sig/dexpace/pipeline.rbs +++ b/gems/dexpace-core/sig/dexpace/pipeline.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Pipeline include Dexpace::Closeable diff --git a/gems/dexpace-core/sig/dexpace/pipeline/async_driver.rbs b/gems/dexpace-core/sig/dexpace/pipeline/async_driver.rbs index 0c224f6..d54ec83 100644 --- a/gems/dexpace-core/sig/dexpace/pipeline/async_driver.rbs +++ b/gems/dexpace-core/sig/dexpace/pipeline/async_driver.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Pipeline::AsyncDriver is a private_constant and not public API (design P2-15): this # declaration exists because the strict `core` Steep target checks every file under lib/ and # needs the class and its methods declared to type its call sites in Cursor, Builder and diff --git a/gems/dexpace-core/sig/dexpace/pipeline/builder.rbs b/gems/dexpace-core/sig/dexpace/pipeline/builder.rbs index 2f7f91c..b6797fd 100644 --- a/gems/dexpace-core/sig/dexpace/pipeline/builder.rbs +++ b/gems/dexpace-core/sig/dexpace/pipeline/builder.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Pipeline class Builder diff --git a/gems/dexpace-core/sig/dexpace/pipeline/cursor.rbs b/gems/dexpace-core/sig/dexpace/pipeline/cursor.rbs index ece37dd..16548d5 100644 --- a/gems/dexpace-core/sig/dexpace/pipeline/cursor.rbs +++ b/gems/dexpace-core/sig/dexpace/pipeline/cursor.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Pipeline class Cursor diff --git a/gems/dexpace-core/sig/dexpace/pipeline/entry.rbs b/gems/dexpace-core/sig/dexpace/pipeline/entry.rbs index f3f6027..6d7663e 100644 --- a/gems/dexpace-core/sig/dexpace/pipeline/entry.rbs +++ b/gems/dexpace-core/sig/dexpace/pipeline/entry.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Pipeline class Entry < Data diff --git a/gems/dexpace-core/sig/dexpace/pipeline/stage.rbs b/gems/dexpace-core/sig/dexpace/pipeline/stage.rbs index 1b3de45..8108ef3 100644 --- a/gems/dexpace-core/sig/dexpace/pipeline/stage.rbs +++ b/gems/dexpace-core/sig/dexpace/pipeline/stage.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Pipeline class Stage < Data diff --git a/gems/dexpace-core/sig/dexpace/pipeline/stages.rbs b/gems/dexpace-core/sig/dexpace/pipeline/stages.rbs index 7da5a2f..7c3288c 100644 --- a/gems/dexpace-core/sig/dexpace/pipeline/stages.rbs +++ b/gems/dexpace-core/sig/dexpace/pipeline/stages.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Pipeline module Stages diff --git a/gems/dexpace-core/sig/dexpace/pipeline/step.rbs b/gems/dexpace-core/sig/dexpace/pipeline/step.rbs index 2949514..48bd218 100644 --- a/gems/dexpace-core/sig/dexpace/pipeline/step.rbs +++ b/gems/dexpace-core/sig/dexpace/pipeline/step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Pipeline module Step diff --git a/gems/dexpace-core/sig/dexpace/pipeline/sync_driver.rbs b/gems/dexpace-core/sig/dexpace/pipeline/sync_driver.rbs index 56e44de..6a73541 100644 --- a/gems/dexpace-core/sig/dexpace/pipeline/sync_driver.rbs +++ b/gems/dexpace-core/sig/dexpace/pipeline/sync_driver.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Pipeline::SyncDriver is a private_constant and not public API (design P2-15): this # declaration exists because the strict `core` Steep target checks every file under lib/ and # needs the class and its two methods declared to type its call sites in Cursor, Builder and diff --git a/gems/dexpace-core/sig/dexpace/pipeline/transform_step.rbs b/gems/dexpace-core/sig/dexpace/pipeline/transform_step.rbs index 8322d0e..e00237d 100644 --- a/gems/dexpace-core/sig/dexpace/pipeline/transform_step.rbs +++ b/gems/dexpace-core/sig/dexpace/pipeline/transform_step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Pipeline class TransformStep diff --git a/gems/dexpace-core/sig/dexpace/proxy.rbs b/gems/dexpace-core/sig/dexpace/proxy.rbs index d881d05..116a435 100644 --- a/gems/dexpace-core/sig/dexpace/proxy.rbs +++ b/gems/dexpace-core/sig/dexpace/proxy.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Proxy < Data include Model diff --git a/gems/dexpace-core/sig/dexpace/proxy/host_pattern.rbs b/gems/dexpace-core/sig/dexpace/proxy/host_pattern.rbs index 33bb6f5..825cc0d 100644 --- a/gems/dexpace-core/sig/dexpace/proxy/host_pattern.rbs +++ b/gems/dexpace-core/sig/dexpace/proxy/host_pattern.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Proxy class HostPattern < Data diff --git a/gems/dexpace-core/sig/dexpace/proxy/resolution.rbs b/gems/dexpace-core/sig/dexpace/proxy/resolution.rbs index 9a888d6..49e7abf 100644 --- a/gems/dexpace-core/sig/dexpace/proxy/resolution.rbs +++ b/gems/dexpace-core/sig/dexpace/proxy/resolution.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::ProxyResolution is a private_constant and not public API (P2-15): this declaration # exists because the strict `core` Steep target checks every file under lib/ and needs the module # and its one entry point declared to type Proxy.resolve. RBS has no visibility for a constant, so @@ -9,6 +11,7 @@ module Dexpace CRED_PASS: String NON_PROXY_PROP: String CREDENTIAL_BEFORE_AT: Regexp + AUTHORITY_START: Regexp SHOWN_AS: String def self?.resolve: (Configuration configuration, Instrumentation::Logger logger) -> Proxy? @@ -24,6 +27,7 @@ module Dexpace username: String?, password: String?) -> Proxy? def split_url: (String url, Instrumentation::Logger logger) -> Array[untyped] def shown: (String url) -> String + def scrub_userinfo: (String url) -> String def url_problem: (String? host, String? raw_port) -> String? def scheme_type: (String? scheme) -> Proxy::Type def credentials: (String? userinfo) -> Array[String?] diff --git a/gems/dexpace-core/sig/dexpace/proxy/type.rbs b/gems/dexpace-core/sig/dexpace/proxy/type.rbs index 6a0aff2..482cd20 100644 --- a/gems/dexpace-core/sig/dexpace/proxy/type.rbs +++ b/gems/dexpace-core/sig/dexpace/proxy/type.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Proxy class Type < Data diff --git a/gems/dexpace-core/sig/dexpace/recovery.rbs b/gems/dexpace-core/sig/dexpace/recovery.rbs index 8ba5cdc..31526cd 100644 --- a/gems/dexpace-core/sig/dexpace/recovery.rbs +++ b/gems/dexpace-core/sig/dexpace/recovery.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Recovery def self.buffer_error_body: (Dexpace::Response response) -> Dexpace::Response diff --git a/gems/dexpace-core/sig/dexpace/recovery/client_identity_step.rbs b/gems/dexpace-core/sig/dexpace/recovery/client_identity_step.rbs index 602e0e2..de92cf0 100644 --- a/gems/dexpace-core/sig/dexpace/recovery/client_identity_step.rbs +++ b/gems/dexpace-core/sig/dexpace/recovery/client_identity_step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Recovery class ClientIdentityStep diff --git a/gems/dexpace-core/sig/dexpace/recovery/error_mapping_step.rbs b/gems/dexpace-core/sig/dexpace/recovery/error_mapping_step.rbs index 3c25a4a..f0e741f 100644 --- a/gems/dexpace-core/sig/dexpace/recovery/error_mapping_step.rbs +++ b/gems/dexpace-core/sig/dexpace/recovery/error_mapping_step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Recovery class ErrorMappingStep diff --git a/gems/dexpace-core/sig/dexpace/recovery/idempotency_key_step.rbs b/gems/dexpace-core/sig/dexpace/recovery/idempotency_key_step.rbs index 2a8d80c..09a57bd 100644 --- a/gems/dexpace-core/sig/dexpace/recovery/idempotency_key_step.rbs +++ b/gems/dexpace-core/sig/dexpace/recovery/idempotency_key_step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Recovery class IdempotencyKeyStep diff --git a/gems/dexpace-core/sig/dexpace/recovery/orchestrator.rbs b/gems/dexpace-core/sig/dexpace/recovery/orchestrator.rbs index cbe4404..f462bba 100644 --- a/gems/dexpace-core/sig/dexpace/recovery/orchestrator.rbs +++ b/gems/dexpace-core/sig/dexpace/recovery/orchestrator.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Recovery class Orchestrator diff --git a/gems/dexpace-core/sig/dexpace/recovery/ownership.rbs b/gems/dexpace-core/sig/dexpace/recovery/ownership.rbs index 621cb85..3230260 100644 --- a/gems/dexpace-core/sig/dexpace/recovery/ownership.rbs +++ b/gems/dexpace-core/sig/dexpace/recovery/ownership.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Recovery::Ownership is a private_constant and not public API (design P2-15): this # declaration exists because the strict `core` Steep target checks every file under lib/ and # needs the module and its one method declared to type the two call sites in ResponseChain. RBS diff --git a/gems/dexpace-core/sig/dexpace/recovery/request_chain.rbs b/gems/dexpace-core/sig/dexpace/recovery/request_chain.rbs index 282d6ac..dc3a58f 100644 --- a/gems/dexpace-core/sig/dexpace/recovery/request_chain.rbs +++ b/gems/dexpace-core/sig/dexpace/recovery/request_chain.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Recovery class RequestChain diff --git a/gems/dexpace-core/sig/dexpace/recovery/response_chain.rbs b/gems/dexpace-core/sig/dexpace/recovery/response_chain.rbs index e489855..b70f136 100644 --- a/gems/dexpace-core/sig/dexpace/recovery/response_chain.rbs +++ b/gems/dexpace-core/sig/dexpace/recovery/response_chain.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Recovery class ResponseChain diff --git a/gems/dexpace-core/sig/dexpace/recovery/transform.rbs b/gems/dexpace-core/sig/dexpace/recovery/transform.rbs index b62db08..deac068 100644 --- a/gems/dexpace-core/sig/dexpace/recovery/transform.rbs +++ b/gems/dexpace-core/sig/dexpace/recovery/transform.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Recovery # The static half of R8's contract -- what phase 4c's generic adapter names at its parameter. diff --git a/gems/dexpace-core/sig/dexpace/redirect/chain.rbs b/gems/dexpace-core/sig/dexpace/redirect/chain.rbs index aa57115..7cbfbef 100644 --- a/gems/dexpace-core/sig/dexpace/redirect/chain.rbs +++ b/gems/dexpace-core/sig/dexpace/redirect/chain.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Redirect::Chain is a private_constant and not public API: this declaration exists # because the strict `core` Steep target checks every file under lib/ and needs the class and # its readers declared to type Step. RBS has no visibility for a constant, so the privacy lives diff --git a/gems/dexpace-core/sig/dexpace/redirect/condition_snapshot.rbs b/gems/dexpace-core/sig/dexpace/redirect/condition_snapshot.rbs index a05749d..5c39a65 100644 --- a/gems/dexpace-core/sig/dexpace/redirect/condition_snapshot.rbs +++ b/gems/dexpace-core/sig/dexpace/redirect/condition_snapshot.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Redirect # REDIR-20: the read-only, defensively-copied condition snapshot a redirect predicate is diff --git a/gems/dexpace-core/sig/dexpace/redirect/emitter.rbs b/gems/dexpace-core/sig/dexpace/redirect/emitter.rbs index 983d59b..6380582 100644 --- a/gems/dexpace-core/sig/dexpace/redirect/emitter.rbs +++ b/gems/dexpace-core/sig/dexpace/redirect/emitter.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Redirect::Emitter is a private_constant and not public API: this declaration exists # because the strict `core` Steep target checks every file under lib/ and needs the class and # its four writers declared to type Step. RBS has no visibility for a constant, so the privacy diff --git a/gems/dexpace-core/sig/dexpace/redirect/events.rbs b/gems/dexpace-core/sig/dexpace/redirect/events.rbs index 0cf6260..2e99b78 100644 --- a/gems/dexpace-core/sig/dexpace/redirect/events.rbs +++ b/gems/dexpace-core/sig/dexpace/redirect/events.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Redirect # REDIR-28's event names, the redirect layer's own vocabulary. diff --git a/gems/dexpace-core/sig/dexpace/redirect/location.rbs b/gems/dexpace-core/sig/dexpace/redirect/location.rbs index 1b47270..47abb63 100644 --- a/gems/dexpace-core/sig/dexpace/redirect/location.rbs +++ b/gems/dexpace-core/sig/dexpace/redirect/location.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Redirect::Location is a private_constant and not public API: this declaration exists # because the strict `core` Steep target checks every file under lib/ and needs the module and # its one function declared to type Step's call site. RBS has no visibility for a constant, so diff --git a/gems/dexpace-core/sig/dexpace/redirect/origin.rbs b/gems/dexpace-core/sig/dexpace/redirect/origin.rbs index 285deba..c7e6b2d 100644 --- a/gems/dexpace-core/sig/dexpace/redirect/origin.rbs +++ b/gems/dexpace-core/sig/dexpace/redirect/origin.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Redirect::Origin is a private_constant and not public API: this declaration exists # because the strict `core` Steep target checks every file under lib/ and needs the module and # its two functions declared to type Step's call sites. RBS has no visibility for a constant, so diff --git a/gems/dexpace-core/sig/dexpace/redirect/reissue.rbs b/gems/dexpace-core/sig/dexpace/redirect/reissue.rbs index 1651a8b..70eb911 100644 --- a/gems/dexpace-core/sig/dexpace/redirect/reissue.rbs +++ b/gems/dexpace-core/sig/dexpace/redirect/reissue.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Redirect::Reissue is a private_constant and not public API: this declaration exists # because the strict `core` Steep target checks every file under lib/ and needs the module and # its one function declared to type Step's call site. RBS has no visibility for a constant, so diff --git a/gems/dexpace-core/sig/dexpace/redirect/scheme_downgrade_error.rbs b/gems/dexpace-core/sig/dexpace/redirect/scheme_downgrade_error.rbs index 7f5b015..145f927 100644 --- a/gems/dexpace-core/sig/dexpace/redirect/scheme_downgrade_error.rbs +++ b/gems/dexpace-core/sig/dexpace/redirect/scheme_downgrade_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Redirect # REDIR-15: an HTTPS -> HTTP hop refused because the caller did not opt in. diff --git a/gems/dexpace-core/sig/dexpace/redirect/step.rbs b/gems/dexpace-core/sig/dexpace/redirect/step.rbs index 49cda7d..f6ee19c 100644 --- a/gems/dexpace-core/sig/dexpace/redirect/step.rbs +++ b/gems/dexpace-core/sig/dexpace/redirect/step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # The redirect layer: chapter 10's synchronous pillar step and its vocabulary. module Redirect diff --git a/gems/dexpace-core/sig/dexpace/registry.rbs b/gems/dexpace-core/sig/dexpace/registry.rbs index a57cafe..44b88d4 100644 --- a/gems/dexpace-core/sig/dexpace/registry.rbs +++ b/gems/dexpace-core/sig/dexpace/registry.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace class Registry # Two private_constants with no visibility in RBS, declared so Steep can type the snapshot diff --git a/gems/dexpace-core/sig/dexpace/resilience/async_retry_step.rbs b/gems/dexpace-core/sig/dexpace/resilience/async_retry_step.rbs index 99b6577..937f501 100644 --- a/gems/dexpace-core/sig/dexpace/resilience/async_retry_step.rbs +++ b/gems/dexpace-core/sig/dexpace/resilience/async_retry_step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Resilience # The asynchronous stage-based retry step at Stages::RETRY, over 4c's _AsyncStep. The keyword diff --git a/gems/dexpace-core/sig/dexpace/resilience/pacing_parsers.rbs b/gems/dexpace-core/sig/dexpace/resilience/pacing_parsers.rbs index 4c4cfe3..7b78cdc 100644 --- a/gems/dexpace-core/sig/dexpace/resilience/pacing_parsers.rbs +++ b/gems/dexpace-core/sig/dexpace/resilience/pacing_parsers.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Resilience::PacingParsers is a private_constant and not public API (P6-11): this # declaration exists because the strict `core` Steep target types Policy.pacing_delay's calls into # it. RBS has no visibility for a constant, so the privacy lives in diff --git a/gems/dexpace-core/sig/dexpace/resilience/policy.rbs b/gems/dexpace-core/sig/dexpace/resilience/policy.rbs index ee04eb9..6f65d6f 100644 --- a/gems/dexpace-core/sig/dexpace/resilience/policy.rbs +++ b/gems/dexpace-core/sig/dexpace/resilience/policy.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Resilience # The one shared retry policy core (RETRY-13, RECOV-30, R5). `random` is untyped on the two diff --git a/gems/dexpace-core/sig/dexpace/resilience/recovery_retry.rbs b/gems/dexpace-core/sig/dexpace/resilience/recovery_retry.rbs index 706b830..6ab3ed0 100644 --- a/gems/dexpace-core/sig/dexpace/resilience/recovery_retry.rbs +++ b/gems/dexpace-core/sig/dexpace/resilience/recovery_retry.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Resilience # The recovery-chain retry engine (RECOV-17..30, RECOV-34), a Dexpace::Transport decorator diff --git a/gems/dexpace-core/sig/dexpace/resilience/resend.rbs b/gems/dexpace-core/sig/dexpace/resilience/resend.rbs index f1e897e..7b0a490 100644 --- a/gems/dexpace-core/sig/dexpace/resilience/resend.rbs +++ b/gems/dexpace-core/sig/dexpace/resilience/resend.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Resilience # RETRY-5 through RETRY-8, RECOV-18: the re-sendability gate (R5), and REDIR-6's body-only diff --git a/gems/dexpace-core/sig/dexpace/resilience/retry_settings.rbs b/gems/dexpace-core/sig/dexpace/resilience/retry_settings.rbs index a571920..1846f79 100644 --- a/gems/dexpace-core/sig/dexpace/resilience/retry_settings.rbs +++ b/gems/dexpace-core/sig/dexpace/resilience/retry_settings.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Resilience # RECOV-34, RETRY-12: the one retry configuration both stacks build from. `random` is untyped diff --git a/gems/dexpace-core/sig/dexpace/resilience/retry_step.rbs b/gems/dexpace-core/sig/dexpace/resilience/retry_step.rbs index 454fbe1..f674d32 100644 --- a/gems/dexpace-core/sig/dexpace/resilience/retry_step.rbs +++ b/gems/dexpace-core/sig/dexpace/resilience/retry_step.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Resilience # The synchronous stage-based retry step at Stages::RETRY (RETRY-1..45's stage half). The diff --git a/gems/dexpace-core/sig/dexpace/resilience/retry_step_helpers.rbs b/gems/dexpace-core/sig/dexpace/resilience/retry_step_helpers.rbs index 1796647..3c54e9f 100644 --- a/gems/dexpace-core/sig/dexpace/resilience/retry_step_helpers.rbs +++ b/gems/dexpace-core/sig/dexpace/resilience/retry_step_helpers.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Resilience::RetryStepHelpers is a private_constant and not public API (P6-11): this # declaration exists because the strict `core` Steep target types its two includers, RetryStep # and AsyncRetryStep. RBS has no visibility for a constant, so the privacy lives in diff --git a/gems/dexpace-core/sig/dexpace/retryability.rbs b/gems/dexpace-core/sig/dexpace/retryability.rbs index 8a4ef2d..816b3cb 100644 --- a/gems/dexpace-core/sig/dexpace/retryability.rbs +++ b/gems/dexpace-core/sig/dexpace/retryability.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # What .retryable_status? accepts beside an Integer: a Dexpace::Status, or anything answering # #code with one. diff --git a/gems/dexpace-core/sig/dexpace/serde.rbs b/gems/dexpace-core/sig/dexpace/serde.rbs index 0b97b93..a28243b 100644 --- a/gems/dexpace-core/sig/dexpace/serde.rbs +++ b/gems/dexpace-core/sig/dexpace/serde.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # The static half of the codec seam: the six methods, typed. `value` is untyped because a # codec's value space is its own. Phase 7a settled the three clauses phase 2 left loose: diff --git a/gems/dexpace-core/sig/dexpace/serde/decode_context.rbs b/gems/dexpace-core/sig/dexpace/serde/decode_context.rbs index b5199aa..f66bee8 100644 --- a/gems/dexpace-core/sig/dexpace/serde/decode_context.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/decode_context.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde # The `ctx` a witness receives: the path from the document root, the decode's target type diff --git a/gems/dexpace-core/sig/dexpace/serde/decoding_handler.rbs b/gems/dexpace-core/sig/dexpace/serde/decoding_handler.rbs index 5b67fae..23c4da3 100644 --- a/gems/dexpace-core/sig/dexpace/serde/decoding_handler.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/decoding_handler.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde # SERDE-27's response-decoding handler, a Dexpace::_ResponseHandler supplied into diff --git a/gems/dexpace-core/sig/dexpace/serde/deserialization_error.rbs b/gems/dexpace-core/sig/dexpace/serde/deserialization_error.rbs index 0060f36..9742c8d 100644 --- a/gems/dexpace-core/sig/dexpace/serde/deserialization_error.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/deserialization_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde class DeserializationError < Dexpace::Serde::Error diff --git a/gems/dexpace-core/sig/dexpace/serde/error.rbs b/gems/dexpace-core/sig/dexpace/serde/error.rbs index c6e81f4..0ce1a56 100644 --- a/gems/dexpace-core/sig/dexpace/serde/error.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde class Error < ::StandardError diff --git a/gems/dexpace-core/sig/dexpace/serde/instant.rbs b/gems/dexpace-core/sig/dexpace/serde/instant.rbs index 0178937..30da4d8 100644 --- a/gems/dexpace-core/sig/dexpace/serde/instant.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/instant.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde # The ISO-8601 instant witness (SERDE-24): the decode half parses, the encode half renders at diff --git a/gems/dexpace-core/sig/dexpace/serde/list.rbs b/gems/dexpace-core/sig/dexpace/serde/list.rbs index 9e7eda0..e088427 100644 --- a/gems/dexpace-core/sig/dexpace/serde/list.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/list.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde # The Array combinator (SERDE-6): a witness for Array[element], built by value. A frozen Data; diff --git a/gems/dexpace-core/sig/dexpace/serde/map.rbs b/gems/dexpace-core/sig/dexpace/serde/map.rbs index 44246f5..4d7e84f 100644 --- a/gems/dexpace-core/sig/dexpace/serde/map.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/map.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde # The Hash combinator (SERDE-6): a witness for Hash[key, value], built by value from a key diff --git a/gems/dexpace-core/sig/dexpace/serde/native.rbs b/gems/dexpace-core/sig/dexpace/serde/native.rbs index 870fd1a..eb7fa99 100644 --- a/gems/dexpace-core/sig/dexpace/serde/native.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/native.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde # A private_constant with no visibility in RBS; declared because the strict `core` Steep diff --git a/gems/dexpace-core/sig/dexpace/serde/nullable.rbs b/gems/dexpace-core/sig/dexpace/serde/nullable.rbs index 9045a88..1679247 100644 --- a/gems/dexpace-core/sig/dexpace/serde/nullable.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/nullable.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde # The nullable combinator (SERDE-6): a witness accepting a wire null where its element would diff --git a/gems/dexpace-core/sig/dexpace/serde/scalars.rbs b/gems/dexpace-core/sig/dexpace/serde/scalars.rbs index 3fe19aa..adf35ee 100644 --- a/gems/dexpace-core/sig/dexpace/serde/scalars.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/scalars.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Serde::Scalars is a private_constant and not public API: this declaration exists # because the strict `core` Steep target checks every file under lib/ and needs the module, its # table and its witness class declared to type the combinators' constructors. RBS has no diff --git a/gems/dexpace-core/sig/dexpace/serde/serialization_error.rbs b/gems/dexpace-core/sig/dexpace/serde/serialization_error.rbs index f6b4fd9..889ddf9 100644 --- a/gems/dexpace-core/sig/dexpace/serde/serialization_error.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/serialization_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde class SerializationError < Dexpace::Serde::Error diff --git a/gems/dexpace-core/sig/dexpace/serde/status_aware_handler.rbs b/gems/dexpace-core/sig/dexpace/serde/status_aware_handler.rbs index 45b41b3..9440874 100644 --- a/gems/dexpace-core/sig/dexpace/serde/status_aware_handler.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/status_aware_handler.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde # SERDE-28's status-aware response handler, a Dexpace::_ResponseHandler supplied into diff --git a/gems/dexpace-core/sig/dexpace/serde/tristate.rbs b/gems/dexpace-core/sig/dexpace/serde/tristate.rbs index 0fb11fa..85db267 100644 --- a/gems/dexpace-core/sig/dexpace/serde/tristate.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/tristate.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde # The three-state PATCH type (SERDE-14–SERDE-20, SERDE-30): a module included by ABSENT, NULL diff --git a/gems/dexpace-core/sig/dexpace/serde/witness.rbs b/gems/dexpace-core/sig/dexpace/serde/witness.rbs index b1013e4..3dbf624 100644 --- a/gems/dexpace-core/sig/dexpace/serde/witness.rbs +++ b/gems/dexpace-core/sig/dexpace/serde/witness.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde # The witness protocol (design §7.3, §10.14): any object answering .dexpace_load(parsed, ctx). diff --git a/gems/dexpace-core/sig/dexpace/sse.rbs b/gems/dexpace-core/sig/dexpace/sse.rbs index 2920d13..8920fd3 100644 --- a/gems/dexpace-core/sig/dexpace/sse.rbs +++ b/gems/dexpace-core/sig/dexpace/sse.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module SSE MAX_LINE_BYTES: Integer diff --git a/gems/dexpace-core/sig/dexpace/sse/event.rbs b/gems/dexpace-core/sig/dexpace/sse/event.rbs index 45a5c44..75e452e 100644 --- a/gems/dexpace-core/sig/dexpace/sse/event.rbs +++ b/gems/dexpace-core/sig/dexpace/sse/event.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module SSE # The immutable five-field event value (SSE-20, SSE-21, SSE-22). diff --git a/gems/dexpace-core/sig/dexpace/sse/limit_exceeded_error.rbs b/gems/dexpace-core/sig/dexpace/sse/limit_exceeded_error.rbs index c717a56..3b11f09 100644 --- a/gems/dexpace-core/sig/dexpace/sse/limit_exceeded_error.rbs +++ b/gems/dexpace-core/sig/dexpace/sse/limit_exceeded_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module SSE # SSE-19: a line or an event block crossed its documented bound and was rejected. diff --git a/gems/dexpace-core/sig/dexpace/sse/line_reader.rbs b/gems/dexpace-core/sig/dexpace/sse/line_reader.rbs index bbbb649..f97f8b3 100644 --- a/gems/dexpace-core/sig/dexpace/sse/line_reader.rbs +++ b/gems/dexpace-core/sig/dexpace/sse/line_reader.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module SSE # SSE-2's line machine over a _ByteSource#getbyte (P7-20), with SSE-19's line cap. diff --git a/gems/dexpace-core/sig/dexpace/sse/reader.rbs b/gems/dexpace-core/sig/dexpace/sse/reader.rbs index 3787ef7..74ccd5d 100644 --- a/gems/dexpace-core/sig/dexpace/sse/reader.rbs +++ b/gems/dexpace-core/sig/dexpace/sse/reader.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module SSE # SSE-1's field state machine over LineReader: SSE-3 through SSE-18, SSE-19's event cap. diff --git a/gems/dexpace-core/sig/dexpace/sse/sentinel.rbs b/gems/dexpace-core/sig/dexpace/sse/sentinel.rbs index 2eecefb..d2389e5 100644 --- a/gems/dexpace-core/sig/dexpace/sse/sentinel.rbs +++ b/gems/dexpace-core/sig/dexpace/sse/sentinel.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module SSE # SSE-34's two non-value outcomes, as a closed two-instance type (P7-23). diff --git a/gems/dexpace-core/sig/dexpace/sse/stream.rbs b/gems/dexpace-core/sig/dexpace/sse/stream.rbs index f40dcd3..daba788 100644 --- a/gems/dexpace-core/sig/dexpace/sse/stream.rbs +++ b/gems/dexpace-core/sig/dexpace/sse/stream.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module SSE # The resource-owning single-pass streaming facade (SSE-23 through SSE-32, SSE-39, SSE-40). diff --git a/gems/dexpace-core/sig/dexpace/sse/stream_state_error.rbs b/gems/dexpace-core/sig/dexpace/sse/stream_state_error.rbs index 2b12410..9e97ba1 100644 --- a/gems/dexpace-core/sig/dexpace/sse/stream_state_error.rbs +++ b/gems/dexpace-core/sig/dexpace/sse/stream_state_error.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module SSE # SSE-26's second view and SSE-27's post-close request: an illegal-state error. diff --git a/gems/dexpace-core/sig/dexpace/sse/typed_stream.rbs b/gems/dexpace-core/sig/dexpace/sse/typed_stream.rbs index 6c288fc..6748cbc 100644 --- a/gems/dexpace-core/sig/dexpace/sse/typed_stream.rbs +++ b/gems/dexpace-core/sig/dexpace/sse/typed_stream.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module SSE # The typed adapter over a Stream and a caller-supplied mapper (SSE-33 through SSE-36). diff --git a/gems/dexpace-core/sig/dexpace/suppressible.rbs b/gems/dexpace-core/sig/dexpace/suppressible.rbs index 55294fb..6ae717a 100644 --- a/gems/dexpace-core/sig/dexpace/suppressible.rbs +++ b/gems/dexpace-core/sig/dexpace/suppressible.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # The self-type is ::Exception because #detailed_message calls `super`, and every includer and # every object Dexpace.attach_suppressed extends is one. The keywords Ruby's printer passes diff --git a/gems/dexpace-core/sig/dexpace/transport.rbs b/gems/dexpace-core/sig/dexpace/transport.rbs index 7cb3d02..cca3a86 100644 --- a/gems/dexpace-core/sig/dexpace/transport.rbs +++ b/gems/dexpace-core/sig/dexpace/transport.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace # The static half of the seam -- what a consumer's own `steep check` sees at a parameter -- and # the artifact that replaces the styleguide's Sorbet abstract module diff --git a/gems/dexpace-core/sig/dexpace/uuid.rbs b/gems/dexpace-core/sig/dexpace/uuid.rbs index 2dba9d2..ac2dc36 100644 --- a/gems/dexpace-core/sig/dexpace/uuid.rbs +++ b/gems/dexpace-core/sig/dexpace/uuid.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module UUID # A private_constant; declared because the strict `core` Steep target types its use. diff --git a/gems/dexpace-core/sig/dexpace/version.rbs b/gems/dexpace-core/sig/dexpace/version.rbs index 35dc193..73401b7 100644 --- a/gems/dexpace-core/sig/dexpace/version.rbs +++ b/gems/dexpace-core/sig/dexpace/version.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace VERSION: String end diff --git a/gems/dexpace-core/test/dexpace/async/delay_test.rb b/gems/dexpace-core/test/dexpace/async/delay_test.rb index 1cd8e35..9f6ce99 100644 --- a/gems/dexpace-core/test/dexpace/async/delay_test.rb +++ b/gems/dexpace-core/test/dexpace/async/delay_test.rb @@ -19,6 +19,15 @@ class WithoutSchedulerTest < DexpaceTestCase assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Async.delay("1") } end + # Phase 10, repairing 8b's review R1-1 in core: `is_a?(Numeric)` plus `negative?` admitted a NaN + # (false to both `negative?` and `zero?`, so it reached the scheduler's wait, which never ends) + # and raised a bare NoMethodError for a Complex. P8-77's shape, the pool's own guard. + test "CFG-18: a NaN or a Complex delay is refused with InvalidArgumentError, unscheduled" do + [Float::NAN, Complex(1, 1), Complex(1, 0)].each do |odd| + assert_raises(Dexpace::InvalidArgumentError, odd.inspect) { Dexpace::Async.delay(odd) } + end + end + test "CFG-18: a zero delay returns a future already settled, with no scheduler consulted" do assert_nil(Fiber.scheduler) future = Dexpace::Async.delay(0) diff --git a/gems/dexpace-core/test/dexpace/auth/bearer_stamper_test.rb b/gems/dexpace-core/test/dexpace/auth/bearer_stamper_test.rb index f14f6b1..258a44e 100644 --- a/gems/dexpace-core/test/dexpace/auth/bearer_stamper_test.rb +++ b/gems/dexpace-core/test/dexpace/auth/bearer_stamper_test.rb @@ -140,6 +140,25 @@ class RejectionTest < DexpaceTestCase assert_equal(4, provider.fetches) end + # 6c's review R4-1, pinned by phase 10: every rejection above starts from a COLD cache, so a + # stamper that validated only a first fetch and skipped the check when REFRESHING a cached + # token survived three mutations. Each rejection again, from a warm cache past its margin: the + # error surfaces, the cached token is left exactly as it was, and the next call refetches. + test "AUTH-35: every rejection holds on the REFRESH path too, the warm cache untouched" do + [-> {}, -> { Object.new }, "bad\r\ntoken"].each do |rejected| + clock = FakeClock.new(now: Time.at(0)) + warm = BearerToken.build(token: "t1", expiry: Time.at(100)) + provider = ScriptedBearerProvider.new(warm, rejected, "t3") + subject = stamper(provider, clock: clock) + subject.call(https_request) + clock.advance(80) # inside the 30 s margin: the next call refreshes + + assert_raises(Dexpace::Auth::ProviderError) { subject.call(https_request) } + assert_same(warm, subject.instance_variable_get(:@token), "the cache was touched") + assert_equal(["Bearer t3"], authorization(subject.call(https_request))) + end + end + test "AUTH-35, AUTH-11: a raising provider propagates its own error, uncached; next retries" do provider = ScriptedBearerProvider.new(RuntimeError.new("boom"), "t2") subject = stamper(provider) diff --git a/gems/dexpace-core/test/dexpace/clock_test.rb b/gems/dexpace-core/test/dexpace/clock_test.rb index 933c564..f38223a 100644 --- a/gems/dexpace-core/test/dexpace/clock_test.rb +++ b/gems/dexpace-core/test/dexpace/clock_test.rb @@ -72,6 +72,17 @@ class SleepTest < DexpaceTestCase assert_raises(Dexpace::InvalidArgumentError) { clock.sleep(1, cancellation: :token) } end + # Phase 10, repairing 8b's review R1-1 in core (P8-77's shape): a NaN passed both checks and + # parked in Queue#pop(timeout: NaN) forever on every row -- a sleep no elapsed time ends -- and + # a Complex raised a bare NoMethodError from `negative?`. Asserted on deadline_in, which shares + # the guard, so no thread is ever parked by this test. + test "CFG-15, CFG-17: a NaN or a Complex duration is refused before any wait" do + [Float::NAN, Complex(1, 1)].each do |odd| + assert_raises(Dexpace::InvalidArgumentError, odd.inspect) { Dexpace::Clock.deadline_in(odd) } + assert_raises(Dexpace::InvalidArgumentError, odd.inspect) { Dexpace::Clock::SYSTEM.sleep(odd) } + end + end + test "CFG-17: a zero duration returns nil promptly, allocating no queue and no subscription" do clock = Dexpace::Clock::SYSTEM source = Dexpace::Cancellation.source diff --git a/gems/dexpace-core/test/dexpace/config_test.rb b/gems/dexpace-core/test/dexpace/config_test.rb index 218ef9f..db447d7 100644 --- a/gems/dexpace-core/test/dexpace/config_test.rb +++ b/gems/dexpace-core/test/dexpace/config_test.rb @@ -50,6 +50,28 @@ def teardown assert_equal("proxy.corp", Dexpace.configuration.raw_property("https.proxyHost")) end + # 5a's review R1-2, repaired by phase 10: every configure that added a property wrapped the + # inherited source in one more closure, so a lookup walked one frame per configure ever made + # and nothing released them. The base source records how deep below the lookup it was + # reached; after 200 configures that depth must equal the depth after 2. + test "CFG-13: a lookup's depth does not grow with the number of configures" do + depths = [] + base = lambda do |_key| + depths << caller.size + nil + end + Dexpace.configure { |c| c.property_source = base } + Dexpace.configure { |c| c.property("a", "1") } + Dexpace.configure { |c| c.property("b", "2") } + Dexpace.configuration.string("absent") + 200.times { |i| Dexpace.configure { |c| c.property("k#{i}", i.to_s) } } + Dexpace.configuration.string("absent") + + assert_equal(depths.first, depths.last, "the property chain grew with the configure count") + assert_equal("1", Dexpace.configuration.string("a")) + assert_equal("199", Dexpace.configuration.string("k199")) + end + test "CFG-13: reset_config! restores Configuration::EMPTY and returns it" do Dexpace.configure { |c| c.override("K", "V") } diff --git a/gems/dexpace-core/test/dexpace/configuration/builder_test.rb b/gems/dexpace-core/test/dexpace/configuration/builder_test.rb index 897e58a..e626785 100644 --- a/gems/dexpace-core/test/dexpace/configuration/builder_test.rb +++ b/gems/dexpace-core/test/dexpace/configuration/builder_test.rb @@ -200,7 +200,11 @@ class BuilderContractTest < DexpaceTestCase assert_equal("1", seeded.string("K")) assert_equal(" v ", seeded.string("trim")) - assert_nil(Configuration::Builder.new(property_source: nil).build.string("K")) + # Hermetic (phase 10, 5a's review R1-1): without env_source: the builder read the HOST's + # environment, and a machine exporting K turned this red. + assert_nil(Configuration::Builder.new(property_source: nil, + env_source: Configuration::Sources::NONE,) + .build.string("K")) end test "SEAM-29 / HTTP-3: .new is private, .build validates, #with re-validates through .build" do diff --git a/gems/dexpace-core/test/dexpace/http/body/response_body_test.rb b/gems/dexpace-core/test/dexpace/http/body/response_body_test.rb index 5862867..7c693ec 100644 --- a/gems/dexpace-core/test/dexpace/http/body/response_body_test.rb +++ b/gems/dexpace-core/test/dexpace/http/body/response_body_test.rb @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT require_relative "../../../test_helper" +require_relative "../../../support/pinned_ceiling" require "dexpace" require_relative "../../../support/fake_sink" require_relative "../../../support/fake_source" @@ -181,7 +182,9 @@ class PreviewTest < DexpaceTestCase [ceiling + 1, ceiling * 2, ::Float::INFINITY].each do |cap| recorder = CountingStream.new("abc") - assert_equal("abc".b, over(recorder).preview(cap: cap)) + preview = PinnedCeiling.with_default_ceiling { over(recorder).preview(cap: cap) } + + assert_equal("abc".b, preview) assert_equal(ceiling, recorder.maxlens.first, "cap #{cap}") end end diff --git a/gems/dexpace-core/test/dexpace/http/body/stream_body_test.rb b/gems/dexpace-core/test/dexpace/http/body/stream_body_test.rb index 49ae742..bfea0d1 100644 --- a/gems/dexpace-core/test/dexpace/http/body/stream_body_test.rb +++ b/gems/dexpace-core/test/dexpace/http/body/stream_body_test.rb @@ -311,6 +311,23 @@ class ConstructionTest < DexpaceTestCase assert_includes(error.message, "#readpartial") end + # Phase 3b's review R2-1, repaired by phase 10: the rewind probe read #pos off a closed stream + # and a bare IOError ("closed stream") escaped `rescue Dexpace::Error` at construction. + test "rejects an already-closed stream with the SDK's error, closed StringIO and File alike" do + io = StringIO.new(+"a") + io.close + file = Tempfile.new("closed") + file.close + + [io, file].each do |closed| + error = assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Body.stream(closed) } + + assert_includes(error.message, "already closed") + end + ensure + file&.unlink + end + test "rejects a content_length below the -1 sentinel" do assert_raises(Dexpace::InvalidArgumentError) do Dexpace::Body.stream(StringIO.new(+"a"), content_length: -2) diff --git a/gems/dexpace-core/test/dexpace/http/body_test.rb b/gems/dexpace-core/test/dexpace/http/body_test.rb index aa2f91f..6ad289d 100644 --- a/gems/dexpace-core/test/dexpace/http/body_test.rb +++ b/gems/dexpace-core/test/dexpace/http/body_test.rb @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT require_relative "../../test_helper" +require_relative "../../support/pinned_ceiling" require "dexpace" require_relative "../../support/fake_body" require_relative "../../support/fake_sink" @@ -367,7 +368,7 @@ def read_twice(copy) test "over a body larger than the cap, stops reading rather than discarding" do oversized = OversizedBody.new(2 * Dexpace::Body::MAX_BUFFERED_ERROR_BODY_BYTES) - copy = Dexpace::Body.buffer_bounded(oversized) + copy = PinnedCeiling.with_default_ceiling { Dexpace::Body.buffer_bounded(oversized) } assert_equal(Dexpace::Body::MAX_BUFFERED_ERROR_BODY_BYTES, copy.content_length) assert_operator(oversized.yielded_bytes, :<, oversized.total) @@ -471,7 +472,7 @@ def fits_cap_wrapper(content = "héllo") ceiling = Dexpace::IO::MAX_MATERIALIZED_BYTES [ceiling + 1, ceiling * 2, ::Float::INFINITY].each do |cap| delegate = BufferBoundedTest::SourceDouble.new("body") - Dexpace::Body.buffer_bounded(delegate, cap: cap) + PinnedCeiling.with_default_ceiling { Dexpace::Body.buffer_bounded(delegate, cap: cap) } assert_equal(ceiling, delegate.source.calls.first, "cap #{cap}") end diff --git a/gems/dexpace-core/test/dexpace/http/header_syntax_test.rb b/gems/dexpace-core/test/dexpace/http/header_syntax_test.rb index 8020059..df7ec61 100644 --- a/gems/dexpace-core/test/dexpace/http/header_syntax_test.rb +++ b/gems/dexpace-core/test/dexpace/http/header_syntax_test.rb @@ -15,6 +15,19 @@ class DexpaceHeaderSyntaxTest < DexpaceTestCase assert_equal("X-Trace", Syntax.validate_name!("\tX-Trace\t")) end + # Phase 1's review R3-4, repaired by phase 10: every predicate called `#b` on its argument and + # raised NoMethodError for a non-String, outside `rescue Dexpace::Error`. A predicate answers. + test "every predicate is total: a non-String is false, and a validator raises the SDK's error" do + [42, nil, :accept, ["a"]].each do |odd| + refute(Syntax.valid_name?(odd), odd.inspect) + refute(Syntax.valid_outbound_value?(odd), odd.inspect) + refute(Syntax.valid_inbound_value?(odd), odd.inspect) + refute(Syntax.token?(odd), odd.inspect) + end + assert_raises(Dexpace::InvalidArgumentError) { Syntax.validate_name!(42) } + assert_raises(Dexpace::InvalidArgumentError) { Syntax.validate_outbound_value!(42, name: "X") } + end + test "rejects a name whose only content is a NUL that String#strip would remove" do assert_raises(Dexpace::InvalidArgumentError) { Syntax.validate_name!("a\0") } end diff --git a/gems/dexpace-core/test/dexpace/http/protocol_test.rb b/gems/dexpace-core/test/dexpace/http/protocol_test.rb index 6b73f03..c57a5e5 100644 --- a/gems/dexpace-core/test/dexpace/http/protocol_test.rb +++ b/gems/dexpace-core/test/dexpace/http/protocol_test.rb @@ -23,7 +23,22 @@ class DexpaceProtocolTest < DexpaceTestCase error = assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Protocol.parse("spdy/3") } assert_includes(error.message, "spdy/3") - assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Protocol.parse("http/1.0") } + # The negative half of phase 10's repair: widening the table by one form must not turn a + # loud failure into a silent one for every other version. + ["http/0.9", "HTTP/3", "http/1", "http/1.10"].each do |text| + assert_raises(Dexpace::InvalidArgumentError, text) { Dexpace::Protocol.parse(text) } + end + end + + # HTTP-33 names http/1.1 and http/2 as EXAMPLES of the canonical form; a real HTTP/1.0 status + # line is one any server may send, and both adapters feed their native version straight here, so + # before phase 10 an HTTP/1.0 response made both ResponseMappers raise (8a's hand-forward). + test "parses HTTP/1.0 to its own canonical form, and build accepts exactly that form" do + ["HTTP/1.0", "http/1.0"].each do |text| + assert_equal("http/1.0", Dexpace::Protocol.parse(text).wire) + end + assert_equal(Dexpace::Protocol::HTTP_1_0, Dexpace::Protocol.parse("HTTP/1.0")) + assert_equal(Dexpace::Protocol::HTTP_1_0, Dexpace::Protocol.build(wire: "http/1.0")) end test "requires an identifier, naming the field" do diff --git a/gems/dexpace-core/test/dexpace/http/response_test.rb b/gems/dexpace-core/test/dexpace/http/response_test.rb index 45515ec..838030a 100644 --- a/gems/dexpace-core/test/dexpace/http/response_test.rb +++ b/gems/dexpace-core/test/dexpace/http/response_test.rb @@ -103,8 +103,9 @@ def request assert_equal(["1"], derived.build.headers["X-A"]) end - test "with re-validates, so a derived response cannot carry a status outside the range" do - assert_raises(Dexpace::InvalidArgumentError) { response.with(status: 600) } + test "with re-validates, so a derived response cannot carry a status that is not a code" do + # A String and not 600 or 1000: phase 10 made Status total over every Integer (HTTP-10). + assert_raises(Dexpace::InvalidArgumentError) { response.with(status: "404") } assert_raises(Dexpace::InvalidArgumentError) { response.with(protocol: nil) } assert_equal(Dexpace::Status::NOT_FOUND, response.with(status: 404).status) end @@ -159,7 +160,7 @@ def response(status: Dexpace::Status::OK) end test "build rejects a protocol, a status, a request and headers it cannot coerce" do - assert_raises(Dexpace::InvalidArgumentError) { response(status: 99) } + assert_raises(Dexpace::InvalidArgumentError) { response(status: "200") } # every Integer maps builder = Dexpace::Response.builder builder.request = request builder.protocol = "spdy/3" diff --git a/gems/dexpace-core/test/dexpace/http/status_test.rb b/gems/dexpace-core/test/dexpace/http/status_test.rb index 17bb30c..9d6e782 100644 --- a/gems/dexpace-core/test/dexpace/http/status_test.rb +++ b/gems/dexpace-core/test/dexpace/http/status_test.rb @@ -48,10 +48,30 @@ class DexpaceStatusTest < DexpaceTestCase refute_predicate(Dexpace::Status.of(399), :error?) end - test "rejects a code outside the protocol's range as a caller mistake" do - assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Status.of(99) } - assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Status.of(600) } + # HTTP-10 (MUST): "mapping ANY code MUST return a Status (never throw)"; TRANSPORT-24 (MUST): any + # code the server returns, vendor codes included, is surfaced rather than rejected. Net::HTTP + # delivers a 600 or LinkedIn's 999 as an HTTPUnknownResponse, and before phase 10 this guard + # refused both, which made both transports raise on a head the requirement says to surface. A + # first repair stopped at 0..999 and still threw on 1000 and -1 (review round 0, R0-5), so the + # whole Integer line maps and only a non-Integer -- not a code at all -- is refused. + test "maps every Integer code, vendor codes past 599 included, and refuses only a non-code" do + [-1, 0, 99, 600, 999, 1000, 2**70].each do |code| + assert_equal(code, Dexpace::Status.of(code).code) + assert_nil(Dexpace::Status.of(code).canonical_name) + refute_predicate(Dexpace::Status.of(code), :standard?) + end assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Status.of("200") } + assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Status.of(200.0) } + end + + # The 100-599 guard moved to its own predicate (phase 10), so a caller who wants the protocol's + # classes still asks one question; a code outside them belongs to none of HTTP-11's classes. + test "standard? is the protocol's own range, and a code outside it has no class" do + assert_predicate(Dexpace::Status.of(100), :standard?) + assert_predicate(Dexpace::Status.of(599), :standard?) + refute_predicate(Dexpace::Status.of(999), :standard?) + refute_predicate(Dexpace::Status.of(999), :error?) + refute_predicate(Dexpace::Status.of(99), :informational?) end test "requires a code, naming the field" do @@ -66,9 +86,9 @@ class DexpaceStatusTest < DexpaceTestCase end # HTTP-10's totality as a property rather than five examples. - test "construction is total over the protocol's range" do + test "construction is total over the Integer line" do sample(count: 256) do |rng| - code = rng.rand(100..599) + code = rng.rand(-100_000..100_000) assert_equal(code, Dexpace::Status.of(code).code) end diff --git a/gems/dexpace-core/test/dexpace/http/url_test.rb b/gems/dexpace-core/test/dexpace/http/url_test.rb index 98e3a15..9259eb0 100644 --- a/gems/dexpace-core/test/dexpace/http/url_test.rb +++ b/gems/dexpace-core/test/dexpace/http/url_test.rb @@ -26,6 +26,25 @@ class DexpaceUrlTest < DexpaceTestCase assert_kind_of(::URI::InvalidURIError, error.cause) end + # HTTP-47, repaired by phase 10 (phase 2's review R3-1 and R3-2, phase 1's round-3 bullet): + # three inputs escaped `rescue Dexpace::Error` or surfaced later as a lower-level error -- + # `mailto://host` raised URI::InvalidComponentError from the scheme's constructor, + # `ftp://h/f;type=x` parsed and then raised FrozenError from every #to_s of the frozen result, + # and a host-less `http:` or `http:///p` parsed as an absolute URI::HTTP a transport then + # choked on. + test "every malformed, unrenderable or host-less http URL fails with the SDK's error" do + ["mailto://host", "ftp://h/f;type=x", "http:", "http:///p", "https:"].each do |input| + error = assert_raises(Dexpace::InvalidArgumentError, input) { Dexpace::URL.parse!(input) } + + assert_includes(error.message, input.inspect, input) + end + end + + test "a non-http absolute URL is still admitted: which schemes dispatch is a transport's call" do + assert_equal("ftp://h/f", Dexpace::URL.parse!("ftp://h/f").to_s) + assert_equal("mailto:x@y", Dexpace::URL.parse!("mailto:x@y").to_s) + end + test "rejects a relative URI, naming it" do error = assert_raises(Dexpace::InvalidArgumentError) { Dexpace::URL.parse!("/relative") } diff --git a/gems/dexpace-core/test/dexpace/instrumentation/async_step_test.rb b/gems/dexpace-core/test/dexpace/instrumentation/async_step_test.rb index d619d62..ebc1dbe 100644 --- a/gems/dexpace-core/test/dexpace/instrumentation/async_step_test.rb +++ b/gems/dexpace-core/test/dexpace/instrumentation/async_step_test.rb @@ -330,6 +330,26 @@ class BodyLevelTest < DexpaceTestCase assert_equal(8, sink.payloads[1][Keys::HTTP_RESPONSE_BODY_SIZE]) end + # 5b's final review (R3-2), repaired by phase 10: at BODY the derivation was registered before + # the settlement work, so the derived future's fulfilment -- and every callback the CALLER + # had put on it -- ran before the step read the clock, and the recorded duration included the + # caller's own continuation. The sync path records before it returns; so must this one. + test "OBS-24, OBS-34: at BODY the recorded duration excludes the caller's continuation" do + meter = Dexpace::RecordingMeter.new + clock = FakeClock.new(monotonic: 10.0) + step = async_step(RecordingSink.new, level: HTTPLogging::BODY, preview_bytes: 8, meter: meter, + clock: clock,) + request = build_request + transport = FakeAsyncTransport.new(response: build_response(request), settle_later: true) + + future = pipeline(step, transport).call(request) + future.on_settle { |_settlement| clock.advance(5.0) } # the caller's continuation + clock.advance(0.25) + transport.settle + + assert_equal([{ amount: 250.0, attributes: nil }], meter.histograms.first.records) + end + test "P5-94: the derived future forwards a failure as the same object and cancel propagates" do sink = RecordingSink.new step = async_step(sink, level: HTTPLogging::BODY, preview_bytes: 8) diff --git a/gems/dexpace-core/test/dexpace/instrumentation/downstream_wirings_test.rb b/gems/dexpace-core/test/dexpace/instrumentation/downstream_wirings_test.rb index 89d13a6..10be478 100644 --- a/gems/dexpace-core/test/dexpace/instrumentation/downstream_wirings_test.rb +++ b/gems/dexpace-core/test/dexpace/instrumentation/downstream_wirings_test.rb @@ -273,5 +273,64 @@ def chain_with_https_proxy(url) end end end + + # Phase 5b's final review (R3-1), repaired by phase 10: CFG-24's belt was anchored at the + # start of the redactor's output (`\A[^/?#]*@`), so any spelling that puts a `/` BEFORE the + # userinfo -- a scheme with one or three slashes, a leading slash -- reaches the belt with the + # credential after the anchor's reach, and the redactor, reading an opaque or path-only URI, + # writes it back as given. Measured before the repair on 4.0.6 and 3.2.11: the password in + # `Kernel#warn` and in the config sink, eight spellings of eight. The belt is now unanchored + # and applied to every `@` a warning shows. OBS-11 ("unconditionally"), CFG-22. + test "R3-1, OBS-11, CFG-22: a credential after a slash reaches neither channel" do + ["http:/user:secret@proxy.corp:3128", "http:///user:secret@proxy.corp", + "/http://user:secret@proxy.corp:3128", "socks5:/user:secret@p:1", + "https:///user:secret@proxy.corp:3128", "http:////user:secret@proxy.corp", + "x/user:secret@proxy.corp:3128", "http:/a/user:secret@proxy.corp:3128",].each do |url| + sink = RecordingSink.new + + warnings = WarningCapture.record do + logger = Logger.build(sink: sink) + + assert_nil(Dexpace::Proxy.resolve(chain_with_https_proxy(url), logger: logger), url) + end + + assert_equal(1, warnings.size, url) + [warnings.first, sink.payloads.inspect].each do |channel| + refute_includes(channel, "secret", url) + refute_includes(channel, "user:", url) + end + end + end + + # Phase 10's review round 0 (R0-4): the unanchored belt stops at the first `/`, `?` or `#`, + # so a password holding one left its prefix in both channels -- `http:/user:pa/ss@h:1` warned + # `http:/user:pa/***:***@h:1` -- on 4.0.6 and 3.2.11. The raw value is now scrubbed from the + # authority's start through the LAST `@`, before the redactor can split it. Each case names + # the password and the prefix a split would have left. The last three put an `@` INSIDE the + # password ahead of a reserved character: a scrub through the FIRST `@` leaves `c/`, `b#` and + # `y/` in the warning, which only a through-the-LAST-`@` scrub removes (review round 1, R1-1). + test "R0-4, OBS-11, CFG-22: a password holding a reserved character leaks no prefix" do + [["http:/user:pa/ss@h:1", "pa/"], ["http://user:p/ss@proxy.corp", "p/ss"], + ["user:se#cret@proxy.corp:3128", "se#"], ["http://user:p#ss@proxy.corp", "p#"], + ["http://user:p?ss@proxy.corp", "p?"], ["http:/user:pa?ss@h:1", "pa?"], + ["http://user:se@cret@proxy.corp", "se@"], ["http://user:se@c/ret@proxy.corp", "c/"], + ["http://user:a@b#c@proxy.corp:1", "b#"], + ["user:x@y/z@proxy.corp:3128", "y/"],].each do |url, prefix| + sink = RecordingSink.new + + warnings = WarningCapture.record do + logger = Logger.build(sink: sink) + + assert_nil(Dexpace::Proxy.resolve(chain_with_https_proxy(url), logger: logger), url) + end + + assert_equal(1, warnings.size, url) + [warnings.first, sink.payloads.inspect].each do |channel| + refute_includes(channel, prefix, url) + refute_includes(channel, "user", url) + assert_includes(channel, "***:***@", url) + end + end + end end end diff --git a/gems/dexpace-core/test/dexpace/instrumentation/event_test.rb b/gems/dexpace-core/test/dexpace/instrumentation/event_test.rb index c6a030e..51211c8 100644 --- a/gems/dexpace-core/test/dexpace/instrumentation/event_test.rb +++ b/gems/dexpace-core/test/dexpace/instrumentation/event_test.rb @@ -107,6 +107,13 @@ def live(sink, severity: Severity::INFO) event.emit end end + # Every racer PARKED on the gate before it opens, asserted rather than assumed (phase 10, + # 5b's review R3-3; phase 9's P9-29 shape): without it the first thread could finish before + # the fourth was created, which on 3.2 made the "race" a sequential second call and the + # test a copy of the one above. + Thread.pass until threads.all? { |thread| thread.status == "sleep" } + + assert_equal(4, gate.num_waiting, "every racer is parked on the gate") 4.times { gate << true } threads.each(&:join) diff --git a/gems/dexpace-core/test/dexpace/instrumentation/http_tracer_test.rb b/gems/dexpace-core/test/dexpace/instrumentation/http_tracer_test.rb index 565061a..9fc8564 100644 --- a/gems/dexpace-core/test/dexpace/instrumentation/http_tracer_test.rb +++ b/gems/dexpace-core/test/dexpace/instrumentation/http_tracer_test.rb @@ -115,4 +115,20 @@ def operation_started(context) assert_in_delta(0.0, per_call, 0.0, "the null HTTP tracer allocates per call") end + + # Phase 10's R6, pinned: CTX-14's factory on the correlation bundle and OBS-29's HTTP-tracer + # factory are two objects. The bundle's is keyed by instrumentation library and shared -- its + # no-op answers ONE tracer whatever library asks, which OBS-25's no-allocation rule requires -- + # and that tracer is a SPAN tracer, not an HTTPTracer; OBS-29's per-operation tracer is the + # retry steps' `http_tracer_factory:`. A documentation repair: no signature moved, and this + # passed before the YARD was written, so it pins the distinction rather than proving a repair. + test "CTX-14, OBS-29: the bundle's factory makes shared span tracers, never an HTTPTracer" do + factory = Dexpace::Instrumentation::Bundle::NONE.tracer_factory + first = factory.tracer("x", "1") + + assert_same(first, factory.tracer("y", "2")) + assert_same(first, factory.tracer(name: "x", version: "1")) + refute_kind_of(Dexpace::Instrumentation::HTTPTracer, first) + assert_kind_of(Dexpace::Instrumentation::HTTPTracer, NULL) + end end diff --git a/gems/dexpace-core/test/dexpace/model_test.rb b/gems/dexpace-core/test/dexpace/model_test.rb index 389288f..17f488c 100644 --- a/gems/dexpace-core/test/dexpace/model_test.rb +++ b/gems/dexpace-core/test/dexpace/model_test.rb @@ -89,6 +89,28 @@ def initialize(code:) end # XCUT-15: a String the caller still holds is externally-mutable state a model must not alias. + # Phase 1's review R3-1, repaired by phase 10 (HTTP-5, XCUT-15, SEAM-29): a Hash built with a + # default proc made Ractor.make_shareable raise TypeError ("allocator undefined for Proc") out of + # every model that owns a collection -- RequestOptions.build(tags:) among them -- outside `rescue + # Dexpace::Error`. The model owns the ENTRIES; the caller's default proc is behaviour and is not + # carried, and the caller's Hash keeps it. + test "own takes a Hash with a default proc, keeps its entries and drops the proc" do + tags = Hash.new { |_hash, _key| 1 }.merge("k" => "v") + options = Dexpace::RequestOptions.build(tags: tags, timeout: nil, max_retries: nil) + + assert_equal({ "k" => "v" }, options.tags) + assert_nil(options.tags["missing"]) + assert_nil(options.tags.default_proc) + refute_nil(tags.default_proc, "the caller's Hash was changed") + end + + test "own refuses a value it cannot make shareable with the SDK's error, never Ruby's" do + error = assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Model.own({ "a" => proc { 1 } }) } + + assert_includes(error.message, "cannot own") + assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Model.own({ "a" => ::Thread::Mutex.new }) } + end + test "frozen_string returns a frozen copy of a mutable string and the same frozen string" do mutable = +"Accept" copy = Dexpace::Model.frozen_string(mutable) diff --git a/gems/dexpace-core/test/dexpace/page/matrix_facts_test.rb b/gems/dexpace-core/test/dexpace/page/matrix_facts_test.rb index 1e557ba..cfac068 100644 --- a/gems/dexpace-core/test/dexpace/page/matrix_facts_test.rb +++ b/gems/dexpace-core/test/dexpace/page/matrix_facts_test.rb @@ -186,7 +186,8 @@ class SpliceAndFiberTest < DexpaceTestCase refute_same(element, owned.first) assert_predicate(owned.first, :frozen?) - assert_raises(TypeError) { Dexpace::Model.own([proc {}]) } + # Phase 10: the SDK's own error now, never Ruby's TypeError (phase 1's review R3-1). + assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Model.own([proc {}]) } assert_empty(Data.define(:a).new(a: 1).instance_variables) end diff --git a/gems/dexpace-core/test/dexpace/pipeline/standard_test.rb b/gems/dexpace-core/test/dexpace/pipeline/standard_test.rb index e4460a7..8940d08 100644 --- a/gems/dexpace-core/test/dexpace/pipeline/standard_test.rb +++ b/gems/dexpace-core/test/dexpace/pipeline/standard_test.rb @@ -194,6 +194,19 @@ class WiringTest < DexpaceTestCase refute_empty(sink.payloads) end + # 6b's review R3-1, pinned by phase 10: the async preset builds its retry step on TWO branches + # -- with an http_tracer_factory: and without -- and only the first carried a settings: + # assertion, so the second could drop `settings:` with the suite green. max_retries: 0 on the + # no-factory branch leaves a 503 unretried where the default schedule would retry it. + test "PIPE-39: settings: reaches the async retry step on the no-tracer-factory branch too" do + transport = ScriptedAsyncTransport.new([response_with(503), response_with(200)]) + pipeline = Dexpace::AsyncPipeline.standard(transport, redirect: :unsupported, + settings: settings(max_retries: 0),) + + assert_equal(503, pipeline.call(seed_request).value.status.code) + assert_equal(1, transport.calls.size) + end + test "PIPE-39: level: BODY needs preview_bytes:, threaded through both presets" do body = Dexpace::Instrumentation::HTTPLogging::BODY assert_raises(Dexpace::InvalidArgumentError) { Dexpace::Pipeline.standard(sync_transport, level: body) } diff --git a/gems/dexpace-core/test/dexpace/recovery_test.rb b/gems/dexpace-core/test/dexpace/recovery_test.rb index 31d9c3b..bd1f6bc 100644 --- a/gems/dexpace-core/test/dexpace/recovery_test.rb +++ b/gems/dexpace-core/test/dexpace/recovery_test.rb @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT require_relative "../test_helper" +require_relative "../support/pinned_ceiling" require_relative "../support/recovery_fixtures" require "dexpace" @@ -59,7 +60,9 @@ class DexpaceRecoveryTest < DexpaceTestCase cap = Dexpace::Body::MAX_BUFFERED_ERROR_BODY_BYTES response = build_response(500, body: response_body("x" * (cap + 1))) - bytes = Dexpace::Recovery.buffer_error_body(response).body_bytes + # The default ceiling, pinned: an exported MAX_MATERIALIZED_BYTES below 1 MiB clamps the cap + # and turned this red (phase 10, 5b's hand-off). + bytes = PinnedCeiling.with_default_ceiling { Dexpace::Recovery.buffer_error_body(response).body_bytes } assert_equal(cap, bytes.bytesize) assert_equal(("x" * cap).b, bytes) diff --git a/gems/dexpace-core/test/dexpace/redirect/step_test.rb b/gems/dexpace-core/test/dexpace/redirect/step_test.rb index eafc4d6..d27e980 100644 --- a/gems/dexpace-core/test/dexpace/redirect/step_test.rb +++ b/gems/dexpace-core/test/dexpace/redirect/step_test.rb @@ -813,8 +813,12 @@ class LifecycleTest < DexpaceTestCase assert_equal(["close failed"], Dexpace.suppressed(error).map(&:message)) end - test "REDIR-23: a chain of 5,000 hops is followed iteratively -- flat on the stack, in " \ - "under a few seconds" do + # REDIR-23's clause is "iteratively, without unbounded recursion", and the depth comparison + # below is the whole proof. The ten-second wall-clock bound this test used to carry was the + # "in under a few seconds" gloss, not the requirement, and it was the one load-sensitive + # assertion in the suites: 10.6 s and 12.6 s under a load average above 10 while the + # flatness half still passed (phase 8a's hand-off; phase 10 dropped the bound). + test "REDIR-23: a chain of 5,000 hops is followed iteratively -- flat on the stack" do depths = [] script = Array.new(5_000) { |i| redirect_to("https://h/#{i + 1}") } script << lambda { |_r, _o, _c| @@ -826,14 +830,12 @@ class LifecycleTest < DexpaceTestCase redirect_to("https://h/1") } script[0] = probe - started = Process.clock_gettime(Process::CLOCK_MONOTONIC) response, transport = follow(redirect_step(max_hops: 5_000), script, request: seed_request("https://h/0"),) assert_equal(200, response.status.code) assert_equal(5_001, transport.calls.size) assert_equal(depths.first, depths.last) # the 5,000th drive sits at the first drive's depth - assert_operator(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started, :<, 10.0) end end diff --git a/gems/dexpace-core/test/dexpace/registry_test.rb b/gems/dexpace-core/test/dexpace/registry_test.rb index cac3ecd..877c94e 100644 --- a/gems/dexpace-core/test/dexpace/registry_test.rb +++ b/gems/dexpace-core/test/dexpace/registry_test.rb @@ -558,4 +558,56 @@ class Swap < DexpaceTestCase assert_equal(:real, subject.resolve) end end + + # SEAM-5, XCUT-23 (phase 10, phase 9's hand-off): #resolve is a `loop do`, and phase 9 found + # that conditioning its `return hand_out(resolved)` guard on anything else spins it forever + # with no progress. The loop's termination is argued at #resolve; these pin the progress it + # rests on -- every state a caller can meet returns in a bounded time -- with the call on its + # own thread joined under a bound, so a regression fails here rather than hanging the run. + class Termination < DexpaceTestCase + include Builds + + def resolve_within(subject, seconds = 2.0) + worker = ::Thread.new { subject.resolve } + + assert(worker.join(seconds), "#resolve made no progress within #{seconds}s") + worker.value + end + + test "an installed provider over a still-registered factory returns without a scan" do + subject = registry + subject.register(:key, -> { :scanned }, core: CORE) + subject.install(:installed) + + assert_equal(:installed, resolve_within(subject)) + end + + test "a waiter whose claim owner raised retries, takes its own claim and returns" do + subject = registry + entered = ::Thread::Queue.new + release = ::Thread::Queue.new + attempts = 0 + subject.register(:key, lambda { + attempts += 1 + next :built unless attempts == 1 + + entered << true + release.pop + raise ::IOError, "the first build fails" + }, core: CORE,) + owner = ::Thread.new do + subject.resolve + rescue ::IOError => error + error + end + entered.pop + waiter = ::Thread.new { subject.resolve } + Thread.pass until waiter.status == "sleep" + release << true + + assert_kind_of(::IOError, owner.value) + assert(waiter.join(2.0), "the waiter made no progress after the owner's claim failed") + assert_equal(:built, waiter.value) + end + end end diff --git a/gems/dexpace-core/test/dexpace/resilience/recovery_retry_test.rb b/gems/dexpace-core/test/dexpace/resilience/recovery_retry_test.rb index 9518cd9..866f6df 100644 --- a/gems/dexpace-core/test/dexpace/resilience/recovery_retry_test.rb +++ b/gems/dexpace-core/test/dexpace/resilience/recovery_retry_test.rb @@ -97,13 +97,29 @@ def send_through(built, request: retry_request, cancellation: Dexpace::Cancellat transport.calls.each { |(sent, _options, _token)| assert_same(request, sent) } end + # Each thread draws from its OWN script (phase 10, phase 8a's review R1): the shared + # alternating script handed responses out in call order, so a thread switch between one + # thread's 503 and its retry gave that thread a second 503 and spent its one retry -- red two + # runs in five under load on 3.2.11, a scheduling outcome rather than a budget leak. Keyed by + # thread, the budget invariant is asserted without assuming which response a call draws. test "RETRY-42 / RECOV-28: eight concurrent calls through one engine keep their budgets apart" do - built, transport = engine(Array.new(16) { |i| retry_response(i.even? ? 503 : 200) }, - settings: retry_settings(max_retries: 1),) - results = Array.new(8) { ::Thread.new { send_through(built).status.code } }.map(&:value) + scripts = ::Hash.new { |hash, thread| hash[thread] = [503, 200] }.compare_by_identity + lock = ::Thread::Mutex.new + calls = [] + transport = lambda do |_request, _options, _cancellation| + code = lock.synchronize do + calls << ::Thread.current + scripts[::Thread.current].shift + end + retry_response(code) + end + built = RecoveryRetry.build(transport: transport, settings: retry_settings(max_retries: 1)) + threads = Array.new(8) { ::Thread.new { send_through(built).status.code } } + results = threads.map(&:value) - assert_equal(16, transport.calls.size) - assert_equal(8, results.count(200), results.inspect) + assert_equal(16, calls.size) + assert_equal([2] * 8, threads.map { |thread| calls.count { |caller| caller.equal?(thread) } }) + assert_equal([200] * 8, results) end # RECOV-17, RECOV-18, RETRY-4, RETRY-25, RETRY-37: the two axes, on this stack. diff --git a/gems/dexpace-core/test/dexpace_test.rb b/gems/dexpace-core/test/dexpace_test.rb index db23a81..3be79eb 100644 --- a/gems/dexpace-core/test/dexpace_test.rb +++ b/gems/dexpace-core/test/dexpace_test.rb @@ -222,7 +222,7 @@ class Layers < DexpaceTestCase # the way the three phase-4 lanes once did. class PhaseSixLayers < DexpaceTestCase # A consumer requires "dexpace" and nothing else: the retry layer resolves too (phase 6a) -- - # the five public Resilience constants, the flat error, and the two private helpers and the + # the six public Resilience constants, the flat error, and the two private helpers and the # two private per-call classes as unreachable as Dexpace::Hooks. Phase 6b and 6c add their # own constants under Resilience beside these. test "requiring dexpace alone makes the whole retry layer resolve, its helpers private" do diff --git a/gems/dexpace-core/test/support/pinned_ceiling.rb b/gems/dexpace-core/test/support/pinned_ceiling.rb new file mode 100644 index 0000000..c881981 --- /dev/null +++ b/gems/dexpace-core/test/support/pinned_ceiling.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# Pins the materialisation ceiling to its default for one block, at the OVERRIDE tier -- the one +# CFG-1 puts above the environment -- so a test asserting against IO::MAX_MATERIALIZED_BYTES +# means the default whatever the host exports. Phase 10's repair of four phase-3b and phase-4b +# tests that asserted the default while the code reads the live value per call (5a's P5-56): +# with MAX_MATERIALIZED_BYTES=4096 exported, all four failed. The slot is restored in `ensure` +# (testing/4ef070df). +module PinnedCeiling + extend self + + def with_default_ceiling + Dexpace.configure do |builder| + builder.override(Dexpace::Configuration::Keys::MAX_MATERIALIZED_BYTES, + Dexpace::IO::MAX_MATERIALIZED_BYTES.to_s,) + end + yield + ensure + Dexpace.reset_config! + end +end diff --git a/gems/dexpace-serde-json/sig/dexpace/serde/json.rbs b/gems/dexpace-serde-json/sig/dexpace/serde/json.rbs index 6bad8ff..cb12253 100644 --- a/gems/dexpace-serde-json/sig/dexpace/serde/json.rbs +++ b/gems/dexpace-serde-json/sig/dexpace/serde/json.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde # The reference wire codec over Ruby's json: the floor it asserts (P7-7), the core constraint diff --git a/gems/dexpace-serde-json/sig/dexpace/serde/json/codec.rbs b/gems/dexpace-serde-json/sig/dexpace/serde/json/codec.rbs index 2e71e9e..929278e 100644 --- a/gems/dexpace-serde-json/sig/dexpace/serde/json/codec.rbs +++ b/gems/dexpace-serde-json/sig/dexpace/serde/json/codec.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde module JSON diff --git a/gems/dexpace-serde-json/sig/dexpace/serde/json/version.rbs b/gems/dexpace-serde-json/sig/dexpace/serde/json/version.rbs index cce65f5..ec503da 100644 --- a/gems/dexpace-serde-json/sig/dexpace/serde/json/version.rbs +++ b/gems/dexpace-serde-json/sig/dexpace/serde/json/version.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Serde module JSON diff --git a/gems/dexpace-transport-async_http/lib/dexpace/transport/async_http/response_mapper.rb b/gems/dexpace-transport-async_http/lib/dexpace/transport/async_http/response_mapper.rb index 402f679..6514a63 100644 --- a/gems/dexpace-transport-async_http/lib/dexpace/transport/async_http/response_mapper.rb +++ b/gems/dexpace-transport-async_http/lib/dexpace/transport/async_http/response_mapper.rb @@ -12,11 +12,11 @@ module AsyncHTTP # response object exists: a malformed inbound header NAME (`Protocol::HTTP1::BadHeader`, # P8-38, the TRANSPORT-14 waiver) and a non-numeric Content-Length # (`Protocol::HTTP1::BadRequest`, the TRANSPORT-27 waiver) -- both wrap as retryable - # transport failures. Dexpace::Protocol admits HTTP/1.1 and HTTP/2 only (HTTP-33) and - # Dexpace::Status 100-599 (HTTP-10's reading), so an `HTTP/1.0` head and a `999` status each - # raise Dexpace::InvalidArgumentError here, after the head, with the native body closed by - # the exchange's own release -- the disposition dexpace-transport-net_http records, and the - # two phase-1 questions on phase 10's inbound list. A private_constant of AsyncHTTP. + # transport failures. Since phase 10 Dexpace::Protocol admits HTTP/1.0 beside HTTP/1.1 and + # HTTP/2 (HTTP-33) and Dexpace::Status every Integer code (HTTP-10, TRANSPORT-24), so an + # `HTTP/1.0` head and a vendor `999` both map; a version the model does not know still + # raises Dexpace::InvalidArgumentError here, after the head, with the native body closed by + # the exchange's own release. A private_constant of AsyncHTTP. module ResponseMapper extend self diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http.rbs index 2b52a4a..0d5d584 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Transport # The reference asynchronous transport over async-http (phase 8c). Nothing from async, diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/adapter.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/adapter.rbs index 7f6997c..cd331fb 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/adapter.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/adapter.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Transport module AsyncHTTP diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/clients.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/clients.rbs index 7bf3aed..8ec5bbb 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/clients.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/clients.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::AsyncHTTP::Clients is a private_constant and not public API: this # declaration exists because the `async_http` Steep target checks every file under lib/ and needs # the class declared to type Adapter's call sites. RBS has no visibility for a constant, so the diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/drop_policy.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/drop_policy.rbs index cf86ae4..461823f 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/drop_policy.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/drop_policy.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Transport module AsyncHTTP diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/endpoints.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/endpoints.rbs index b30599e..84fab2a 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/endpoints.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/endpoints.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::AsyncHTTP::Endpoints is a private_constant and not public API: this # declaration exists because the `async_http` Steep target checks every file under lib/ and needs # the module declared to type Clients' and RequestMapper's call sites. RBS has no visibility for a diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/errors.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/errors.rbs index 0e2ed08..28e8d7e 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/errors.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/errors.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::AsyncHTTP::Errors is a private_constant and not public API: this # declaration exists because the `async_http` Steep target checks every file under lib/ and needs # the module and its three functions declared to type Adapter's, Exchange's and ResponseBody's diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/exchange.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/exchange.rbs index ea6fc01..8e5c243 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/exchange.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/exchange.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::AsyncHTTP::Exchange is a private_constant and not public API: this # declaration exists because the `async_http` Steep target checks every file under lib/ and needs # the class declared to type Adapter's call site. RBS has no visibility for a constant, so the diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/request_body.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/request_body.rbs index a8fd436..7a99264 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/request_body.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/request_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::AsyncHTTP::RequestBody is a private_constant and not public API: this # declaration exists because the `async_http` Steep target checks every file under lib/ and needs # the class declared to type RequestMapper's call site. RBS has no visibility for a constant, so diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/request_mapper.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/request_mapper.rbs index b268bc3..922d090 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/request_mapper.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/request_mapper.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::AsyncHTTP::RequestMapper is a private_constant and not public API: this # declaration exists because the `async_http` Steep target checks every file under lib/ and needs # the module and its one function declared to type Adapter's call site. RBS has no visibility for diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/response_body.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/response_body.rbs index c042eed..b39c7ce 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/response_body.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/response_body.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::AsyncHTTP::ResponseBody is a private_constant and not public API: this # declaration exists because the `async_http` Steep target checks every file under lib/ and needs # the class declared to type ResponseMapper's call site. RBS has no visibility for a constant, so diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/response_mapper.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/response_mapper.rbs index c1cab97..02c1861 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/response_mapper.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/response_mapper.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::AsyncHTTP::ResponseMapper is a private_constant and not public API: this # declaration exists because the `async_http` Steep target checks every file under lib/ and needs # the module and its one function declared to type Exchange's call site. RBS has no visibility for diff --git a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/version.rbs b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/version.rbs index ef1b356..3c049db 100644 --- a/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/version.rbs +++ b/gems/dexpace-transport-async_http/sig/dexpace/transport/async_http/version.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Transport module AsyncHTTP diff --git a/gems/dexpace-transport-async_http/test/dexpace/transport/async_http/fiber_single_flight_test.rb b/gems/dexpace-transport-async_http/test/dexpace/transport/async_http/fiber_single_flight_test.rb new file mode 100644 index 0000000..43eaa54 --- /dev/null +++ b/gems/dexpace-transport-async_http/test/dexpace/transport/async_http/fiber_single_flight_test.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +require_relative "../../../test_helper" +require "dexpace/transport/async_http" + +# Exercises: XCUT-12 (its fiber-scheduler form), XCUT-11, AUTH-34, AUTH-35, AUTH-37, ASYNC-6. +# +# Phase 10's verification of XCUT-12 under a fiber scheduler, the form phase 9 could not assemble +# (dexpace-conformance declares dexpace-core alone and cannot open a reactor) and routed here. +# Phase 10's design (R9) expected a lock held across a fetch to DEADLOCK two fibers of one thread; +# measured on 3.3.12 and 4.0.6 with async 2.46.0, it does not -- `Thread::Mutex` is per-fiber and +# a second fiber blocking on it parks on the scheduler -- so this is recorded as a verification, +# not a repair (phase 10's P10-21). What a thread-only race CANNOT see, and this can, is a +# single-flight guard keyed by THREAD identity: every fiber of one reactor shares one thread, so +# such a guard lets every fiber fetch. The racers here are fibers of one reactor on one thread, +# asserted rather than assumed, and the provider SUSPENDS inside #fetch -- a Thread::Queue pop, +# which a fiber scheduler routes through the reactor -- so every racer reaches the refresh before +# the one fetch completes. Lives in this gem because it already declares async-http (8a's +# precedent of a driver inside the adapter gem); runs on 3.3+ only (P8-36). +class AsyncHTTPFiberSingleFlightTest < DexpaceTestCase + RACERS = 8 + + # A provider whose #fetch counts itself and suspends until the test releases it. + class AsyncHTTPParkedProvider + attr_reader :fetches + + def initialize + @fetches = 0 + @gate = ::Thread::Queue.new + end + + def fetch + @fetches += 1 + @gate.pop + Dexpace::Auth::BearerToken.build(token: "t#{@fetches}", expiry: ::Time.now + 3600) + end + + # One pass for every racer, so a guard that let every fiber fetch fails the count rather than + # parking the reactor forever. + def release = RACERS.times { @gate << true } + end + + def request + Dexpace::Request.build(method: "GET", url: "https://example.test/", headers: Dexpace::Headers::EMPTY) + end + + # Starts RACERS fibers under one reactor, each recording its thread and calling `stamp`, lets + # every one run to its suspension point, then releases the provider. + def race(provider, &stamp) + threads = [] + stamped = [] + Sync do |task| + fibers = Array.new(RACERS) do + task.async do + threads << ::Thread.current + stamped << yield(request) + end + end + task.yield until threads.size == RACERS + provider.release + fibers.each(&:wait) + end + [threads, stamped] + end + + test "XCUT-12, AUTH-34: BearerStamper refreshes once for every fiber of one reactor" do + provider = AsyncHTTPParkedProvider.new + stamper = Dexpace::Auth::BearerStamper.new(provider: provider) + + threads, stamped = race(provider) { |req| stamper.call(req) } + + assert_equal(1, threads.uniq.size, "the racers were fibers of one thread") + assert_equal(1, provider.fetches, "a refresh under one reactor was not single-flight") + assert_equal(["Bearer t1"], stamped.map { |r| r.headers["Authorization"] }.flatten.uniq) + end + + test "XCUT-12, AUTH-37: AsyncBearerStamper coalesces every fiber onto one fetch" do + provider = AsyncHTTPParkedProvider.new + stamper = Dexpace::Auth::AsyncBearerStamper.new(provider: provider) + + threads, stamped = race(provider) { |req| stamper.stamp(req).value } + + assert_equal(1, threads.uniq.size, "the racers were fibers of one thread") + assert_equal(1, provider.fetches, "the async stamper fetched more than once") + assert_equal(RACERS, stamped.size) + end + + # AUTH-35 under the same reactor: a provider result the stamper rejects is not cached, so the + # next fiber fetches again -- single-flight must not mean "a failure is shared forever". + test "AUTH-35: a rejected fetch caches nothing, and the next fiber fetches afresh" do + results = [nil, Dexpace::Auth::BearerToken.build(token: "ok", expiry: ::Time.now + 3600)] + provider = Object.new + provider.define_singleton_method(:fetch) { results.shift } + stamper = Dexpace::Auth::BearerStamper.new(provider: provider) + + Sync do + assert_raises(Dexpace::Auth::ProviderError) { stamper.call(request) } + assert_equal(["Bearer ok"], stamper.call(request).headers["Authorization"]) + end + end +end diff --git a/gems/dexpace-transport-async_http/test/dexpace/transport/async_http/parent_cancellation_test.rb b/gems/dexpace-transport-async_http/test/dexpace/transport/async_http/parent_cancellation_test.rb index aa13ea1..6d4aa46 100644 --- a/gems/dexpace-transport-async_http/test/dexpace/transport/async_http/parent_cancellation_test.rb +++ b/gems/dexpace-transport-async_http/test/dexpace/transport/async_http/parent_cancellation_test.rb @@ -148,7 +148,8 @@ class CloseDisciplineTest < DexpaceTestCase end end - # TRANSPORT-22's adaptation-failure half: a head phase 1's model refuses -- a 999 status -- + # TRANSPORT-22's adaptation-failure half: a head phase 1's model refuses -- an HTTP/1.2 version, + # a 999 status until phase 10 widened Status -- # raises InvalidArgumentError through the future, and the native body the exchange was holding # is closed exactly once before the future settles. test "TRANSPORT-22: an adaptation failure after the head closes the native body exactly once " \ @@ -158,7 +159,7 @@ class CloseDisciplineTest < DexpaceTestCase client.define_singleton_method(:retries) { 0 } client.define_singleton_method(:pool) { Object.new.tap { |pool| def pool.close = nil } } client.define_singleton_method(:call) do |_native| - ::Protocol::HTTP::Response.new("HTTP/1.1", 999, ::Protocol::HTTP::Headers.new, native) + ::Protocol::HTTP::Response.new("HTTP/1.2", 200, ::Protocol::HTTP::Headers.new, native) end adapter = AsyncHTTP.using(client) @@ -187,7 +188,7 @@ class CloseDisciplineTest < DexpaceTestCase client.define_singleton_method(:retries) { 0 } client.define_singleton_method(:pool) { Object.new.tap { |pool| def pool.close = nil } } client.define_singleton_method(:call) do |_native| - ::Protocol::HTTP::Response.new("HTTP/1.1", 999, ::Protocol::HTTP::Headers.new, native) + ::Protocol::HTTP::Response.new("HTTP/1.2", 200, ::Protocol::HTTP::Headers.new, native) end adapter = AsyncHTTP.using(client) diff --git a/gems/dexpace-transport-async_http/test/dexpace/transport/async_http/response_mapper_test.rb b/gems/dexpace-transport-async_http/test/dexpace/transport/async_http/response_mapper_test.rb index 5aee477..8d1ebf2 100644 --- a/gems/dexpace-transport-async_http/test/dexpace/transport/async_http/response_mapper_test.rb +++ b/gems/dexpace-transport-async_http/test/dexpace/transport/async_http/response_mapper_test.rb @@ -60,12 +60,16 @@ class HeadTest < DexpaceTestCase assert_equal("http/2", response.protocol.wire) end - # Phase 1's Status guards 100-599 and Protocol admits HTTP/1.1 and HTTP/2: a head outside either - # is InvalidArgumentError after the head, the disposition 8a records and phase 10 decides. - test "a status outside 100-599 or an HTTP/1.0 head raises InvalidArgumentError from the " \ - "model" do - assert_raises(Dexpace::InvalidArgumentError) { map(native_response(status: 999)) } - assert_raises(Dexpace::InvalidArgumentError) { map(native_response(version: "HTTP/1.0")) } + # HTTP-10, HTTP-33, TRANSPORT-24: phase 10 widened Status to every code a status line can carry + # and Protocol by HTTP/1.0, the two phase-1 questions 8a routed. Until then this test asserted + # both heads raised; it is that pin, inverted. + test "a vendor 999 status and an HTTP/1.0 head both map" do + assert_equal(999, map(native_response(status: 999)).status.code) + assert_equal("http/1.0", map(native_response(version: "HTTP/1.0")).protocol.wire) + end + + test "a version the model does not know is InvalidArgumentError after the head (HTTP-33)" do + assert_raises(Dexpace::InvalidArgumentError) { map(native_response(version: "HTTP/1.2")) } end test "TRANSPORT-14: a control byte in an inbound value is dropped, that header only, and " \ diff --git a/gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/request_mapper.rb b/gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/request_mapper.rb index 22cedcb..fb859d4 100644 --- a/gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/request_mapper.rb +++ b/gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/request_mapper.rb @@ -74,7 +74,7 @@ def copy_headers(request, native, logger) end # P8-3: the switch is `#[]=`, never `#add_field`, because only `#[]=` flips - # @decode_content off as a side effect. Re-assigning the caller's own value preserves it + # `@decode_content` off as a side effect. Re-assigning the caller's own value preserves it # while still tripping the flip; assigning then deleting leaves the flag off and the # header absent when the caller set none. def suppress_decode_content(native) diff --git a/gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/response_mapper.rb b/gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/response_mapper.rb index eda21cd..9c073ea 100644 --- a/gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/response_mapper.rb +++ b/gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/response_mapper.rb @@ -11,13 +11,12 @@ module NetHTTP # connection-close framing takes over instead of Net::HTTPResponse#content_length raising # Net::HTTPHeaderSyntaxError. Headers come from `#to_hash` and nothing else: `#[]` joins # with ", " and `#each_capitalized` re-cases, both of which would corrupt a multi-valued - # Set-Cookie. Dexpace::Protocol admits HTTP/1.1 and HTTP/2 only (HTTP-33), so an HTTP/1.0 - # response raises Dexpace::InvalidArgumentError here; no TRANSPORT ID asks for 1.0 and the - # gap is phase 1's to widen. The status is total over 100-599, phase 1's Status guard - # (HTTP-10's reading): Net::HTTP parses any three digits and delivers a 999 or a 600 as an - # HTTPUnknownResponse, and such a head raises Dexpace::InvalidArgumentError here the same - # way, after the head and with the connection released (TRANSPORT-22); whether TRANSPORT-24's - # "any code" reaches 600-999 is a phase-1 model question on phase 10's inbound list. + # Set-Cookie. Since phase 10 Dexpace::Protocol admits HTTP/1.0 beside HTTP/1.1 and HTTP/2 + # (HTTP-33), so an HTTP/1.0 response maps; a version it does not know (HTTP/1.2) still + # raises Dexpace::InvalidArgumentError here, after the head and with the connection released + # (TRANSPORT-22). The status is total over every code a status line can carry: Net::HTTP + # parses any three decimal places and delivers a 999 or a 600 as an HTTPUnknownResponse, + # and Dexpace::Status maps every Integer (HTTP-10, TRANSPORT-24). # A private_constant of NetHTTP. module ResponseMapper extend self diff --git a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http.rbs b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http.rbs index b7e9924..752af39 100644 --- a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http.rbs +++ b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Transport # The reference synchronous transport over net/http (phase 8a). Nothing from net/http names diff --git a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/adapter.rbs b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/adapter.rbs index e29a89d..596cbb1 100644 --- a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/adapter.rbs +++ b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/adapter.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Transport module NetHTTP diff --git a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/deadline.rbs b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/deadline.rbs index 2fd9713..1ce3c5b 100644 --- a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/deadline.rbs +++ b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/deadline.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::NetHTTP::Deadline is a private_constant and not public API: this declaration # exists because the `net_http` Steep target checks every file under lib/ and needs the class # declared to type Adapter's and ResponsePump's call sites. RBS has no visibility for a constant, diff --git a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/failures.rbs b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/failures.rbs index 1b56eeb..78a020f 100644 --- a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/failures.rbs +++ b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/failures.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::NetHTTP::Failures is a private_constant and not public API: this declaration # exists because the `net_http` Steep target checks every file under lib/ and needs the module and # its one function declared to type Adapter's and ResponsePump's call sites. RBS has no visibility diff --git a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/proxy_route.rbs b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/proxy_route.rbs index 17aa987..7dfd332 100644 --- a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/proxy_route.rbs +++ b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/proxy_route.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::NetHTTP::ProxyRoute is a private_constant and not public API: this # declaration exists because the `net_http` Steep target checks every file under lib/ and needs # the snapshot declared to type Adapter's call site. RBS has no visibility for a constant, so the diff --git a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/request_mapper.rbs b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/request_mapper.rbs index dab4ed5..7911273 100644 --- a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/request_mapper.rbs +++ b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/request_mapper.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::NetHTTP::RequestMapper is a private_constant and not public API: this # declaration exists because the `net_http` Steep target checks every file under lib/ and needs # the module and its one function declared to type Adapter's call site. RBS has no visibility for diff --git a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/response_mapper.rbs b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/response_mapper.rbs index 4e3772c..15b7916 100644 --- a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/response_mapper.rbs +++ b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/response_mapper.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::NetHTTP::ResponseMapper is a private_constant and not public API: this # declaration exists because the `net_http` Steep target checks every file under lib/ and needs # the module and its one function declared to type Adapter's call site. RBS has no visibility for diff --git a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/response_pump.rbs b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/response_pump.rbs index d811195..61fca39 100644 --- a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/response_pump.rbs +++ b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/response_pump.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::NetHTTP::ResponsePump is a private_constant and not public API: this # declaration exists because the `net_http` Steep target checks every file under lib/ and needs # the class declared to type Adapter's and ResponseMapper's call sites. RBS has no visibility for diff --git a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/tls_settings.rbs b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/tls_settings.rbs index 53e4343..d7a9f11 100644 --- a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/tls_settings.rbs +++ b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/tls_settings.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # Dexpace::Transport::NetHTTP::TLSSettings is a private_constant and not public API: this # declaration exists because the `net_http` Steep target checks every file under lib/ and needs # the module and its two functions declared to type NetHTTP.build's and Adapter's call sites. diff --git a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/version.rbs b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/version.rbs index a24427c..f6ce108 100644 --- a/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/version.rbs +++ b/gems/dexpace-transport-net_http/sig/dexpace/transport/net_http/version.rbs @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module Dexpace module Transport module NetHTTP diff --git a/gems/dexpace-transport-net_http/test/dexpace/transport/net_http/adapter_test.rb b/gems/dexpace-transport-net_http/test/dexpace/transport/net_http/adapter_test.rb index 6b49409..b26a7d8 100644 --- a/gems/dexpace-transport-net_http/test/dexpace/transport/net_http/adapter_test.rb +++ b/gems/dexpace-transport-net_http/test/dexpace/transport/net_http/adapter_test.rb @@ -375,14 +375,16 @@ class WireTest < DexpaceTestCase assert_equal(1, server.await_closed_connection(timeout: 2)) end - # The body is UNDRAINABLE -- a hundred bytes declared, two sent, the connection held -- so + # The head is an HTTP/1.2 status line, which Net::HTTP parses and Dexpace::Protocol refuses + # (HTTP-33) -- an HTTP/1.0 head until phase 10 added that wire form. The body is UNDRAINABLE + # -- a hundred bytes declared, two sent, the connection held -- so # only the adapter's own close in Adapter#dispatch's rescue can release it: with that guard # removed the producer stays blocked in read_body on the 98 bytes that never come, the server # never sees the peer close, and the bound below elapses. A two-byte body drained itself and # released the connection whether or not the guard existed (review round 0's mutation H). test "TRANSPORT-22: an adaptation failure after the head releases the connection and raises" do server = wire(lambda do |conn, _head| - conn.write("HTTP/1.0 200 OK\r\nContent-Length: 100\r\n\r\nok") + conn.write("HTTP/1.2 200 OK\r\nContent-Length: 100\r\n\r\nok") conn.read # holds the connection, and the 98 undelivered bytes, until the peer closes it end) diff --git a/gems/dexpace-transport-net_http/test/dexpace/transport/net_http/response_mapper_test.rb b/gems/dexpace-transport-net_http/test/dexpace/transport/net_http/response_mapper_test.rb index b31f8e2..6e1ca7c 100644 --- a/gems/dexpace-transport-net_http/test/dexpace/transport/net_http/response_mapper_test.rb +++ b/gems/dexpace-transport-net_http/test/dexpace/transport/net_http/response_mapper_test.rb @@ -120,10 +120,20 @@ class HeadTest < DexpaceTestCase assert_equal(%w[a=1 b=2], response.headers["Set-Cookie"]) end - test "an HTTP/1.0 head raises InvalidArgumentError: Protocol admits 1.1 and 2 only (HTTP-33)" do + # HTTP-33, TRANSPORT-24: phase 10 added http/1.0 to Protocol's wire forms. Until then this head + # raised Dexpace::InvalidArgumentError out of the mapper, and a real HTTP/1.0 server could + # reach it; the pin that asserted the raise is this test, inverted. + test "an HTTP/1.0 head maps, carrying Protocol::HTTP_1_0 (HTTP-33)" do native = ::Net::HTTPOK.new("1.0", "200", "OK") native["Content-Length"] = "0" + assert_equal("http/1.0", build(native).protocol.wire) + end + + test "a version Protocol does not know still raises InvalidArgumentError (HTTP-33)" do + native = ::Net::HTTPOK.new("1.2", "200", "OK") + native["Content-Length"] = "0" + assert_raises(Dexpace::InvalidArgumentError) { build(native) } end end diff --git a/rbs_collection.yaml b/rbs_collection.yaml index 7812de5..c3b91ac 100644 --- a/rbs_collection.yaml +++ b/rbs_collection.yaml @@ -1,8 +1,10 @@ -# Third-party RBS, resolved from the gems the bundle actually holds. No third-party row exists -# in phase 0 because no gemspec declares a third-party dependency yet: json arrives with -# dexpace-serde-json's codec in phase 7, net-http and async-http with the transports in phase 8, -# and each adds its own row here then. Core never gets one -- its allowlist is stdlib only, and -# stdlib signatures ship with rbs itself. +# Third-party RBS, resolved from the gems the bundle actually holds. As built, no third-party +# gem needed a row to be ADDED: json's and net-http's signatures are rbs's own stdlib set (`rbs +# collection install` resolves both with `source: type: stdlib`, and json 3.0.2 ships no sig/ -- +# the codec's one JSON::Coder reference is settled in the Steepfile's :serde_json target), and +# async-http's closure needs one row that IGNORES a stale entry, below. Core never gets one -- its +# allowlist is stdlib only. (Corrected by phase 10: this header said json and net-http would each +# "add its own row here", which neither did.) sources: - type: git name: ruby/gem_rbs_collection diff --git a/tasks/gates.rake b/tasks/gates.rake index a3b05b8..63edf8e 100644 --- a/tasks/gates.rake +++ b/tasks/gates.rake @@ -395,4 +395,49 @@ namespace :gates do puts "gates:seam_names: #{core_files(root).size} core files name no adapter (SEAM-2)." end + + # Phase 10's three gates (design addenda A8, A9, A11). Each reads absolute paths joined to + # gate_root and relativises only its message -- P9-27's lesson, applied from the first line. + # + # gates:ledger_audit's third assertion resolves every `Dexpace::` constant a verdict row names, so + # the task loads core and every adapter whose Ruby floor this interpreter meets; a gem it cannot + # load leaves its constants unresolvable and the row an offence, which is the safe direction. The + # gate runs in CI's `gates` job, on the development Ruby. + desc "P10-2: docs/deviations.md still matches design section 10, every verdict citing real code" + task :ledger_audit do + require_relative "../tools/ledger_audit" + require_relative "../tools/versions" + CLEAN_BUNDLE_ENTRIES.each do |name, (feature, _constant)| + next unless DexpaceVersions.gem_supported?(name) + + require feature + end + found = LedgerAudit.offences(root: gate_root) + abort(found.join("\n")) unless found.empty? + + puts "gates:ledger_audit: every register row matches its design section 10 entry, " \ + "and every verdict cites resolvable as-built evidence." + end + + desc "NFR-13, NFR-3: every shipped signature carries the SPDX header and declares something" + task :spdx_rbs do + require_relative "../tools/spdx_rbs" + root = gate_root + paths = Dir.glob(File.join(root, SpdxRbs::GLOB)) + found = SpdxRbs.offences(root: root, paths: paths) + abort(relative_to(root, found).join("\n")) unless found.empty? + + puts "gates:spdx_rbs: #{paths.size} shipped signatures carry the header and declare something." + end + + desc "NFR-6: AstScan.parse is the only RubyVM::AbstractSyntaxTree.parse_file in the tooling" + task :sole_parse do + require_relative "../tools/sole_parse" + root = gate_root + found = SoleParse.offences(root: root) + abort(relative_to(root, found).join("\n")) unless found.empty? + + puts "gates:sole_parse: AstScan.parse is the one parse_file; " \ + "#{SoleParse::ALLOWED.size} deliberate test contrasts allowlisted with reasons." + end end diff --git a/tasks/quality.rake b/tasks/quality.rake index b6ca086..97b37e7 100644 --- a/tasks/quality.rake +++ b/tasks/quality.rake @@ -13,8 +13,12 @@ require_relative "../tools/interpreter" def bundle = Interpreter.executable("bundle") desc "NFR-7: RuboCop, findings fatal, no autocorrection" +# `--ignore-parent-exclusion`: RuboCop takes AllCops/Exclude from the topmost .rubocop.yml on the +# path, so a worktree nested under a parent checkout's `.claude/` inherited that checkout's +# `.claude/**/*` exclusion and the gate inspected almost nothing (phase 10, phase 1's finding). task :rubocop do - sh(bundle, "exec", "rubocop", "--fail-level=convention", "--format", "progress") + sh(bundle, "exec", "rubocop", "--fail-level=convention", "--ignore-parent-exclusion", "--format", + "progress",) end # `--autocorrect` (safe only), never `--autocorrect-all`: `-A` applies unsafe corrections too, and diff --git a/test/fixtures/gates/ledger/chapter.md b/test/fixtures/gates/ledger/chapter.md new file mode 100644 index 0000000..0e1e197 --- /dev/null +++ b/test/fixtures/gates/ledger/chapter.md @@ -0,0 +1,7 @@ +## 10. A synthetic two-entry ledger for gates:ledger_audit's own tests + +1. **The probe seam is retired; its contract is not.** *Touches* **SEAM-3**–**SEAM-5**, **IO-6**. + *Judged.* Nothing else. +2. **A second entry about ownership.** *Touches* **BODY-8**. *Judged.* + +Two things that are deliberately **not** deviations: none, in this fixture. diff --git a/test/fixtures/gates/ledger/gems/dexpace-probe/lib/probe.rb b/test/fixtures/gates/ledger/gems/dexpace-probe/lib/probe.rb new file mode 100644 index 0000000..6703112 --- /dev/null +++ b/test/fixtures/gates/ledger/gems/dexpace-probe/lib/probe.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# A file that exists, so a ledger row can cite it as evidence. +module Dexpace + module Probe; end +end diff --git a/test/fixtures/gates/ledger/register_docs_only.md b/test/fixtures/gates/ledger/register_docs_only.md new file mode 100644 index 0000000..a354e66 --- /dev/null +++ b/test/fixtures/gates/ledger/register_docs_only.md @@ -0,0 +1,4 @@ +| # | Title | IDs touched | Status | +|---|---|---|---| +| 1 | The probe seam is retired; its contract is not | SEAM-3–SEAM-5, IO-6 | as-built: confirmed — see `docs/work/mvp/phase3/phase3a/some-design.md` | +| 2 | A second entry about ownership | BODY-8 | design only — not yet built | diff --git a/test/fixtures/gates/ledger/register_extra_ids.md b/test/fixtures/gates/ledger/register_extra_ids.md new file mode 100644 index 0000000..fdc616c --- /dev/null +++ b/test/fixtures/gates/ledger/register_extra_ids.md @@ -0,0 +1,4 @@ +| # | Title | IDs touched | Status | +|---|---|---|---| +| 1 | The probe seam is retired; its contract is not | SEAM-3–SEAM-5, IO-6 | design only — not yet built | +| 2 | A second entry about ownership | BODY-8, BODY-9 | design only — not yet built | diff --git a/test/fixtures/gates/ledger/register_ok.md b/test/fixtures/gates/ledger/register_ok.md new file mode 100644 index 0000000..620d894 --- /dev/null +++ b/test/fixtures/gates/ledger/register_ok.md @@ -0,0 +1,4 @@ +| # | Title | IDs touched | Status | +|---|---|---|---| +| 1 | The probe seam is retired; its contract is not | SEAM-3–SEAM-5, IO-6 | as-built: confirmed — `gems/dexpace-probe/lib/probe.rb` defines `Dexpace::Probe` | +| 2 | A second entry about ownership | BODY-8 | design only — not yet built | diff --git a/test/fixtures/gates/ledger/register_swapped.md b/test/fixtures/gates/ledger/register_swapped.md new file mode 100644 index 0000000..9b6827a --- /dev/null +++ b/test/fixtures/gates/ledger/register_swapped.md @@ -0,0 +1,4 @@ +| # | Title | IDs touched | Status | +|---|---|---|---| +| 1 | A second entry about ownership | BODY-8 | design only — not yet built | +| 2 | The probe seam is retired; its contract is not | SEAM-3–SEAM-5, IO-6 | design only — not yet built | diff --git a/test/fixtures/gates/ledger/register_undefined.md b/test/fixtures/gates/ledger/register_undefined.md new file mode 100644 index 0000000..4346e86 --- /dev/null +++ b/test/fixtures/gates/ledger/register_undefined.md @@ -0,0 +1,4 @@ +| # | Title | IDs touched | Status | +|---|---|---|---| +| 1 | The probe seam is retired; its contract is not | SEAM-3–SEAM-5, IO-6 | as-built: confirmed — `gems/dexpace-probe/lib/probe.rb` defines `Dexpace::Completer` | +| 2 | A second entry about ownership | BODY-8 | design only — not yet built | diff --git a/test/fixtures/gates/ledger/register_wrong_ids.md b/test/fixtures/gates/ledger/register_wrong_ids.md new file mode 100644 index 0000000..50a00c2 --- /dev/null +++ b/test/fixtures/gates/ledger/register_wrong_ids.md @@ -0,0 +1,4 @@ +| # | Title | IDs touched | Status | +|---|---|---|---| +| 1 | The probe seam is retired; its contract is not | SEAM-3–SEAM-5 | design only — not yet built | +| 2 | A second entry about ownership | BODY-8 | design only — not yet built | diff --git a/test/fixtures/gates/ledger/workspace/docs/deviations.md b/test/fixtures/gates/ledger/workspace/docs/deviations.md new file mode 100644 index 0000000..50a00c2 --- /dev/null +++ b/test/fixtures/gates/ledger/workspace/docs/deviations.md @@ -0,0 +1,4 @@ +| # | Title | IDs touched | Status | +|---|---|---|---| +| 1 | The probe seam is retired; its contract is not | SEAM-3–SEAM-5 | design only — not yet built | +| 2 | A second entry about ownership | BODY-8 | design only — not yet built | diff --git a/test/fixtures/gates/ledger/workspace/docs/sdk-design-ruby/10-deliberate-deviations-from-the-reference-contract.md b/test/fixtures/gates/ledger/workspace/docs/sdk-design-ruby/10-deliberate-deviations-from-the-reference-contract.md new file mode 100644 index 0000000..0e1e197 --- /dev/null +++ b/test/fixtures/gates/ledger/workspace/docs/sdk-design-ruby/10-deliberate-deviations-from-the-reference-contract.md @@ -0,0 +1,7 @@ +## 10. A synthetic two-entry ledger for gates:ledger_audit's own tests + +1. **The probe seam is retired; its contract is not.** *Touches* **SEAM-3**–**SEAM-5**, **IO-6**. + *Judged.* Nothing else. +2. **A second entry about ownership.** *Touches* **BODY-8**. *Judged.* + +Two things that are deliberately **not** deviations: none, in this fixture. diff --git a/test/fixtures/gates/ledger/workspace/gems/dexpace-probe/lib/probe.rb b/test/fixtures/gates/ledger/workspace/gems/dexpace-probe/lib/probe.rb new file mode 100644 index 0000000..6703112 --- /dev/null +++ b/test/fixtures/gates/ledger/workspace/gems/dexpace-probe/lib/probe.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# A file that exists, so a ledger row can cite it as evidence. +module Dexpace + module Probe; end +end diff --git a/test/fixtures/gates/require_allowlist/dot_segments.rb b/test/fixtures/gates/require_allowlist/dot_segments.rb new file mode 100644 index 0000000..9fea85b --- /dev/null +++ b/test/fixtures/gates/require_allowlist/dot_segments.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# Gate fixture. A `dexpace/` prefix that climbs back out of the namespace: `dexpace/../json` is +# `json` to Kernel#require, and a prefix test reads it as the gem's own path (phase 0's R3-2). +require "dexpace/../json" +require "dexpace/./../base64" diff --git a/test/fixtures/gates/sole_parse/direct.rb b/test/fixtures/gates/sole_parse/direct.rb new file mode 100644 index 0000000..b7480e5 --- /dev/null +++ b/test/fixtures/gates/sole_parse/direct.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# A tool that parses a file directly, outside AstScan.parse: gates:sole_parse must report it. +module RogueDirect + def self.scan(path) = RubyVM::AbstractSyntaxTree.parse_file(path) +end diff --git a/test/fixtures/gates/sole_parse/owner.rb b/test/fixtures/gates/sole_parse/owner.rb new file mode 100644 index 0000000..5596619 --- /dev/null +++ b/test/fixtures/gates/sole_parse/owner.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# An owner file: the parse method is exempt, a second method in the same file is not. +module AstScan + def parse(path) + [1].each { RubyVM::AbstractSyntaxTree.parse_file(path) } + end + + def sneaky(path) = RubyVM::AbstractSyntaxTree.parse_file(path) +end diff --git a/test/fixtures/gates/sole_parse/prism.rb b/test/fixtures/gates/sole_parse/prism.rb new file mode 100644 index 0000000..19bbe69 --- /dev/null +++ b/test/fixtures/gates/sole_parse/prism.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# Prism returns its diagnostics on the result and never routes them through Warning.warn, so a +# Prism parse is not the hazard the gate exists for and must not be reported. +module PrismOnly + def self.scan(path) = Prism.parse_file(path) +end diff --git a/test/fixtures/gates/sole_parse/reflective.rb b/test/fixtures/gates/sole_parse/reflective.rb new file mode 100644 index 0000000..1d37dc0 --- /dev/null +++ b/test/fixtures/gates/sole_parse/reflective.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# The two reflective spellings on the same receiver, by Symbol and by String. The cop directives +# are the fixture's own: the reflective spelling is what the gate must see. +module RogueReflective + # rubocop:disable Style/SendWithLiteralMethodName, Performance/StringIdentifierArgument + def self.by_symbol(path) = ::RubyVM::AbstractSyntaxTree.send(:parse_file, path) + def self.by_string(path) = RubyVM::AbstractSyntaxTree.public_send("parse_file", path) + # rubocop:enable Style/SendWithLiteralMethodName, Performance/StringIdentifierArgument +end diff --git a/test/fixtures/gates/sole_parse/workspace/tools/x.rb b/test/fixtures/gates/sole_parse/workspace/tools/x.rb new file mode 100644 index 0000000..b7480e5 --- /dev/null +++ b/test/fixtures/gates/sole_parse/workspace/tools/x.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# A tool that parses a file directly, outside AstScan.parse: gates:sole_parse must report it. +module RogueDirect + def self.scan(path) = RubyVM::AbstractSyntaxTree.parse_file(path) +end diff --git a/test/fixtures/gates/spdx_rbs/empty.rbs b/test/fixtures/gates/spdx_rbs/empty.rbs new file mode 100644 index 0000000..a7baca1 --- /dev/null +++ b/test/fixtures/gates/spdx_rbs/empty.rbs @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +# Nothing but comments: passes rbs validate, tells a type checker nothing. diff --git a/test/fixtures/gates/spdx_rbs/header_on_line_three.rbs b/test/fixtures/gates/spdx_rbs/header_on_line_three.rbs new file mode 100644 index 0000000..d5d0815 --- /dev/null +++ b/test/fixtures/gates/spdx_rbs/header_on_line_three.rbs @@ -0,0 +1,7 @@ + + +# SPDX-License-Identifier: MIT + +module Dexpace + VERSION: String +end diff --git a/test/fixtures/gates/spdx_rbs/missing_header.rbs b/test/fixtures/gates/spdx_rbs/missing_header.rbs new file mode 100644 index 0000000..35dc193 --- /dev/null +++ b/test/fixtures/gates/spdx_rbs/missing_header.rbs @@ -0,0 +1,3 @@ +module Dexpace + VERSION: String +end diff --git a/test/fixtures/gates/spdx_rbs/ok.rbs b/test/fixtures/gates/spdx_rbs/ok.rbs new file mode 100644 index 0000000..73401b7 --- /dev/null +++ b/test/fixtures/gates/spdx_rbs/ok.rbs @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: MIT + +module Dexpace + VERSION: String +end diff --git a/test/fixtures/gates/spdx_rbs/workspace/gems/dexpace-probe/sig/dexpace/probe.rbs b/test/fixtures/gates/spdx_rbs/workspace/gems/dexpace-probe/sig/dexpace/probe.rbs new file mode 100644 index 0000000..35dc193 --- /dev/null +++ b/test/fixtures/gates/spdx_rbs/workspace/gems/dexpace-probe/sig/dexpace/probe.rbs @@ -0,0 +1,3 @@ +module Dexpace + VERSION: String +end diff --git a/test/fixtures/surface/dexpace-core.txt b/test/fixtures/surface/dexpace-core.txt index 0cb841f..a6f9b08 100644 --- a/test/fixtures/surface/dexpace-core.txt +++ b/test/fixtures/surface/dexpace-core.txt @@ -864,6 +864,7 @@ Dexpace::Protocol#wire Dexpace::Protocol.build Dexpace::Protocol.parse Dexpace::Protocol::ALIASES : Hash +Dexpace::Protocol::HTTP_1_0 : Dexpace::Protocol Dexpace::Protocol::HTTP_1_1 : Dexpace::Protocol Dexpace::Protocol::HTTP_2 : Dexpace::Protocol Dexpace::Protocol::WIRE_FORMS : Array @@ -1274,6 +1275,7 @@ Dexpace::Status#error? Dexpace::Status#informational? Dexpace::Status#redirect? Dexpace::Status#server_error? +Dexpace::Status#standard? Dexpace::Status#success? Dexpace::Status.build Dexpace::Status.canonical_name diff --git a/test/gates/default_task_test.rb b/test/gates/default_task_test.rb index e6cbe69..296e230 100644 --- a/test/gates/default_task_test.rb +++ b/test/gates/default_task_test.rb @@ -9,27 +9,30 @@ # test:gates` red for seventeen tasks, and a suite that is expected to be red is a suite nobody # reads. class DefaultTaskTest < GateCase - # Phase 0's seventeen, 7b's gates:serde_boundary and phase 9's three repository-wide invariant - # scans -- twenty-one. A gate outside DEFAULT_GATES is off `task default:` and therefore + # Phase 0's seventeen, 7b's gates:serde_boundary, phase 9's three repository-wide invariant + # scans and phase 10's three -- gates:ledger_audit, gates:spdx_rbs, gates:sole_parse -- + # twenty-four. A gate outside DEFAULT_GATES is off `task default:` and therefore # advisory, which NFR-17 forbids, so this list is what makes the count a fact. EXPECTED = %w[ rubocop cops:test rbs:validate steep test:gems test:gates gates:gemspec_audit gates:require_allowlist gates:serde_boundary gates:cause_walk gates:bounded_map - gates:seam_names gates:clean_bundle gates:rbs_surface + gates:seam_names gates:ledger_audit gates:spdx_rbs gates:sole_parse gates:clean_bundle + gates:rbs_surface gates:sig_diff gates:surface_snapshot gates:single_instance gates:versions gates:reproducible yard bundler_audit ].freeze - # The COUNT, asserted against the repository and not only against the literal above: the three - # tasks phase 9 added are gates because they are in `task default:`, and a count read off - # EXPECTED alone would still pass if all three left DEFAULT_GATES together. - test "there are twenty-one gates, and every one of them is blocking" do + # The COUNT, asserted against the repository and not only against the literal above: the six + # tasks phases 9 and 10 added are gates because they are in `task default:`, and a count read off + # EXPECTED alone would still pass if all six left DEFAULT_GATES together. + test "there are twenty-four gates, and every one of them is blocking" do prerequisites = `bundle exec rake -s -P`.split("\n").drop_while { |l| l != "rake default" } .drop(1).take_while { |line| line.start_with?(" ") }.map(&:strip) - assert_equal(21, EXPECTED.size) - assert_equal(21, prerequisites.size, "the default task's prerequisite count drifted") - %w[gates:cause_walk gates:bounded_map gates:seam_names].each do |gate| + assert_equal(24, EXPECTED.size) + assert_equal(24, prerequisites.size, "the default task's prerequisite count drifted") + %w[gates:cause_walk gates:bounded_map gates:seam_names gates:ledger_audit gates:spdx_rbs + gates:sole_parse].each do |gate| assert_includes(prerequisites, gate, "#{gate} is not blocking") end end diff --git a/test/gates/ledger_audit_test.rb b/test/gates/ledger_audit_test.rb new file mode 100644 index 0000000..8486300 --- /dev/null +++ b/test/gates/ledger_audit_test.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# gates:ledger_audit keeps docs/deviations.md tied to design section 10 (phase 10's plan, Task 2; +# design addendum A11; P10-2). NFR-17: the gate is proven against deliberately failing fixtures and +# through its RAKE TASK pointed at a fixture workspace, not only by calling the module. +require_relative "../support/gate_case" +require_relative "../../tools/ledger_audit" + +class LedgerAuditTest < GateCase + FIXTURES = File.expand_path("../fixtures/gates/ledger", __dir__) + RESOLVE = ->(constant) { constant == "Dexpace::Probe" } + + def offences(register) + LedgerAudit.offences(root: FIXTURES, chapter: "chapter.md", register: register, + resolve: RESOLVE,) + end + + test "a conforming register -- one verdict row with real evidence, one unbuilt row -- is clean" do + assert_empty(offences("register_ok.md")) + end + + test "a row whose ID set is narrower than its chapter entry is an offence naming the ID" do + found = offences("register_wrong_ids.md") + + assert_equal(["row 1 ID set differs: chapter-only IO-6"], found) + end + + test "a row whose ID set is WIDER than its entry is an offence: equality, not a subset" do + assert_equal(["row 2 ID set differs: row-only BODY-9"], offences("register_extra_ids.md")) + end + + test "two swapped rows drift on subject and on IDs, both rows named" do + found = offences("register_swapped.md").join("\n") + + assert_includes(found, "row 1 subject drifted") + assert_includes(found, "row 2 subject drifted") + end + + test "a verdict row citing only a design document has no as-built evidence (P10-2)" do + assert_equal(["row 1 carries a verdict and no resolvable as-built evidence (P10-2)"], + offences("register_docs_only.md"),) + end + + test "a verdict row naming a constant that does not exist is an offence" do + assert_equal(["row 1 names Dexpace::Completer, which is not defined"], + offences("register_undefined.md"),) + end + + test "the default resolver reaches a private constant and refuses an unknown one" do + require "dexpace" + + assert(LedgerAudit::DEFAULT_RESOLVE.call("Dexpace::BoundedMap")) + assert(LedgerAudit::DEFAULT_RESOLVE.call("Dexpace::Async::Completer")) + refute(LedgerAudit::DEFAULT_RESOLVE.call("Dexpace::Completer")) + end + + test "an A–B range in either document expands, and an unknown prefix is not an ID" do + assert_equal(%w[IO-1 IO-2 IO-3 SEAM-29], + LedgerAudit.ids_in("**IO-1**–**IO-3**, ISO-8601, `SEAM-29`"),) + end + + # The live register against the frozen chapter: 19 rows, 123 distinct IDs (124 with the closing + # note's RETRY-28, which is not a row), every verdict's evidence resolvable. + test "the live register matches design section 10" do + %w[dexpace dexpace/transport/net_http dexpace/serde/json dexpace/async/thread + dexpace/conformance].each { |feature| require feature } + + assert_empty(LedgerAudit.offences(root: ROOT)) + end + + test "the rake task, pointed at a failing fixture workspace, exits non-zero with the offence" do + workspace = { "DEXPACE_GATE_ROOT" => File.join(FIXTURES, "workspace") } + + assert_gate_rejects("gates:ledger_audit", workspace, + "row 1 ID set differs: chapter-only IO-6",) + end +end diff --git a/test/gates/require_allowlist_test.rb b/test/gates/require_allowlist_test.rb index b63d468..0511561 100644 --- a/test/gates/require_allowlist_test.rb +++ b/test/gates/require_allowlist_test.rb @@ -156,3 +156,17 @@ def scan(fixture, gem: nil) .join("\n") end end + +# Phase 0's review R3-2, repaired by phase 10 (SEAM-1): `reachable?` was +# `name.start_with?("dexpace/")`, so a feature that climbs back out of the namespace passed as the +# gem's own path. Its own class so the suite above stays under Metrics/ClassLength. +class RequireAllowlistDotSegmentTest < GateCase + test "refuses a dexpace/ path carrying a dot segment" do + path = File.join(ROOT, RequireAllowlistTest::FIXTURES, "dot_segments.rb") + found = RequireAllowlist.scan_file(path, permitted: [], lib_root: File.dirname(path), gem: nil) + .join("\n") + + assert_includes(found, "dot_segments.rb:6") + assert_includes(found, "dot_segments.rb:7") + end +end diff --git a/test/gates/rubocop_config_test.rb b/test/gates/rubocop_config_test.rb index 637cb38..28a8634 100644 --- a/test/gates/rubocop_config_test.rb +++ b/test/gates/rubocop_config_test.rb @@ -58,4 +58,14 @@ class RubocopConfigTest < GateCase assert_includes(gate, "--fail-level=convention") refute_includes(gate, "--autocorrect") end + + # Phase 1's implementation found it and phase 10 repairs it (NFR-7): RuboCop takes + # AllCops/Exclude from the TOPMOST .rubocop.yml on the path, so in an agent worktree nested + # under the parent checkout's .claude/ the parent's `.claude/**/*` line excluded the whole + # worktree and the gate inspected 8 files of 126 and reported clean. One flag, the strict way. + test "the gate ignores a parent checkout's exclusions, so a nested worktree is inspected" do + gate = File.read(File.join(ROOT, "tasks/quality.rake"))[/^task :rubocop do.*?^end$/m] + + assert_includes(gate, "--ignore-parent-exclusion") + end end diff --git a/test/gates/sole_parse_test.rb b/test/gates/sole_parse_test.rb new file mode 100644 index 0000000..06a2730 --- /dev/null +++ b/test/gates/sole_parse_test.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# gates:sole_parse (phase 10's plan, Task 6; design addendum A9). NFR-6: AstScan.parse's $VERBOSE +# window is the one route to RubyVM::AbstractSyntaxTree.parse_file; NFR-17: blocking, and proven +# red through its rake task against a fixture workspace. +require_relative "../support/gate_case" +require_relative "../support/gate_warning_capture" +require_relative "../../tools/sole_parse" + +class SoleParseTest < GateCase + include GateWarningCapture + + FIXTURES = File.expand_path("../fixtures/gates/sole_parse", __dir__) + + def fixture(name) = File.join(FIXTURES, name) + + test "a direct parse_file outside AstScan.parse is an offence, carrying its path argument" do + found = SoleParse.offences_in(fixture("direct.rb")) + + assert_equal(1, found.size) + assert_includes(found.first, "direct.rb:6") + end + + test "the reflective spellings on the same receiver are offences, by Symbol and by String" do + assert_equal(2, SoleParse.offences_in(fixture("reflective.rb")).size) + end + + test "Prism.parse_file is not the hazard and is not reported" do + assert_empty(SoleParse.offences_in(fixture("prism.rb"))) + end + + test "the owner METHOD is exempt, a nested block included; the file's other methods are not" do + found = SoleParse.offences_in(fixture("owner.rb"), owner: true) + + assert_equal(1, found.size) + assert_includes(found.first, "owner.rb:10") + end + + test "every allowlist entry names a live file the scan still reports, with a reason" do + SoleParse::ALLOWED.each do |path, reason| + refute_empty(reason.strip) + refute_empty(SoleParse.offences_in(File.join(ROOT, path)), + "#{path} is allowlisted and silences nothing",) + end + end + + test "the repository's tooling has exactly one parse_file, AstScan.parse's own" do + assert_empty(SoleParse.offences(root: ROOT)) + end + + # The window the gate protects, still working: a bare parse of the unused-binding fixture warns + # and the same parse through AstScan.parse does not. Under -w only (SuiteRunner appends it). + test "AstScan.parse's window still suppresses what a bare parse emits" do + unless $VERBOSE + skip("the fixture's diagnostic is only emitted under -w; SuiteRunner appends it") + end + + path = File.expand_path("../fixtures/gates/invariants/warns_unused.rb", __dir__) + + assert_equal(1, capture_warnings { ::RubyVM::AbstractSyntaxTree.send(:parse_file, path) }.size) + assert_empty(capture_warnings { AstScan.parse(path) }) + end + + test "the rake task, pointed at a failing fixture workspace, exits non-zero naming the file" do + assert_gate_rejects("gates:sole_parse", { "DEXPACE_GATE_ROOT" => fixture("workspace") }, + "tools/x.rb:6: RubyVM::AbstractSyntaxTree.parse_file outside AstScan",) + end +end diff --git a/test/gates/spdx_rbs_test.rb b/test/gates/spdx_rbs_test.rb new file mode 100644 index 0000000..578cdc8 --- /dev/null +++ b/test/gates/spdx_rbs_test.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# gates:spdx_rbs (phase 10's plan, Task 5; design addendum A8). NFR-13's header on shipped +# signatures, NFR-3's declared-but-empty residue, NFR-17's blocking fixture. +require_relative "../support/gate_case" +require_relative "../../tools/spdx_rbs" + +class SpdxRbsTest < GateCase + FIXTURES = File.expand_path("../fixtures/gates/spdx_rbs", __dir__) + + def offences(name) = SpdxRbs.offences_in(File.join(FIXTURES, name)) + + test "a signature without the header is an offence" do + found = offences("missing_header.rbs") + + assert_equal(1, found.size) + assert_includes(found.first, "SPDX-License-Identifier") + end + + # PackagingSuite's spdx_header_coverage reads the first TWO lines, so it passes this file; the + # gate is the stricter of the two on purpose -- line 1 is where NFR-13's header block starts. + test "a header that is present but not on line 1 is an offence" do + assert_includes(offences("header_on_line_three.rbs").first, "line 1 is not") + end + + # rbs 4.2.0 returns [buffer, directives, declarations]; `.flatten.compact.empty?` keeps the Buffer + # and is never true, so this is the test that pins the third element being read. + test "a signature with no declarations is an offence" do + assert_equal(["#{File.join(FIXTURES, "empty.rbs")}: declares nothing (NFR-3)"], + offences("empty.rbs"),) + end + + test "the conforming fixture is clean" do + assert_empty(offences("ok.rbs")) + end + + test "every shipped signature in the six gems is clean" do + assert_empty(SpdxRbs.offences(root: ROOT)) + assert_operator(Dir.glob(File.join(ROOT, SpdxRbs::GLOB)).size, :>=, 300) + end + + test "the rake task, pointed at a failing fixture workspace, exits non-zero naming the file" do + workspace = { "DEXPACE_GATE_ROOT" => File.join(FIXTURES, "workspace") } + + assert_gate_rejects("gates:spdx_rbs", workspace, + "gems/dexpace-probe/sig/dexpace/probe.rbs: line 1 is not",) + end +end diff --git a/tools/invariant_gates.rb b/tools/invariant_gates.rb index 4063540..eeba386 100644 --- a/tools/invariant_gates.rb +++ b/tools/invariant_gates.rb @@ -75,7 +75,11 @@ module InvariantGates "to the bound instead of overshooting permanently, which is XCUT-14's drain clause " \ "exactly; every evicted client's pool is retired outside the lock. Phase 9's planning " \ "adjudicated this as the one TRUE positive of six, against 8c's filed plan fence rather " \ - "than its code, and 8c shipped the bound its Task 8 promised", + "than its code, and 8c shipped the bound its Task 8 promised. The MECHANISM behind this " \ + "reading (phase 10, 2026-09-25): clients_test.rb's three XCUT-14 cases -- 'the map is " \ + "bounded at MAX_ORIGINS and drains back to the cap after each insert', 'an evicted " \ + "client's pool is retired and closed, never merely dropped' and 'a client whose reactor " \ + "has closed is evicted before a live one' -- go red if the cap, the loop or the close goes", }.freeze extend self diff --git a/tools/ledger_audit.rb b/tools/ledger_audit.rb new file mode 100644 index 0000000..f613d94 --- /dev/null +++ b/tools/ledger_audit.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +# The body of gates:ledger_audit (phase 10, design addendum A11; P10-2 made mechanical). +# +# docs/deviations.md is the as-built audit of design section 10, and a register audited once +# drifts: a chapter entry gains an ID the row never lists, two rows swap, or a verdict row stops +# pointing at anything real. Three decidable assertions keep it tied to the chapter: +# +# 1. Row N carries chapter entry N's subject -- the register title, emphasis stripped, is a WORD +# SUBSEQUENCE of the entry's bolded lead. Equality is the wrong test (the register's titles +# are deliberately shorter); a subsequence is decidable and a renamed or swapped entry still +# breaks it. +# 2. Row N's `IDs touched` column, every range expanded, EQUALS the ID set extracted from entry +# N's text. That is what makes phase 10's count of 124 reproducible from the tree. +# 3. A row whose status is anything but `design only -- not yet built` cites at least one +# `gems/...` path that exists, every `gems/...` path it cites exists, and every `Dexpace::` +# constant it names resolves. A row citing only `docs/...` fails: a design document is never +# evidence. +# +# Every path is joined to `root` -- never read relative to the process CWD, which is the defect +# phase 9 shipped and repaired as P9-27: with DEXPACE_GATE_ROOT pointed at a fixture, a +# CWD-relative read opens the real repository's files and the gate can never be shown to reject +# its fixture. +# +# Assertion 3 needs the gems LOADED to resolve a constant; the rake task requires them and the +# resolver reads the running process. `const_get(name, false)` reaches a private_constant too, so +# a row naming one passes -- the rule is that the name is real, not that it is public. +module LedgerAudit + PREFIXES = %w[SEAM HTTP IO BODY CTX PIPE RECOV RETRY REDIR AUTH PAGE SSE SERDE OBS CFG + TRANSPORT ASYNC XCUT NFR].freeze + CHAPTER = "docs/sdk-design-ruby/10-deliberate-deviations-from-the-reference-contract.md" + REGISTER = "docs/deviations.md" + UNBUILT = "design only — not yet built" + ENTRY = /^(\d+)\.\s+\*\*(.+?)\*\*(.*?)(?=^\d+\.\s+\*\*|^Two things that are deliberately)/m + RANGE = /\b([A-Z]+)-(\d+)\s*[–—]\s*([A-Z]+)-(\d+)\b/ + SINGLE = /\b([A-Z]+)-(\d+)\b/ + CITED_PATH = %r{gems/[\w./-]+} + CONSTANT = /\bDexpace(?:::[A-Z]\w*)+/ + + # Resolves a `Dexpace::A::B` path against the running process. A NameError anywhere on the + # path (including an unloaded root) is "not defined". + DEFAULT_RESOLVE = lambda do |path| + path.split("::").reduce(::Object) { |scope, segment| scope.const_get(segment, false) } + true + rescue ::NameError + false + end + + class << self + # @param root [String] the workspace every relative path is joined to + # @param resolve [#call] answers whether a `Dexpace::` constant path is defined + # @return [Array] one line per offence + def offences(root:, chapter: CHAPTER, register: REGISTER, resolve: DEFAULT_RESOLVE) + entries = chapter_entries(File.read(File.join(root, chapter))) + rows = register_rows(File.read(File.join(root, register))) + count(rows, entries) + entries.zip(rows).each_with_index.flat_map do |(entry, row), index| + row.nil? ? [] : check(index + 1, entry, row, root, resolve) + end + end + + # Every canonical ID in the text, each `A-n–A-m` range expanded. Public because the + # checklist's own row count is read through it. + def ids_in(text) + plain = text.gsub(/[`*]/, "") + (ranged(plain) + singles(plain)).uniq.sort + end + + private + + def count(rows, entries) + return [] if rows.size == entries.size + + ["row count #{rows.size} != chapter entry count #{entries.size}"] + end + + def check(number, entry, row, root, resolve) # rubocop:disable Metrics/ParameterLists -- the module keeps no state (`class << self`), so the per-run root and resolver are threaded to every row check rather than held + drift(number, entry, row) + evidence(number, row[:status], root, resolve) + end + + def ranged(plain) + plain.scan(RANGE).flat_map do |first, low, last, high| + next [] unless first == last && PREFIXES.include?(first) + + (low.to_i..high.to_i).map { |number| "#{first}-#{number}" } + end + end + + def singles(plain) + plain.scan(SINGLE).filter_map do |prefix, number| + "#{prefix}-#{number}" if PREFIXES.include?(prefix) + end + end + + # Each entry is "N. **Subject.** ..." up to the next "N. **" or the closing note. + def chapter_entries(text) + text.scan(ENTRY).map do |_number, subject, body| + { subject: normalise(subject), ids: ids_in(subject + body) } + end + end + + def register_rows(text) + text.lines.grep(/^\|\s*\d+\s*\|/).map do |line| + cells = line.split("|").map(&:strip) + { subject: normalise(cells[2].to_s), ids: ids_in(cells[3].to_s), status: cells[4].to_s } + end + end + + # Assertions 1 and 2. + def drift(number, entry, row) + out = [] + unless subsequence?(row[:subject], entry[:subject]) + out << "row #{number} subject drifted from entry #{number}" + end + missing = entry[:ids] - row[:ids] + extra = row[:ids] - entry[:ids] + unless missing.empty? + out << "row #{number} ID set differs: chapter-only #{missing.join(", ")}" + end + out << "row #{number} ID set differs: row-only #{extra.join(", ")}" unless extra.empty? + out + end + + # Assertion 3, for a row that carries a verdict. + def evidence(number, status, root, resolve) + return [] if status.include?(UNBUILT) + + paths = status.scan(CITED_PATH).map { |path| path.chomp(".") } + real = paths.select { |path| File.exist?(File.join(root, path)) } + if real.empty? + return ["row #{number} carries a verdict and no resolvable as-built evidence (P10-2)"] + end + + (paths - real).map { |path| "row #{number} cites #{path}, which does not exist" } + + undefined(number, status, resolve) + end + + def undefined(number, status, resolve) + status.scan(CONSTANT).uniq.reject { |constant| resolve.call(constant) } + .map { |constant| "row #{number} names #{constant}, which is not defined" } + end + + def normalise(text) = text.gsub(/[`*]/, "").downcase.gsub(/[^a-z0-9 ]/, " ").split.join(" ") + + # A register title is shorter than the chapter's sentence, so equality is the wrong test; a + # word subsequence is decidable and a renamed entry still breaks it. + def subsequence?(short, long) + words = long.split + short.split.all? do |word| + index = words.index(word) + index && (words = words.drop(index + 1)) + end + end + end +end diff --git a/tools/require_allowlist.rb b/tools/require_allowlist.rb index 412b176..db6b835 100644 --- a/tools/require_allowlist.rb +++ b/tools/require_allowlist.rb @@ -71,6 +71,10 @@ def binds?(gem) NO_TRANSPORT = Denial.new(reason: NO_TRANSPORT_REASON, scope: nil) NO_SOCKET = Denial.new(reason: NO_TRANSPORT_REASON, scope: %w[dexpace-core dexpace-transport-*]) + # The two path segments that make a `dexpace/`-prefixed feature name something outside the + # namespace (`dexpace/../json` is `json`), refused by #reachable?. + DOT_SEGMENTS = %w[. ..].freeze + # Names that pass a category test and must still fail. Without this list the gate's message # would be "not in the allowlist", which says nothing about why. DENIED = { @@ -157,8 +161,13 @@ def reason_for(name, permitted, gem) # A declared dependency, or a path inside the gem's own namespace -- including core's entry # point itself, which every adapter requires by the name `dexpace` and not `dexpace/...`. + # + # A dot segment disqualifies the prefix: `dexpace/../json` is `json` to Kernel#require, and a + # bare `start_with?` read it as the gem's own path (phase 10, repairing phase 0's review R3-2). def reachable?(name, permitted) - permitted.include?(name) || name == "dexpace" || name.start_with?("dexpace/") + return true if permitted.include?(name) || name == "dexpace" + + name.start_with?("dexpace/") && !name.split("/").intersect?(DOT_SEGMENTS) end # An adapter may require the one third-party gem its own gemspec declares. In phase 0 no diff --git a/tools/sole_parse.rb b/tools/sole_parse.rb new file mode 100644 index 0000000..62d1c38 --- /dev/null +++ b/tools/sole_parse.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +require_relative "ast_scan" + +# The body of gates:sole_parse (phase 10, design addendum A9; NFR-6, NFR-17). +# +# AstScan.parse opens a `$VERBOSE = nil` window around `RubyVM::AbstractSyntaxTree.parse_file`, +# because a SCANNED file's own -w diagnostics reach `Warning.warn` and fire phase 0's FatalWarnings. +# The rule that keeps that the single entry point was a comment in tools/ast_scan.rb ("nothing else +# in this file may call `parse_file`"); this is the assertion, over every repository tool. +# +# Scoped to a `parse_file` whose receiver is the constant `RubyVM::AbstractSyntaxTree` -- `Prism`'s +# `parse_file` (tools/gemspec_audit.rb, require_scan.rb, serde_boundary.rb) returns its diagnostics +# on the result and never routes them through `Warning.warn`, so it is not this hazard and a +# receiver-blind scan reports three false positives there. The reflective spellings +# (`send(:parse_file, …)`, `public_send("parse_file", …)`) on the same receiver are caught too, by +# Symbol on `:LIT` (3.2, 3.3) and `:SYM` (3.4, 4.0) alike. +# +# Deliberately NOT built on AstScan.receiver_calls: that helper records a direct hit only when the +# send carries no argument (5b's `Event#cause(error)` narrowing), and every `parse_file` carries a +# path. Its two private helpers the walk needs are re-stated here rather than made public, which +# would widen phase 9's interface. +# +# Stated gap: a receiver reached through a variable (`ast = RubyVM::AbstractSyntaxTree; +# ast.parse_file(p)`) and a method name held in a variable are undecidable statically, the residue +# phase 9 wrote down for gates:cause_walk. +module SoleParse + TREES = ["tools/**/*.rb", "tasks/**/*.rake", "test/gates/**/*.rb", + ".claude/skills/**/*.rb",].freeze + TARGET = :parse_file + RECEIVER = %w[RubyVM::AbstractSyntaxTree ::RubyVM::AbstractSyntaxTree].freeze + OWNER_FILE = "tools/ast_scan.rb" + OWNER_METHOD = :parse + + # Each entry is a file that calls `parse_file` bare ON PURPOSE, with the reason. Both are the + # contrast that proves AstScan.parse's window: a bare parse of `warns_unused.rb` must emit the + # warning the window suppresses, or the window's own test cannot discriminate. The integrity test + # asserts every entry still names a file the scan reports, so a stale entry silences nothing. + ALLOWED = { + "test/gates/phase9_ruby_facts_test.rb" => + "the bare parse is the measured contrast for AstScan.parse's $VERBOSE window (phase 9)", + "test/gates/invariant_gates_test.rb" => + "the bare parse proves the fixture is not inert before asserting the gate emits nothing", + "test/gates/sole_parse_test.rb" => + "the reflective bare parse is this gate's own contrast: the window it protects still works", + }.freeze + + class << self + # @param root [String] + # @param paths [Array] absolute paths + # @return [Array] "path:line: ..." with absolute paths; the task relativises + def offences(root:, paths: TREES.flat_map { |glob| Dir.glob(File.join(root, glob)) }.sort) + allowed = ALLOWED.keys.map { |path| File.join(root, path) } + owner = File.join(root, OWNER_FILE) + scanned = paths.reject { |path| allowed.include?(path) } + scanned.flat_map { |path| offences_in(path, owner: path == owner) } + end + + def offences_in(path, owner: false) + lines(AstScan.parse(path), owner: owner).map do |line| + "#{path}:#{line}: RubyVM::AbstractSyntaxTree.parse_file outside AstScan.#{OWNER_METHOD} " \ + "(NFR-6)" + end + end + + private + + # Depth-first, carrying the owner flag down, so a block nested inside AstScan#parse is still + # inside it. The exemption is the owner METHOD, not the owner file: the rule is about the + # file's other methods, and a file-wide exemption could not enforce it. + def lines(node, owner:, inside: false, out: []) + return out unless node.is_a?(::RubyVM::AbstractSyntaxTree::Node) + + inside ||= owner && defines_owner?(node) + out << node.first_lineno if !inside && target?(node) + node.children.each { |child| lines(child, owner: owner, inside: inside, out: out) } + out + end + + # `:DEFN` carries the method name first; `:DEFS` (`def self.parse`) carries the receiver first. + def defines_owner?(node) + case node.type + when :DEFN then node.children[0] == OWNER_METHOD + when :DEFS then node.children[1] == OWNER_METHOD + else false + end + end + + def target?(node) + return false unless %i[CALL QCALL].include?(node.type) + return false unless RECEIVER.include?(render(node.children[0])) + + called = node.children[1] + return true if called == TARGET + + AstScan::REFLECTIVE_SENDS.include?(called) && first_argument_name(node) == TARGET + end + + def first_argument_name(node) + args = node.children[2] + return nil unless args.is_a?(::RubyVM::AbstractSyntaxTree::Node) && args.type == :LIST + + literal_name(args.children.first) + end + + # A String or Symbol literal's name, nil for anything dynamic. + def literal_name(node) + return nil unless node.is_a?(::RubyVM::AbstractSyntaxTree::Node) + + value = node.children.first + return value.to_sym if node.type == :STR && value.is_a?(::String) + + AstScan::SYMBOL_TYPES.include?(node.type) && value.is_a?(::Symbol) ? value : nil + end + + def render(node) + return nil unless node.is_a?(::RubyVM::AbstractSyntaxTree::Node) + + case node.type + when :CONST then node.children[0].to_s + when :COLON3 then "::#{node.children[0]}" + when :COLON2 + left = render(node.children[0]) + left.nil? ? nil : "#{left}::#{node.children[1]}" + end + end + end +end diff --git a/tools/spdx_rbs.rb b/tools/spdx_rbs.rb new file mode 100644 index 0000000..eced974 --- /dev/null +++ b/tools/spdx_rbs.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +# SPDX-License-Identifier: MIT + +require "pathname" +require "rbs" + +# The body of gates:spdx_rbs (phase 10, design addendum A8; NFR-13, NFR-3). +# +# NFR-13's conformance clause is "scan ALL source files for the required header", and sig/ ships +# inside every gem, so the signatures are shipped source. Phase 0 mechanised the header as a +# RuboCop cop, and a cop parses Ruby -- it cannot reach a .rbs file, which is how 307 shipped +# signatures went out with none (phase 9's aggregate run, 2026-09-23). The header is ONE line: +# `# frozen_string_literal: true` has no meaning in RBS. +# +# The second assertion -- no shipped signature declares nothing -- is NFR-3's: an empty .rbs passes +# `rbs validate` and `steep check` and tells a consumer's type checker nothing. On rbs 4.2.0 +# `RBS::Parser.parse_signature` returns `[buffer, directives, declarations]`, and the plan's +# `.flatten.compact.empty?` is never true because the Buffer survives the flatten (measured on an +# empty string and on a comment-only file). The declarations are the third element. +module SpdxRbs + HEADER = "# SPDX-License-Identifier: MIT" + GLOB = "gems/*/sig/**/*.rbs" + + def self.offences(root:, paths: Dir.glob(File.join(root, GLOB))) + paths.flat_map { |path| offences_in(path) } + end + + def self.offences_in(path) + source = File.read(path) + out = [] + out << "#{path}: line 1 is not `#{HEADER}` (NFR-13)" unless source.lines.first&.chomp == HEADER + _buffer, _directives, declarations = RBS::Parser.parse_signature( + RBS::Buffer.new(name: Pathname(path), content: source), + ) + out << "#{path}: declares nothing (NFR-3)" if declarations.empty? + out + rescue RBS::ParsingError => error + ["#{path}: does not parse -- #{error.message}"] + end +end