Skip to content

Add WisdomAI domain converter (both directions)#13

Open
mwu-wisdom wants to merge 109 commits into
apache:mainfrom
mwu-wisdom:wisdom-converter
Open

Add WisdomAI domain converter (both directions)#13
mwu-wisdom wants to merge 109 commits into
apache:mainfrom
mwu-wisdom:wisdom-converter

Conversation

@mwu-wisdom

@mwu-wisdom mwu-wisdom commented Jul 10, 2026

Copy link
Copy Markdown

What

Adds converters/wisdom, a bidirectional converter between WisdomAI domain exports (format 1.0, the JSON produced by wisdom's exportDomain API and consumed by importDomain) and Ossie semantic model YAML. It follows the layout and conventions of the dbt converter (apache-ossie dependency, ConverterResult/ConverterIssue loss reporting, argparse CLI). Also registers WISDOM as a well-known custom-extension vendor in the spec documents and OSIVendor enum.

ossie-wisdom wisdom-to-osi -i domain-export.json -o semantic_model.yaml
ossie-wisdom osi-to-wisdom -i semantic_model.yaml -o domain-export.json

Import mapping (wisdom → Ossie)

Ossie Wisdom
semantic_model[].name / description domain ref.name / description
ai_context (model level) domainSystemInstructions + domain knowledge contents as a bulleted list
datasets[] tables (location.database.schema.dbTablesource; primaryKey.columns or isPrimaryKey flags → primary_key)
datasets[].fields[] columns (bare, dialect-quoted names) and formulas (verbatim expressions); displayNamelabel; DATE/DATETIME/TIMESTAMP → dimension.is_time
relationships[] relationship graph edges — cardinality folded into edge direction (from = many side); compound AND-joins flattened to composite key arrays; OR/non-equi joins dropped with a warning
metrics[] per-table measures hoisted to the model level, cross-table name collisions resolved by table-name prefixing

Expressions are emitted verbatim under the Ossie dialect mapped from the table's connection (snowflake → SNOWFLAKE, databricks → DATABRICKS); other dialects fall back to ANSI_SQL verbatim with an UNSUPPORTED_DIALECT warning.

Export mapping (Ossie → wisdom)

The exact inverse, so round-trips are stable in both directions:

  • Model ai_context splits back into domainSystemInstructions (leading text) + one knowledge item per - bullet.
  • A field whose expression is just its own (possibly quoted) name becomes a column; anything else becomes a formula. dimension.is_time maps to a TIMESTAMP data type (wisdom re-derives exact types from the warehouse).
  • Relationships become MANY_TO_ONE edges; the ai_context notes written by the import direction restore ONE_TO_ONE/MANY_TO_MANY, and composite keys become compound AND join conditions.
  • Metrics attach to the table their expression references (fallback to the first dataset with a warning).
  • IDs are derived deterministically from element names so re-runs are reproducible; connections are per-dialect placeholders expected to be remapped at import.
  • Not representable in wisdom — dropped with warnings: semantic models beyond the first, unique_keys, custom_extensions, and ai_context on fields/metrics (plus synonyms/examples anywhere).

Testing

  • 27 tests against a synthetic fixture covering all four cardinalities, compound AND-join flattening, OR-join dropping, dialect fallback, hidden columns, stale measures, duplicate fields, metric name collisions, full round-trip equality (OSI → wisdom → OSI yields an identical OSIDocument), and determinism of the emitted export.
  • Verified end-to-end against two real wisdom domain exports (Snowflake and BigQuery connections): the Snowflake output passes validation/validate.py fully (schema, uniqueness, reference integrity, sqlglot SQL parsing), and OSI → wisdom → OSI round-trips are byte-identical YAML for both.

🤖 Generated with Claude Code

kurtStirewalt and others added 30 commits May 8, 2026 13:46
… ontology spec that refs the core spec so that both may evolve independently.
Implements OSI → Honeydew and Honeydew → OSI conversion with full
round-trip fidelity. Relationship names are stored natively on the
relation, and relations are always placed on the many side.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix empty-string expression bypassing None guard
- Restore calculated_attribute routing via HONEYDEW type hint
- Preserve bool datatype through OSI round-trip
- Store string metric ai_context in osi metadata for recovery
- Warn on duplicate metric names instead of silently overwriting
- Restore connection_expr from HONEYDEW custom_extension on OSI→Honeydew
- Warn on malformed JSON in _read_osi_metadata instead of silent drop
- Remove filter limitation from README (not applicable)
- Update Honeydew docs link to honeydew.ai/docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ssion guards

- Restore Honeydew-specific fields (display_name, hidden, format_string,
  timegrain, owner, folder) in OSI→Honeydew direction by reading them back
  from the HONEYDEW custom_extension on both entity and attribute objects
- Add path traversal guard in main() using os.path.normpath + startswith check
- Guard against whitespace-only expressions (not expr or not expr.strip())
- Warn when field expression is a non-dict value instead of silently dropping it
- Simplify elif not from_cols: to else: in _osi_relation_to_honeydew
- Strengthen test_ai_context_string_preserved to assert the string value is
  recoverable in description
- Add 5 new tests: display_name/format round-trip, calc attr Honeydew fields,
  entity owner, whitespace expression skipped, non-dict expression warns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rewrite entire test file from classes to module-level pytest functions;
  parametrize repeated cases (is_simple_identifier, parse_osi_source,
  field datatypes, entity honeydew fields, dataset/calc attr fields,
  empty/whitespace expressions, path traversal guard, check_safe_path)
- Extract _check_safe_path into a named helper in the converter (was
  inlined in main()) so path traversal logic is independently testable
- Add parametrized test_check_safe_path covering ../evil.yml and
  ../../etc/passwd rejection and legitimate nested paths
- Add test_empty_or_whitespace_metric_expression_skipped to cover the
  OSI→Honeydew whitespace guard on metrics (was previously untested)
- Parametrize entity/attr/calc Honeydew field round-trip tests
  (owner, display_name, hidden, folder each verified independently)
- Update _write_workspace helper to pass through entity-level fields
  (owner, display_name, hidden, folder) so round-trip tests use the
  standard _honeydew_roundtrip() helper instead of manual workspace setup
- Promote _HD_ATTR_KEYS tuple to module-level constant (was defined
  inside the field loop on every iteration)
- Drop from __future__ import annotations — requires Python 3.12+
- Add pyproject.toml with requires-python = ">=3.12"
- Add Python 3.12+ requirement section to README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove requirements.txt (superseded by pyproject.toml)
- Extract _fields_to_honeydew() from _dataset_to_files(): field
  classification is now a named, independently testable function; four
  direct unit tests added
- Warn when a relationship has neither from_columns nor connection_expr
  so callers discover the incomplete join before it reaches Honeydew
- Round-trip the vendors list: non-HONEYDEW vendors are stored in the
  workspace osi metadata on OSI→Honeydew and merged back on the return
  trip (HONEYDEW always appears first)
- Add main() CLI smoke tests via subprocess: osi-to-honeydew writes the
  expected workspace.yml, honeydew-to-osi writes a parseable OSI YAML,
  and path traversal in an entity name is rejected with exit code 1
- Update README setup instructions to use pip install . / pip install -e .

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Collapse 13 individual OSI→Honeydew tests, 7 Honeydew→OSI tests,
10 OSI round-trip tests, and 13 Honeydew round-trip tests into four
@pytest.mark.parametrize blocks. Every assertion now compares the
entire output dict rather than cherry-picked fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Docstring: relationship name is mapped directly to Honeydew's relation
  name field (not osi metadata); ai_context is mapped natively to
  description/labels/AI metadata, not treated as having no equivalent
- README mapping table: rename rows to use Honeydew's canonical terms
  (Source Attribute, Calculated Attribute) and add missing ai_context row
- README limitations: rewrite the confusing "One dataset per entity" bullet
  to clarify that OSI dataset = one table/query, Honeydew supports multiple
  dataset files per entity but the converter generates exactly one

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rror changes to the core semantic model spec.
ralfbecher and others added 29 commits July 14, 2026 19:11
…ead constants

Addresses review feedback on this PR:

1. Stop vendoring a copy of the OSI spec. validation._osi_schema_path resolves
   the OSI core schema from a search path: a copy vendored beside the package if
   present, otherwise core-spec/osi-schema.json from the enclosing checkout. The
   vendored osi-schema.json is deleted, so OSI-core validation now links the
   single canonical core-spec copy with nothing to keep in sync.

2. Omit ontology-schema validation. ossie ships no OSI ontology schema and the
   converter emits ontology documents but never consumes them, so the vendored
   osi-ontology-schema.json is removed and validate_osi_ontology runs semantic
   checks only (unique concepts, reference integrity).

3. Remove the unused _OSI_KNOWN_VENDORS and _OSI_KNOWN_DIALECTS constants
   (only re-exported; passthrough uses _INTERNAL_VENDORS, dialect parsing uses
   _SQL_PARSEABLE_DIALECTS).

Only the OBML schema (OrionBelt's own format) stays vendored. mypy override
added for the optional orionbelt engine import. 145 tests pass; ruff clean;
mypy clean with the dev extra.
…spec

The vendored osi-schema.json copy was removed so the source tree carries no
duplicate of the spec. But a pip-installed wheel has no core-spec/ alongside it,
so validate_osi would silently skip schema validation. force-include the single
canonical core-spec/osi-schema.json into the built wheel: no tracked duplicate,
yet a pip install still validates OSI documents self-contained. In-tree runs
still resolve it from core-spec/ via validation._osi_schema_path.
python: accept free-form custom extension vendors
Co-authored-by: Emil Sadek <esadek@users.noreply.github.com>
Co-authored-by: Emil Sadek <esadek@users.noreply.github.com>
* Test commit

* Add the expression language spec

* Extract embedded base64 image to img folder in core-spec

Replace inline base64 PNG in expression_language.md with a relative
file reference to core-spec/img/osi_layers.png, which renders correctly
both locally and in the GitHub UI.


* Update expression language status to Proposed Final



* Remove links to old doc

* Address reviewer comments

* Shrink casting to supported core.

* Addressed some feedback

* Move to Ossie name

* Update time

---------
Co-authored-by: Will Pugh <willpugh@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add Omni bidirectional converter

Adds converters/omni: an offline OSI <-> Omni semantic model converter,
filling the Omni spoke in the hub-and-spoke architecture.

- Export (osi-omni export): OSI -> Omni model files (views/*.view.yaml,
  relationships.yaml, a generated topic, model.yaml when stashed).
- Import (osi-omni import): Omni -> OSI. Omni-only features are preserved
  in custom_extensions[OMNI], so Omni -> OSI -> Omni is lossless.
- Unified osi-omni CLI plus a string-in / files-dict-out Python API.
- Example-based unit tests, fixture round-trips (incl. TPC-DS), and
  Hypothesis property-based round-trip tests with a seeded-random fallback.
- Adds the OMNI vendor row to converters/index.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Handle real-world Omni model layouts in the OSI converter

Testing against production Omni instances surfaced model shapes the
fixtures did not cover; importing all of them now round-trips losslessly
(verified on a 3,498-file and a 427-file production model plus the
Ecommerce demo model, all validating against the OSI spec):

- Resolve schema-qualified view names (schema__table) from the
  "# Reference this view as" header, accept per-schema folder layouts,
  and preserve original file paths through a round trip.
- Stash-and-restore instead of erroring: extends-only views, non-equi/
  range joins, joins touching query/extends views, and fields or
  measures using Omni template ({{...}}) syntax.
- Preserve explicit Omni defaults verbatim (join_type, relationship_type,
  redundant table_name, self-named sql), non-canonical on_sql (aliases,
  casing, spacing), ${view.field} compound keys, empty-string metadata,
  and timeframes: [].
- Accept real Omni identifiers (_fivetran_id, trailing underscores,
  camelCase); collision checks stay case-insensitive.
- Quote source parts that need it ("Omni Views".upload), tag date values
  in the JSON stash, and suffix duplicate generated relationship names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Catch sqlglot TokenError in the SQL validator

An expression that cannot be tokenized (e.g. templated SQL) crashed the
validator instead of being reported as a finding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…(#206)

Mirrors orionbelt-semantic-layer#201 (canonical source) into the ossie converter:

- Metric round-trip: OSItoOBML resolves metric references by BOTH the OSI
  dataset/field name and the physical code (source-table code and field
  expression, quoted or bare), so metrics emitted against physical codes
  (e.g. SUM(fact_orders.amount)) round-trip instead of dropping when a data
  object's code differs from its display name. Resolved refs are bracket-quoted
  so display names with spaces stay intact through the dataset.column parsers.

- Dimension collision: _extract_dimensions qualifies a field name that occurs
  in more than one dataset (Orders.date / Invoices.date) with its data object
  and warns, instead of silently overwriting the earlier dimension.

- Validation robustness: validate_osi guards its semantic loops against
  malformed structures (datasets/fields that are not lists of dicts) and returns
  schema errors instead of raising AttributeError.

Adds tests/test_osi_converter_roundtrip_robustness.py. 154 converter tests pass;
ruff clean; mypy clean with the dev extra.
* feat(cli): scaffold go module with cobra command tree

The OSI converters currently require manual environment setup and
per-converter invocation. This lays the foundation for a unified CLI
(osi) that will discover and invoke converters as plugins via a
stdin/stdout JSON protocol.

Creates cli/ at the repo root as a self-contained Go module
(github.com/open-semantic-interchange/osi/cli). All commands are
stubbed — no logic is wired in this commit.

Design decisions:
- Go chosen for static binary distribution; no runtime dependency
  for end users (brew/apt installable)
- internal/osidir owns ~/.osi/plugins/ initialization and respects
  $OSI_PLUGIN_DIR for override; uses os.UserHomeDir() rather than
  $HOME for Windows portability
- PersistentPreRunE on the root command ensures dir init runs before
  every subcommand; commented caveat that Cobra does not chain this
  automatically if a subcommand defines its own
- MarkFlagsMutuallyExclusive("from", "to") handles the both-set case
  on osi convert; the neither-set case is validated manually in RunE
  since Cobra only guards against both being provided

Build pipeline (Makefile, .goreleaser.yaml) and CI follow in
separate commits.

* chore(cli): add makefile and goreleaser build pipeline

Enables local builds and cross-platform release artifacts for the
OSI CLI.

Makefile provides standard targets for day-to-day development:
build, test, lint, release-dry-run, and clean. All targets are
designed to run from within cli/.

GoReleaser config targets linux/amd64, linux/arm64, darwin/amd64,
darwin/arm64, and windows/amd64. windows/arm64 is excluded — no
widely available CI runner and negligible current demand.

CGO_ENABLED=0 is set for fully static binaries, enabling
cross-compilation from any host without a C toolchain. Version,
commit, and date are injected at build time via ldflags from the
vars declared in main.go.

* ci(cli): add github actions workflow for build and test

Runs go build, go vet, and go test on every push and pull request
that touches cli/ or the workflow file itself.

The paths filter prevents CLI changes from triggering unrelated
workflows in this polyglot repo and vice versa.

Go version is derived from go-version-file: cli/go.mod so the
workflow automatically picks up any future toolchain bumps without
a separate workflow edit. defaults.run.working-directory avoids
repeating cd cli/ on every step.

Cross-platform build testing is not included — CGO_ENABLED=0
means the linux/amd64 build is representative of all targets.
GoReleaser snapshot builds are deferred to a future release workflow.

* test(cli): add unit tests for internal/osidir

Covers the only logic in F1 that warrants testing: $OSI_PLUGIN_DIR
env var override, default path construction via os.UserHomeDir(),
directory creation, and idempotent re-invocation of EnsurePluginDir.

t.Setenv is used throughout so env var mutations are automatically
restored after each test. t.TempDir is used for filesystem tests so
no cleanup is needed and tests are safe to run in parallel.

* chore(cli): rename osi to ossie throughout cli scaffold

Project branding has changed from OSI to OSSIE. Updates all
user-facing and internal references in the CLI:

- Binary name: osi → ossie
- Go module path: .../osi/cli → .../ossie/cli
- Default plugin directory: ~/.osi → ~/.ossie
- Environment variable: OSI_PLUGIN_DIR → OSSIE_PLUGIN_DIR
- GoReleaser project name and archive ids
- All command descriptions and flag help text
- Output directory default: ./osi-output → ./ossie-output

* chore(cli): rename internal osidir package to ossiedir

Completes the OSI → OSSIE rename by updating the internal package
directory, package declaration, and import reference in cmd/root.go.

* fix(ossiedir): rename defaultOSIDir constant to defaultOssieDir

The osi → ossie rename missed this unexported constant. The value
(.ossie) was already correct; only the identifier name was stale.

* chore(cli): fix .gitignore to ignore cli/ossie build artifact

The osi → ossie rename missed the gitignore entry, so the local
build artifact cli/ossie would no longer be ignored by git.

* chore(ossiedir): rename osidir.go and osidir_test.go to ossiedir

File names were inconsistent with the package directory name (ossiedir/).
Pure rename — no code changes.

* Swap references to Open Semantic Interchange to Apache

Ossie has moved from the Open Semantic Interchange to incubation with
the Apache Software Foundation. As such, the references in this PR
needed to be updated accordingly.

* chore(cli): align go.mod version with .tool-versions pin

cli/go.mod declared `go 1.22` while cli/.tool-versions pins
golang 1.26.2, giving contributors two conflicting sources of
truth for which Go version this module targets. Flagged in
review by khush-bhatia on PR #151.

There's no existing compatibility requirement forcing a lower
minimum, so align go.mod to the same version .tool-versions
already pins rather than introduce a toolchain directive or
lower the asdf pin.

* refactor(cli): let cobra enforce --from/--to via MarkFlagsOneRequired

convert.go hand-rolled a check for the "neither --from nor --to
set" case alongside MarkFlagsMutuallyExclusive, which already
covers the "both set" case. Suggested by khush-bhatia on PR #151
that cobra's MarkFlagsOneRequired covers this directly.

Combining MarkFlagsMutuallyExclusive with MarkFlagsOneRequired on
the same flag set gives "exactly one of" semantics entirely
through cobra's flag annotations, so the manual check and its
now-unneeded fmt.Errorf branch in runConvert are removed.

* Apply suggestions from code review

Co-authored-by: Khushboo Bhatia <khushboo.kanjani@gmail.com>

* chore(cli): stop tracking .tool-versions, gitignore for local use

cli/go.mod and cli/.tool-versions each pinned the Go version
independently — even after b834d75 aligned their values, nothing
forced the two to move together, so they could drift apart again
on the next bump.

Removed cli/.tool-versions from the repo so go.mod is the only
in-repo source of truth. Verified this doesn't break tooling:
CI already resolves its Go version from go.mod via actions/setup-go's
`go-version-file: cli/go.mod` (.github/workflows/cli-ci.yml), and
asdf's golang plugin (~/.asdf/plugins/golang/bin/parse-legacy-file)
reads the version straight out of go.mod's `go` directive when no
.tool-versions is present.

Rather than deleting the file with no path back, gitignored
cli/.tool-versions so contributors whose version manager still
wants its own pin file can keep one locally without risk of it
getting committed and re-introducing a second tracked source of
truth. Treats the toolchain-manager pin as personal environment
config, not project config — the same rationale as gitignoring
editor-specific config instead of committing it.

Caveats:
- Relies on asdf's legacy-version-file lookup, which is off by
  default and must be enabled per-user via `legacy_version_file =
  yes` in ~/.asdfrc (or ASDF_LEGACY_VERSION_FILE=yes) for `asdf
  install`/`asdf current` to auto-detect the version from go.mod.
  Not adding a repo-level workaround since asdf has no per-project
  way to set this.
- No automated enforcement that a contributor's local pin matches
  go.mod; a stale local .tool-versions can still silently drift, as
  demonstrated on this machine when the global asdf fallback (golang
  1.20.1) was too old to parse a 3-component go directive. Accepted
  as the tradeoff for not owning tooling-manager config in the repo.

---------

Co-authored-by: Khushboo Bhatia <khushboo.kanjani@gmail.com>
…efile (#225)

Co-authored-by: Emil Sadek <esadek@users.noreply.github.com>
* Checkin snapshot files for consistent testing

* Add header to ambr files

* Keep syrupy snapshots command
…icks) (#224)

Bidirectional, offline converter between Apache Ossie semantic models and Databricks
Unity Catalog Metric Views (YAML v1.1), filling the DATABRICKS spoke already listed in
converters/README.md. Packaged like the sibling spokes: pyproject.toml
(apache-ossie-databricks), an ossie_databricks package under src/, ASF license headers,
and a tests/ suite (example-based + Hypothesis property-based round-trip; 74 tests).
PyYAML is the only runtime dependency.

Co-authored-by: jackstein21 <82542300+jackstein21@users.noreply.github.com>
Co-authored-by: Emil Sadek <esadek@users.noreply.github.com>
* Switch to UV

* Switch to UV

* Switch to UV

* Restored ASF header
Adds converters/wisdom, a one-way converter from a WisdomAI domain
export JSON (format 1.0) to an Ossie semantic model YAML, following
the layout of the dbt converter.

Mapping: domain -> semantic model; domain knowledge and system
instructions -> model-level ai_context; tables/columns/formulas ->
datasets/fields; relationship graph -> relationships (cardinality
folded into edge direction, compound AND-joins flattened to composite
keys); per-table measures -> model-level metrics.

Expressions are emitted verbatim under the dialect mapped from the
table's connection (snowflake, databricks), falling back to ANSI_SQL
with a warning for unsupported dialects. Information loss is reported
through the same ConverterResult/ConverterIssue pattern as ossie-dbt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds WISDOM to the OSIVendor enum, the vendor examples in the spec
documents and JSON schema, and the supported-vendors table in the
converter guide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds osi-to-wisdom, the reverse of the wisdom converter: an Ossie
document becomes a wisdom domain export (format 1.0) consumable by
wisdom's importDomain API.

The mapping inverts wisdom-to-osi so round-trips are stable in both
directions: model ai_context splits back into system instructions and
knowledge items, fields split into columns and formulas, relationship
direction is read as many-to-one with ai_context notes restoring
one-to-one/many-to-many and composite keys becoming compound AND join
conditions, and metrics attach to the table their expression
references. IDs are deterministic (derived from names) and connections
are per-dialect placeholders remapped at import time.

Verified: Ossie -> wisdom -> Ossie round-trips of two real domain
exports are byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BigQuery connections now map to the BIGQUERY dialect added to the spec
instead of falling back to ANSI_SQL with a warning, with backtick
identifier quoting; both directions round-trip it. The pyproject
follows the repo's UV layout (dependency-groups, uv sources for the
in-repo apache-ossie).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.