Skip to content

feat(metadata): walk identity.reference backwards in origin @via hops - #560

Merged
dmealing merged 3 commits into
mainfrom
fm/mo-rc19-pr2-paths
Nov 12, 2026
Merged

dmealing merged 3 commits into
mainfrom
fm/mo-rc19-pr2-paths

Conversation

@dmealing

@dmealing dmealing commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • An origin @via hop can now walk an identity.reference backwards, from the referenced entity to the entity that declares it, named by the holder's collection name in lowerCamel (e.g. Patient.consents walks Consent.patient). A declared relationship or reference of the same name still wins. The loader refuses two candidate holders with ERR_AMBIGUOUS_PATH. The reverse hop is to-many: it is valid in an aggregate and rejected in a passthrough with ERR_ORIGIN_CARDINALITY. Implemented in the TypeScript, C#, Java and Python loaders, with the new helpers findReverseReferenceHops / reverseHopName exported from @metaobjectsdev/metadata in TypeScript.
  • Adds the collection-name helper in Java (CollectionNames) and Python (collection_names.py), matching the existing TypeScript and C# behavior: the object's own @collectionName, else the configured pluralizer.
  • Adds three shared conformance fixtures (origin-reverse-reference-hop, origin-reverse-hop-collection-name, error-origin-reverse-hop-ambiguous), and updates CHANGELOG.md, docs/features/relationships.md, docs/features/migrations/physical-naming.md, docs/CONFORMANCE.md, AGENTS.md and examples/showcase/site-payload.json for the new fixture count (455 to 458).
  • Scope: this pull request carries only those 3 of the 21 shared fixtures planned for the vocabulary-and-loaders work, which is landing as consecutive pull requests under a 1,000-production-line limit. The other 18 land with the parts that follow: the expression and origin-filter grammar (7), projection @filter (7), and flattened-member paths, two references onto one target and the report "required" rule (4). feat(metadata): add @collectionName and naming.collections pluralizer across all five ports #558 landed @collectionName and the naming.collections pluralizer.
Validation

Risk Assessment

✅ Low: Reverse @via hop logic is consistent across all four loader implementations and the three fixtures gate it. The prior fix round's doc, message and Java isAbstract changes check out; only comment-level nits remain.

Testing

Live: reverse-hop origin @via feature driven through the TS CLI and the Python loader on disposable project copies. Ambiguous reverse hop gives ERR_AMBIGUOUS_PATH; reverse-hop passthrough gives the new reverse-hop message; declared-reference and collection-name fixtures load clean. Not live: Python 6-case conformance subset, Java metadata module, C# conformance, and TS/Java broad regression runs were unit/conformance harness runs only and are recorded as untested-live. Python full unit suite did not finish (killed); loader and conformance dirs pass. Integration tests need pg8000 and Postgres and were not run. Verdict go on the live-driven scenarios; Python full unit suite and the 21-vs-3 fixture count question remain open.

  • Live validation: ✅ go - 5 of 10 scenarios driven live against the product
Scenario Result Live Evidence
TS CLI: ambiguous reverse @via hop (two identity.reference holders) is rejected with ERR_AMBIGUOUS_PATH ✅ pass live meta verify run in a disposable copy of error-origin-reverse-hop-ambiguous (synthetic copy of fixture input)
TS CLI: declared reference hop loads and verify is clean ✅ pass live meta verify run in a disposable copy of origin-reverse-reference-hop
TS CLI: collection-name reverse hop (CaseHistory) loads and verify is clean ✅ pass live meta verify run in a disposable copy of origin-reverse-hop-collection-name
TS CLI: origin.passthrough over a reverse hop fails with the reverse-hop message ✅ pass live meta verify in a disposable copy of passthrough-reverse (synthetic edit of fixture input)
Python loader: ambiguous reverse hop -> ERR_AMBIGUOUS_PATH; other two fixtures load clean ✅ pass live a small Python driver over disposable fixture copies
Python conformance corpus includes the 6 new reverse/collection fixtures ⏸️ untested no Only a pytest conformance harness run was recorded (pytest tests/conformance/test_conformance.py -k reverse|collection); not driven against the live product, so no live pass is established.
Java metadata module loads and validates shared fixtures (incl. new ones) ⏸️ untested no Only a unit/conformance test run was recorded (mvn -o -q -pl metadata test, 908 ConformanceTest cases); not driven against the live product, so no live pass is established.
C# loader validates shared fixtures (incl. new ones) ⏸️ untested no Only a conformance test run was recorded (dotnet test MetaObjects.Conformance.Tests, 2414 passed); not driven against the live product, so no live pass is established.
Full regression: TS metadata package and Java metadata module ⏸️ untested no Only broad unit regression runs were recorded (bun test 3981 pass; mvn -o -pl metadata test 3349 pass); not driven against the live product, so no live pass is established.
Full regression: Python unit suite ⏸️ untested no Ran past 15 min without finishing; loader+conformance subset passed (1608). Integration tests need pg8000 and Postgres, not run.
  • Outcome: ⚠️ 2 issues (1 warning, 1 info) across 1 run (37m31s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 3 infos
  • ⚠️ docs/features/relationships.md:176 - The @via section still states 'Inverse navigation (parent → many children) still needs a relationship.composition on the parent — a bare FK has no inverse edge.' This change makes that false: an origin @via hop may now walk an identity.reference backwards, named by the holder's collection name in lowerCamel (Patient.consents), to-many, ERR_AMBIGUOUS_PATH on two candidates, declared relationship/reference wins. The CHANGELOG and physical-naming.md were updated but the primary feature doc for @via was not, so it now contradicts loader behaviour in all five ports. Update lines 176-178 to describe the reverse hop (and that single-hop-unique inference still stays relationship-only). Not in the changed-file list, so not fixed inline.
  • ℹ️ server/typescript/packages/metadata/src/loader/validation-passes.ts:1065 - When a passthrough crosses a reverse hop, ERR_ORIGIN_CARDINALITY says '@via hop "patient" is to-many (@Cardinality "many")'. It names the walked reference ("patient"), not the path segment the author wrote ("consents"), and claims a declared @Cardinality that does not exist. Same message in every port: server/csharp/MetaObjects/Loader/ValidationPasses.cs:1368, server/java/metadata/src/main/java/com/metaobjects/loader/ValidationPhase.java:4923, server/python/src/metaobjects/loader/validation_passes.py:1993. Consider saying 'reverse hop over identity.reference "patient"' for hops in the reverse set. The message text must stay identical across ports.
  • ℹ️ server/java/metadata/src/main/java/com/metaobjects/loader/ReverseReferenceHops.java:55 - ReverseReferenceHops.isAbstract is a verbatim copy of the private ValidationPhase.isAbstract (ValidationPhase.java:4043), in the same package. Make the ValidationPhase helper package-private and reuse it, so there is one own-only @isAbstract rule.

🔧 Fix applied.
3 infos still open:

  • ℹ️ server/java/metadata/src/main/java/com/metaobjects/loader/ValidationPhase.java:4591 - Inline fix: the new Java helper rootObjects had been inserted between the Javadoc of resolveRootObject and its method, detaching that doc. Moved the helper above the Javadoc.
  • ℹ️ server/typescript/packages/metadata/src/core/relationship/find-reference.ts:75 - Inline fix: reworded the garbled reverseHopName Javadoc (it had a duplicated, half-templated phrase).
  • ℹ️ fixtures/conformance/origin-reverse-reference-hop/expected.json - No fixture asserts the passthrough-across-reverse-hop ERR_ORIGIN_CARDINALITY message, so the byte-identical wording across ports is unverified. The three new fixtures cover only the happy path, a collection-name override and the ambiguous case.
⚠️ **Test** - 2 issues (1 warning, 1 info)
  • ⚠️ Python full unit suite (pytest --ignore=tests/integration) did not complete within 15 min and was killed; no result. Loader and conformance dirs pass (1608). Needs a rerun to confirm no Python unit regressions.
  • ℹ️ Intent says 21 new shared fixtures for PR 1, but this diff (976427a..babb69c) adds only 3 new fixture directories (error-origin-reverse-hop-ambiguous, origin-reverse-hop-collection-name, origin-reverse-reference-hop). Confirm the other 18 are on another commit or not yet landed.
  • Live validation: ✅ go - 5 of 10 scenarios driven live against the product
Scenario Result Live Evidence
TS CLI: ambiguous reverse @via hop (two identity.reference holders) is rejected with ERR_AMBIGUOUS_PATH ✅ pass live meta verify run in a disposable copy of error-origin-reverse-hop-ambiguous (synthetic copy of fixture input)
TS CLI: declared reference hop loads and verify is clean ✅ pass live meta verify run in a disposable copy of origin-reverse-reference-hop
TS CLI: collection-name reverse hop (CaseHistory) loads and verify is clean ✅ pass live meta verify run in a disposable copy of origin-reverse-hop-collection-name
TS CLI: origin.passthrough over a reverse hop fails with the reverse-hop message ✅ pass live meta verify in a disposable copy of passthrough-reverse (synthetic edit of fixture input)
Python loader: ambiguous reverse hop -> ERR_AMBIGUOUS_PATH; other two fixtures load clean ✅ pass live a small Python driver over disposable fixture copies
Python conformance corpus includes the 6 new reverse/collection fixtures ⏸️ untested no Only a pytest conformance harness run was recorded (pytest tests/conformance/test_conformance.py -k reverse|collection); not driven against the live product, so no live pass is established.
Java metadata module loads and validates shared fixtures (incl. new ones) ⏸️ untested no Only a unit/conformance test run was recorded (mvn -o -q -pl metadata test, 908 ConformanceTest cases); not driven against the live product, so no live pass is established.
C# loader validates shared fixtures (incl. new ones) ⏸️ untested no Only a conformance test run was recorded (dotnet test MetaObjects.Conformance.Tests, 2414 passed); not driven against the live product, so no live pass is established.
Full regression: TS metadata package and Java metadata module ⏸️ untested no Only broad unit regression runs were recorded (bun test 3981 pass; mvn -o -pl metadata test 3349 pass); not driven against the live product, so no live pass is established.
Full regression: Python unit suite ⏸️ untested no Ran past 15 min without finishing; loader+conformance subset passed (1608). Integration tests need pg8000 and Postgres, not run.
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • bun test in server/typescript/packages/metadata (conformance.test.ts, conformance-twin-roundtrip.test.ts, then full package)
  • live: bun server/typescript/packages/cli/dist/bin/meta.js verify on disposable copies of error-origin-reverse-hop-ambiguous, origin-reverse-reference-hop, origin-reverse-hop-collection-name
  • live: same CLI on a disposable passthrough-over-reverse-hop project (origin.passthrough variant of origin-reverse-reference-hop)
  • PYTHONPATH=server/python/src python3 -m pytest tests/conformance/test_conformance.py -k reverse|collection (6 new fixture cases pass)
  • live: python loader via tests/conformance/conformance_adapter.load_fixture on disposable copies of the 3 fixtures
  • PYTHONPATH=src python3 -m pytest tests/loader tests/conformance (1608 pass)
  • python -m pytest --ignore=tests/integration (full unit run; stalled and was killed, no result)
  • mvn -o -q -pl metadata test in server/java (3349 run, 0 fail, 0 err)
  • dotnet restore + dotnet test MetaObjects.Conformance.Tests --no-restore (2414 passed, 0 failed)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…hop

An origin @via hop may now walk an identity.reference backwards, from the entity it
targets to the entity holding it, named by the holder's collection name in lower
camel case (@collectionName when declared, else the project's pluralizer). The hop
is to-many, and two references answering one name are refused with
ERR_AMBIGUOUS_PATH; a declared relationship or reference of the same name still
wins. TypeScript, C#, Java (Kotlin through it) and Python implement it, gated by the
shared fixtures origin-reverse-reference-hop, origin-reverse-hop-collection-name and
error-origin-reverse-hop-ambiguous.
@dmealing
dmealing merged commit c1a8da5 into main Nov 12, 2026
9 checks passed
@dmealing
dmealing deleted the fm/mo-rc19-pr2-paths branch November 12, 2026 10:13
dmealing added a commit that referenced this pull request Nov 12, 2026
…ct aggregates (#561)

* docs(agent-context): route report wording to object.report before @SQL

The meta init agent guide gains one rule: a grouped total, dashboard number or
read-only report view is an object.report over dimension.* / measure.*, with a
hand-written @SQL view body as the last resort. The metaobjects-authoring
routing table sends report, dashboard, totals, GROUP BY and per-month wording to
declaring-reports.md first, and the @SQL sections of read-views-and-projections.md
and sources.md point an aggregating body at it and say what @SQL is for.

* fix(codegen-ts): emit view columns fed by a reverse identity.reference @via hop

An origin @via hop that walks an identity.reference backwards (#560) loaded, but the
projection view emitter resolved hops only through relationships and forward references
and silently dropped the aggregate and first columns from CREATE VIEW. It now falls back
to the same reverse-hop lookup the loader uses and joins from the reference holder.
Gated by a Postgres test that migrates, inserts and reads the view.

* fix(metadata): refuse origin.aggregate sum/avg/min/max over a value-object field

@Of naming a field.object (flattened or jsonb) loaded and emitted SUM(<field>), a column
that does not exist. All four loaders (Kotlin through Java) now refuse it with
ERR_INVALID_ORIGIN: a value object is several columns or a document, and summing a
Money-shaped value would add amounts in different currencies. count is unaffected.
Gated by the new error-origin-aggregate-value-object conformance fixture.

* chore: declare non-production paths for no-mistakes triage

Fixtures, the metamodel spec and its generated per-port copies, the generated reference
site and the showcase payload.

* fix(java): guard a missing @agg before the value-object aggregate check

List.of(...).contains(null) throws, so an origin.aggregate with no @agg over a
resolvable @Of crashed the loader instead of reporting the missing attribute.

* no-mistakes(document): regenerate showcase payload fixture count
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