read(Path configDir) {
ErrorCode.ERR_MALFORMED_JSON);
}
+ // `JsonParser.parseString` ALWAYS parses leniently, no matter how the
+ // caller would like to configure it — a well-known Gson quirk: both
+ // `JsonParser.parseReader` and `Gson#fromJson(JsonReader, ...)` force
+ // `setLenient(true)` on the reader internally before walking it, so
+ // unquoted keys, single-quoted strings, and `NaN`/`Infinity` literals all
+ // parse clean — silently accepting a file that is not actually valid JSON,
+ // contradicting this class's own javadoc and diverging from every other
+ // port's stock JSON parser (TS `JSON.parse`, Python `json.loads`, C#
+ // `System.Text.Json`, all strict by default). A `JsonReader` walked
+ // directly with `setLenient(false)` is the only way to get genuinely
+ // strict parsing out of Gson; used here purely to VALIDATE (the walked
+ // structure is discarded — `JsonParser.parseString` below still builds the
+ // actual `JsonElement` tree, unchanged).
+ try (JsonReader strict = new JsonReader(new StringReader(content))) {
+ strict.setLenient(false);
+ strict.skipValue();
+ if (strict.peek() != JsonToken.END_DOCUMENT) {
+ throw new MalformedJsonException("trailing content after the top-level value");
+ }
+ } catch (IOException e) {
+ throw new MetaDataException(
+ path + " exists but could not be parsed as JSON: " + e.getMessage(),
+ ErrorCode.ERR_MALFORMED_JSON);
+ }
+
JsonElement parsed;
try {
parsed = JsonParser.parseString(content);
diff --git a/server/java/metadata/src/main/java/com/metaobjects/config/SourceResolver.java b/server/java/metadata/src/main/java/com/metaobjects/config/SourceResolver.java
index 74b7e7f3a..e500b26e1 100644
--- a/server/java/metadata/src/main/java/com/metaobjects/config/SourceResolver.java
+++ b/server/java/metadata/src/main/java/com/metaobjects/config/SourceResolver.java
@@ -18,13 +18,16 @@
import com.metaobjects.ErrorCode;
import com.metaobjects.MetaDataException;
import com.metaobjects.loader.DirectorySource;
+import com.metaobjects.loader.FileSource;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.stream.Stream;
/**
* Turns a declared source SET ({@code .metaobjects/config.json}'s {@code sources}, or
@@ -57,6 +60,16 @@ private SourceResolver() {}
* an unresolved PATH regardless of which is declared first
* ({@code unsupported-kind-precedes-unresolved-path-when-path-is-declared-first}/
* {@code -second}).
+ *
+ * Pass 2 processes the validated specs in CONTENT order (natural string
+ * ordering of each spec's {@code path}), not declared order — mirroring the
+ * reference implementation's {@code orderedPathSpecs}
+ * ({@code sources.ts}: kind-validated, then sorted by
+ * {@code JSON.stringify(spec)}, which for a validated {@code path}-only spec
+ * reduces to sorting by the path string alone). This does not change the
+ * resolved file SET — de-duplication is order-independent — only which
+ * declared path's {@code ERR_SOURCE_UNRESOLVED} fires first when more than one
+ * is simultaneously unresolvable, which order-independence alone cannot pin.
*/
public static List resolveSources(Path configDir, List