Skip to content

Narrow decoded records from the shape check, and restore Pi field paths - #2783

Merged
trissim merged 3 commits into
mainfrom
fix/fits-wire-typeguard
Oct 23, 2026
Merged

trissim merged 3 commits into
mainfrom
fix/fits-wire-typeguard

Conversation

@trissim

@trissim trissim commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Two fixes in field_codec.py.

1. The casts after fits_wire are deleted. #2778 left two cast(dict, data) calls right after fits_wire. Slop: IDEN-3. The fact "data is a dict" is established by fits_wire and was restated by the cast; the check now narrows it with a TypeGuard.

  • _RecordDecoder.fits_wire returns TypeGuard[dict[str, object]], so checked() returns data directly.
  • _FamilyRecordDecoder.tagged() (TypeGuard) names the family form. fits_wire and member() both use it. That also removes member()'s unbound _FamilyRecordDecoder.fits_wire(self, data) call, which only existed to skip the leaf document's override.
  • _declared_member(name: object) now matches DeclaredFamily.member_named(name: object), the owner it forwards to.

2. Refused Pi payload fields have their field path again. tests/test_native_transcript.py was red on main for two reasons.

  • Content case (code regression, fixed at the owner). Bisected to fix: keep resolved Pi values out of wire decoding (P428) #2543 (94526e1). It moved Pi payloads onto PiPayload.decode_fields, which builds the member directly. The step that put a refused field under its key and fix lived only as a hand-written try/except inside _RecordDecoder.decode_selected, so the new path lost it: content: 42 refused as a bare Expected a JSON array..
    • FieldPlan.locating(problems) now owns that step, through MalformedRecord._Located.
    • decode_selected's try/except is deleted in favour of it.
    • PiPayload.decode_fields uses the same owner and raises RecordProblems.
    • Result: message.content: Expected a JSON array.; fix: supply typing.Annotated[tuple[...PiContent, ...], ...], the same text the good revisions printed.
  • JSON case (test expectation). Since 79cd5dc, NativeEntry.read goes through FieldCodec.parse. Bad JSON is now MalformedRecord("Invalid JSON: ..."), which MalformedRecord's docstring declares (field_codec.py:57-60), so the test now expects that.
  • The test's match checked nothing. It was declared in the parametrize table but never passed to pytest.raises. It is now applied. The content regex matches the field path and type without pinning the annotation's repr.

Checks

  • Local: tests/test_native_transcript.py, tests/test_field_codec.py and the pi tests: 156 passed.
  • mypy, measured the way tools/debt_ratchet.py does, base vs head:
    • field_codec.py: 112 → 112, identical error set.
    • pi_payloads.py: 111 → 111, identical error set.

🤖 Generated with Claude Code

Citation fixture and others added 2 commits October 23, 2026 09:51
_RecordDecoder.fits_wire returns TypeGuard[dict[str, object]], so checked()
returns the narrowed value and its cast(dict, data) is gone. The family form
gets its own check, tagged(), also a TypeGuard; fits_wire and member() both
use it, which deletes member()'s cast and its unbound
_FamilyRecordDecoder.fits_wire(self, data) call that existed only to skip
the leaf document's override. _declared_member takes the wire value as
object, like DeclaredFamily.member_named, the owner it forwards to.

mypy on field_codec.py: 112 before, 112 after, identical error set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2543 (94526e1) moved Pi payloads from normalize-then-FieldCodec.decode
to PiPayload.decode_fields, which constructs the member directly. The
per-field try/except that located a refusal under its key and fix lived
only inside _RecordDecoder.decode_selected, so the new path dropped it: a
transcript entry with content 42 refused as bare "Expected a JSON array."
instead of "message.content: ...; fix: supply ...".

FieldPlan.locating(problems) now owns that wrapping (MalformedRecord._Located
records the refusal under the field key and suppresses it so sibling
refusals are reported together). decode_selected's hand-written try/except
is deleted in favour of it, and PiPayload.decode_fields uses the same owner
and raises RecordProblems, so both decode paths derive the location from
one place.

test_native_transcript: the parametrized match was declared but never
passed to pytest.raises, so it asserted nothing. It is now applied. The
JSON case expects MalformedRecord("Invalid JSON: ...") since 79cd5dc
moved NativeEntry.read onto FieldCodec.parse; the content case matches the
field path and type without pinning the annotation's repr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trissim trissim changed the title Narrow decoded records from the shape check instead of casting after it Narrow decoded records from the shape check, and restore Pi field paths Oct 23, 2026
@trissim
trissim marked this pull request as ready for review October 23, 2026 13:56
Copilot AI balanced review requested due to automatic review settings October 23, 2026 13:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@github-actions

Copy link
Copy Markdown

debt-src at 37beb25, every finding:

grew NoneAbsence:src/agent_comms/field_codec.py +3
introduced NoneAbsence:src/agent_comms/field_codec.py at lines [103, 103, 104]

debt-src counted three new NoneAbsence spellings at field_codec.py:103-104:
the exit parameters were copied from _Refusal as `X | None`. The locator
only asks whether the exception is a MalformedRecord, so `object` states
exactly what it needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trissim
trissim merged commit 64d995f into main Oct 23, 2026
7 of 8 checks passed
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.

2 participants