Skip to content

Commit 7c3fec7

Browse files
dmealingclaude
andcommitted
fr5c(java): merge-phase contributors + ERR_MERGE_CONFLICT envelope + WARN_DUPLICATE_DECLARATION
Java port of the FR5c TS reference (`329ba34`) per ADR-0009: - Overlay-merge phase in `CanonicalJsonParser.processNode` snapshots a node's pre-merge canonical shape + own-attrs map BEFORE re-applying the new contribution. When two or more files contribute to the same logical node AND the merge produces a semantic change (via FR5a `SemanticDiff`), the merged node's `source` is upgraded to `format: "merged"` with an alphabetically-ordered `files[]` and a `contributors[]` whose first entry is role `overlay-base` and all subsequent entries are `overlay-extension`. Single-file nodes keep their FR5a parse-time `JsonSource`/`YamlSource` unchanged. - `ERR_MERGE_CONFLICT` (new ErrorCode) raised when two contributors set the same `@attr` to different non-empty values on the same node. Envelope is `format: "merged"` with both contributors listed and `jsonPath` pointing at the conflicting attr. Last-writer-wins remains the merge default for non-conflicting cases (one side unset, same value); the recorded error surfaces the divergence so a consumer can fix it. Array-shaped JSON values (e.g. `["a","b"]`) are normalized to the same comma-delimited form `MetaAttribute.getValueAsString()` emits, so identical-array contributions are not flagged spuriously. - `WARN_DUPLICATE_DECLARATION` emitted at the merge boundary when a second contribution declares the same node with no semantic change (`SemanticDiff` returns false on pre/post canonical-serialize). The node's `source` is NOT upgraded to merged in this case — only the new envelope-warning channel surfaces it. - `MetaDataLoader` gains a parallel `envelopeWarnings: List<LoaderWarning>` channel (cross-port aligned with TS `ParseResult.envelopeWarnings`). `addEnvelopeWarning` also pushes the warning's message into the legacy `warnings: List<String>` channel so the existing conformance harness — which reads warnings as a flat string list — picks up FR5c warnings without needing a new contract. Both `addError` and `addEnvelopeWarning` are now `public` to support the cross-subpackage parser call sites (matching the precedent set by `addPendingExtends`). - `tagNodeWithJsonSource` now preserves `MergedSource` in addition to `JsonSource`/`YamlSource`, so a third contributor cannot revert an already-upgraded source envelope back to a single-file shape. - Conformance runner relaxes a too-strict guard that aborted tree- dependent checks when any error was recorded. FR5c records errors non-fatally via `addError`, and existing fixtures (e.g. `overlay-attr-last-writer-wins`) now legitimately produce a recorded `ERR_MERGE_CONFLICT` alongside a valid tree. TS conformance behaves the same way — only `expected-errors.json` fixtures check the error set. Only a thrown exception now aborts tree checks. - `EntityHasPrimaryIdentityValidationTest.warningsResetAcrossLoadCalls` reuses the same source id across two `load()` calls so the merge- attribution code does not see a NEW contributor on the second load and emit a `WARN_DUPLICATE_DECLARATION` orthogonal to the test's intent (which is verifying the per-batch warning accumulator reset). Three shared conformance fixtures all green: - `error-merge-conflict-attr` — `ERR_MERGE_CONFLICT` envelope with both contributors listed alphabetically. - `warning-duplicate-declaration` — three duplicate-declaration warning strings (one per merged sub-node). - `merge-three-way-no-conflict` — three-file overlay produces a `MergedSource` with all three files in alphabetical order. Tests: 717 → 727 metadata module (+10 unit tests in `Fr5cMergeAttributionTest`, +1 conformance lint pair via the two new fixtures); full Java suite 21/21 modules green. Cross-port aligned with the TS reference behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 243b2ac commit 7c3fec7

6 files changed

Lines changed: 757 additions & 11 deletions

File tree

server/java/metadata/src/main/java/com/metaobjects/ErrorCode.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ public enum ErrorCode {
133133
/** An object declares more than one source node with role=primary. */
134134
ERR_SOURCE_MULTIPLE_PRIMARY,
135135

136+
/**
137+
* Two contributing files set the same {@code @attr} on the same node to
138+
* different non-empty values during overlay merge (FR5c). Envelope is
139+
* {@code format: "merged"} with both contributors listed.
140+
*/
141+
ERR_MERGE_CONFLICT,
142+
136143
/** An internal loader error with no stable error code. */
137144
ERR_UNKNOWN,
138145
}

server/java/metadata/src/main/java/com/metaobjects/loader/MetaDataLoader.java

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,23 @@ public class MetaDataLoader implements LoaderConfigurable {
146146
// merges the two into a single per-fixture error set.
147147
private final List<MetaDataException> errors = new ArrayList<>();
148148

149+
// FR5c — envelope-shaped warnings accumulator (cross-port parallel channel).
150+
//
151+
// Distinct from the {@link #warnings} legacy-string channel: envelope
152+
// warnings already carry their own {@code WARN_*} code + {@link
153+
// com.metaobjects.source.ErrorSource} provenance and are produced by
154+
// FR5c-onward sites (the canonical first emitter is the duplicate-
155+
// declaration site in {@code CanonicalJsonParser}). The conformance harness
156+
// today reads warnings as a flat string list via {@link #getWarnings()};
157+
// FR5c parser sites also push the {@link com.metaobjects.source.LoaderWarning#message()}
158+
// string into the legacy channel so the harness sees both forms without
159+
// needing a new contract.
160+
//
161+
// Cleared at the start of every {@link #load(List)} so a subsequent load on
162+
// the same loader does not see stale warnings. Mirrors the TS
163+
// {@code envelopeWarnings: LoaderWarning[]} channel on {@code ParseResult}.
164+
private final List<com.metaobjects.source.LoaderWarning> envelopeWarnings = new ArrayList<>();
165+
149166
/**
150167
* Deferred-extends queue. The parser cannot always resolve a node's
151168
* {@code extends} ref at parse time (e.g. cross-file forward references).
@@ -548,12 +565,16 @@ public List<MetaDataException> getErrors() {
548565
}
549566

550567
/**
551-
* Append a validation error. Package-private — only the loader-package
552-
* validation passes should be calling this.
568+
* Append a validation error. Loader-internal API — callers are the
569+
* loader's validation passes and the FR5c merge-attribution site in
570+
* {@link com.metaobjects.loader.parser.json.CanonicalJsonParser} (which
571+
* lives in a sub-package, so this method must be {@code public} for
572+
* cross-package access — mirroring the precedent set by
573+
* {@link #addPendingExtends(PendingExtends)}).
553574
*
554575
* @param error the error to record; ignored when {@code null}
555576
*/
556-
void addError(MetaDataException error) {
577+
public void addError(MetaDataException error) {
557578
if (error == null) return;
558579
errors.add(error);
559580
}
@@ -567,6 +588,55 @@ void clearErrors() {
567588
errors.clear();
568589
}
569590

591+
/**
592+
* Returns the FR5c envelope-shaped warnings accumulated during the most
593+
* recent {@link #load(List)}. Each entry carries a {@code WARN_*} code
594+
* plus a {@link com.metaobjects.source.ErrorSource} envelope (canonical
595+
* first emitter: {@code WARN_DUPLICATE_DECLARATION} with
596+
* {@link com.metaobjects.source.MergedSource}).
597+
*
598+
* <p>Cross-port aligned with the TS {@code ParseResult.envelopeWarnings}
599+
* channel and the C# / Python {@code LoaderWarning[]} return surfaces.</p>
600+
*
601+
* @return an unmodifiable snapshot of envelope warnings (never {@code null})
602+
*/
603+
public List<com.metaobjects.source.LoaderWarning> getEnvelopeWarnings() {
604+
return Collections.unmodifiableList(new ArrayList<>(envelopeWarnings));
605+
}
606+
607+
/**
608+
* Append a FR5c envelope-shaped warning. Loader-internal API — callers
609+
* are the loader pipeline plus the FR5c merge-attribution site in
610+
* {@link com.metaobjects.loader.parser.json.CanonicalJsonParser} (which
611+
* lives in a sub-package, so this method must be {@code public} for
612+
* cross-package access — mirroring the precedent set by
613+
* {@link #addPendingExtends(PendingExtends)}).
614+
*
615+
* <p>This method ALSO pushes the warning's {@link
616+
* com.metaobjects.source.LoaderWarning#message()} into the legacy
617+
* {@link #warnings} channel so the existing conformance harness — which
618+
* reads a flat string list via {@link #getWarnings()} — picks up FR5c
619+
* warnings without needing a new contract. The envelope is available
620+
* separately via {@link #getEnvelopeWarnings()} for callers that want
621+
* the full provenance.</p>
622+
*
623+
* @param warning the envelope warning; ignored when {@code null}
624+
*/
625+
public void addEnvelopeWarning(com.metaobjects.source.LoaderWarning warning) {
626+
if (warning == null) return;
627+
envelopeWarnings.add(warning);
628+
// Mirror into the legacy channel for the conformance harness.
629+
warnings.add(warning.message());
630+
}
631+
632+
/**
633+
* Clear accumulated envelope warnings. Called at the start of
634+
* {@link #load(List)} so a fresh batch does not see stale entries.
635+
*/
636+
void clearEnvelopeWarnings() {
637+
envelopeWarnings.clear();
638+
}
639+
570640
///////////////////////////////////////////////////////////////////////
571641
// ClassLoader
572642

@@ -1131,6 +1201,7 @@ public MetaDataLoader load(List<MetaDataSource> sources) {
11311201
// diagnostics produced by THIS batch.
11321202
clearWarnings();
11331203
clearErrors();
1204+
clearEnvelopeWarnings();
11341205
pendingExtends.clear();
11351206

11361207
for (MetaDataSource source : sources) {

0 commit comments

Comments
 (0)