Skip to content

fix(python): complete the dotted @references head-parse in router_generator and M:N derivation - #372

Merged
dmealing merged 4 commits into
mainfrom
followup/python-dotted-and-docs
Sep 14, 2026
Merged

dmealing merged 4 commits into
mainfrom
followup/python-dotted-and-docs

Conversation

@dmealing

Copy link
Copy Markdown
Member

Summary

Three follow-ups found while reviewing #368, plus one stale count. Stacked on fix/368-assoc-ref-disambiguation (#371) — it introduces the canonical helper both code fixes reuse.

@references on an identity.reference may use a dotted form naming an explicit field set — Team.id, acme::sport::Team.id, Program.fieldA,fieldB — where the entity name is the segment before the first . after the last ::. That form is normative (spec/metamodel/identity.json). #371 found Python diverging from the other three ports on it and added reference_target_entity(). These are the two remaining Python sites with the same blindness.

router_generator — corrupted ReverseFk.target_entity

reverse_fks_for stripped the package but not the dotted tail, so acme::sport::Team.id yielded target_entity = "Team.id" instead of Team.

The generated finder names were never affected — they derive from fk_field, not this value — so the symptom is narrower than it first looks: it is a wrong value in a documented return field (ReverseFk.target_entity, "the bare target entity (T)") that existing tests already assert on directly. A second test pins that the emitted router source is unchanged, rather than overclaiming impact.

derive_m2m_fields — the gap #371 deliberately parked

_ref_target_entity had the identical defect, and carried a KNOWN GAP comment naming the exact one-line repair. #371 left it alone because changing it alters M:N derivation behaviour and that branch was already large.

Applied here: a junction whose identity.reference uses the dotted form now resolves its target instead of raising M2MDerivationError. No existing test or shared fixture changed outcome.

Not consolidating the two head-parses

Python has two implementations of this rule: the new reference_target_entity and the pre-existing private naming_refs._split_child_tail. They are left separate and cross-referenced by docstring, because they differ on three axes — pipeline phase (pre-resolution raw-string desugar vs. post-resolution accessor), visibility (private internal vs. public), and scope (five ref-bearing attribute kinds carrying the tail forward vs. one @references case discarding it). Merging them would mean either promoting a desugar internal to public API or having the lower-level module depend on a higher-level one, for a two-line coincidental match rather than a shared contract.

README.md corpus count

README.md:204 said 314 metamodel fixtures; the real count is 329. Pre-existing drift — already wrong before #368, and outside the five spots scripts/site/counts.test.ts enforces. The neighbouring "22 corpora" claim was checked and is correct.

Verification

Python 2151 passed (baseline 2148 + 3 new tests), identical across two runs. scripts/ci-local.sh --quick passes. metamodelVersion and expected-registry.json untouched. Python and README.md only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Lfd8nat1WcSpXffd8YetiC

dmealing and others added 4 commits September 13, 2026 23:32
… dotted @references

reverse_fks_for() (ADR-0038) stripped the package off @references but not the
FR-024 dotted `Entity.field` / `Entity.a,b` explicit-fields tail, so a dotted
reference (e.g. "acme::sport::Team.id") left ReverseFk.target_entity as the raw
"Team.id" instead of the bare "Team" the field's own docstring promises and
test_reverse_fks_for_game_session already asserts on.

The emitted router source is unaffected today -- the finder METHOD NAME derives
only from the FK-holding entity's own name + FK field, never from target_entity
-- so the user-visible symptom is a corrupted value in a public, tested return,
not a wrong or missing finder. Fixed by reusing (not reimplementing) #371's
relationship_references.reference_target_entity() for the head-parse, same as
derive_m2m_fields's parked KNOWN GAP in the next commit.

Adds two tests: one pinning the fixed target_entity value (the actual
symptom), one confirming the finder name was already correct so the fix's
scope is understood precisely.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfd8nat1WcSpXffd8YetiC
…nces

Applies the one-line repair #371 deliberately parked as a KNOWN GAP:
_ref_target_entity() compared a junction reference's WHOLE @references value
against a bare entity name, so the normative dotted `Entity.field` /
`Entity.a,b` explicit-fields form ("Team.id") never matched "Team" and a M:N
relationship through such a junction derived no fields at all --
resolve_m2m_descriptors raised M2MDerivationError for a junction shape that
should resolve like any other.

Delegates the dotted-tail split to relationship_references.reference_target_entity
(the #368 fix's canonical head-parse) and strips the package prefix to keep
this function's existing bare-name contract. No behaviour changes for the
non-dotted case this port already handled.

Verified against the full Python suite (2151 passed, up from a 2148 baseline
by exactly the 3 tests these two commits add) and the shared conformance
corpus -- no existing M:N test's outcome changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfd8nat1WcSpXffd8YetiC
… not merging them

Consolidation check for the two prior fixes: with router_generator.py and
derive_m2m_fields.py now both reusing (not reimplementing) relationship_references
.reference_target_entity(), the "up to three implementations" this follow-up set
out to check narrows to two that pre-date it -- reference_target_entity() itself
and naming_refs._split_child_tail(), which happen to run the same character-level
dot search.

Left them separate: _split_child_tail is private to the desugar pass (runs
pre-resolution, over raw authored strings, across five different ref-bearing
attribute kinds, and keeps the tail for reattachment), while
reference_target_entity is a public accessor over a resolved identity.reference
MetaData node, specific to @references, and discards the tail. Merging is
technically possible without a circular import (naming_refs.py has no runtime
dependency on meta.core.relationship today) but would mean either promoting a
desugar-internal to public API or inverting that layering, for a one-line
coincidence rather than a shared contract. No behaviour change; docstring
cross-references only, in both directions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfd8nat1WcSpXffd8YetiC
Pre-existing drift (already wrong before #368), outside the five spots
scripts/site/counts.test.ts enforces (docs/CONFORMANCE.md's table + arithmetic
line, AGENTS.md) -- those already correctly say 329 fixtures / 22 corpora, so
only this one prose line had rotted. Verified against the filesystem
(`ls -d fixtures/conformance/*/ | wc -l` -> 329) rather than trusting either
number. Checked the rest of README.md for other stale corpus counts (the "22
cross-language conformance corpora" line, and no others) -- none found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfd8nat1WcSpXffd8YetiC
@dmealing

Copy link
Copy Markdown
Member Author

Merge-order note — interacts with #373.

#373 (M:N self-join declaring-entity fix, also stacked on #371) touches the function immediately below _ref_target_entity in derive_m2m_fields.py, and makes it delegate to the same canonical reference_target_entity head-parse this PR adopts.

Without that, merging these two in either order would have produced a semantic half-merge: one side of the same if handling dotted @references and the other not, so a dotted junction reference would resolve on the target side and fail on the source side. #373 removes that — both sides now go through the canonical parse.

What remains is a textual conflict only: the two PRs edit adjacent hunks of the same file. Whichever merges second will need a trivial resolution, and the correct result is that both functions delegate to reference_target_entity.

One consequence worth knowing when reviewing #373 in light of this one: with both landed, a Python junction authored with dotted @references derives M:N fields where it previously threw — measured as @references: "Post.id" / "Tag.id" going from must declare one identity.reference to "Post" and one to "Tag" to (postId, tagId).

Base automatically changed from fix/368-assoc-ref-disambiguation to main September 14, 2026 11:07
@dmealing
dmealing merged commit e613da2 into main Sep 14, 2026
1 check passed
@dmealing
dmealing deleted the followup/python-dotted-and-docs branch September 14, 2026 11:08
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