Skip to content

Tier A bug burn-down: #633 + #634 + #556 + #591#649

Merged
aallan merged 5 commits into
mainfrom
fix-post-630-cleanups
May 11, 2026
Merged

Tier A bug burn-down: #633 + #634 + #556 + #591#649
aallan merged 5 commits into
mainfrom
fix-post-630-cleanups

Conversation

@aallan

@aallan aallan commented May 11, 2026

Copy link
Copy Markdown
Owner

Summary

Tier A bug burn-down — four bug closures in one PR. All small, all independent, all pure-quality fixes (no breaking changes, no new user-facing behaviour beyond [E615] diagnostic accuracy).

Closes #633 — defensive cycle guard

_resolve_base_type_name (in vera/wasm/inference.py) now carries an explicit _seen cycle-detection accumulator, restoring consistency with the post-#630 _canonical_named_type walker that already had one. Defence-in-depth: cyclic type aliases are user errors that should be rejected upfront by the type checker (filed separately as #648 — the upstream RecursionError observed while validating #633's reachability), but a bug in the upstream rejection must not turn into a stack-blow inside codegen.

Closes #634 — SlotRef span fidelity

SlotRef and other AST nodes constructed inside InterpolatedString.parts now carry source spans in original-source coordinates instead of synthetic-wrapper coordinates. _parse_interp_expr previously wrapped each segment in a dummy fn interpExpr() { <SEG> } for parsing, placing the segment at wrapper line 3, col 3 — and Lark-emitted spans inside the parsed body were never translated back. The result: [E615] diagnostics on interpolated expressions landed on line 3 of the user's file regardless of where the string literal actually was.

Fix: _split_interpolation now records each segment's offset within the raw string; string_lit computes the segment's original line/column from the outer string-literal span; _parse_interp_expr walks the parsed AST and remaps every Span via a new _remap_spans_inplace helper.

Test assertions tightened: test_e615_fires_on_adt_in_interpolation now pins both line (8) and column (15) of the diagnostic at the SlotRef's position; test_multiple_e615_in_one_interpolation pins per-segment column fidelity (two SlotRefs in the same string get distinct columns 15 and 37 matching their actual source positions).

Closes #556 — Unit-fn-in-statement-position regression pin

The user-visible bug class (calling a user-defined @Unit-returning function in statement position trips WASM validation with type mismatch: expected a type but nothing on stack) was already fixed by #584 in v0.0.135 (_is_void_expr in vera/wasm/context.py now recognises user-defined @Unit fns via _fn_ret_types). The specific repro shape from the original #556 report — a pure helper (no IO effect) followed by a unit-literal final expression — wasn't pinned by the existing conformance test ch07_unit_fn_nontail.vera (which covered IO-effect variants only). Added TestUserUnitFnInStatementPosition556 with two cases: the exact #556 repro plus the where-block variant from the follow-up comment.

Closes #591 — HTTP / Inference UTF-8 hygiene

Three network-response decode sites in vera/codegen/api.py no longer leak Python UnicodeDecodeError text into Vera-level Result::Err strings. Two strategies, chosen per-site by user intent:

  • Http.get / Http.posterrors="replace". A remote server returning non-UTF-8 bytes surfaces as U+FFFD substitutions in the Ok-branch string, preserving the data. User intent for these calls is "fetch this URL"; preserving the body beats preserving the (already-rare) signal that bytes weren't cleanly UTF-8.
  • Inference.complete_call_inference_provider now catches UnicodeDecodeError explicitly and re-raises as RuntimeError("Inference provider '<name>' returned a response body that is not valid UTF-8 (invalid byte at position N)."). Non-UTF-8 from an LLM API is genuinely broken; we want loud failure with a Vera-native message, not the codec can't decode byte 0x... Python form.

Three structural assertions in tests/test_runtime_traps.py::TestNetworkResponseUtf8Hygiene591, mirroring the #589 coverage shape.

What I discovered along the way

While verifying #633's reachability claim, I confirmed that cyclic type aliases (type A = B; type B = A;) currently pass vera check cleanly then crash vera compile with a RecursionError from vera/codegen/core.py:_type_expr_to_wasm_type. Filed as #648 — the proper fix is a post-registration cycle-detection pass in vera/checker/registration.py emitting [E132] with location and rationale. The #633 cycle guard is belt-and-braces for that family.

Burn-down context

Today's bug-burn-down: started at 99 open issues, this PR closes 4, files 1 (#648), and the upcoming spec docs PR will close ~3 more. Net trajectory: 99 → ~93.

Test plan

  • pytest tests/ — 3,802 passed, 14 skipped
  • mypy vera/ — Success: no issues found in 58 source files
  • python scripts/check_conformance.py — All 86 conformance programs pass
  • python scripts/check_examples.py — All 34 examples pass (check + verify)
  • python scripts/check_doc_counts.py — Documentation counts consistent (3802 tests)
  • ruff check --select S vera/ — All checks passed

Closes #633.
Closes #634.
Closes #556.
Closes #591.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Prevented infinite recursion for cyclic type aliases during compilation.
    • Improved interpolation diagnostics so reported spans point to exact source locations.
    • Harmonised UTF‑8 handling so invalid bytes don’t leak internal codec errors into user-facing messages.
  • Tests

    • Added regression tests covering cyclic aliases, interpolation span fidelity, and UTF‑8 decode hygiene.
  • Documentation

    • Updated changelog, known-issues, testing metrics and roadmap counts.

Review Change Stack

Two small post-PR-#631 cleanups, both surfaced during that PR's
review:

- **#633** — `_resolve_base_type_name` in `vera/wasm/inference.py`
  now carries an explicit `_seen` cycle-detection accumulator,
  restoring consistency with the sibling `_canonical_named_type`
  walker that already had one.  Defence-in-depth: cyclic type
  aliases are user errors that should be rejected upfront by the
  type checker (filed as #648 — the upstream `RecursionError`
  observed while validating #633's reachability), but a bug in
  the upstream rejection must not turn into a stack-blow inside
  codegen.  New unit test exercises the guard directly via
  injected cyclic `_type_aliases` (two-, three-, and self-cycle
  cases).

- **#634** — `SlotRef` and other AST nodes constructed inside
  `InterpolatedString.parts` now carry source spans in
  original-source coordinates instead of synthetic-wrapper
  coordinates.  `_parse_interp_expr` previously wrapped each
  segment in a dummy `fn interpExpr() { <SEG> }` for parsing,
  placing the segment at wrapper line 3, col 3 — and Lark spans
  emitted inside the parsed body were never translated back, so
  diagnostics on interpolated expressions landed on line 3 of
  the user's file regardless of where the string literal actually
  was.  `_split_interpolation` now records each segment's offset
  within the raw string; `string_lit` computes the segment's
  original line/column from the outer string-literal span; and
  `_parse_interp_expr` walks the parsed AST and remaps every
  `Span` via a new `_remap_spans_inplace` helper (uses
  `object.__setattr__` to bypass frozen-dataclass guard — safe
  because `Node.span` is declared with `compare=False` precisely
  to allow this kind of late correction).

  The softened assertion in
  `TestE615LoudInterpolationFallthrough630::test_e615_fires_on_adt_in_interpolation`
  is tightened to pin both line (8) **and** column (15) of the
  diagnostic at the SlotRef's position, and
  `test_multiple_e615_in_one_interpolation` now pins per-segment
  column fidelity — two SlotRefs in the same string get two
  distinct columns (15 and 37) matching their actual source
  positions.

KNOWN_ISSUES.md: removed entries for #633 and #634; added
entry for #648.

Closes #633.
Closes #634.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b455498-42e7-4f6c-8974-423203facd20

📥 Commits

Reviewing files that changed from the base of the PR and between b623aaf and c7fbf63.

📒 Files selected for processing (2)
  • TESTING.md
  • tests/test_runtime_traps.py

📝 Walkthrough

Walkthrough

Adds cycle detection to alias resolution, remaps interpolated-expression spans to original-source coordinates for correct diagnostic pinning, hardens UTF‑8 decoding in host/provider code paths, and updates tests, changelog and known-issues metadata.

Changes

Cycle-Detection Fix (Issue #633)

Layer / File(s) Summary
Cycle-Detection Accumulator
vera/wasm/inference.py
_resolve_base_type_name rewritten to accept _seen: frozenset[str] and short‑circuit on repeats to avoid infinite recursion.
Cyclic Alias Test
tests/test_wasm_coverage.py
test_cyclic_alias_does_not_recurse_forever constructs WasmContext with 2/3/self cycles and asserts termination and opaque-name returns.
Changelog Entry
CHANGELOG.md
Documents the cycle-detection fix under Fixed.

Interpolation Span-Remapping Fix (Issue #634)

Layer / File(s) Summary
Interpolation Segment Format
vera/transform.py
_split_interpolation returns expression segments as (expr_text, offset_in_raw) tuples capturing each \( opener index.
Span Remapping Infrastructure
vera/transform.py
Added _remap_spans_inplace traversal to update Span fields in dataclass AST nodes in-place using a mapping function.
Expression Parsing with Base Coordinates
vera/transform.py
_parse_interp_expr accepts optional base_line/base_col and remaps parsed spans from synthetic wrapper back to original coordinates.
STRING_LIT and string_lit Integration
vera/transform.py
VeraTransformer.STRING_LIT widened to include (str,int) segments; string_lit computes per-segment base coordinates (literal meta + offset) and passes them to _parse_interp_expr.
E615 Diagnostic Span Assertions
tests/test_codegen.py
test_e615_fires_on_adt_in_interpolation and test_multiple_e615_in_one_interpolation now assert exact location.line and location.column values for the failing SlotRef segments.
Changelog Entry
CHANGELOG.md
Documents interpolation span remapping and E615 test tightening under Fixed.

UTF‑8 Hygiene in Codegen/API (Issue #591)

Layer / File(s) Summary
Inference provider decoding
vera/codegen/api.py
_call_inference_provider decodes provider body with strict UTF‑8 and converts UnicodeDecodeError into a Vera RuntimeError reporting invalid byte position before JSON decode.
Host http_get/http_post decoding
vera/codegen/api.py
WASM host http_get/http_post now decode response bytes using errors='replace' so invalid bytes become replacement characters and OK-path strings remain usable.
Runtime tests
tests/test_runtime_traps.py
New tests assert errors='replace' decoding and that inference helper raises a Vera-native RuntimeError containing “not valid UTF-8” when strict decode fails.

Regression Tests & Documentation / Metadata

Layer / File(s) Summary
User @Unit-in-statement regression
tests/test_codegen.py
Added TestUserUnitFnInStatementPosition556 ensuring user-defined @Unit helpers called in statement position do not emit stray drop in generated WAT.
Escaped backslash-paren literal test
tests/test_codegen.py
Added test_escaped_backslash_before_paren_is_literal verifying "\(" is treated as a literal, not an interpolation opener.
KNOWN_ISSUES update
KNOWN_ISSUES.md
Removes closed issues #633, #634, and #591 from the “Bugs” table and adds #648 describing cyclic-alias RecursionError.
TESTING / ROADMAP metrics
TESTING.md, ROADMAP.md
Test totals and per-file test counts/line counts updated to reflect added/modified tests (reported total tests updated to 3,803).
CHANGELOG documentation
CHANGELOG.md
Documentation subsection updated to reference KNOWN_ISSUES changes (removed closed issues, added #648).

Sequence Diagram(s)

(No sequence diagrams generated.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • aallan/vera#629: Prior inference changes affecting alias/refinement resolution used by _resolve_base_type_name.
  • aallan/vera#590: Overlaps UTF‑8 decoding and host I/O handling; touches vera/codegen/api.py and related tests.
  • aallan/vera#454: Modifies alias-resolution/canonicalisation logic in vera/wasm/inference.py, related to _resolve_base_type_name.

Suggested labels

compiler, tests, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly references four specific issue numbers (#633, #634, #556, #591) that form the primary substance of this PR, all of which are addressed by the included changes and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-post-630-cleanups

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.95652% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.95%. Comparing base (2407624) to head (c7fbf63).

Files with missing lines Patch % Lines
vera/transform.py 86.84% 10 Missing ⚠️
vera/codegen/api.py 75.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #649      +/-   ##
==========================================
- Coverage   90.97%   90.95%   -0.02%     
==========================================
  Files          59       59              
  Lines       23137    23197      +60     
  Branches      259      259              
==========================================
+ Hits        21048    21098      +50     
- Misses       2082     2092      +10     
  Partials        7        7              
Flag Coverage Δ
javascript 57.36% <ø> (ø)
python 94.73% <86.95%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

PR #649's CI lint step (`ruff check --select S vera/`) flagged two
S101 violations on the new `assert isinstance(segment, ...)` calls
in `string_lit`.  S101 is the right call here — `assert` statements
are stripped under `python -O`, so they shouldn't be load-bearing
in production code, even when their primary purpose is mypy
narrowing.

Refactored to isinstance-based dispatch, which provides both the
mypy narrowing and runtime safety without the assert.  The
alternating-layout contract of `_split_interpolation` is now
enforced by the if/else structure rather than asserted, which is
the more honest expression of the invariant.

Co-Authored-By: Claude <noreply@anthropic.invalid>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
vera/transform.py (1)

228-258: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip escaped pairs while scanning non-interpolation text.

_has_interpolation() skips escaped characters, but _split_interpolation() does not. That means a valid string like "...\\(literal) ... \(@Int.0)" is mis-split once a real interpolation exists later: the second backslash of \\( is reinterpreted as an opener, so the literal fragment becomes a bogus interpolation segment.

Suggested fix
     while i < len(raw):
         if raw[i] == "\\" and i + 1 < len(raw) and raw[i + 1] == "(":
             # Flush literal buffer
             parts.append("".join(buf))
             buf = []
@@
             parts.append((expr_text, i))
             i = j
         else:
-            buf.append(raw[i])
-            i += 1
+            if raw[i] == "\\" and i + 1 < len(raw):
+                buf.append(raw[i])
+                buf.append(raw[i + 1])
+                i += 2
+            else:
+                buf.append(raw[i])
+                i += 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vera/transform.py` around lines 228 - 258, _split_interpolation currently
treats a single '\' as potential interpolation opener while scanning
non-interpolation text, causing sequences like "\\(" to be mis-parsed; change
the scanning loop in _split_interpolation to skip escaped pairs the same way
_has_interpolation does: when encountering a backslash and there is a next
character, append both characters to buf and advance i by 2 (treat as literal),
only treating "\(" followed by "(" as the start of an expression; preserve
existing flushing of buf into parts, error handling, and the logic that finds
the matching ')' and empty-expression checks (use the same variables raw, i,
buf, parts, expr_text, j, depth, meta, and error_code references).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@vera/wasm/inference.py`:
- Around line 1535-1545: The function _resolve_base_type_name currently returns
the revisited alias when a prefixed chain forms a cycle because it lumps "name
in _seen" with "name not in self._type_aliases"; change it to preserve and
return the original caller's alias by introducing a root_name parameter (e.g.,
def _resolve_base_type_name(self, name, _seen=frozenset(), root_name=None) and
set root_name = name on the first call), then split the guard into two checks:
if name in _seen: return root_name, and if name not in self._type_aliases:
return name; ensure all recursive calls (the calls inside the ast.RefinementType
and NamedType branches) pass the same root_name and _seen so cycles return the
original caller's name rather than the internal revisited node.

---

Outside diff comments:
In `@vera/transform.py`:
- Around line 228-258: _split_interpolation currently treats a single '\' as
potential interpolation opener while scanning non-interpolation text, causing
sequences like "\\(" to be mis-parsed; change the scanning loop in
_split_interpolation to skip escaped pairs the same way _has_interpolation does:
when encountering a backslash and there is a next character, append both
characters to buf and advance i by 2 (treat as literal), only treating "\("
followed by "(" as the start of an expression; preserve existing flushing of buf
into parts, error handling, and the logic that finds the matching ')' and
empty-expression checks (use the same variables raw, i, buf, parts, expr_text,
j, depth, meta, and error_code references).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 102c91fc-8acd-4d24-a687-8010a3efc2f3

📥 Commits

Reviewing files that changed from the base of the PR and between 2407624 and 1c58421.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen.py
  • tests/test_wasm_coverage.py
  • vera/transform.py
  • vera/wasm/inference.py

Comment thread vera/wasm/inference.py Outdated
Two more Tier A items in today's bug burn-down (following #633 + #634
in the previous commits on this branch):

- **#556** — calling a user-defined `@Unit`-returning function in
  statement position used to fail WASM validation
  (`type mismatch: expected a type but nothing on stack`).  The
  bug class was already fixed by #584 in v0.0.135 (the
  `_is_void_expr` registry update in `vera/wasm/context.py`).
  The original #556 report's *exact* repro shape — a pure helper
  (no IO effect) followed by a unit-literal final expression —
  wasn't pinned by the existing conformance test
  `ch07_unit_fn_nontail.vera` (which covered IO-effect variants
  only).  Added `TestUserUnitFnInStatementPosition556` with two
  regression cases (the pure-helper variant and the where-block
  variant from the follow-up comment).

- **#591** — three network-response UTF-8 decode sites in
  `vera/codegen/api.py` no longer leak Python
  `UnicodeDecodeError` text into Vera-level `Result::Err`
  strings.  Two strategies, chosen per-site by user intent:

  - **`Http.get` / `Http.post`** (lines 2826, 2858) — switched to
    `errors="replace"`.  A remote server returning non-UTF-8
    bytes surfaces as U+FFFD substitutions in the Ok-branch
    string, preserving the data.  User intent for these calls
    is "fetch this URL"; preserving the body beats preserving
    the (already-rare) signal that bytes weren't cleanly UTF-8.

  - **`Inference.complete`** (line 756, in
    `_call_inference_provider`) — now catches `UnicodeDecodeError`
    explicitly and re-raises as `RuntimeError("Inference
    provider '<name>' returned a response body that is not
    valid UTF-8 (invalid byte at position N).")`.  The
    `host_inference_complete` wrapper's existing
    `except Exception` catches this and writes the
    Vera-shaped message into the Err branch.  Non-UTF-8 from
    an LLM API is genuinely broken; we want loud failure with
    a Vera-native message, not the `codec can't decode byte
    0x...` Python form.

  Three structural assertions in
  `tests/test_runtime_traps.py::TestNetworkResponseUtf8Hygiene591`,
  mirroring the #589 coverage shape.

KNOWN_ISSUES.md: removed entry for #591.
TESTING.md, ROADMAP.md: test counts refreshed (3,797 → 3,802).

Closes #556.
Closes #591.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan aallan changed the title Fix #633 and #634 — post-#630 cleanup pass Tier A bug burn-down: #633 + #634 + #556 + #591 May 11, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_runtime_traps.py`:
- Around line 2063-2079: The test's current string assertions in
test_http_post_uses_errors_replace (and the other affected assertions around the
2093-2107 range) can pass if the target text appears only in comments; change
them to assert on the actual decode-and-raise expression shape so comments can't
satisfy them. Concretely, update the checks to look for the decode call that
includes errors="replace" as part of the same expression (e.g. ensure the source
contains the substring pattern of resp.read().decode( ... , errors="replace") or
the decode call immediately followed by a raise (the specific test function
names to edit are test_http_post_uses_errors_replace and the analogous test
around the 2093-2107 block); make the assertion fail if that contiguous pattern
is not present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2b1f062c-1c8f-40a2-8136-168477568721

📥 Commits

Reviewing files that changed from the base of the PR and between 47621eb and 882a35e.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen.py
  • tests/test_runtime_traps.py
  • vera/codegen/api.py

Comment thread tests/test_runtime_traps.py
Two valid findings, both fixed with new regression coverage:

**Finding 1 — `_resolve_base_type_name` returned the revisited
cycle node, not the caller's original name, on prefix-chain
cycles.**

For aliases `{A: B, B: C, C: B}` (A leads INTO a B↔C cycle that
doesn't include A itself), calling with `A` previously returned
`B` (the revisited cycle node where `name in _seen` first hit),
not `A` (the caller's input).  My initial test only covered FULL
cycles (`A→B→A`, `A→B→C→A`, `X→X`) where caller-name == revisited-
name by definition, hiding the bug.

Fix: thread a `_root_name` parameter through the recursion (set
to the original `name` on first call, preserved across recursion).
The cycle guard now returns `_root_name`, so the function's
identity-on-cycle contract is "return what you were asked about,
unchanged".

Added a regression test case covering the prefix-chain shape:
`_resolve_base_type_name("A")` with `{A: B, B: C, C: B}` must
return `"A"`, not `"B"`.

**Finding 2 — `_split_interpolation` mis-segmented escaped
backslashes.**

`_has_interpolation` correctly skipped escaped pairs (so a string
like `"\\("` was seen as having no interpolation at all), but
`_split_interpolation` rescanned the second character as a fresh
start and mis-parsed the `\\(` as the opener of an interpolation
segment.  Result for `"a\\(b)c"`: a parse-time crash (no matching
`)`) where the user expected a literal `a\(b)c`.

Pre-existing bug (not introduced by this PR), but CodeRabbit
flagged it because PR #649 touches the function.  Fixed
opportunistically since the scope is small and the
two-helpers-must-agree invariant is exactly what the file already
encodes.

Fix: aligned `_split_interpolation`'s escape-skipping with
`_has_interpolation`'s shape — when a `\` is followed by any
character other than `(`, both characters are appended to the
literal buffer and `i` advances by 2 so the escaped character is
not re-scanned as a potential interpolation opener.

Added `test_escaped_backslash_before_paren_is_literal` to
`TestStringInterpolation` exercising the `"a\\(b)c"` case
end-to-end.

Doc counts refreshed (3,802 → 3,803).

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented May 11, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Both valid — fixed in b623aaf.

Finding 1 (_resolve_base_type_name cycle guard): you're right that the prefix-chain shape {A: B, B: C, C: B} returned the revisited cycle node B instead of the caller's input A. My initial test only covered full cycles (where caller-name == revisited-name by definition), hiding the bug. Threaded a _root_name parameter through the recursion so the guard returns the caller's original input. Added a prefix-chain test case as the regression pin.

Finding 2 (_split_interpolation escape skipping): valid and a pre-existing bug — _has_interpolation correctly skipped escaped pairs but _split_interpolation didn't, so a string like "a\\(b)c" parse-crashed instead of resolving to a literal a\\(b)c. Aligned the escape-skipping shape with _has_interpolation and added test_escaped_backslash_before_paren_is_literal as the regression pin. Fixed opportunistically since I'm in the file and the two-helpers-must-agree invariant is exactly what this code is supposed to encode.

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan: Thanks for the detailed write-up — both fixes look exactly right.

For Finding 1, threading _root_name as the anchor for the cycle-guard return value is the correct shape: the caller's input is the only semantically meaningful fallback (the revisited node is an implementation detail of the alias chain, not the name the caller asked about). The prefix-chain regression pin {A→B, B→C, C→B} is precisely the case that the original full-cycle-only tests couldn't catch, so that's a solid addition.

For Finding 2, the two-helpers-must-agree invariant is the right way to think about it. Since _has_interpolation is the predicate that gates whether _split_interpolation is even called, any escape-sequence shape that one skips and the other doesn't creates a reachability gap that turns into a parse crash — exactly what you observed with "a\\(b)c". Opportunistic fix is well-justified here given the shared invariant.

CodeRabbit-flagged: the previous assertions in
`TestNetworkResponseUtf8Hygiene591::test_http_post_uses_errors_replace`
checked the bare substring `'errors="replace"'`, which also
appears in the explanatory comment block immediately above the
real decode call.  A future regression that removed
`errors="replace"` from the actual decode while leaving the
comment intact would pass the test spuriously.

`test_inference_complete_catches_unicode_decode_error` wasn't
vulnerable in current code (the production `except` keyword does
not appear in nearby comments — they use backtick-quoted
`` `UnicodeDecodeError` ``) but a future comment containing the
literal substring `except UnicodeDecodeError` would break it.

Both tests now use `re.search` with patterns that bind the target
phrase to the surrounding code expression:

- **http_post**: the regex
  `r'resp\.read\(\)\.decode\([^)]*errors="replace"'` requires
  `errors="replace"` to be part of the same `resp.read().decode(...)`
  call.  `[^)]*` permits the multi-line form (with `"utf-8",`
  argument on its own line) while forbidding any closing paren
  between `decode(` and `errors="replace"` — so a comment placed
  outside the call cannot satisfy the match.

- **inference**: two regexes, one for the contiguous
  `except UnicodeDecodeError ... raise RuntimeError(` block
  (up to 10 lines between to allow whitespace and the `from None`
  trailer), one for the `not valid UTF-8` phrase inside the
  `raise RuntimeError(...)` argument string.  The DOTALL-flag
  bounded `.{0,400}?` permits adjacent-literal f-string
  concatenation across multiple lines (the production code
  splits the message across three `f"..."` for readability).

Validated the regexes reject the comment-only-stub failure mode
with a direct counter-example test (substituted a body where
`errors="replace"` lives only in a comment and the decode is
strict-mode; regex correctly returned None).

Added `import re` to the test module's imports.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented May 11, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Valid finding on test_http_post_uses_errors_replace — fixed in c7fbf63. Verified the bare substring errors="replace" appeared in both the explanatory comment block above the call site and the actual decode call, so a future regression removing the code while leaving the comment intact would have passed the test spuriously.

For test_inference_complete_catches_unicode_decode_error: the current code is not vulnerable (production except keyword does not appear in nearby comments — they use backtick-quoted \UnicodeDecodeError`), but the substring assertion was structurally fragile against a future comment containing the literal except UnicodeDecodeError`. Hardened defensively under the same banner.

Both tests now use re.search with patterns that bind the target phrase to the surrounding code expression (resp.read().decode([^)]*errors="replace" for http_post, and except UnicodeDecodeError ... raise RuntimeError( block + raise's string argument check for inference). Validated the regex rejects the comment-only-stub failure mode with a direct counter-example test.

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan: Both hardenings are structurally correct.

For test_http_post_uses_errors_replace, anchoring to resp.read().decode([^)]*errors="replace" is the right shape — the pattern requires the parameter to be inside the decode call expression, so a comment containing errors="replace" in isolation can't satisfy it. That closes the comment-drift false-positive exactly.

For test_inference_complete_catches_unicode_decode_error, the two-clause pattern (except UnicodeDecodeError … raise RuntimeError( plus the string argument check) is good structural binding. A comment can contain either clause but not both in the syntactic arrangement a real except/raise block produces — so the combined pattern is sound. The counter-example rejection test for the comment-only stub is the right way to pin that the regex itself is doing what's claimed; that's a useful meta-test to have.

@aallan aallan merged commit 75a3982 into main May 11, 2026
22 checks passed
@aallan aallan deleted the fix-post-630-cleanups branch May 11, 2026 10:17
aallan added a commit that referenced this pull request May 11, 2026
Recovery PR for the release-prep work that should have been part
of #649 (Tier A bug burn-down) but was missed.  Per the project's
established pattern since v0.0.8: release prep belongs in the
feature PR; post-merge is tag + release ONLY.  I broke that rule
on #649; this PR is the recovery.

Release prep:

- **Version bump** across the six tracked sites (0.0.143 →
  0.0.144): `vera/__init__.py` (`__version__` + `version`),
  `pyproject.toml`, `README.md` (active-development line: 143 →
  144 releases, 3,796 → 3,803 tests), `docs/index.html` (hero
  version-link span), `HISTORY.md` (new row + "58 active
  development days" + "144 tagged releases"), and `uv.lock`.

- **CHANGELOG cut** — renamed `## [Unreleased]` to
  `## [0.0.144] - 2026-05-11`, added a fresh empty `## [Unreleased]`,
  and added the `[0.0.144]` link reference plus bumped the
  `[Unreleased]` link comparison base.

Limitations sweep (KNOWN_ISSUES.md, vera/README.md, spec/*):

- **KNOWN_ISSUES.md** — removed the `#595` Bug row (closed
  2026-05-07; the runtime-workaround entry referencing it has
  been reworded to drop the now-redundant "closes #595"
  parenthetical, since #595 was closed independently of the
  workaround being removed).  Removed the `#605` Documentation
  row (closed 2026-05-07; `decreases` doc gap fixed).

- **spec/00-introduction.md** — removed the `#226 Typed holes`
  row from the "Future Features" table (closed; typed holes
  shipped).

- **vera/README.md** — checked, no stale references.

- **spec/06-contracts.md**, **spec/08-modules.md**,
  **spec/12-runtime.md** limitation tables — all entries verified
  open via `scripts/check_limitations_sync.py --check-states`.

All consistency checks green:
- `python scripts/check_version_sync.py` — 0.0.144 consistent across 6 files
- `python scripts/check_doc_counts.py` — counts consistent
- `python scripts/check_limitations_sync.py --check-states` — 34 in KNOWN_ISSUES.md, 18 in vera/README.md, 4 across spec chapters; all live-state-verified

No code changes.  Pure release-prep + limitations-table sweep.

Co-Authored-By: Claude <noreply@anthropic.invalid>
aallan added a commit that referenced this pull request May 11, 2026
v0.0.144 release prep (recovery — should have been in #649)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment