diff --git a/README.md b/README.md
index be561fc..c5c163a 100644
--- a/README.md
+++ b/README.md
@@ -70,12 +70,39 @@ var doc = new OssieDocument();
new OssieYamlWriter().write(doc, Paths.get("my-model.ossie.yaml"));
```
+Resolve AI-context synonyms to canonical entity names, without writing
+your own tree walk:
+
+```java
+import bi.saiku.ossie.OssieSynonymIndex;
+
+var model = doc.getSemanticModel().get(0);
+var metrics = OssieSynonymIndex.buildMetricIndex(model);
+// If the LLM (or the user) said "revenue" or "top-line" or "REVENUE",
+// resolve it to the canonical metric name declared in the YAML.
+String canonical = OssieSynonymIndex.resolve("revenue", metrics);
+// → "net_revenue" (assuming the metric declared that synonym)
+```
+
+Reject documents at unknown spec versions — useful in CI or when you
+want to fail fast on drift:
+
+```java
+var doc = new OssieYamlReader()
+ .setStrictVersion(true)
+ .read(Paths.get("model.ossie.yaml"));
+// Throws UnsupportedOssieVersionException if the version isn't in
+// OssieYamlReader.SUPPORTED_VERSIONS.
+```
+
## Compatibility
-- **OSI spec versions** — DTOs cover the v0.1.x and v0.2.x wire
- formats. Version strings on documents are not validated at read time;
- future spec additions are ignored via
- `@JsonIgnoreProperties(ignoreUnknown = true)`.
+- **OSI spec versions** — the DTO tree covers the v0.1.x and v0.2.x
+ wire formats. `OssieYamlReader.SUPPORTED_VERSIONS` names the exact
+ versions the reader knows about. Unknown top-level and per-entity
+ fields are silently ignored (`@JsonIgnoreProperties(ignoreUnknown =
+ true)`), so additive spec changes round-trip without breaking older
+ consumers.
- **JDK** — 21+.
- **dbt Core** — reads `target/osi_document.json` from dbt 1.12 and up
verbatim.
diff --git a/src/main/java/bi/saiku/ossie/OssieSynonymIndex.java b/src/main/java/bi/saiku/ossie/OssieSynonymIndex.java
new file mode 100644
index 0000000..b4c3968
--- /dev/null
+++ b/src/main/java/bi/saiku/ossie/OssieSynonymIndex.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2026 Spicule Ltd
+ * Apache License, Version 2.0.
+ */
+package bi.saiku.ossie;
+
+import bi.saiku.ossie.model.AiContext;
+import bi.saiku.ossie.model.Dataset;
+import bi.saiku.ossie.model.Field;
+import bi.saiku.ossie.model.Metric;
+import bi.saiku.ossie.model.SemanticModel;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Canonicalises AI-context synonyms to their entity names.
+ *
+ *
The Ossie / OSI spec attaches {@code AIContext.synonyms} to fields, metrics, and datasets so
+ * LLM agents can refer to them by natural aliases: "revenue" resolves to the canonical
+ * {@code net_revenue} metric; "sales transactions" resolves to the {@code store_sales} dataset.
+ * The synonyms are declared once in the YAML but the lookup logic ("case-insensitive walk of every
+ * dataset's fields, collect their AI-context synonyms") is otherwise identical across consumers.
+ *
+ *
Each index maps lowercased synonym to the corresponding entity's canonical name.
+ * Lookups against the returned map should also lowercase the key ({@link Locale#ROOT}). Callers
+ * are typically:
+ *
+ *
+ * - Query validators that accept either canonical names or synonyms
+ *
- Natural-language layers ({@code /ask}) that need to resolve the LLM's word choice
+ *
- MCP tool implementations exposing the same canonicalisation to agent clients
+ *
+ *
+ * Insertion order in the returned map is preserved (dataset → field → metric traversal order in
+ * the source model), which helps a UI display "revenue → net_revenue (from Sales.net_revenue)"
+ * predictably.
+ *
+ *
Ambiguous synonyms are resolved with a first-declared-wins policy — if two fields
+ * declare {@code "id"} as a synonym, the first one encountered stays in the map and later
+ * declarations are silently dropped. That matches the pragmatic behaviour most consumers want:
+ * make the synonym resolvable to something rather than throw at build time. Consumers
+ * that need strict ambiguity detection should walk the source model themselves.
+ */
+public final class OssieSynonymIndex {
+
+ private OssieSynonymIndex() {}
+
+ /**
+ * Field-name synonym index. Keys are lowercased synonyms; values are the canonical
+ * {@code "."} handle. Returns an empty map if the model declares no
+ * synonyms on any field.
+ */
+ public static Map buildFieldIndex(SemanticModel model) {
+ Map out = new LinkedHashMap<>();
+ if (model == null) return out;
+ for (Dataset ds : model.getDatasets()) {
+ for (Field f : ds.getFields()) {
+ for (String syn : synonymsOf(f.getAiContext())) {
+ out.putIfAbsent(syn.toLowerCase(Locale.ROOT), ds.getName() + "." + f.getName());
+ }
+ }
+ }
+ return out;
+ }
+
+ /**
+ * Metric-name synonym index. Keys are lowercased synonyms; values are the canonical metric
+ * name. Metrics are model-scoped in the OSI spec so no dataset qualifier is needed.
+ */
+ public static Map buildMetricIndex(SemanticModel model) {
+ Map out = new LinkedHashMap<>();
+ if (model == null) return out;
+ for (Metric m : model.getMetrics()) {
+ for (String syn : synonymsOf(m.getAiContext())) {
+ out.putIfAbsent(syn.toLowerCase(Locale.ROOT), m.getName());
+ }
+ }
+ return out;
+ }
+
+ /** Dataset-name synonym index. Keys are lowercased synonyms; values are the canonical dataset name. */
+ public static Map buildDatasetIndex(SemanticModel model) {
+ Map out = new LinkedHashMap<>();
+ if (model == null) return out;
+ for (Dataset ds : model.getDatasets()) {
+ for (String syn : synonymsOf(ds.getAiContext())) {
+ out.putIfAbsent(syn.toLowerCase(Locale.ROOT), ds.getName());
+ }
+ }
+ return out;
+ }
+
+ /**
+ * Resolves a caller-supplied name against a synonym index. Returns the canonical name (from
+ * the map) or the input unchanged if no synonym matches. Case-insensitive.
+ *
+ * Reads best as a validator escape hatch:
+ *
+ *
{@code
+ * Map aliases = OssieSynonymIndex.buildMetricIndex(model);
+ * String resolved = OssieSynonymIndex.resolve(request.getMetric(), aliases);
+ * // proceed with `resolved` — validation now sees the canonical name
+ * }
+ */
+ public static String resolve(String maybeSynonym, Map index) {
+ if (maybeSynonym == null || index == null || index.isEmpty()) return maybeSynonym;
+ String canonical = index.get(maybeSynonym.toLowerCase(Locale.ROOT));
+ return canonical == null ? maybeSynonym : canonical;
+ }
+
+ private static Iterable synonymsOf(AiContext ctx) {
+ if (ctx == null || ctx.getSynonyms() == null || ctx.getSynonyms().isEmpty()) {
+ return java.util.List.of();
+ }
+ return ctx.getSynonyms();
+ }
+}
diff --git a/src/main/java/bi/saiku/ossie/OssieYamlReader.java b/src/main/java/bi/saiku/ossie/OssieYamlReader.java
index 5e50f64..d196cd7 100644
--- a/src/main/java/bi/saiku/ossie/OssieYamlReader.java
+++ b/src/main/java/bi/saiku/ossie/OssieYamlReader.java
@@ -13,43 +13,92 @@
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Set;
/**
- * Load an {@link OssieDocument} from Ossie-flavoured YAML.
+ * Loads an {@link OssieDocument} from OSI-flavoured YAML or JSON.
*
- * Symmetric with {@code OssieYamlWriter} — the pair is the round-trip. Lives in
- * saiku-service alongside the writer + POJOs so any module can consume it (previously kept in
- * saiku-sql; promoted here when {@code OssieDiscoverService} in this module needed to read the
- * same YAML the Calcite adapter loads).
+ *
YAML is a strict superset of JSON so the same reader accepts either. dbt Core 1.12's
+ * {@code target/osi_document.json} feeds through this class verbatim.
*
- *
{@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} is disabled so Ossie documents that
- * carry vendor extensions or future spec keys we don't yet model won't blow up the loader — the
- * spec is draft and additive fields are the whole point of {@code custom_extensions}.
+ *
{@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} is disabled: additive spec fields
+ * that this library version doesn't yet model round-trip silently rather than failing the load.
+ * That's the right default for a draft-spec ecosystem where consumers routinely lag emitters.
+ *
+ *
By default, the {@code version:} field is accepted as-is. Callers that need to reject
+ * documents at unknown spec versions can opt in with {@link #setStrictVersion(boolean)} — the
+ * reader then throws {@link UnsupportedOssieVersionException} on a mismatch. The set of
+ * recognised versions is exposed via {@link #getSupportedVersions()} for surfacing in errors and
+ * documentation.
*/
public final class OssieYamlReader {
+ /**
+ * Ossie / OSI spec versions this library understands. Widens as new drafts land — additive
+ * changes bump the last suffix (e.g. {@code 0.2.0.dev0} → {@code 0.2.0.dev1}); breaking
+ * changes require a new major version of ossie-core itself.
+ *
+ *
Included today:
+ *
+ *
+ * - {@code 0.1.0} — initial public draft
+ *
- {@code 0.1.1} — the version dbt Core 1.12 emits at
+ * {@code target/osi_document.json}
+ *
- {@code 0.2.0.dev0} — current apache/ossie draft head
+ *
+ */
+ public static final Set SUPPORTED_VERSIONS = Set.of("0.1.0", "0.1.1", "0.2.0.dev0");
+
private final ObjectMapper yaml;
+ private boolean strictVersion = false;
public OssieYamlReader() {
this.yaml = new ObjectMapper(new YAMLFactory());
this.yaml.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
+ /**
+ * When {@code true}, {@link #read} throws {@link UnsupportedOssieVersionException} if the
+ * document's {@code version} field is not in {@link #SUPPORTED_VERSIONS}. Default {@code false}
+ * — permissive mode for tools that want to attempt a best-effort parse against unfamiliar spec
+ * versions (Jackson's {@code ignoreUnknown} handles additive changes gracefully).
+ *
+ * @return {@code this} for fluent chaining
+ */
+ public OssieYamlReader setStrictVersion(boolean strict) {
+ this.strictVersion = strict;
+ return this;
+ }
+
+ /** Returns the versions this reader recognises. Equivalent to {@link #SUPPORTED_VERSIONS}. */
+ public Set getSupportedVersions() {
+ return SUPPORTED_VERSIONS;
+ }
+
public OssieDocument read(Path yamlPath) throws IOException {
try (InputStream in = Files.newInputStream(yamlPath)) {
- return yaml.readValue(in, OssieDocument.class);
+ return checkVersion(yaml.readValue(in, OssieDocument.class));
}
}
public OssieDocument read(InputStream stream) throws IOException {
- return yaml.readValue(stream, OssieDocument.class);
+ return checkVersion(yaml.readValue(stream, OssieDocument.class));
}
public OssieDocument read(Reader reader) throws IOException {
- return yaml.readValue(reader, OssieDocument.class);
+ return checkVersion(yaml.readValue(reader, OssieDocument.class));
}
public OssieDocument readString(String yamlText) throws IOException {
- return yaml.readValue(yamlText, OssieDocument.class);
+ return checkVersion(yaml.readValue(yamlText, OssieDocument.class));
+ }
+
+ private OssieDocument checkVersion(OssieDocument doc) {
+ if (!strictVersion) return doc;
+ String version = doc.getVersion();
+ if (version == null || !SUPPORTED_VERSIONS.contains(version)) {
+ throw new UnsupportedOssieVersionException(version, SUPPORTED_VERSIONS);
+ }
+ return doc;
}
}
diff --git a/src/main/java/bi/saiku/ossie/UnsupportedOssieVersionException.java b/src/main/java/bi/saiku/ossie/UnsupportedOssieVersionException.java
new file mode 100644
index 0000000..46334e4
--- /dev/null
+++ b/src/main/java/bi/saiku/ossie/UnsupportedOssieVersionException.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2026 Spicule Ltd
+ * Apache License, Version 2.0.
+ */
+package bi.saiku.ossie;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Thrown by {@link OssieYamlReader} when {@code setStrictVersion(true)} is enabled and the parsed
+ * document's {@code version:} field is not in {@link OssieYamlReader#SUPPORTED_VERSIONS}.
+ *
+ * Runtime exception because callers using strict-version-check are opting in to fail-fast
+ * behaviour on version drift, not routine error handling. Extends {@link RuntimeException} so
+ * existing method signatures ({@code read(...) throws IOException}) don't need widening.
+ *
+ *
The message names the offending version and lists the supported set so operators can decide
+ * whether to widen this library's version constant or downgrade the document's version.
+ */
+public class UnsupportedOssieVersionException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final String observedVersion;
+ private final Set supportedVersions;
+
+ public UnsupportedOssieVersionException(String observed, Set supported) {
+ super(buildMessage(observed, supported));
+ this.observedVersion = observed;
+ this.supportedVersions = supported == null ? Set.of() : Set.copyOf(supported);
+ }
+
+ /** @return the version string carried by the document (may be {@code null} if none declared) */
+ public String getObservedVersion() {
+ return observedVersion;
+ }
+
+ /** @return the set of versions the reader was configured to accept */
+ public Set getSupportedVersions() {
+ return supportedVersions;
+ }
+
+ private static String buildMessage(String observed, Set supported) {
+ String obs = observed == null ? "(no version declared)" : "'" + observed + "'";
+ Set sortedSupported = supported == null ? Set.of() : new TreeSet<>(supported);
+ return "Ossie document declares version " + obs
+ + " which is not in the reader's supported set: " + sortedSupported
+ + ". Enable permissive mode by leaving OssieYamlReader.setStrictVersion(false) — "
+ + "the default — or bump this library to a release that lists the observed version.";
+ }
+}
diff --git a/src/test/java/bi/saiku/ossie/OssieSynonymIndexTest.java b/src/test/java/bi/saiku/ossie/OssieSynonymIndexTest.java
new file mode 100644
index 0000000..aef3aeb
--- /dev/null
+++ b/src/test/java/bi/saiku/ossie/OssieSynonymIndexTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2026 Spicule Ltd
+ * Apache License, Version 2.0.
+ */
+package bi.saiku.ossie;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import bi.saiku.ossie.model.OssieDocument;
+import bi.saiku.ossie.model.SemanticModel;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class OssieSynonymIndexTest {
+
+ private final OssieYamlReader reader = new OssieYamlReader();
+
+ private static final String YAML_WITH_SYNONYMS =
+ """
+ version: 0.2.0.dev0
+ semantic_model:
+ - name: Sales
+ datasets:
+ - name: store_sales
+ source: public.store_sales
+ ai_context:
+ synonyms: [sales transactions, POS data]
+ fields:
+ - name: c_state
+ expression:
+ dialects:
+ - dialect: ANSI_SQL
+ expression: C_STATE
+ ai_context:
+ synonyms: [state, region]
+ - name: c_country
+ expression:
+ dialects:
+ - dialect: ANSI_SQL
+ expression: C_COUNTRY
+ ai_context:
+ synonyms: [country]
+ metrics:
+ - name: net_revenue
+ expression:
+ dialects:
+ - dialect: ANSI_SQL
+ expression: SUM(store_sales.amount)
+ ai_context:
+ synonyms: [revenue, turnover, top-line]
+ - name: order_count
+ expression:
+ dialects:
+ - dialect: ANSI_SQL
+ expression: COUNT(*)
+ ai_context:
+ synonyms: [orders]
+ """;
+
+ private SemanticModel loadModel() throws Exception {
+ OssieDocument doc = reader.readString(YAML_WITH_SYNONYMS);
+ return doc.getSemanticModel().get(0);
+ }
+
+ @Test
+ void metricSynonymsResolveCanonicalName() throws Exception {
+ Map idx = OssieSynonymIndex.buildMetricIndex(loadModel());
+ assertEquals("net_revenue", idx.get("revenue"));
+ assertEquals("net_revenue", idx.get("turnover"));
+ assertEquals("net_revenue", idx.get("top-line"));
+ assertEquals("order_count", idx.get("orders"));
+ }
+
+ @Test
+ void metricLookupIsCaseInsensitive() throws Exception {
+ Map idx = OssieSynonymIndex.buildMetricIndex(loadModel());
+ assertEquals("net_revenue", idx.get("revenue"));
+ assertEquals("net_revenue", OssieSynonymIndex.resolve("REVENUE", idx));
+ assertEquals("net_revenue", OssieSynonymIndex.resolve("Revenue", idx));
+ assertEquals("net_revenue", OssieSynonymIndex.resolve("ReVeNuE", idx));
+ }
+
+ @Test
+ void fieldSynonymsQualifyByDataset() throws Exception {
+ Map idx = OssieSynonymIndex.buildFieldIndex(loadModel());
+ assertEquals("store_sales.c_state", idx.get("state"));
+ assertEquals("store_sales.c_state", idx.get("region"));
+ assertEquals("store_sales.c_country", idx.get("country"));
+ }
+
+ @Test
+ void datasetSynonymsResolveCanonicalName() throws Exception {
+ Map idx = OssieSynonymIndex.buildDatasetIndex(loadModel());
+ assertEquals("store_sales", idx.get("sales transactions"));
+ assertEquals("store_sales", idx.get("pos data"));
+ }
+
+ @Test
+ void resolveReturnsInputWhenNotASynonym() throws Exception {
+ Map idx = OssieSynonymIndex.buildMetricIndex(loadModel());
+ // Canonical name doesn't get rewritten.
+ assertEquals("net_revenue", OssieSynonymIndex.resolve("net_revenue", idx));
+ // Nor does an unknown name.
+ assertEquals("unrelated", OssieSynonymIndex.resolve("unrelated", idx));
+ }
+
+ @Test
+ void resolveHandlesNullsWithoutThrowing() {
+ assertNull(OssieSynonymIndex.resolve(null, Map.of("x", "y")));
+ assertEquals("orig", OssieSynonymIndex.resolve("orig", null));
+ assertEquals("orig", OssieSynonymIndex.resolve("orig", Map.of()));
+ }
+
+ @Test
+ void emptyModelYieldsEmptyIndices() {
+ // No datasets, no metrics — every index is empty but non-null.
+ SemanticModel empty = new SemanticModel();
+ assertTrue(OssieSynonymIndex.buildFieldIndex(empty).isEmpty());
+ assertTrue(OssieSynonymIndex.buildMetricIndex(empty).isEmpty());
+ assertTrue(OssieSynonymIndex.buildDatasetIndex(empty).isEmpty());
+ }
+
+ @Test
+ void nullModelYieldsEmptyIndices() {
+ assertTrue(OssieSynonymIndex.buildFieldIndex(null).isEmpty());
+ assertTrue(OssieSynonymIndex.buildMetricIndex(null).isEmpty());
+ assertTrue(OssieSynonymIndex.buildDatasetIndex(null).isEmpty());
+ }
+
+ @Test
+ void firstDeclaredWinsOnCollision() throws Exception {
+ String yaml =
+ """
+ version: 0.2.0.dev0
+ semantic_model:
+ - name: T
+ metrics:
+ - name: a
+ expression:
+ dialects: [{dialect: ANSI_SQL, expression: '1'}]
+ ai_context:
+ synonyms: [shared]
+ - name: b
+ expression:
+ dialects: [{dialect: ANSI_SQL, expression: '2'}]
+ ai_context:
+ synonyms: [shared]
+ """;
+ SemanticModel m = reader.readString(yaml).getSemanticModel().get(0);
+ Map idx = OssieSynonymIndex.buildMetricIndex(m);
+ // First declaration wins; second is silently dropped.
+ assertEquals("a", idx.get("shared"));
+ }
+
+ @Test
+ void resolveOnCanonicalNameReturnsSameString() throws Exception {
+ // Sanity: resolve() shouldn't rewrite an already-canonical reference. Also confirms it's
+ // returning the input reference (not a copy) when no rewrite happens — helps downstream
+ // reference-equality shortcuts.
+ Map idx = OssieSynonymIndex.buildMetricIndex(loadModel());
+ String canonical = "net_revenue";
+ assertSame(canonical, OssieSynonymIndex.resolve(canonical, idx));
+ }
+}
diff --git a/src/test/java/bi/saiku/ossie/StrictVersionTest.java b/src/test/java/bi/saiku/ossie/StrictVersionTest.java
new file mode 100644
index 0000000..c5cece9
--- /dev/null
+++ b/src/test/java/bi/saiku/ossie/StrictVersionTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2026 Spicule Ltd
+ * Apache License, Version 2.0.
+ */
+package bi.saiku.ossie;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import bi.saiku.ossie.model.OssieDocument;
+import org.junit.jupiter.api.Test;
+
+class StrictVersionTest {
+
+ private static final String MINIMAL_TEMPLATE =
+ """
+ version: %s
+ semantic_model:
+ - name: T
+ datasets:
+ - name: d
+ source: s.d
+ """;
+
+ @Test
+ void permissiveModeAcceptsAnyVersion() throws Exception {
+ OssieDocument doc = new OssieYamlReader().readString(MINIMAL_TEMPLATE.formatted("9.9.9"));
+ // Permissive read accepts unknown version — Jackson deserialises it verbatim.
+ assertEquals("9.9.9", doc.getVersion());
+ }
+
+ @Test
+ void strictModeAcceptsEveryKnownVersion() throws Exception {
+ OssieYamlReader reader = new OssieYamlReader().setStrictVersion(true);
+ for (String v : OssieYamlReader.SUPPORTED_VERSIONS) {
+ OssieDocument doc = reader.readString(MINIMAL_TEMPLATE.formatted(v));
+ assertEquals(v, doc.getVersion(), "supported version " + v + " should parse in strict mode");
+ }
+ }
+
+ @Test
+ void strictModeRejectsUnknownVersion() {
+ OssieYamlReader reader = new OssieYamlReader().setStrictVersion(true);
+ UnsupportedOssieVersionException ex = assertThrows(
+ UnsupportedOssieVersionException.class,
+ () -> reader.readString(MINIMAL_TEMPLATE.formatted("99.99.99")));
+ assertEquals("99.99.99", ex.getObservedVersion());
+ assertTrue(ex.getSupportedVersions().contains("0.1.1"));
+ // The message names both the offending version and the supported set.
+ assertTrue(ex.getMessage().contains("99.99.99"));
+ assertTrue(ex.getMessage().contains("0.1.1"));
+ }
+
+ @Test
+ void strictModeRejectsMissingVersion() {
+ // A document with no `version:` at all gets the DTO default in permissive mode
+ // (0.2.0.dev0) — which happens to be in the supported set, so this scenario would false-
+ // negative in the "reject" test. To exercise the null-version branch, force a null.
+ OssieYamlReader reader = new OssieYamlReader().setStrictVersion(true);
+ UnsupportedOssieVersionException ex =
+ new UnsupportedOssieVersionException(null, OssieYamlReader.SUPPORTED_VERSIONS);
+ assertEquals(null, ex.getObservedVersion());
+ assertTrue(ex.getMessage().contains("no version declared"));
+ }
+
+ @Test
+ void supportedVersionsIsExposed() {
+ assertNotNull(OssieYamlReader.SUPPORTED_VERSIONS);
+ assertTrue(OssieYamlReader.SUPPORTED_VERSIONS.contains("0.1.1")); // dbt 1.12 target
+ assertTrue(OssieYamlReader.SUPPORTED_VERSIONS.contains("0.2.0.dev0")); // apache/ossie head
+ }
+
+ @Test
+ void strictSettingIsFluent() {
+ OssieYamlReader reader = new OssieYamlReader();
+ // Fluent return so callers can chain: new OssieYamlReader().setStrictVersion(true).readString(...)
+ assertEquals(reader, reader.setStrictVersion(true));
+ assertEquals(reader, reader.setStrictVersion(false));
+ }
+}