Skip to content

Carry the UAST structural facts across the Rust/Python boundary - #26

Open
n1ckyb wants to merge 1 commit into
release/v0.0.2-rcfrom
fix/uast-facts-cross-boundary
Open

Carry the UAST structural facts across the Rust/Python boundary#26
n1ckyb wants to merge 1 commit into
release/v0.0.2-rcfrom
fix/uast-facts-cross-boundary

Conversation

@n1ckyb

@n1ckyb n1ckyb commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

The engine computed three facts for every language and Python threw them away.

uast.rs emits early_exit_count, negated_condition_count and has_guard_clause for function entities, and asserts them in 12 of its own Rust tests — all passing. NodeFacts carried no fields for any of them, so pydantic dropped them silently at the DTO boundary.

Measured before, on a function with two early returns:

early_exit_count = ABSENT

Why these matter more than three fields

They are the first facts derived from the pruned canonical tree rather than a per-language extractor — so they are available for every language, not only the ones with bespoke support. That makes them precisely the facts a Go or Java binding would rely on, and they reached nobody.

Verified after the change, on a real guard clause:

def send(msg):
    if not msg:           # negated_condition_count = 1
        return None       # early_exit_count        = 1
    return deliver(msg)   # has_guard_clause        = True

has_guard_clause is deliberately tri-state

The pruned tree can prove a guard is present but cannot prove one is absent — statement order survives pruning, operators do not. So None means "not determinable here", never "no guard". Collapsing it to False would let a consumer draw a conclusion the evidence does not support.

The tests

Four acceptance tests, including one asserting the fields exist on the model at all. The defect was never a wrong value — it was a missing field, and pydantic drops unknown keys without complaint.

Proven to bite: removing the field fails all four; restoring it passes all four.

Verification

Full suite: 2412 passed, 0 failed (2408 before, +4 new).

The wider lesson

Rust being the certified path does not help if the value never reaches the caller, and nothing on either side was watching the join. A Rust test proving a fact is computed and a Python test proving it is delivered are different assertions — this repo had the first and not the second.

🤖 Generated with Claude Code

…undary

The cross-language structural facts (#9) were computed by the engine and thrown
away before any caller saw them.

uast.rs emits three facts for function entities - early_exit_count,
negated_condition_count, has_guard_clause - and asserts them in 12 of its own
Rust tests, all passing. NodeFacts carried no fields for any of them, so pydantic
dropped them silently at the DTO boundary. Measured before this change, on a
function with two early returns:

    early_exit_count = ABSENT

These are the first facts derived from the PRUNED CANONICAL TREE rather than from
a per-language extractor, so they are available for EVERY language rather than
only the ones with bespoke support - exactly the facts a non-Python binding would
rely on, and they reached nobody.

Verified after the change, on a real guard clause:

    def send(msg):
        if not msg:          negated_condition_count = 1
            return None      early_exit_count        = 1
        return deliver(msg)  has_guard_clause        = True

has_guard_clause stays tri-state on purpose. The pruned tree can prove a guard is
PRESENT but cannot prove one is ABSENT - statement order survives pruning,
operators do not - so None means "not determinable here", never "no guard".
Collapsing it to False would let a consumer draw a conclusion the evidence does
not support.

Adds Python acceptance tests, including one asserting the FIELDS EXIST on the
model at all. The defect was never a wrong value; it was a missing field, and
pydantic drops unknown keys without complaint. Proven to bite: removing the field
fails all four, restoring it passes all four.

Full suite: 2412 passed, 0 failed (2408 before, +4 new).

The wider lesson is about the certified-path policy. Rust being authoritative
does not help if the value never reaches the caller, and nothing on either side
was watching the join - a Rust test proving a fact is COMPUTED and a Python test
proving it is DELIVERED are different assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
n1ckyb added a commit that referenced this pull request Aug 10, 2026
Both exist in the monorepo and have NEVER run in this repo's CI.

## test_rename_does_not_hide_behaviour.py (3 tests)

The acceptance tests for intentumdiff-core#18: a rename that also changed the
body was collapsed into a single REFACTORING, discarding the behavioural change
and telling the reviewer nothing had changed. That is the worst output this
engine can produce, and the guard against it has never executed here.

Gated on the engine fix, which is verified present on core's RC
(`carries_entity_name` in draft_suppressors.rs) before syncing - landing the test
first would have arrived red.

## test_live_server_asset_diff.py (13 tests)

Covers the live-server asset_diff op. Verified the op exists on this branch
(5 references in live_server.py) before syncing, same reason.

## Held back deliberately

test_uast_structural_facts.py is NOT in this commit. Its 4 tests need the
early_exit_count / negated_condition_count / has_guard_clause fields on NodeFacts,
which are still only on the unmerged fix/uast-facts-cross-boundary branch. Landing
them now would put 4 red tests on the release branch. They follow once #26 merges.

## Verification note

These cannot be validated from a bare clone: running them here raises
PluginNotFoundError('unknown') because parser components are not staged outside
CI - the condition tracked as #22. CI is the verification, which is the standing
model for this repo (the monorepo has no CI at all).

Found by auditing why the CI suite collects 84 fewer tests than the monorepo.

Co-authored-by: n1ckyb <nicknuxton@icloud.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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