Skip to content

Phase 6b: redirect — documentation and phase record - #80

Merged
Wahbeh-Mohammad merged 13 commits into
mainfrom
23-phase-6b-redirect-docs
Sep 19, 2026
Merged

Wahbeh-Mohammad merged 13 commits into
mainfrom
23-phase-6b-redirect-docs

Conversation

@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor

Closes #23. Third and last PR of phase 6b's stack — the documentation and the phase record for the two PRs below it, and the PR that completes phase 6. Targets the tests PR's branch.

What lands

12 files, +1,303 / −53, all Markdown.

  • The checklist, docs/work/mvp/phase6/phase6b/2026-09-09-phase6b-redirect-checklist.md, written from the build: 28 own rows — 27 ✅ and REDIR-27 ⏳ (declined for v1, citing docs/first-release.md); REDIR-11's three clauses stated in its row ((a) structural, (b) 6c's AUTH-29, (c) a fortiori) and REDIR-25 no longer vacuous now that AsyncPipeline.standard exists — plus the phase-level rows PIPE-39 ✅ (phase 4c's ⏳ row closes here; 4c's own row stays as its record), PIPE-32 ✅, PIPE-24 by construction, the convergence-point-1 row citing 6c's test file, and 15 cross-reference rows; the matrix facts on every interpreter; fifty guards run red; 36 deviations from the plan; the findings routed; postponed work: none.
  • The phase 6b design's ledger gains an "As built" addendum, P6-91–P6-100: the REDIR-17 order (correcting the design's own ledger sentence), the new public names, the .build/no-redactor: shape, the flat error, the scheme-and-host screen, the default-port residue, the downgrade emission, the status key, the raising predicate, the standard constructors' shape. 6a's and 6c's rows are cited as "6a's P6-n" / "6c's P6-n"; phase 10 consolidates all three ledgers into design §10.
  • docs/sdk-documentation/redirect.md — new as-built page, every example run on 4.0.6 and 3.2.11, no credential in any printed result; pipelines.md's two "Pipeline.standard does not exist yet" sentences repaired with a worked preset example; architecture.md, the core README, README.md and docs/README.md ("the fourteen pages written so far") point at it.
  • docs/first-release.md — the PIPE-32/REDIR-25 behavioural-asymmetries entry moved from the future tense to "built by Task 13a on 2026-09-19 in that shape"; the REDIR-27 entry cited, not rewritten.
  • docs/knowledge/notes/redirect-handling.md — a new entry closing the userinfo-no-op note's "floor-straddling and not yet closed" caveat with the four-row re-measurement (never an in-place edit).
  • CLAUDE.md — "Phases 0, 1, 2, 3a, 3b, 4a, 4b, 4c, 5a, 5b, 5c, 6a, 6b and 6c are built — the whole of phase 6", the opening paragraph gains the redirect layer and the two standard constructors, 174 → 184 lib/ files beside version.rb, fifteen private_constant test-mirror exceptions (redirect/origin.rb and redirect/location.rb join), fourteen checklists, and the "Constraints that will bite" lines (the marker is cursor state; userinfo = "" never = nil; URI::RFC3986_PARSER.join; the seed origin; one preset path).
  • The roadmap — the 2026-09-19 "Phase 6b implemented" status note with its three review-round paragraphs, saying the constructors phase 4c postponed have landed, convergence point 1 landed, REDIR-25 stopped being vacuous, the Cursor widening was consumed not at all, and phase 6 is complete; one phase-10 inbound bullet (the REDIR-13 default-port residue), cited by date.

Verification

  • bundle exec rake on Ruby 4.0.6 at this tip: exit 0, all seventeen gates green (2,705 / 67,656, 0 skips, 99.98%, YARD 0 undocumented); honest RuboCop 503 files clean.
  • ruby .claude/skills/housekeeping/probe.rb: no drift, all eight checks; verify_knowledge_structure.rb OK (2,166 harvested, 56 notes).
  • Empty diff under docs/product-spec/, docs/sdk-design-ruby/, docs/knowledge/harvested/, docs/deviations.md, every earlier phase's documents, the phase-6 charter, and 6a's and 6c's documents.
  • The final reviewer re-derived every count CLAUDE.md and the status note state against the tree and ran every redirect.md and pipelines.md example on both interpreters.

Known follow-ups (not blocking)

  • R0-3 (nit, deferred) — the implementer's docs commit subject is 77 characters; the squash title is the PR title.
  • Phase 6 is complete with this merge: umbrella Phase 6 #21 closes by hand; the consolidation of P6-1–P6-12 (6a), P6-1–P6-7 (6c, colliding by number, knowingly) and P6-91–P6-100 into design §10 is phase 10's and a human's.

Phase 6b, Task 2. Widen phase 6a's Dexpace::Resilience::Resend in place with
the body-only predicate REDIR-6 asks -- no body, or a body answering
#replayable? -- beside .eligible?, which folds in RETRY-7's idempotency
clause and would refuse a body-less POST 307 the allowed-method set admits.
The clear error a re-send site raises over a present, non-replayable body is
Dexpace::NotReplayableError, flat under Dexpace:: on 6a's P6-56 precedent
for the same namespace (RetryPredicateError), not under Resilience::.
Neither file is required by the entry file yet; the step's commit wires the
whole phase-6b block in dependency order.
Phase 6b, Tasks 3-6 and the helpers Task 12 split out. Under
lib/dexpace/redirect/: Origin (private) -- REDIR-8's [scheme, host, port]
triple under the no-argument fold, compared against the SEED; Location
(private) -- one URI::RFC3986_PARSER.join against the current hop, a screen
for an http/https scheme AND a host (join resolves mailto:, ftp:, http:foo
and http:///p without raising), the userinfo strip spelled userinfo = ""
and never = nil, then the freeze, raising URI::InvalidURIError by class for
the step to convert; ConditionSnapshot -- REDIR-20's read-only, defensively
copied Data in the phase-1 shape; Events and Keys -- REDIR-28's five names
and four keys under one http.redirect. prefix; SchemeDowngradeError --
REDIR-15's refusal, naming the two authorities and never a path or query;
Chain (private) -- one call's seed, count, visited set and current request;
Emitter (private) -- the four contained, redacted emissions and the one raw
exception, over the logger's redactor; Reissue (private) -- REDIR-15's
downgrade check, REDIR-7's unconditional Authorization strip, REDIR-9/10's
cross-origin Cookie strip, REDIR-5's 303 rebuild by Content-* prefix and
REDIR-6's replayable-body gate. Every file has a sig/ mirror; the private
ones carry hooks.rbs's comment.
Phase 6b, Tasks 8-13. One iterative follower at Stages::REDIRECT that forks a
fresh cursor for EVERY drive, the first included, with the cross-origin
marker as cursor state -- { cross_origin: bool } on every hop, false on the
seed's own and a same-origin one, judged against the SEED origin -- and never
a request header. Per hop: the fast path on a non-redirect status, the
Location resolved once, the snapshot always allocated and a predicate always
consulted on a recognized 3xx, REDIR-17's cap applied over that answer, the
follow-up built inside a frame that closes the current response before any
raise, the superseded response closed BEFORE the next fork, the hop recorded.
Built through .build with .new private, frozen, with logger: as its own
keyword (the Cursor's bundle carries no logger) and no redactor: (one policy
per logging path, the logger's). Wires the ten-file phase-6b block into the
entry file after 6c's, adds REDIRECT_LAYER to the smoke suite's layer table,
and repairs the one existing test the private constructor invalidates: 6c's
guarded convergence test called Redirect::Step.new, and its defined? guard
stops skipping the moment this class exists, so the test now runs and
passes here.
Phase 6b, Task 13a -- the two constructors phase 4c postponed until the
redirect, retry and instrumentation families all existed (PIPE-39).
Pipeline.standard(over, redirect: nil, settings:, http_tracer_factory:,
logger:, level:, preview_bytes:) installs Redirect::Step, RetryStep and
Instrumentation::Step; AsyncPipeline.standard(over, redirect:, ...) installs
AsyncRetryStep and Instrumentation::AsyncStep and nothing at REDIRECT, with
redirect: a REQUIRED keyword admitting :unsupported alone so PIPE-32's
asymmetry is spelled at the call site. Both are written over
Builder#install_preset and nothing else; the positional is a transport or a
Pipeline::Builder already holding one, which is what makes PIPE-24's
empty-pillars rule reachable through the constructor. A nil
http_tracer_factory: leaves the retry family's own private default in
place. Rewrites 4c's three "postponed to phase 6b" YARD sites and the
PIPE-32 paragraph on AsyncPipeline, adds .standard to both signatures, and
flips 4c's two refute_respond_to pins, which this commit invalidates.
Phase 6b, Task 14. `bundle exec rake surface:regenerate`, once, and the 28
new rows read against the object model: AsyncPipeline.standard,
NotReplayableError, Pipeline.standard, Redirect, ConditionSnapshot with its
three readers and .build, Events (five), Keys (four), SchemeDowngradeError,
Step with #call, #stage, .build, DEFAULT_ALLOWED_METHODS and
DEFAULT_MAX_HOPS, and Resend#replayable_body?. Nothing private appears --
no Origin, Location, Chain, Emitter, Reissue or RECOGNIZED_CODES -- and the
step exposes no configuration reader. 1 109 rows become 1 137; every one
is a widening (NFR-4).
…rgence test

Phase 6b, Tasks 1-14. redirect/matrix_facts_test.rb: the design's four
Location facts and the ones the build found, a standing test on every CI row
(the plan's malformed fixture is a VALID URI; the design's Set-of-URI
rationale is false; join resolves host-less and unsupported-scheme targets
without raising; URI#to_s elides an explicit default port).
redirect/step_test.rb, ten nested classes: construction and REDIR-26,
the built-in decision and the predicate route with R9's cap-over-predicate
order, the credential hygiene on EVERY intermediate request through an
AUTH-position probe, both REDIR-8 chains (A -> A -> B and A -> B -> B, the
second pinning the Cookie off the wire), the marker on every drive and
4c's R11 assertions 4 and 5 against the real step, Location resolution
against the current hop with the userinfo strip asserted on the rendered
URL, the downgrade, the 303 rebuild by prefix, the replayable-body gate,
REDIR-22a's ORDER read through a callable transport entry as the follow-up
arrives, 5,000 hops flat on the stack, REDIR-28's records with the raw
malformed exception, and the fork-for-every-drive contract on a SpyCursor.
condition_snapshot_test.rb, events_test.rb, scheme_downgrade_error_test.rb
and error/not_replayable_error_test.rb mirror their files;
pipeline/standard_test.rb is the two constructors' suite (PIPE-24 through a
builder handed in, PIPE-32's required keyword, the keywords reaching the
steps, a source scan for a second installation path). Two new top-level
doubles: RedirectFixtures and CredentialProbe; 6a's ScriptedTransport is
reused as it is. Two earlier-phase tests extended: 6a's resend_test.rb
gains a nested ReplayableBodyTest, and 6c's cross_origin_convergence_test
loses its now-dead skip guard and says who un-guarded it -- run red first
against a scratch REDIRECT stub that forked without the marker.
Review round 0 of the phase-6b stack ran two mutations that survived
on 4.0.6 and 3.2.11, each behind a checklist row whose cited test did
not prove the clause it claimed. Both are coverage gaps in this layer;
no lib/ line changes.

R0-1, PIPE-39: dropping `settings:` from the sync `Pipeline.standard`'s
`RetryStep.build` left `standard_test.rb` green, because the default
settings retry a 503 too -- after a real backoff on Clock::SYSTEM. The
wiring case now holds its FakeClock in a local and asserts the one
wait ran on that clock at the flat settings' zero delay (`[0.0]`),
which the default schedule cannot produce; a second case drives
`settings(max_retries: 0)` and asserts the 503 comes back unretried
after one call. The `settings` fixture takes `**overrides` for it.
`WiringTest` crossed Metrics/ClassLength with the addition, so the two
PIPE-24 cases and the install_preset source scan moved to a new
`InstallationTest`; every case is unchanged.

R0-2, REDIR-9: keeping Cookie and Proxy-Authorization on a CROSS-ORIGIN
303 GET rebuild left the redirect suites green -- every 303 case was
same-origin and carried neither header. `ReissueTest` gains one case
over an `ORIGIN_SCOPED_HEADERS` POST under `follow303: true`: a 303 to a
foreign origin rebuilds a GET carrying none of Cookie,
Proxy-Authorization, Authorization or Content-Type and keeping Accept;
the same 303 to the seed's origin keeps the two origin-scoped headers
and still drops Authorization and Content-Type (REDIR-10 on the
rebuild). The mutation now fails on `to not include "Cookie"`.

Both mutations were re-applied against the new cases and went red on
4.0.6 and 3.2.11; the reviewer's neighbouring mutations (m01, m06, m07,
m26, m30, m33, m35, m41) are still caught.
Review round 1 of the phase-6b stack found one mutation surviving on
4.0.6 and 3.2.11: dropping the fragment from the resolved target left
every redirect suite green. REDIR-13 names "path, query, and fragment",
the REDIR-14 case at LocationTest was titled "a fragment-only ...
reference" over a script carrying none, and no test in the phase drove
a fragment-carrying Location.

LocationTest now drives what it claims: the REDIR-14 chain gains a
fragment-only reference, which resolves against the CURRENT hop and
keeps that hop's path and query (page=2#only, not page=1#only), and a
REDIR-13 case sends a fragment, a percent-encoded fragment, an empty
query and an empty fragment, each asserted byte for byte on the URL
the transport received. The fragment dropped, the empty query dropped,
the empty fragment dropped and a percent-encoded fragment decoded are
each red on both interpreters; the fragment dropped fails the REDIR-14
case too, because the fragment-only hop then revisits the current URL
and the loop check stops the chain.

The addition pushed LocationTest over Metrics/ClassLength (106/100), so
REDIR-15's three downgrade cases and REDIR-18's two screen cases move
to a RefusedTargetTest -- the targets the step refuses to follow -- a
split by concern and not an inline disable, the way the suite's other
classes were cut. Every moved case is unchanged; the header lists the
eighth concern. No lib/ line changed.
Review round 2 found two MUST clauses with no test: an automatic
POST -> GET rewrite on a followed 301 or 302 (the method rewritten, the
body dropped, or both, on 301/302 only) and the case fold dropped from
the 303 rebuild's Content-* prefix test both left every redirect suite
green on 4.0.6 and 3.2.11, although REDIR-3 says "there is deliberately
NO automatic POST -> GET rewrite" and REDIR-5 says "case-insensitively".
The code was right; the suite had a gap.

ReissueTest gains a REDIR-3 case that follows a 301 and a 302 on a POST
and on a PUT under an allowed set that admits the method, asserting on
the transport's second call the original method token, the SAME body
object and the Content-Type still travelling. RebuildTest, new, gains a
REDIR-5 / HTTP-13 case whose POST carries content-type, CONTENT-LENGTH
and cOnTeNt-Language in the caller's casing (asserted on #names before
the hop) and asserts each gone from the rebuilt GET with Accept alone
left. The four mutations are red on both interpreters; the suite's
neighbouring guards were re-run and still fire.

ReissueTest was at 97 code lines, so the five 303 cases and their two
helpers moved unchanged into RebuildTest -- a split by concern (the
method-preserving re-issue against the rebuild that changes the method),
never an inline disable; the step suite is twelve nested classes and its
header lists ten concerns. No lib/ line changed.
…e counts

Phase 6b, Task 14. The checklist at
docs/work/mvp/phase6/phase6b/2026-09-09-phase6b-redirect-checklist.md,
written from what was built: twenty-eight own rows (twenty-seven checked,
REDIR-27 deferred to first-release.md's entry), the phase-level PIPE-39,
PIPE-32, PIPE-24 and convergence-point rows, fifteen cross-reference rows,
the matrix facts on every interpreter, fifty guards run red on 4.0.6 and
3.2.11 with the one equivalent mutant named, the audit groups, thirty
departures from the plan's text and the findings routed. The design gains
its As-built addendum, P6-91 to P6-100, correcting its own ledger
paragraph's cap-before-predicate order to R9's. docs/sdk-documentation/
redirect.md is the fourteenth as-built page, every example run on 4.0.6 and
3.2.11; pipelines.md's two "no Pipeline.standard yet" passages are repaired;
architecture.md, the two READMEs and docs/README.md point at the page.
CLAUDE.md's built-phases paragraph gains the redirect layer and the two
standard constructors, its counts move to 184 lib files, eighteen private
constants without a test mirror and fourteen checklists, and its
constraints list gains four lines. first-release.md's PIPE-32/REDIR-25
entry now says the constructors were built in that shape; the roadmap gains
the phase-6b status note and one phase-10 inbound bullet (the default-port
residue, by date and content); docs/knowledge/notes/redirect-handling.md
gains one Reference entry closing the userinfo note's floor caveat.
Round 0 returned changes_requested with two should-fix findings and
two nits, none touching lib/. The tests-branch commit below this one
closes the two should-fix findings; this commit is the record.

The checklist's REDIR-9 row now cites the cross-origin 303 rebuild
case and states that each of the requirement's two clauses has its own
proof; the PIPE-39 row cites the FakeClock wait and the max_retries: 0
case instead of inferring settings: from a call count the default
schedule reproduces, and counts thirteen cases; the PIPE-24 row cites
InstallationTest, where the split moved its cases; the REDIR-5 row
names its four 303 cases as REDIR-5's now that a fifth exists. Guard 48
is re-described as the tracer half alone, guards 51 and 52 are the
round's two survivors run red on 4.0.6 and 3.2.11, and the battery's
intro counts fifty-two with fifty-one caught. Departures from the plan
gain items 31 and 32 (the two added cases; the three-class split).

The NFR-13 row miscounted the new test files: nine were added under
test/ (seven suites and two doubles), not ten -- R0-4.

The design's As-built addendum gains a review-round-1 paragraph in
6a's shape, adding no ledger row: the round found no behaviour the
document states that the code fails to honour. The roadmap's 6b note
gains its round-1 paragraph, append-only, which also records why the
two over-long commit subjects (R0-3) stay as they are: the fix rules
forbid amending the implementer's commits.
The checklist's REDIR-13 row now cites the fragment case and states
the clause it proves, its REDIR-14 row says the fragment-only reference
resolves against the current hop (a claim the row made before the test
carried one), and every row and guard whose case moved to
RefusedTargetTest cites it: REDIR-9, REDIR-15, REDIR-18, REDIR-22 and
guards 15-18, 22 and 23. Guards 53-56 are the round's four, the battery
intro reads fifty-six with fifty-five caught on both rows, guard 14's
message gains its second case, and departure 33 records the addition
and the split.

Review round 1's nit: departure 32 said step_test.rb's header names
"seven classes" where the suite had ten, and reads eleven now. Two more
stale counts found beside it are corrected in the same pass -- the
Minitest audit row and departure 12 still said the constructors' suite
was two nested classes, and it has been three since round 1's split.

The design's As-built addendum gains a review-round-2 paragraph (no
ledger row: the code honoured every stated behaviour and only the suite
had the gap), the roadmap's 6b note its append-only round-2 paragraph,
and redirect.md's REDIR-13 sentence names the fragment beside the path
and the query, which the suite now proves.
Round 2 found two coverage gaps behind MUST rows and one stale count.
The checklist's REDIR-3 row now cites the case that follows a 301 and
a 302 on a POST and a PUT with the method, the same body object and the
Content-Type asserted; its REDIR-5 row and the HTTP-13 cross-reference
row cite the case whose non-canonical content-* names are removed from
the rebuilt GET; guards 57-60 carry the four mutations with their first
failure lines, the battery intro reads sixty with fifty-nine caught on
both rows, and guards 1, 33, 34, 35 and 52 cite RebuildTest where their
case moved, each re-run at the new tip. Departure 34 records the
addition and the split; departures 31-33 and the Minitest audit row
read the counts as they are now. The design's As-built addendum and the
roadmap's 6b note each gain their append-only round-3 paragraph (no
ledger row: the code honoured both clauses, the suite had the gap).
redirect.md names the case fold beside the prefix strip. docs/README.md
says fourteen pages, which is how many it lists.
@Wahbeh-Mohammad Wahbeh-Mohammad added type:feature New capability or enhancement area:core Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-* area:resilience Retry, recovery, redirects: RETRY-* RECOV-* REDIR-* labels Sep 19, 2026
@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor Author

Review record for the phase 6b stack (#78 → #79 → #80)

4 independent reviews, each by a fresh agent with no memory of the previous one, each re-running every gate itself on every tip (4.0.6 all seventeen individually at the code tip and the full rake at the tests and docs tips; the matrix set on 3.2.11 and 3.4.10) and applying its own mutations on both interpreters, with a fix round by a fresh agent between each. The stack was cut from main at e61864f.

Round Verdict Blocking Should-fix Nits Mutations (caught) Disposition
0 changes required 0 2 2 44 (41) all 4 addressed in fix round 1
1 changes required 0 1 1 60 (54) all 2 addressed in fix round 2
2 changes required 0 2 1 70 (65) all 3 addressed in fix round 3 (R0-3 stays deferred)
3 (final) changes required 0 1 1 67 (65) carried into the PR bodies

Nothing was skipped. The pattern across the four rounds is the point of the record: no round found a code defect — the code branch is the implementer's, unchanged through all four rounds — and every finding was a test gap the reviewer's own mutation battery exposed (a preset keyword unproven, a 303-rebuild clause, the fragment clause, the method-and-body preservation on a followed 301/302, the Content-* case fold), each closed on the tests branch by a mutation-proven case. The stack was cut from main at e61864f, which already holds phase 6a and phase 6c, so it needed no reconciliation pass; it is the lane that closes phase 6.

Round 0 → fixed in round 1

  • R0-1 should-fix — gems/dexpace-core/test/dexpace/pipeline/standard_test.rb:107: settings: threading on the sync preset is unproven — the mutation dropping it survives. Fixed on tests (d696d2b): gems/dexpace-core/test/dexpace/pipeline/standard_test.rb: the sync wiring case holds clock = FakeClock.new, passes settings(clock: clock) and asserts assert_equal([0.0], clock.sleeps.map { |sleep| sleep[:duration] }) -- the retry's one wait on the preset
  • R0-2 should-fix — gems/dexpace-core/test/dexpace/redirect/step_test.rb:389: REDIR-9's 303-rebuild clause has no test — Cookie kept on a cross-origin 303 rebuild survives. Fixed on tests (d696d2b): gems/dexpace-core/test/dexpace/redirect/step_test.rb ReissueTest: ORIGIN_SCOPED_HEADERS = CONTENT_HEADERS.merge(Cookie, Proxy-Authorization), post_with_content_headers(headers:) and a rebuilt_get_for(location) helper; new case `REDIR-9 / REDIR-10 on the
  • R0-3 nit — gems/dexpace-core/test/dexpace/redirect/step_test.rb:1: Two commit subjects exceed 74 characters and 55 body lines exceed 72. Deferred to pr body on docs (579cb9b): The finding is correct (tests subject 80 chars, docs subject 77, 55 body lines over 72) but its suggested fix requires amending the implementer's tests and docs commits, which this round's rules forbid ('never squash, amend or reorder the implementer's or an e
  • R0-4 nit — docs/work/mvp/phase6/phase6b/2026-09-09-phase6b-redirect-checklist.md:104: NFR-13 row miscounts the new test files: nine, not ten. Fixed on docs (579cb9b): Checklist NFR-13 row now reads 'The nineteen new .rb files — ten under lib/, nine under test/ (seven suites and two doubles; round 0 corrected the count from ten)'; verified with git diff --name-only --diff-filter=A main..docs (9 under test/, 10 under

Round 1 → fixed in round 2

  • R1-1 should-fix — gems/dexpace-core/test/dexpace/redirect/step_test.rb:479: REDIR-13's fragment clause has no test — dropping the fragment from the resolved target survives. Fixed on tests (a7192b3): gems/dexpace-core/test/dexpace/redirect/step_test.rb LocationTest: the REDIR-14 case now drives the fragment-only reference its title claimed (chain ?page=2 -> #only -> //other.example/p from https://h/list?page=1 sends https://h/list?page=2#only, resolved aga
  • R1-2 nit — docs/work/mvp/phase6/phase6b/2026-09-09-phase6b-redirect-checklist.md:414: Departure 32 says step_test.rb has "seven classes"; it has ten nested classes. Fixed on docs (66ec26e): Checklist departure 32 now reads "a split, never an inline disable, the way step_test.rb's eleven nested classes are split by concern" (eleven, not the reviewer's ten, because R1-1's fix added RefusedTargetTest; verified by grep of `class .*Test < DexpaceTes

Round 2 → fixed in round 3

  • R2-1 should-fix — gems/dexpace-core/test/dexpace/redirect/step_test.rb:682: REDIR-3's method-and-body preservation on a followed 301/302 has no test — an automatic POST→GET rewrite survives. Fixed on tests (d50ea64): gems/dexpace-core/test/dexpace/redirect/step_test.rb ReissueTest gains 'REDIR-3: a followed 301 and a followed 302 re-issue the ORIGINAL method with the SAME body object and its Content-Type -- there is deliberately NO automatic POST -> GET rewrite, and a PUT
  • R2-2 should-fix — gems/dexpace-core/test/dexpace/redirect/step_test.rb:639: REDIR-5's case-insensitive Content-* removal on the 303 rebuild has no test — dropping the fold survives. Fixed on tests (d50ea64): A new nested RebuildTest ('the 303 rebuild') holds ReissueTest's four 303 cases, CONTENT_HEADERS, ORIGIN_SCOPED_HEADERS, post_with_content_headers and rebuilt_get_for, moved unchanged because ReissueTest stood at 97 code lines against Metrics/ClassLength 100,
  • R2-3 nit — docs/README.md:109: docs/README.md says "the thirteen pages written so far" and then lists fourteen. Fixed on docs (a8e6ba8): docs/README.md:109 reads 'the fourteen pages written so far are ...', matching the fourteen pages the sentence lists and docs/sdk-documentation/ (14 pages beside architecture.md); no other 'thirteen' remains in docs/README.md, README.md, architecture.md or gem

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

  • R3-1 should-fix — gems/dexpace-core/test/dexpace/pipeline/standard_test.rb:181: AsyncPipeline.standard's settings: on the nil-http_tracer_factory branch has no test — dropping it survives on both rows. Mutation m33 in lib/dexpace/async_pipeline.rb: the nil branch of the two-branch build, Resilience::AsyncRetryStep.build(settings: settings, logger: logger) → Resilience::AsyncRetryStep.build(logger: logger) (the else branch, taken only when an http_tracer_factory: is given, left intact). standard_test.rb stays green under it on 4.0.6 AND 3.2.11: 13 runs, 75 assertions, 0 failures, 0 errors. Th
  • R3-2 nit — gems/dexpace-core/test/dexpace/redirect/step_test.rb:678: Commit d50ea64's message says five 303 cases moved unchanged into RebuildTest; four moved and the fifth is new. git show d50ea64 body, last paragraph: 'ReissueTest was at 97 code lines, so the five 303 cases and their two helpers moved unchanged into RebuildTest'. The diff moves four cases ('a 303 is not followed by default', 'opted in, a 303 is re-issued as a GET…', 'REDIR-9 / REDIR-10 on the 303 rebuild', 'a 303 over a NON-replayable body') plus the two constants and two helpers, and ADDS the fifth ('RE

What the final reviewer verified by experiment, both interpreters

  • F2 credential hygiene end to end through a real pipeline: Redirect::Step at REDIRECT, a StateProbe at PRE_AUTH and 6c's REAL Auth::Step (KeyStamper over KeyCredential api_key: 'secret') at AUTH over S — https://a/x → https://b/y: auths ['secret', nil] / markers [false, true]; https://a/x → https://a/y: ['secret','secret'] / [false, false]; A→A→B ['secret','secret',nil] / [false,false,true]; A→B→B ['secret',nil,nil] / [f
  • F3 REDIR-7 on EVERY re-issue: a two-hop same-origin chain with the caller's Authorization, observed by CredentialProbe at the AUTH position and on the wire with no AUTH step — Leaving the redirect step per hop: ['Bearer caller', nil, nil]; on the wire with no AUTH step: ['Bearer caller', nil, nil].
  • F4 Location resolution through the real step — relative y → https://h/v1/y; dotted ../v2/./z against /v1/a/x → https://h/v1/v2/z (RFC 3986; my first expectation of /v2/z was wrong, the code is right); ?p=2 keeps the path; /y#frag and #only (→ https://h/list?page=1#on
  • F5 lifecycle by ORDER — A callable transport entry reads hop 1's closes == 1 at the moment hop 2 is dispatched; the final response is returned open (closes 0); NotReplayableError and SchemeDowngradeError each leave the current response closed (
  • F6 snapshot and predicate — The predicate receives [count, visited external forms incl. the current, frozen? true, status]; << on the set raises FrozenError and the loop is unaffected (200 after 3 calls); false on hop 1 → 302 open after 1 call; a r
  • F7 Task 13a — Pipeline.standard entries [[:redirect, Redirect::Step], [:retry, RetryStep], [:logging, Instrumentation::Step]]; 503→302→200 under flat FakeClock settings returns 200 with urls [x, x, y], the 503 and 302 closed once, the
  • F8 REDIR-28 records on a RecordingSink — hop: from 'https://s/x?sig=', to 'https://h/y?token=', status 302 under 5b's key, count 0, no SECRET/SEED/pass in any payload; malformed: LOCATION_RAW 'ht!tp://u:p@bad' verbatim at :warn; a rejected downgrade emits
  • m33 probe (m33_probe.rb): AsyncPipeline.standard over ScriptedAsyncTransport [503, 200] with flat FakeClock settings and NO http_tracer_factory: — Real code: 'status=200 calls=2 sleeps=[]' (the async step waits through Async.delay, so the clock records nothing at a zero delay); under m33, applied by apply_probe.rb and restored: 'RAISED Dexpace::SeamError: Async.del
  • REDIR-3 and REDIR-5 probes through the real step (inside experiments.rb): POST/PUT × 301/302 under allowed_methods: [method] with a replayable body and Content-Type; a 303 rebuild over content-type / — Every followed 301/302 reaches the transport with the original method token, the same body object (equal?) and Content-Type ['application/json']; the rebuilt GET's names are ['Accept'] alone.
  • lib/dexpace.rb require order (swap.rb, restored byte for byte): step.rb moved to the top of the 6b block; the whole block moved above 6a's; the whole block removed; the ten requires reversed; and each — 'loaded OK :: Dexpace::Redirect::Step' in all four variants and each file loads alone (pipeline.rb itself requires redirect/step): every 6b file require_relatives its dependencies, so the entry-file order is documentatio
  • Mechanical global-constraint checks over the docs tip's git objects (constraints.rb) plus code-line greps — All 19 new .rb files open with the two header lines; the only plain require added under lib is 'uri' and gates:require_allowlist passes on 3.2.11/3.4.10/4.0.6; no require 'set'; URI.join/URI.parse/DEFAULT_PARSER/#merge a
  • CLAUDE.md counts re-derived from the docs tip (counts.rb) — 185 .rb under lib/dexpace/ (184 beside version.rb, and CLAUDE.md spells 'one hundred and eighty-four'); 185 .rbs with every lib file mirrored and no stray sig; exactly 18 lib files without a test mirror and CLAUDE.md nam
  • Cited documents exist where the checklist says; the ledger and the roadmap — docs/first-release.md:344 (the REDIR-27 ⏳ entry) and :418-419 (the standard shape, 'as built by phase 6b's Task 13a on 2026-09-19'); 6c's AUTH-29 row (line 85) and REDIR-11 clause-b row (line 101); 4c's PIPE-39 ⏳ row (li
  • Commit hygiene over main..docs — Twelve commits, correct feat:/test:/docs: prefixes, no attribution or co-author lines, no stray files; the implementer's tests and docs subjects at 80 and 77 characters and 55 body lines over 72 remain (R0-3 deferred); t

Tips reviewed at the final round: code db891fc, tests d50ea64, docs a8e6ba8 on main e61864f.

@Wahbeh-Mohammad
Wahbeh-Mohammad changed the base branch from 23-phase-6b-redirect-tests to main September 19, 2026 16:12
@Wahbeh-Mohammad
Wahbeh-Mohammad merged commit c53638b into main Sep 19, 2026
5 checks passed
@Wahbeh-Mohammad
Wahbeh-Mohammad deleted the 23-phase-6b-redirect-docs branch September 19, 2026 16:12
@Wahbeh-Mohammad Wahbeh-Mohammad mentioned this pull request Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-* area:resilience Retry, recovery, redirects: RETRY-* RECOV-* REDIR-* type:feature New capability or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 6b: Redirect

1 participant