Skip to content

client-method-shape: check a walk's first request, and pass a stream - #10

Merged
kelsayed17 merged 3 commits into
mainfrom
claude/method-shape-walks-and-streams
Oct 29, 2026
Merged

kelsayed17 merged 3 commits into
mainfrom
claude/method-shape-walks-and-streams

Conversation

@kelsayed17

Copy link
Copy Markdown
Owner

What changes

Two forms client-method-shape mishandled. Both were measured by running this repo's engine and the source rule's engine on the same cases.

1. A walk's first request. self.paginate(...)'s one positional argument was never read. A computed path (self.paginate(_path_for(year), ...)), a second positional argument, or a PageRequest built any other way all passed. The walk's first request must now be:

  • a path string (a walk of GETs), or
  • PageRequest(<path string>, body=<request parameter>, write=<WRITER>) with nothing else in it (headgate 0.8.0's walk of POSTs). The writer is an UPPER_CASE name, public or _-prefixed, the same as body='s writer.

Next to a PageRequest, params= and idempotency_key= are refused, because headgate raises for both there. A walk of GETs can still pass idempotency_key=, so pass/paginate_options still passes. There is no walk leg to update: the envelope leg, and the walk leg that paginated it, were already removed from this rule.

2. Form (g), streams. A stream used to be reported at its def line. The rule now passes an async def whose one statement is async with self.stream(...) as <s>: holding only async for <c> in <s>.aiter_bytes(): holding only yield <c>, with no else:.

  • <s> and <c> must be plain names.
  • The item may be bare or inside one pair of parentheses.
  • self.stream(...) takes the same forward checks as a verb (bad-forward-args-common, including the module-level expect= and knob rules).
  • Its keywords are limited to what a GET stream takes: params, headers, timeout, deadline, retry and follow_redirects. So a body option, expect=, cache_ttl=, method= and last_event_id= are all reported.
  • A stream item written without as, as (self.stream(...)) as s, or inside two pairs of parentheses is still reported. These cases are listed under KNOWN GAPS.

The rule header now covers (e) and (g) and the CHANGED-from-source items, and its note mentions both forms. verifier.md is regenerated, conventions.md documents the stream and the walk of POSTs, and CHANGELOG.md has two entries under Unreleased.

Fixtures

  • fail: the source's invalid 281–287 and 289, each rewritten with a module-level reader, limit= and the original fault kept. Also the computed-path and to_builtins(...)/extra= variants, plus walk_first_request_another_constructor, walk_page_request_second_positional, walk_page_request_lowercase_writer, walk_of_posts_idempotency_key, walk_of_posts_params, and 18 stream_* cases (plain with, plain for, two items, no as, wrong callee, number timeout, expect=, body, other iterator, chunk size, other object, yields something else, yields twice, else:, work before or after the loop, stream or chunk bound to an attribute).
  • pass: the source's valid 035 and 036, valid 032 with expect= moved to a module name, walk_of_posts_public_writer and stream_parenthesised.

Mutation check

Each new clause was deleted (or narrowed, for one arm of an any) in turn, and the rule's fixtures were rerun: 44 of 44 mutations fail at least one fixture. Three qualifiers started out inert. Two were made live with new fixtures: kind: identifier on the loop target and on the as alias, via the attribute-binding cases. The third, kind: call on the loop's iterable, was dropped because the grammar makes it redundant. The source's "inside an async def" check was not carried over, because Python's parser already turns that case into a parse-error.

Checklist

  • uv run ruff check, uv run ruff format --check and uv run ty check pass, with no suppression comments
  • uv run python -m pytest --cov passes, at or above the coverage floor (96.82%, floor 96; 1872 passed)
  • A new or changed rule or check has fail and pass fixtures, written first and seen failing (17 failed before the rule change)
  • uv run python scripts/gen_rule_docs.py --check passes, and the skill's references describe the new behaviour
  • uv run headgate-verify examples/ passes with no findings
  • CHANGELOG.md has an entry under "Unreleased"
  • uv lock --check passes

The checks above were run locally.

🤖 Generated with Claude Code

kelsayed17 and others added 2 commits October 28, 2026 21:49
A walk's one positional argument is now read: a path string, or
PageRequest(<path string>, body=<request parameter>, write=<WRITER>) and
nothing else. Beside a PageRequest, params= and idempotency_key= are
refused, which is when headgate 0.8.0 raises; a walk of GETs keeps
idempotency_key=.

Form (g): an async def whose one statement is
`async with self.stream(...) as <s>:` holding only
`async for <c> in <s>.aiter_bytes():` holding only `yield <c>`, no else.
The stream's arguments take the forward checks, and only the keywords a
GET stream takes. The item may be in one pair of parentheses.

Fixtures for every new clause; each clause was deleted in turn and each
deletion fails a fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ures

Raise the headgate floor to 0.8.0, whose PageRequest (body=, write=)
and paginate the rule now checks; the lock moves headgate 0.7.0 to 0.8.0
and nothing else.

A PageRequest may carry the query as params=params, the only way to give
a walk of POSTs one; any other params= value is reported. The header
says a PageRequest without write= (a JSON body) is reported, and
discloses that an Idempotency-Key in headers= beside a PageRequest is
not read, and that line and server-sent-event streams still need an
allow comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kelsayed17

Copy link
Copy Markdown
Owner Author

Review addressed in c670e85:

  1. The headgate floor is now >=0.8.0. uv lock on its own picked 0.9.0, so I locked to 0.8.0 with --upgrade-package headgate==0.8.0. headgate 0.7.0 to 0.8.0 is the only change in the lock. The README badge, the floor sentence in the CHANGELOG, conventions.md and the example-copy pyproject in test_examples.py now say 0.8.
  2. PageRequest now accepts params=params, the bare **params collector. Any other params= value is reported. New fixtures: pass/walk_of_posts_with_a_query, and fail/walk_page_request_query_not_forwarded (a param under another name). fail/case_283 now covers the case where params is not a request parameter. Mutation check: deleting the clause, its ^params$ regex, its request-param match, or params from the keyword whitelist each fails a fixture (4 of 4).
    • The clause has to come before the body= clause, because the $RNAME that request-param binds in an all: carries over to the clauses after it.
      2–4. Updates to the header text:
    • item 3: a PageRequest without write= is reported. In this skill, a verb accepts json=<request parameter>, so this is stricter than a verb, not consistent with one. The header says so.
    • items 2 and 4 are now in KNOWN GAPS:
      • aiter_lines, aiter_events, last_event_id= and method= streams still need an allow comment.
      • An Idempotency-Key in headers= beside a PageRequest isn't read.

Gates against headgate 0.8.0, all passing: uv lock --check, ruff check, ruff format, ty, docs --check, pytest (1874 passed, coverage 96.82%) and headgate-verify examples/.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kelsayed17
kelsayed17 merged commit 47c041d into main Oct 29, 2026
12 checks passed
@kelsayed17
kelsayed17 deleted the claude/method-shape-walks-and-streams branch October 29, 2026 05:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant