fix(python): complete the dotted @references head-parse in router_generator and M:N derivation - #372
Conversation
… 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
|
Merge-order note — interacts with #373. #373 (M:N self-join declaring-entity fix, also stacked on #371) touches the function immediately below Without that, merging these two in either order would have produced a semantic half-merge: one side of the same 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 One consequence worth knowing when reviewing #373 in light of this one: with both landed, a Python junction authored with dotted |
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.@referenceson anidentity.referencemay 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 addedreference_target_entity(). These are the two remaining Python sites with the same blindness.router_generator— corruptedReverseFk.target_entityreverse_fks_forstripped the package but not the dotted tail, soacme::sport::Team.idyieldedtarget_entity = "Team.id"instead ofTeam.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_entityhad the identical defect, and carried aKNOWN GAPcomment 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.referenceuses the dotted form now resolves its target instead of raisingM2MDerivationError. 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_entityand the pre-existing privatenaming_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@referencescase 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.mdcorpus countREADME.md:204said 314 metamodel fixtures; the real count is 329. Pre-existing drift — already wrong before #368, and outside the five spotsscripts/site/counts.test.tsenforces. 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 --quickpasses.metamodelVersionandexpected-registry.jsonuntouched. Python andREADME.mdonly.🤖 Generated with Claude Code
https://claude.ai/code/session_01Lfd8nat1WcSpXffd8YetiC