diff --git a/converters/index.md b/converters/index.md index 6477291b..d9486417 100644 --- a/converters/index.md +++ b/converters/index.md @@ -54,6 +54,7 @@ The OSI specification currently defines extensions for the following vendors: | `SALESFORCE` | Salesforce / Tableau semantic layer | | `DBT` | dbt semantic models | | `DATABRICKS` | Databricks semantic layer | +| `SEMARCHY` | Semarchy semantic model | Each vendor may define custom extensions (via the `custom_extensions` field in the OSI spec) to carry vendor-specific metadata that does not have an equivalent in the core specification. diff --git a/converters/semarchy/.gitignore b/converters/semarchy/.gitignore new file mode 100644 index 00000000..f4e2c6d6 --- /dev/null +++ b/converters/semarchy/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tsbuildinfo diff --git a/converters/semarchy/README.md b/converters/semarchy/README.md new file mode 100644 index 00000000..0c6ddb0e --- /dev/null +++ b/converters/semarchy/README.md @@ -0,0 +1,107 @@ +# osi-semarchy + +Bidirectional converter between [OSI](../../core-spec/spec.md) and the **Semarchy** +xDM semantic model. + +- **Export** — OSI → Semarchy (`osi-to-semarchy`) +- **Import** — Semarchy → OSI (`semarchy-to-osi`) + +Unlike the other reference converters (Python / Java), this one is written in +**TypeScript** (Node ≥ 18, ESM). It is fully offline: it only reads and writes +files. + +## Model shape + +A Semarchy model is a **nested directory tree of `.seml` (YAML) files**, each +file one `_type`-discriminated object (entities under `entities//`, +references under `references/`, …), plus a root `Model` object. Import walks the +tree recursively (accepting `.seml`, `.yaml`, `.yml`), takes the model name from +the `Model` object, and resolves the fully-qualified references Semarchy uses +(`Pkg.entities.Item.Item.UPC`) down to local names. The OSI side is a single +YAML document. Export writes the same native tree — a root `Model` object, +`entities//.Entity.seml`, `entities//unique_keys/…`, and +`references/.Reference.seml`, with fully-qualified references regenerated — +so the output re-imports cleanly (verified on the real ProductRetail model). + +## Install & build + +```bash +cd converters/semarchy +npm install +npm run build # emits dist/ (library + CLI) +npm test # vitest +npm run typecheck +``` + +Requires Node ≥ 18 (declared in `engines`). + +## CLI + +```bash +# OSI (YAML file) -> Semarchy (directory of .seml) +osi-semarchy osi-to-semarchy -i model.yaml -o semarchy-dir/ + +# Semarchy (directory tree of .seml) -> OSI (YAML file) +osi-semarchy semarchy-to-osi -i semarchy-model-src/ -o model.yaml +``` + +## Library + +```ts +import { + osiDocumentToSemarchy, + semarchyToOsi, + readSemarchyDir, + writeSemarchyDir, +} from "osi-semarchy"; +``` + +## Mapping + +| Semarchy | OSI | Notes | +|----------|-----|-------| +| `Entity` | `dataset` | `source` = `<_package>.` | +| `EntityAttribute` | `field` | field `name` = attribute `_name`; `ANSI_SQL` expression = `physicalName`; `Date`/`Timestamp` → `dimension.is_time` | +| `Entity.primaryKey` | `dataset.primary_key` | | +| `UniqueKey` | `dataset.unique_keys` | matched by owning entity; composite order preserved | +| `Reference` | `relationship` | `fromEntity` = many side → `from`; `toEntity` = one side → `to`; FK column from the foreign attribute / role name | +| `SemQLEnricher` | — (export only) | **Import drops it**: the MDM materializes the computed value in the column, so the field stays a plain column. **Export** turns any `SEMARCHY`-dialect field expression back into a `SemQLEnricher` | + +## Design notes + +- **Computed fields / SemQL** (asymmetric by design): + - *Import* does **not** carry a `SemQLEnricher` onto the field. An enricher is + a data-preparation rule; once it runs, the value is materialized in the + column, so a consumer of the model sees a plain column. Enrichers are dropped + with a warning and the field keeps only its `ANSI_SQL` column reference. + - *Export* still supports the reverse: if an OSI model carries a `SEMARCHY` + dialect expression on a field (SemQL is *not* SQL and is never translated), + those fields are grouped back into one `SemQLEnricher` per entity. The entity + attribute's physical column always comes from the `ANSI_SQL` expression, + never the SemQL. `SEMARCHY` was added to the core-spec `Dialect` enum for + this purpose. +- **Validation**: OSI output is validated against `schemas/osi-schema.json` + (draft 2020-12); each emitted Semarchy object is validated against its + per-type schema in `schemas/` (draft-07). Best-effort — skipped with a warning + if a schema file is absent. + +## Limitations + +This converter maps the **core ER model only**. By design (Semarchy is an MDM +platform, not an analytics semantic layer), the roundtrip is **lossy** — the +following are **dropped with a warning**, not preserved in `custom_extensions`: + +- **Metrics** — OSI metrics have no Semarchy equivalent (no measure concept), so + they are dropped on export. +- **SemQL constructs** — `SemQLEnricher` is import-dropped / export-only (see + Design notes); matchers and survivorship rules are dropped. +- **Type definitions** — `ComplexType`, `UserDefinedType`, `LOVType` are dropped. +- **Views** — `DatabaseView`, `BusinessView` are dropped. +- **UI / process objects** — `Form`, `SearchForm`, `Stepper`, `Workflow`, + `DisplayCard`, `CollectionView`, `ActionSet`, `ModelDiagram`, `Application`, + and other MDM-operational objects are dropped. +- **Composite keys**: Semarchy entities use a single primary-key attribute and + references model a single foreign key, so composite OSI primary keys / join + keys are flattened to their first column on export (warned). +- On export, several required Semarchy fields OSI does not carry (physical + names, labels, `entityType`, historization flags) are filled with defaults. diff --git a/converters/semarchy/package-lock.json b/converters/semarchy/package-lock.json new file mode 100644 index 00000000..77116d43 --- /dev/null +++ b/converters/semarchy/package-lock.json @@ -0,0 +1,2524 @@ +{ + "name": "osi-semarchy", + "version": "0.2.0-dev0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "osi-semarchy", + "version": "0.2.0-dev0", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.17.1", + "commander": "^12.1.0", + "yaml": "^2.5.1" + }, + "bin": { + "osi-semarchy": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^22.7.4", + "tsup": "^8.3.0", + "typescript": "^5.6.2", + "vitest": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/converters/semarchy/package.json b/converters/semarchy/package.json new file mode 100644 index 00000000..2a773593 --- /dev/null +++ b/converters/semarchy/package.json @@ -0,0 +1,37 @@ +{ + "name": "osi-semarchy", + "version": "0.2.0-dev0", + "description": "Semarchy semantic model <> OSI converter (bidirectional)", + "license": "Apache-2.0", + "type": "module", + "bin": { + "osi-semarchy": "./dist/cli.js" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "schemas" + ], + "scripts": { + "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "lint": "tsc --noEmit" + }, + "dependencies": { + "ajv": "^8.17.1", + "commander": "^12.1.0", + "yaml": "^2.5.1" + }, + "devDependencies": { + "@types/node": "^22.7.4", + "tsup": "^8.3.0", + "typescript": "^5.6.2", + "vitest": "^2.1.2" + }, + "engines": { + "node": ">=18" + } +} diff --git a/converters/semarchy/schemas/Entity.json b/converters/semarchy/schemas/Entity.json new file mode 100644 index 00000000..b9e69e94 --- /dev/null +++ b/converters/semarchy/schemas/Entity.json @@ -0,0 +1,535 @@ +{ + "description": "Logical entity as defined by the ER modeling standards", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "BuiltInType": { + "enum": [ + "Binary", + "Boolean", + "ByteInteger", + "Date", + "Decimal", + "Integer", + "LongInteger", + "LongText", + "ShortInteger", + "String", + "Timestamp", + "UUID" + ], + "type": "string" + }, + "EntityAttribute": { + "description": "Describes any attribute that ends up stored in a database column.", + "additionalProperties": false, + "properties": { + "_type": { + "const": "EntityAttribute", + "type": "string" + }, + "_name": { + "maxLength": 128, + "pattern": "^[a-zA-Z][a-zA-Z_0-9]*$", + "type": "string" + }, + "label": { + "description": "Label of the object. Used as business-friendly name", + "minLength": 1, + "type": "string" + }, + "description": { + "description": "Description of the object", + "type": "string" + }, + "documentation": { + "description": "The attribute documentation.", + "type": "string" + }, + "dataType": { + "anyOf": [ + { + "$ref": "#/definitions/BuiltInType" + }, + { + "type": "string" + } + ], + "default": "String" + }, + "goldenAttribute": { + "description": "Defines whether attribute makes sense as a golden attribute. Useful to remove useless technical attributes used for matching for example", + "default": true, + "type": "boolean" + }, + "i18ned": { + "description": "Indicates whether this attribute is internationalized (translated).", + "default": false, + "type": "boolean" + }, + "length": { + "description": "Length of atomic attribute", + "default": 128, + "type": "number" + }, + "lovValidationScope": { + "description": "Scope for value in List of Value validation.", + "$ref": "#/definitions/ValidationScopeType" + }, + "mandatory": { + "description": "Indicates whether this attribute is mandatory.", + "default": false, + "type": "boolean" + }, + "mandatoryValidationLabel": { + "description": "Custom error message displayed when a mandatory attribute is left empty.", + "type": "string" + }, + "mandatoryValidationScope": { + "description": "Scope for mandatory value validation.", + "$ref": "#/definitions/ValidationScopeType" + }, + "multiValued": { + "description": "Indicates whether this attribute is a comma-separated list of LOV values", + "default": false, + "type": "boolean" + }, + "physicalName": { + "description": "Physical Name bound to this entity attribute, it is used as a base name when generating the physical database schema", + "pattern": "^[A-Z][A-Z_0-9]*$", + "type": "string" + }, + "precision": { + "description": "Precision of atomic attribute", + "type": "number" + }, + "scale": { + "description": "Scale of atomic attribute", + "type": "number" + }, + "searchable": { + "description": "Indicates whether this attribute participates to search.", + "default": true, + "type": "boolean" + }, + "valueSeparator": { + "description": "Specifies the value separator to use", + "type": "string" + } + }, + "required": [ + "_name", + "_type", + "dataType", + "label", + "physicalName" + ], + "type": "object" + }, + "EntityType": { + "anyOf": [ + { + "description": "Basic entities do not have matching or consolidation capabilities", + "const": "BASIC", + "type": "string" + }, + { + "description": "Records in entities using Fuzzy Matching are matched using a SemQL expression defined in a matcher. \\n(Fuzzy Matching was formerly known as SDPK)", + "const": "FUZZY_MATCHED", + "type": "string" + }, + { + "description": "Records in entities using ID Matching are matched if they have the same IDs. \\nThis matching behavior is well suited when there is a true unique identifier for all the applications communicating with the MDM hub and for simple Data Entry use cases. \\n(ID Matching was formerly known as UDPK)", + "const": "ID_MATCHED", + "type": "string" + } + ] + }, + "GoldenIdGeneration": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "description": "IDs are automatically generated using a database sequence", + "const": "SEQUENCE", + "type": "string" + }, + { + "description": "IDs are automatically created using a UUID generator", + "const": "UUID", + "type": "string" + } + ] + }, + "startWith": { + "description": "Starts With Value when using Sequence for GoldenID Generation.", + "maximum": 2147483647, + "minimum": 1, + "type": "number" + } + }, + "required": [ + "mode" + ], + "type": "object" + }, + "IdGenerationType": { + "anyOf": [ + { + "description": "IDs are manually entered by the user", + "const": "MANUAL", + "type": "string" + }, + { + "description": "IDs are automatically generated using a Semql expression", + "const": "SEMQL", + "type": "string" + }, + { + "description": "IDs are automatically generated using a database sequence", + "const": "SEQUENCE", + "type": "string" + }, + { + "description": "IDs are automatically created using a UUID generator", + "const": "UUID", + "type": "string" + } + ] + }, + "SemQLMatcher": { + "description": "SemQL based matcher", + "additionalProperties": false, + "properties": { + "_type": { + "const": "SemQLMatcher", + "type": "string" + }, + "_name": { + "maxLength": 128, + "pattern": "^[a-zA-Z][a-zA-Z_0-9]*$", + "type": "string" + }, + "description": { + "description": "Description of the object", + "type": "string" + }, + "autoConfirmGoldenThreshold": { + "description": "With a confidence score strictly greater than this value, the matcher will confirm non-singleton golden records.", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "autoConfirmSingletons": { + "description": "Select this option to automatically confirm golden records composed of a single master record", + "default": false, + "type": "boolean" + }, + "matchRules": { + "items": { + "$ref": "#/definitions/SemQLMatchRule" + }, + "minItems": 1, + "type": "array" + }, + "mergeThresholdMergingConfirmed": { + "description": "With a confidence score strictly greater than this value, the matcher will merge confirmed golden records.", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "mergeThresholdMergingConfirmedWithNewMasters": { + "description": "With a confidence score strictly greater than this value, the matcher will add new master records to a confirmed golden record.", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "mergeThresholdMergingConfirmedWithUnconfirmed": { + "description": "With a confidence score strictly greater than this value, the matcher will merge unconfirmed with confirmed golden records.", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "mergeThresholdMergingUnconfirmed": { + "description": "With a confidence score strictly greater than this value, the matcher will merge unconfirmed golden records.", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "mergeThresholdMergingUnconfirmedWithNewMasters": { + "description": "With a confidence score strictly greater than this value, the matcher will add new master records to an unconfirmed golden record.", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "mergeThresholdNewGroup": { + "description": "With a confidence score strictly greater than this value, the matcher will create a golden record from new master records.", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "mergeThresholdRemergingPreviouslySplit": { + "description": "With a confidence score strictly greater than this value, the matcher will merge confirmed golden records previously split by the user.", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "useMultiIterationGrouping": { + "description": "Grouping will run one Iteration per Match Score", + "default": true, + "type": "boolean" + }, + "useTransitiveMatchScore": { + "description": "Use Transitive Match Score while computing Confidence Score", + "default": true, + "type": "boolean" + } + }, + "required": [ + "_name", + "_type", + "autoConfirmGoldenThreshold", + "autoConfirmSingletons", + "matchRules", + "mergeThresholdMergingConfirmed", + "mergeThresholdMergingConfirmedWithNewMasters", + "mergeThresholdMergingConfirmedWithUnconfirmed", + "mergeThresholdMergingUnconfirmed", + "mergeThresholdMergingUnconfirmedWithNewMasters", + "mergeThresholdNewGroup", + "mergeThresholdRemergingPreviouslySplit", + "useMultiIterationGrouping", + "useTransitiveMatchScore" + ], + "type": "object" + }, + "SemQLMatchRuleBinningExpression": { + "description": "Defines a matchrule binning", + "additionalProperties": false, + "properties": { + "expression": { + "description": "Binning expression used to reduce the data set into reasonable buckets before execution of the match process", + "type": "string" + } + }, + "required": [ + "expression" + ], + "type": "object" + }, + "SemQLMatchRule": { + "description": "Defines of a match rule", + "additionalProperties": false, + "properties": { + "_type": { + "const": "SemQLMatchRule", + "type": "string" + }, + "_name": { + "maxLength": 128, + "pattern": "^[a-zA-Z][a-zA-Z_0-9]*$", + "type": "string" + }, + "label": { + "description": "Label of the object. Used as business-friendly name", + "minLength": 1, + "type": "string" + }, + "description": { + "description": "Description of the object", + "type": "string" + }, + "documentation": { + "description": "Documentation displayed in the documentation panel. Markdown syntax is supported.", + "type": "string" + }, + "binningExpressions": { + "items": { + "$ref": "#/definitions/SemQLMatchRuleBinningExpression" + }, + "type": "array" + }, + "color": { + "description": "Color used in the explain record and duplicates management graph edges representing matches between two records", + "type": "string" + }, + "condition": { + "description": "SemQL Condition for matching. When true, the two records are considered as matching with the match score.", + "type": "string" + }, + "matchOnExpression": { + "description": "Expression returning the child records used for matching.", + "type": "string" + }, + "matchScore": { + "description": "Match Score for two records matching according to this match rule.", + "type": "number" + }, + "usingMatchOn": { + "description": "Match using child records of the current entity records.", + "default": false, + "type": "boolean" + } + }, + "required": [ + "_name", + "_type", + "condition", + "label", + "matchScore" + ], + "type": "object" + }, + "SourceIdGeneration": { + "additionalProperties": false, + "properties": { + "idExpression": { + "description": "SemqlExpression used to compute the id when Semql Id Generation is used", + "type": "string" + }, + "mode": { + "$ref": "#/definitions/IdGenerationType" + }, + "startWith": { + "description": "Starts With Value when using Sequence for SourceID Generation.", + "maximum": 2147483647, + "minimum": 1, + "type": "number" + } + }, + "required": [ + "mode" + ], + "type": "object" + }, + "ValidationScopeType": { + "anyOf": [ + { + "description": "Never validate data", + "const": "NONE", + "type": "string" + }, + { + "description": "Validates data after consolidation", + "const": "POST_CONSO", + "type": "string" + }, + { + "description": "Validates source data before consolidation", + "const": "PRE_CONSO", + "type": "string" + }, + { + "description": "Validates data both before and after consolidation", + "const": "PRE_POST", + "type": "string" + } + ] + } + }, + "properties": { + "_package": { + "type": "string" + }, + "_type": { + "const": "Entity", + "type": "string" + }, + "_name": { + "maxLength": 128, + "pattern": "^[a-zA-Z][a-zA-Z_0-9]*$", + "type": "string" + }, + "label": { + "description": "Label of the object. Used as business-friendly name", + "minLength": 1, + "type": "string" + }, + "description": { + "description": "Description of the object", + "type": "string" + }, + "documentation": { + "description": "Entity documentation displayed in the documentation panel. Markdown syntax is supported.", + "type": "string" + }, + "attributes": { + "items": { + "$ref": "#/definitions/EntityAttribute" + }, + "minItems": 1, + "type": "array" + }, + "dataEntryAllowed": { + "description": "Defines whether entity supports creating and editing \"Non Consolidated Golden\" records authored directly in MDM", + "default": true, + "type": "boolean" + }, + "enableDelete": { + "description": "Indicates whether delete is enabled for this entity", + "default": true, + "type": "boolean" + }, + "entityType": { + "description": "Defines the type of entity and the technique used for matching records in this entity.", + "$ref": "#/definitions/EntityType", + "default": "BASIC" + }, + "goldenIdGeneration": { + "description": "Defines how golden record primary keys are generated for fuzzy matched entities.", + "$ref": "#/definitions/GoldenIdGeneration" + }, + "historizeGolden": { + "description": "Select this option to automatically historize changes made on the golden records for this entity. This history is retained according to the entity''s retention policy.", + "default": false, + "type": "boolean" + }, + "historizeMaster": { + "description": "Select this option to automatically historize changes made on the master records for this entity. This history is retained according to the entity''s retention policy.", + "default": false, + "type": "boolean" + }, + "iconUrl": { + "description": "Default Icon representing the records of the entity", + "type": "string" + }, + "matcher": { + "$ref": "#/definitions/SemQLMatcher" + }, + "parentEntity": { + "type": "string" + }, + "physicalTableName": { + "description": "Physical Table Name used as a unique name to generate tables for base entities", + "pattern": "^[A-Z][A-Z_0-9]*$", + "type": "string" + }, + "pluralLabel": { + "description": "Plural label of the object. Used to distinguish between singular and plural instances of this object", + "minLength": 1, + "type": "string" + }, + "primaryKey": { + "description": "An optional reference to an attribute that is the primary key, required unless the entity extends from another one.", + "type": "string" + }, + "sourceIdGeneration": { + "description": "Defines how records ID are generated in Data Entry workflows and integration APIs.", + "$ref": "#/definitions/SourceIdGeneration" + } + }, + "required": [ + "_name", + "_package", + "_type", + "attributes", + "entityType", + "historizeGolden", + "historizeMaster", + "label", + "physicalTableName", + "pluralLabel" + ], + "type": "object" +} \ No newline at end of file diff --git a/converters/semarchy/schemas/README.md b/converters/semarchy/schemas/README.md new file mode 100644 index 00000000..c6f1c489 --- /dev/null +++ b/converters/semarchy/schemas/README.md @@ -0,0 +1,14 @@ +# Schemas + +This directory bundles the JSON Schemas the converter validates against. Both +files are fetched/copied manually and are **required for full validation** (the +converter degrades to skip-with-warning if they are absent). + +| File | Purpose | How to obtain | +|------|---------|---------------| +| `osi-schema.json` | Validate OSI output on import (Semarchy → OSI). | Copy from `core-spec/osi-schema.json`. | +| `semarchy-semantic-model-schema.json` | Validate Semarchy output on export (OSI → Semarchy). | **PENDING** — provide the Semarchy semantic-model JSON Schema. | + +Once `semarchy-semantic-model-schema.json` is present, replace the placeholder +types in `src/semarchy-types.ts` with the real constructs and complete the +mapping in `src/osi-to-semarchy.ts` and `src/semarchy-to-osi.ts`. diff --git a/converters/semarchy/schemas/Reference.json b/converters/semarchy/schemas/Reference.json new file mode 100644 index 00000000..9c188ad0 --- /dev/null +++ b/converters/semarchy/schemas/Reference.json @@ -0,0 +1,245 @@ +{ + "description": "A reference is a relationship from an entity to another entity. It translates into a single ER foreign key.", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "DeletePropagationType": { + "anyOf": [ + { + "description": "Delete referencing records when referenced record is deleted.", + "const": "CASCADE", + "type": "string" + }, + { + "description": "Set reference to null on the referencing record when referenced record is deleted.", + "const": "NULLIFY", + "type": "string" + }, + { + "description": "Prevent referenced record deletion if a referencing record exists.", + "const": "RESTRICT", + "type": "string" + } + ] + }, + "ForeignAttribute": { + "description": "A foreign attribute is the attribute that is derived from a reference relationship. Foreign attributes show up as non modifiable attributes in entities inherited from the relationships where they are referred to.", + "additionalProperties": false, + "properties": { + "_type": { + "const": "ForeignAttribute", + "type": "string" + }, + "entity": { + "type": "string" + }, + "_name": { + "maxLength": 128, + "pattern": "^[a-zA-Z][a-zA-Z_0-9]*$", + "type": "string" + }, + "label": { + "description": "Label of the object. Used as business-friendly name", + "minLength": 1, + "type": "string" + }, + "description": { + "description": "Description of the object", + "type": "string" + }, + "documentation": { + "description": "The attribute documentation.", + "type": "string" + }, + "physicalName": { + "description": "Physical Name of the foreign attribute used when generating the physical database schema", + "pattern": "^[A-Z][A-Z_0-9]*$", + "type": "string" + }, + "searchable": { + "description": "Indicates whether this attribute participates to search.", + "default": true, + "type": "boolean" + } + }, + "required": [ + "_name", + "_type", + "entity", + "label", + "physicalName" + ], + "type": "object" + }, + "SplitDuplicatePropagationType": { + "anyOf": [ + { + "description": "None", + "const": "NONE", + "type": "string" + }, + { + "description": "Reset Matching", + "const": "RESET_MATCHING", + "type": "string" + } + ] + }, + "ValidationScopeType": { + "anyOf": [ + { + "description": "Never validate data", + "const": "NONE", + "type": "string" + }, + { + "description": "Validates data after consolidation", + "const": "POST_CONSO", + "type": "string" + }, + { + "description": "Validates source data before consolidation", + "const": "PRE_CONSO", + "type": "string" + }, + { + "description": "Validates data both before and after consolidation", + "const": "PRE_POST", + "type": "string" + } + ] + } + }, + "properties": { + "_package": { + "type": "string" + }, + "_type": { + "const": "Reference", + "type": "string" + }, + "_name": { + "maxLength": 128, + "pattern": "^[a-zA-Z][a-zA-Z_0-9]*$", + "type": "string" + }, + "label": { + "description": "Label of the object. Used as business-friendly name", + "minLength": 1, + "type": "string" + }, + "description": { + "description": "Description of the object", + "type": "string" + }, + "documentation": { + "description": "Documentation displayed in the documentation panel. Markdown syntax is supported.", + "type": "string" + }, + "brokenValidationLabel": { + "description": "Custom error message displayed for a broken reference", + "type": "string" + }, + "deletePropagation": { + "description": "Defines how delete propagates through this reference", + "$ref": "#/definitions/DeletePropagationType" + }, + "foreignAttribute": { + "$ref": "#/definitions/ForeignAttribute" + }, + "fromEntity": { + "type": "string" + }, + "fromNavigable": { + "description": "Indicates whether this participant association end is navigable from the one side to the many side. By default, it is.", + "default": true, + "type": "boolean" + }, + "fromRoleDocumentation": { + "description": "Documentation of the many side displayed in the documentation panel. Markdown syntax is supported.", + "type": "string" + }, + "fromRoleLabel": { + "description": "Label of the many side", + "minLength": 1, + "type": "string" + }, + "fromRoleName": { + "description": "Name of the many side", + "type": "string" + }, + "fromRolePluralLabel": { + "description": "Plural label of the many side", + "minLength": 1, + "type": "string" + }, + "mandatoryValidationLabel": { + "description": "Custom error message displayed when a mandatory reference is empty.", + "type": "string" + }, + "oneToMany": { + "description": "Indicates whether current reference is a mandatory Many to 1 or an optional Many to 0.", + "default": false, + "type": "boolean" + }, + "physicalName": { + "description": "Physical Name for the reference (used to create indexes)", + "pattern": "^[A-Z][A-Z_0-9]*$", + "type": "string" + }, + "splitDuplicatePropagation": { + "description": "Defines how a parent record split propagates to child records.", + "$ref": "#/definitions/SplitDuplicatePropagationType" + }, + "toEntity": { + "type": "string" + }, + "toNavigable": { + "description": "Indicates whether this participant association end is navigable from the many side to the one side. By default, it is.", + "default": true, + "type": "boolean" + }, + "toRoleDocumentation": { + "description": "Documentation of the one side displayed in the documentation panel. Markdown syntax is supported.", + "type": "string" + }, + "toRoleLabel": { + "description": "Label of the one side", + "minLength": 1, + "type": "string" + }, + "toRoleName": { + "description": "Name of the one side. Used to set the name of the foreign attribute.", + "maxLength": 128, + "pattern": "^[a-zA-Z][a-zA-Z_0-9]*$", + "type": "string" + }, + "toRolePhysicalName": { + "description": "A physical name for the referenced column foreign key", + "pattern": "^[A-Z][A-Z_0-9]*$", + "type": "string" + }, + "validationScope": { + "description": "Scope for reference validation.", + "$ref": "#/definitions/ValidationScopeType" + } + }, + "required": [ + "_name", + "_package", + "_type", + "deletePropagation", + "fromEntity", + "fromRoleLabel", + "fromRoleName", + "fromRolePluralLabel", + "label", + "physicalName", + "toEntity", + "toRoleLabel", + "toRoleName", + "toRolePhysicalName", + "validationScope" + ], + "type": "object" +} \ No newline at end of file diff --git a/converters/semarchy/schemas/SemQLEnricher.json b/converters/semarchy/schemas/SemQLEnricher.json new file mode 100644 index 00000000..5190c439 --- /dev/null +++ b/converters/semarchy/schemas/SemQLEnricher.json @@ -0,0 +1,114 @@ +{ + "description": "SemQL based enricher", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "EnricherScope": { + "anyOf": [ + { + "description": "Never enrich data in integration jobs", + "const": "NONE", + "type": "string" + }, + { + "description": "Enrich data after consolidation", + "const": "POST_CONSO", + "type": "string" + }, + { + "description": "Enrich source data before consolidation", + "const": "PRE_CONSO", + "type": "string" + }, + { + "description": "Enrich data both before and after consolidation", + "const": "PRE_POST", + "type": "string" + } + ] + }, + "SemQLEnricherExpression": { + "description": "The expression used to compute the new value of an attribute in SemQL enricher", + "additionalProperties": false, + "properties": { + "attributeName": { + "description": "Name of the attribute that will take the value of the expression", + "type": "string" + }, + "expression": { + "description": "Expression to map to the attribute", + "type": "string" + } + }, + "required": [ + "attributeName", + "expression" + ], + "type": "object" + } + }, + "properties": { + "_package": { + "type": "string" + }, + "_type": { + "const": "SemQLEnricher", + "type": "string" + }, + "entity": { + "type": "string" + }, + "_name": { + "maxLength": 128, + "pattern": "^[a-zA-Z][a-zA-Z_0-9]*$", + "type": "string" + }, + "label": { + "description": "Label of the object. Used as business-friendly name", + "minLength": 1, + "type": "string" + }, + "description": { + "description": "Description of the object", + "type": "string" + }, + "afterEnrichers": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "condition": { + "description": "Optional SemQL filter to restrict the set of enriched records", + "type": "string" + }, + "enricherExecutionScope": { + "description": "Execution scope of this enricher in data certification jobs", + "$ref": "#/definitions/EnricherScope" + }, + "semQlEnricherExpressions": { + "items": { + "$ref": "#/definitions/SemQLEnricherExpression" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "_name", + "_package", + "_type", + "enricherExecutionScope", + "entity", + "label", + "semQlEnricherExpressions" + ], + "type": "object" +} \ No newline at end of file diff --git a/converters/semarchy/schemas/UniqueKey.json b/converters/semarchy/schemas/UniqueKey.json new file mode 100644 index 00000000..2782737b --- /dev/null +++ b/converters/semarchy/schemas/UniqueKey.json @@ -0,0 +1,96 @@ +{ + "description": "A logical unique key as defined for an entity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "KeyAttribute": { + "description": "A reference to an atomic attribute that is part of the owner key", + "additionalProperties": false, + "properties": { + "attribute": { + "type": "string" + } + }, + "required": [ + "attribute" + ], + "type": "object" + }, + "ValidationScopeType": { + "anyOf": [ + { + "description": "Never validate data", + "const": "NONE", + "type": "string" + }, + { + "description": "Validates data after consolidation", + "const": "POST_CONSO", + "type": "string" + }, + { + "description": "Validates source data before consolidation", + "const": "PRE_CONSO", + "type": "string" + }, + { + "description": "Validates data both before and after consolidation", + "const": "PRE_POST", + "type": "string" + } + ] + } + }, + "properties": { + "_package": { + "type": "string" + }, + "_type": { + "const": "UniqueKey", + "type": "string" + }, + "entity": { + "type": "string" + }, + "_name": { + "maxLength": 128, + "pattern": "^[a-zA-Z][a-zA-Z_0-9]*$", + "type": "string" + }, + "label": { + "description": "Label of the object. Used as business-friendly name", + "minLength": 1, + "type": "string" + }, + "description": { + "description": "Description of the object", + "type": "string" + }, + "keyAttributes": { + "items": { + "$ref": "#/definitions/KeyAttribute" + }, + "minItems": 1, + "type": "array" + }, + "validationLabel": { + "description": "Custom error message displayed when this unique key validation is not met.", + "type": "string" + }, + "validationScope": { + "description": "Scope for unique key validation.", + "$ref": "#/definitions/ValidationScopeType" + } + }, + "required": [ + "_name", + "_package", + "_type", + "entity", + "keyAttributes", + "label", + "validationLabel", + "validationScope" + ], + "type": "object" +} \ No newline at end of file diff --git a/converters/semarchy/schemas/osi-schema.json b/converters/semarchy/schemas/osi-schema.json new file mode 100644 index 00000000..8a9bd821 --- /dev/null +++ b/converters/semarchy/schemas/osi-schema.json @@ -0,0 +1,330 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/open-semantic-interchange/OSI/core-spec/osi-schema.json", + "title": "OSI Core Metadata Specification", + "description": "JSON Schema for validating OSI (Open Semantic Interoperability) semantic model definitions", + "type": "object", + "properties": { + "version": { + "type": "string", + "const": "0.2.0.dev0", + "description": "OSI specification version" + }, + "semantic_model": { + "type": "array", + "description": "Collection of semantic model definitions", + "items": { + "$ref": "#/$defs/SemanticModel" + } + } + }, + "required": ["version", "semantic_model"], + "additionalProperties": false, + "$defs": { + "Dialect": { + "type": "string", + "enum": ["ANSI_SQL", "SNOWFLAKE", "MDX", "TABLEAU", "DATABRICKS", "MAQL", "SEMARCHY"], + "description": "Supported SQL and expression language dialects" + }, + "Vendor": { + "type": "string", + "examples": ["COMMON", "SNOWFLAKE", "SALESFORCE", "DBT", "DATABRICKS", "GOODDATA"], + "description": "Vendor name for custom extensions. Any string value is accepted." + }, + "AIContext": { + "description": "Additional context for AI tools", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "instructions": { + "type": "string", + "description": "Instructions for AI on how to use this entity" + }, + "synonyms": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Alternative names and terms" + }, + "examples": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Sample questions or use cases" + } + }, + "additionalProperties": true + } + ] + }, + "CustomExtension": { + "type": "object", + "description": "Vendor-specific attributes for extensibility", + "properties": { + "vendor_name": { + "$ref": "#/$defs/Vendor" + }, + "data": { + "type": "string", + "description": "JSON string containing vendor-specific data" + } + }, + "required": ["vendor_name", "data"], + "additionalProperties": false + }, + "DialectExpression": { + "type": "object", + "description": "Expression in a specific dialect", + "properties": { + "dialect": { + "$ref": "#/$defs/Dialect" + }, + "expression": { + "type": "string", + "description": "SQL or dialect-specific expression" + } + }, + "required": ["dialect", "expression"], + "additionalProperties": false + }, + "Expression": { + "type": "object", + "description": "Expression definition with multi-dialect support", + "properties": { + "dialects": { + "type": "array", + "items": { + "$ref": "#/$defs/DialectExpression" + }, + "minItems": 1 + } + }, + "required": ["dialects"], + "additionalProperties": false + }, + "Dimension": { + "type": "object", + "description": "Dimension metadata", + "properties": { + "is_time": { + "type": "boolean", + "description": "Indicates if this is a time-based dimension for temporal filtering" + } + }, + "additionalProperties": false + }, + "Field": { + "type": "object", + "description": "Row-level attribute for grouping, filtering, and metric expressions", + "properties": { + "name": { + "type": "string", + "description": "Unique identifier for the field within the dataset" + }, + "expression": { + "$ref": "#/$defs/Expression" + }, + "dimension": { + "$ref": "#/$defs/Dimension" + }, + "label": { + "type": "string", + "description": "Label for categorization" + }, + "description": { + "type": "string", + "description": "Human-readable description" + }, + "ai_context": { + "$ref": "#/$defs/AIContext" + }, + "custom_extensions": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomExtension" + } + } + }, + "required": ["name", "expression"], + "additionalProperties": false + }, + "Dataset": { + "type": "object", + "description": "Logical dataset representing a business entity (fact or dimension table)", + "properties": { + "name": { + "type": "string", + "description": "Unique identifier for the dataset" + }, + "source": { + "type": "string", + "description": "Reference to underlying physical table/view (database.schema.table) or query" + }, + "primary_key": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Primary key columns (single or composite)" + }, + "unique_keys": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Array of unique key definitions (each can be single or composite)" + }, + "description": { + "type": "string", + "description": "Human-readable description" + }, + "ai_context": { + "$ref": "#/$defs/AIContext" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/$defs/Field" + } + }, + "custom_extensions": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomExtension" + } + } + }, + "required": ["name", "source"], + "additionalProperties": false + }, + "Relationship": { + "type": "object", + "description": "Foreign key relationship between datasets", + "properties": { + "name": { + "type": "string", + "description": "Unique identifier for the relationship" + }, + "from": { + "type": "string", + "description": "Dataset on the many side of the relationship" + }, + "to": { + "type": "string", + "description": "Dataset on the one side of the relationship" + }, + "from_columns": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "Foreign key columns in the 'from' dataset" + }, + "to_columns": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "Primary/unique key columns in the 'to' dataset" + }, + "ai_context": { + "$ref": "#/$defs/AIContext" + }, + "custom_extensions": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomExtension" + } + } + }, + "required": ["name", "from", "to", "from_columns", "to_columns"], + "additionalProperties": false + }, + "Metric": { + "type": "object", + "description": "Quantitative measure defined on business data", + "properties": { + "name": { + "type": "string", + "description": "Unique identifier for the metric" + }, + "expression": { + "$ref": "#/$defs/Expression" + }, + "description": { + "type": "string", + "description": "Human-readable description of what the metric measures" + }, + "ai_context": { + "$ref": "#/$defs/AIContext" + }, + "custom_extensions": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomExtension" + } + } + }, + "required": ["name", "expression"], + "additionalProperties": false + }, + "SemanticModel": { + "type": "object", + "description": "Top-level container representing a complete semantic model", + "properties": { + "name": { + "type": "string", + "description": "Unique identifier for the semantic model" + }, + "description": { + "type": "string", + "description": "Human-readable description" + }, + "ai_context": { + "$ref": "#/$defs/AIContext" + }, + "datasets": { + "type": "array", + "items": { + "$ref": "#/$defs/Dataset" + }, + "minItems": 1, + "description": "Collection of logical datasets" + }, + "relationships": { + "type": "array", + "items": { + "$ref": "#/$defs/Relationship" + }, + "description": "Defines how datasets are connected" + }, + "metrics": { + "type": "array", + "items": { + "$ref": "#/$defs/Metric" + }, + "description": "Quantifiable measures spanning datasets" + }, + "custom_extensions": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomExtension" + } + } + }, + "required": ["name", "datasets"], + "additionalProperties": false + } + } +} diff --git a/converters/semarchy/src/cli.ts b/converters/semarchy/src/cli.ts new file mode 100644 index 00000000..4de6abbd --- /dev/null +++ b/converters/semarchy/src/cli.ts @@ -0,0 +1,66 @@ +#!/usr/bin/env node +/** + * osi-semarchy CLI. + * + * osi-semarchy osi-to-semarchy -i model.yaml -o semarchy-dir/ + * osi-semarchy semarchy-to-osi -i semarchy-dir/ -o model.yaml + * + * The Semarchy side is a directory of `_type`-discriminated YAML objects; the + * OSI side is a single YAML document. + */ +import { readFileSync, writeFileSync } from "node:fs"; +import { Command } from "commander"; +import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; +import { osiDocumentToSemarchy } from "./osi-to-semarchy.js"; +import { semarchyToOsi } from "./semarchy-to-osi.js"; +import { readSemarchyDir, writeSemarchyDir } from "./io.js"; +import { validateOsi, validateSemarchy, type ValidationOutcome } from "./validation.js"; +import type { OSIDocument } from "./osi-types.js"; + +function emitWarnings(warnings: string[]): void { + for (const w of warnings) process.stderr.write(`warning: ${w}\n`); +} + +function reportValidation(label: string, outcome: ValidationOutcome): void { + if (outcome.skipped) { + process.stderr.write(`warning: ${label} ${outcome.errors[0]}\n`); + } else if (!outcome.valid) { + process.stderr.write(`error: ${label} validation failed:\n`); + for (const e of outcome.errors) process.stderr.write(` - ${e}\n`); + process.exitCode = 1; + } +} + +const program = new Command(); +program + .name("osi-semarchy") + .description("Bidirectional OSI <> Semarchy semantic model converter"); + +program + .command("osi-to-semarchy") + .description("Convert an OSI model (YAML file) to a Semarchy model (directory of YAML)") + .requiredOption("-i, --input ", "input OSI YAML file") + .requiredOption("-o, --output ", "output Semarchy directory") + .action((opts: { input: string; output: string }) => { + const doc = parseYaml(readFileSync(opts.input, "utf8")) as OSIDocument; + const { model, warnings } = osiDocumentToSemarchy(doc); + emitWarnings(warnings); + reportValidation("semarchy output", validateSemarchy(model)); + writeSemarchyDir(opts.output, model); + }); + +program + .command("semarchy-to-osi") + .description("Convert a Semarchy model (directory of YAML) to an OSI model (YAML file)") + .requiredOption("-i, --input ", "input Semarchy directory") + .requiredOption("-o, --output ", "output OSI YAML file") + .action((opts: { input: string; output: string }) => { + const warnings: string[] = []; + const model = readSemarchyDir(opts.input, warnings); + const { document, warnings: mapWarnings } = semarchyToOsi(model, warnings); + emitWarnings(mapWarnings); + reportValidation("osi output", validateOsi(document)); + writeFileSync(opts.output, stringifyYaml(document)); + }); + +program.parse(); diff --git a/converters/semarchy/src/dialect.ts b/converters/semarchy/src/dialect.ts new file mode 100644 index 00000000..73e3ac42 --- /dev/null +++ b/converters/semarchy/src/dialect.ts @@ -0,0 +1,50 @@ +/** + * Dialect selection with the OSI fallback chain. + * + * Per the converter guide: prefer the vendor dialect, fall back to ANSI_SQL, + * and warn (or error) if neither is present. Never silently drop an expression. + */ +import type { OSIDialect, OSIExpression } from "./osi-types.js"; + +export const SEMARCHY_DIALECT: OSIDialect = "SEMARCHY"; +export const ANSI_DIALECT: OSIDialect = "ANSI_SQL"; + +export interface DialectSelection { + dialect: OSIDialect; + expression: string; +} + +/** + * Select the best expression for the Semarchy target using the fallback chain + * SEMARCHY -> ANSI_SQL. Returns `undefined` and pushes a warning if neither is + * available; callers decide whether that is a warning or a hard error. + */ +export function selectExpression( + expr: OSIExpression | undefined, + context: string, + warnings: string[], +): DialectSelection | undefined { + if (!expr || expr.dialects.length === 0) { + warnings.push(`[dialect] ${context}: no expression defined`); + return undefined; + } + + const semarchy = expr.dialects.find((d) => d.dialect === SEMARCHY_DIALECT); + if (semarchy) { + return { dialect: SEMARCHY_DIALECT, expression: semarchy.expression }; + } + + const ansi = expr.dialects.find((d) => d.dialect === ANSI_DIALECT); + if (ansi) { + warnings.push( + `[dialect] ${context}: no SEMARCHY dialect, falling back to ANSI_SQL`, + ); + return { dialect: ANSI_DIALECT, expression: ansi.expression }; + } + + warnings.push( + `[dialect] ${context}: neither SEMARCHY nor ANSI_SQL present ` + + `(have: ${expr.dialects.map((d) => d.dialect).join(", ")})`, + ); + return undefined; +} diff --git a/converters/semarchy/src/index.ts b/converters/semarchy/src/index.ts new file mode 100644 index 00000000..8167fba5 --- /dev/null +++ b/converters/semarchy/src/index.ts @@ -0,0 +1,11 @@ +/** Public library API for the OSI <> Semarchy converter. */ +export * from "./osi-types.js"; +export * from "./semarchy-types.js"; +export { osiToSemarchy, osiDocumentToSemarchy } from "./osi-to-semarchy.js"; +export type { ExportResult } from "./osi-to-semarchy.js"; +export { semarchyToOsi } from "./semarchy-to-osi.js"; +export type { ImportResult } from "./semarchy-to-osi.js"; +export { selectExpression } from "./dialect.js"; +export { readSemarchyDir, writeSemarchyDir } from "./io.js"; +export { validateSemarchy, validateOsi, OSI_SCHEMA_PATH } from "./validation.js"; +export type { ValidationOutcome } from "./validation.js"; diff --git a/converters/semarchy/src/io.ts b/converters/semarchy/src/io.ts new file mode 100644 index 00000000..99085480 --- /dev/null +++ b/converters/semarchy/src/io.ts @@ -0,0 +1,133 @@ +/** + * Directory-tree I/O for Semarchy models. + * + * A Semarchy model is a nested directory of `.seml` (YAML) files; each file is + * one `_type`-discriminated object (entities under `entities//`, + * references under `references/`, etc.). We walk the tree recursively, group the + * mapped object types into a `SemarchyModel`, take the model name from the + * `Model` object, and warn about (drop) everything else. + * + * On write we emit one file per object, named `<_name>.<_type>.seml`. + */ +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + statSync, + writeFileSync, +} from "node:fs"; +import { join } from "node:path"; +import { parseAllDocuments, stringify as stringifyYaml } from "yaml"; +import type { + SemarchyEnricher, + SemarchyEntity, + SemarchyModel, + SemarchyModelObject, + SemarchyObject, + SemarchyReference, + SemarchyUniqueKey, +} from "./semarchy-types.js"; +/** Types consumed as model metadata (not mapped, but not warned about). */ +const META_TYPES = new Set(["Model"]); +const MODEL_FILE_RE = /\.(seml|ya?ml)$/i; + +/** Recursively collect model files under a directory. */ +function walk(dir: string): string[] { + if (!existsSync(dir) || !statSync(dir).isDirectory()) { + throw new Error(`not a directory: ${dir}`); + } + const out: string[] = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) out.push(...walk(full)); + else if (MODEL_FILE_RE.test(entry.name)) out.push(full); + } + return out.sort(); +} + +/** Read a directory tree of Semarchy objects into a grouped model. */ +export function readSemarchyDir( + dir: string, + warnings: string[] = [], +): SemarchyModel { + const entities: SemarchyEntity[] = []; + const references: SemarchyReference[] = []; + const uniqueKeys: SemarchyUniqueKey[] = []; + const enrichers: SemarchyEnricher[] = []; + let modelObj: SemarchyModelObject | undefined; + + for (const file of walk(dir)) { + for (const doc of parseAllDocuments(readFileSync(file, "utf8"))) { + const obj = doc.toJSON() as SemarchyObject | null; + if (!obj || typeof obj !== "object" || typeof obj._type !== "string") { + continue; + } + const type = obj._type; + if (type === "Entity") entities.push(obj as SemarchyEntity); + else if (type === "Reference") references.push(obj as SemarchyReference); + else if (type === "UniqueKey") uniqueKeys.push(obj as SemarchyUniqueKey); + else if (type === "SemQLEnricher") enrichers.push(obj as SemarchyEnricher); + else if (type === "Model") modelObj = obj as SemarchyModelObject; + else if (!META_TYPES.has(type)) { + warnings.push( + `[drop] unsupported Semarchy type "${type}"` + + (obj._name ? ` (${obj._name})` : "") + + ` in ${file}`, + ); + } + } + } + + if (!modelObj) { + warnings.push(`[model] no Model object found in ${dir}; using "model"`); + } + + return { + name: modelObj?._name ?? modelObj?.label ?? "model", + pkg: modelObj?._package ?? modelObj?._name ?? "model", + entities, + references, + uniqueKeys, + enrichers, + }; +} + +/** Last dot-separated segment of a Semarchy fully-qualified name. */ +function localName(fqn: string): string { + const parts = fqn.split("."); + return parts[parts.length - 1] ?? fqn; +} + +function writeObject(dir: string, obj: SemarchyObject): void { + mkdirSync(dir, { recursive: true }); + const file = `${obj._name ?? "unnamed"}.${obj._type}.seml`; + writeFileSync(join(dir, file), stringifyYaml(obj)); +} + +/** + * Write a Semarchy model to a directory tree mirroring the native layout: + * .Model.seml + * entities//.Entity.seml + * entities//unique_keys/.UniqueKey.seml + * references/.Reference.seml + */ +export function writeSemarchyDir(dir: string, model: SemarchyModel): void { + mkdirSync(dir, { recursive: true }); + if (model.modelObject) writeObject(dir, model.modelObject); + + for (const entity of model.entities) { + writeObject(join(dir, "entities", entity._name), entity); + } + for (const uk of model.uniqueKeys) { + const entity = localName(uk.entity); + writeObject(join(dir, "entities", entity, "unique_keys"), uk); + } + for (const enricher of model.enrichers) { + const entity = localName(enricher.entity); + writeObject(join(dir, "entities", entity, "enrichers"), enricher); + } + for (const ref of model.references) { + writeObject(join(dir, "references"), ref); + } +} diff --git a/converters/semarchy/src/osi-to-semarchy.ts b/converters/semarchy/src/osi-to-semarchy.ts new file mode 100644 index 00000000..021c00dc --- /dev/null +++ b/converters/semarchy/src/osi-to-semarchy.ts @@ -0,0 +1,280 @@ +/** + * Export: OSI -> Semarchy. + * + * Scope (per project decision): dataset -> Entity, field -> EntityAttribute, + * unique_keys -> UniqueKey, relationship -> Reference. OSI metrics have no + * Semarchy target and are dropped with a warning. Semarchy requires several + * fields OSI does not carry (physical names, labels, entity type, etc.); those + * are filled with sensible defaults so the output validates against the + * Semarchy schemas. + */ +import type { + OSIDataset, + OSIDocument, + OSIField, + OSIRelationship, + OSISemanticModel, +} from "./osi-types.js"; +import type { + SemarchyEnricher, + SemarchyEntity, + SemarchyEntityAttribute, + SemarchyModel, + SemarchyModelObject, + SemarchyReference, + SemarchyUniqueKey, +} from "./semarchy-types.js"; + +export interface ExportResult { + model: SemarchyModel; + warnings: string[]; +} + +// --- Fully-qualified name helpers (mirror Semarchy's FQN conventions) --- +/** Package of an entity: `.entities.`. */ +function entityPackage(pkg: string, entity: string): string { + return `${pkg}.entities.${entity}`; +} +/** FQN of an entity: `.entities..`. */ +function entityFqn(pkg: string, entity: string): string { + return `${entityPackage(pkg, entity)}.${entity}`; +} +/** FQN of an attribute: `.entities...`. */ +function attributeFqn(pkg: string, entity: string, attr: string): string { + return `${entityFqn(pkg, entity)}.${attr}`; +} + +/** Semarchy `_name` pattern: ^[a-zA-Z][a-zA-Z_0-9]*$. */ +function logicalName(name: string, warnings: string[], what: string): string { + const cleaned = name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^([^a-zA-Z])/, "n$1"); + if (cleaned !== name) { + warnings.push(`[rename] ${what} "${name}" -> "${cleaned}" (Semarchy name rules)`); + } + return cleaned; +} + +/** Semarchy physicalName pattern: ^[A-Z][A-Z_0-9]*$. */ +function physicalName(name: string): string { + const upper = name.toUpperCase().replace(/[^A-Z0-9_]/g, "_"); + return /^[A-Z]/.test(upper) ? upper : `T_${upper}`; +} + +/** Best-effort Semarchy data type from an OSI field. */ +function dataTypeOf(field: OSIField): string { + return field.dimension?.is_time ? "Timestamp" : "String"; +} + +/** The expression for a given dialect on a field, if present. */ +function dialectExpression(field: OSIField, dialect: string): string | undefined { + return field.expression.dialects.find((d) => d.dialect === dialect)?.expression; +} + +function fieldToAttribute( + field: OSIField, + warnings: string[], +): SemarchyEntityAttribute { + // The physical column is the ANSI_SQL (column-reference) expression, NOT the + // SEMARCHY one — a SEMARCHY expression is a SemQL computation and becomes an + // enricher, not a physical column name. + const column = dialectExpression(field, "ANSI_SQL") ?? field.name; + return { + _type: "EntityAttribute", + _name: logicalName(field.name, warnings, "attribute"), + label: field.label ?? field.name, + physicalName: physicalName(column), + dataType: dataTypeOf(field), + ...(field.description ? { description: field.description } : {}), + }; +} + +function datasetToEntity( + dataset: OSIDataset, + pkg: string, + warnings: string[], +): SemarchyEntity { + const fields = dataset.fields ?? []; + if (fields.length === 0) { + warnings.push( + `[warn] dataset "${dataset.name}": no fields; Semarchy entities require ` + + `at least one attribute`, + ); + } + // source is `db.schema.table`; the physical table name is the last segment. + const table = dataset.source.split(".").pop() ?? dataset.name; + const name = logicalName(dataset.name, warnings, "entity"); + + const entity: SemarchyEntity = { + _type: "Entity", + _package: entityPackage(pkg, name), + _name: name, + label: dataset.name, + pluralLabel: `${dataset.name}s`, + physicalTableName: physicalName(table), + entityType: "BASIC", + historizeGolden: false, + historizeMaster: false, + attributes: fields.map((f) => fieldToAttribute(f, warnings)), + }; + if (dataset.description) entity.description = dataset.description; + + if (dataset.primary_key?.length) { + if (dataset.primary_key.length > 1) { + warnings.push( + `[warn] dataset "${dataset.name}": composite primary key ` + + `[${dataset.primary_key.join(", ")}] flattened to first column ` + + `(Semarchy uses a single primary-key attribute)`, + ); + } + const pk = logicalName(dataset.primary_key[0]!, warnings, "attribute"); + entity.primaryKey = attributeFqn(pkg, name, pk); + } + + return entity; +} + +function uniqueKeysOf( + dataset: OSIDataset, + pkg: string, + warnings: string[], +): SemarchyUniqueKey[] { + const entity = logicalName(dataset.name, warnings, "entity"); + return (dataset.unique_keys ?? []).map((cols, i) => ({ + _type: "UniqueKey", + _package: entityPackage(pkg, entity), + _name: logicalName(`${dataset.name}_uk${i + 1}`, warnings, "unique key"), + label: `${dataset.name} unique key ${i + 1}`, + entity: entityFqn(pkg, entity), + keyAttributes: cols.map((c) => ({ + attribute: attributeFqn(pkg, entity, logicalName(c, warnings, "attribute")), + })), + validationLabel: `${dataset.name} unique key ${i + 1} must be unique`, + validationScope: "NONE", + })); +} + +function relationshipToReference( + rel: OSIRelationship, + pkg: string, + warnings: string[], +): SemarchyReference { + if (rel.from_columns.length > 1 || rel.to_columns.length > 1) { + warnings.push( + `[warn] relationship "${rel.name}": composite foreign key flattened to ` + + `the first column (Semarchy references model a single foreign key)`, + ); + } + const fromCol = rel.from_columns[0] ?? "REF"; + const from = logicalName(rel.from, warnings, "entity"); + const to = logicalName(rel.to, warnings, "entity"); + const fkName = logicalName(fromCol, warnings, "attribute"); + return { + _type: "Reference", + _package: `${pkg}.references`, + _name: logicalName(rel.name, warnings, "reference"), + label: rel.name, + physicalName: physicalName(rel.name), + fromEntity: entityFqn(pkg, from), + toEntity: entityFqn(pkg, to), + fromRoleLabel: rel.from, + fromRoleName: logicalName(rel.from, warnings, "role"), + fromRolePluralLabel: `${rel.from}s`, + toRoleLabel: rel.to, + toRoleName: to, + toRolePhysicalName: physicalName(fromCol), + deletePropagation: "RESTRICT", + validationScope: "NONE", + foreignAttribute: { + _type: "ForeignAttribute", + _name: fkName, + label: rel.to, + physicalName: physicalName(fromCol), + entity: entityFqn(pkg, from), + }, + }; +} + +/** + * Build a SemQL enricher for a dataset from its computed fields — those that + * carry a SEMARCHY dialect expression. Returns undefined when there are none. + */ +function enricherFor( + dataset: OSIDataset, + pkg: string, + warnings: string[], +): SemarchyEnricher | undefined { + const expressions = (dataset.fields ?? []) + .map((f) => ({ f, semql: dialectExpression(f, "SEMARCHY") })) + .filter((x): x is { f: OSIField; semql: string } => x.semql !== undefined) + .map(({ f, semql }) => ({ + attributeName: logicalName(f.name, warnings, "attribute"), + expression: semql, + })); + if (expressions.length === 0) return undefined; + + const entity = logicalName(dataset.name, warnings, "entity"); + return { + _type: "SemQLEnricher", + _package: entityPackage(pkg, entity), + _name: `${entity}Enricher`, + label: `${dataset.name} enricher`, + entity: entityFqn(pkg, entity), + enricherExecutionScope: "PRE_CONSO", + semQlEnricherExpressions: expressions, + }; +} + +/** Convert a single OSI semantic model to a grouped Semarchy model. */ +export function osiToSemarchy( + osi: OSISemanticModel, + warnings: string[] = [], +): ExportResult { + const pkg = logicalName(osi.name, warnings, "package"); + + const entities = (osi.datasets ?? []).map((d) => + datasetToEntity(d, pkg, warnings), + ); + const uniqueKeys = (osi.datasets ?? []).flatMap((d) => + uniqueKeysOf(d, pkg, warnings), + ); + const references = (osi.relationships ?? []).map((r) => + relationshipToReference(r, pkg, warnings), + ); + const enrichers = (osi.datasets ?? []) + .map((d) => enricherFor(d, pkg, warnings)) + .filter((e): e is SemarchyEnricher => e !== undefined); + + for (const metric of osi.metrics ?? []) { + warnings.push( + `[drop] metric "${metric.name}": Semarchy xDM has no measure concept`, + ); + } + + const modelObject: SemarchyModelObject = { + _type: "Model", + _package: pkg, + _name: pkg, + label: osi.name, + ...(osi.description ? { description: osi.description } : {}), + }; + + return { + model: { + name: osi.name, + pkg, + modelObject, + entities, + references, + uniqueKeys, + enrichers, + }, + warnings, + }; +} + +/** Convenience wrapper over a full OSI document (converts the first model). */ +export function osiDocumentToSemarchy(doc: OSIDocument): ExportResult { + if (!doc.semantic_model?.length) { + throw new Error("OSI document has no semantic_model entries"); + } + return osiToSemarchy(doc.semantic_model[0]!); +} diff --git a/converters/semarchy/src/osi-types.ts b/converters/semarchy/src/osi-types.ts new file mode 100644 index 00000000..4e547904 --- /dev/null +++ b/converters/semarchy/src/osi-types.ts @@ -0,0 +1,123 @@ +/** + * TypeScript types for the OSI core model. + * + * Mirrors `python/src/osi/models.py` (the canonical Pydantic types) and + * `core-spec/osi-schema.json`. Keep in sync when the spec changes. + */ + +/** Supported SQL and expression language dialects (core-spec Dialect enum). */ +export type OSIDialect = + | "ANSI_SQL" + | "SNOWFLAKE" + | "MDX" + | "TABLEAU" + | "DATABRICKS" + | "MAQL" + | "SEMARCHY"; + +/** + * Vendor name for custom extensions. The spec accepts any string, but these are + * the well-known values. Use "SEMARCHY" for this converter and "COMMON" for + * cross-vendor metadata (e.g. ai_context that a vendor cannot represent). + */ +export type OSIVendor = + | "COMMON" + | "SNOWFLAKE" + | "SALESFORCE" + | "DBT" + | "DATABRICKS" + | "GOODDATA" + | "SEMARCHY" + | (string & {}); + +export interface OSIAIContextObject { + instructions?: string; + synonyms?: string[]; + examples?: string[]; +} + +/** ai_context is either a bare string or a structured object. */ +export type OSIAIContext = string | OSIAIContextObject; + +/** Vendor-specific metadata carried as a serialized JSON string in `data`. */ +export interface OSICustomExtension { + vendor_name: OSIVendor; + /** JSON string containing vendor-specific data. */ + data: string; +} + +export interface OSIDialectExpression { + dialect: OSIDialect; + expression: string; +} + +export interface OSIExpression { + dialects: OSIDialectExpression[]; +} + +export interface OSIDimension { + is_time?: boolean; +} + +export interface OSIField { + name: string; + expression: OSIExpression; + dimension?: OSIDimension; + label?: string; + description?: string; + ai_context?: OSIAIContext; + custom_extensions?: OSICustomExtension[]; +} + +export interface OSIDataset { + name: string; + /** `database.schema.table` style source reference. */ + source: string; + primary_key?: string[]; + unique_keys?: string[][]; + description?: string; + ai_context?: OSIAIContext; + fields?: OSIField[]; + custom_extensions?: OSICustomExtension[]; +} + +export interface OSIRelationship { + name: string; + /** Many-side dataset (serialized as `from` in YAML/JSON). */ + from: string; + /** One-side dataset. */ + to: string; + /** Positionally corresponds to `to_columns`; preserve order. */ + from_columns: string[]; + to_columns: string[]; + ai_context?: OSIAIContext; + custom_extensions?: OSICustomExtension[]; +} + +export interface OSIMetric { + name: string; + expression: OSIExpression; + description?: string; + ai_context?: OSIAIContext; + custom_extensions?: OSICustomExtension[]; +} + +export interface OSISemanticModel { + name: string; + description?: string; + ai_context?: OSIAIContext; + datasets: OSIDataset[]; + relationships?: OSIRelationship[]; + metrics?: OSIMetric[]; + custom_extensions?: OSICustomExtension[]; +} + +export interface OSIDocument { + version: string; + dialects?: OSIDialect[]; + vendors?: OSIVendor[]; + semantic_model: OSISemanticModel[]; +} + +/** The OSI spec version this converter targets. */ +export const OSI_VERSION = "0.2.0.dev0"; diff --git a/converters/semarchy/src/semarchy-to-osi.ts b/converters/semarchy/src/semarchy-to-osi.ts new file mode 100644 index 00000000..6cd1dd1c --- /dev/null +++ b/converters/semarchy/src/semarchy-to-osi.ts @@ -0,0 +1,150 @@ +/** + * Import: Semarchy -> OSI. + * + * Scope (per project decision): map Entity -> dataset, EntityAttribute -> field, + * UniqueKey -> unique_keys, Reference -> relationship. Everything else is dropped + * with a warning (lossy roundtrip is accepted). No metrics are produced: Semarchy + * xDM has no aggregate-measure concept. + * + * Cross-references use attribute `_name` (Semarchy references attributes by + * `_name`), so OSI field names are attribute `_name`s and the ANSI_SQL + * expression carries the physical column name. + */ +import { OSI_VERSION } from "./osi-types.js"; +import type { + OSIDataset, + OSIDocument, + OSIField, + OSIRelationship, + OSISemanticModel, +} from "./osi-types.js"; +import { isTimeDataType } from "./semarchy-types.js"; +import type { + SemarchyEntity, + SemarchyModel, + SemarchyReference, +} from "./semarchy-types.js"; + +/** Last dot-separated segment of a Semarchy fully-qualified name. */ +function localName(fqn: string): string { + const parts = fqn.split("."); + return parts[parts.length - 1] ?? fqn; +} + +export interface ImportResult { + document: OSIDocument; + warnings: string[]; +} + +function attributeToField( + attr: SemarchyEntity["attributes"][number], +): OSIField { + const column = attr.physicalName || attr._name; + const field: OSIField = { + name: attr._name, + expression: { dialects: [{ dialect: "ANSI_SQL", expression: column }] }, + }; + if (attr.label) field.label = attr.label; + const description = attr.description ?? attr.documentation; + if (description) field.description = description; + if (isTimeDataType(String(attr.dataType))) { + field.dimension = { is_time: true }; + } + return field; +} + +function entityToDataset( + entity: SemarchyEntity, + model: SemarchyModel, +): OSIDataset { + const dataset: OSIDataset = { + name: entity._name, + // Qualify the physical table with the model's root package. + source: `${model.pkg}.${entity.physicalTableName}`, + fields: entity.attributes.map((a) => attributeToField(a)), + }; + + const description = entity.description ?? entity.documentation; + if (description) dataset.description = description; + // primaryKey is a FQN (`Pkg.entities.X.X.attr`); take the attribute name. + if (entity.primaryKey) dataset.primary_key = [localName(entity.primaryKey)]; + + const uniqueKeys = model.uniqueKeys + .filter((uk) => localName(uk.entity) === entity._name) + .map((uk) => uk.keyAttributes.map((k) => localName(k.attribute))); + if (uniqueKeys.length) dataset.unique_keys = uniqueKeys; + + return dataset; +} + +function referenceToRelationship( + ref: SemarchyReference, + entitiesByName: Map, + warnings: string[], +): OSIRelationship | undefined { + // fromEntity/toEntity are FQNs; resolve by local (last-segment) name. + const toEntity = entitiesByName.get(localName(ref.toEntity)); + const toColumn = toEntity?.primaryKey && localName(toEntity.primaryKey); + if (!toColumn) { + warnings.push( + `[drop] reference "${ref._name}": cannot resolve primary key of ` + + `to-entity "${ref.toEntity}"; relationship skipped`, + ); + return undefined; + } + // Semarchy expresses the FK implicitly; the foreign attribute on the many side + // is the best available "from" column, else the role name. + const fromColumn = ref.foreignAttribute?._name ?? ref.toRoleName; + if (!fromColumn) { + warnings.push( + `[drop] reference "${ref._name}": cannot resolve foreign key column; ` + + `relationship skipped`, + ); + return undefined; + } + // OSI relationships carry no description; Semarchy reference metadata beyond + // the join columns is dropped (accepted, per the minimal-scope decision). + return { + name: ref._name, + from: localName(ref.fromEntity), + to: localName(ref.toEntity), + from_columns: [fromColumn], + to_columns: [toColumn], + }; +} + +/** Convert a grouped Semarchy model into an OSI document. */ +export function semarchyToOsi( + model: SemarchyModel, + warnings: string[] = [], +): ImportResult { + const entitiesByName = new Map(model.entities.map((e) => [e._name, e])); + + const datasets = model.entities.map((e) => entityToDataset(e, model)); + + // SemQL enrichers are a data-preparation rule: in the MDM the computed value + // is already materialized in the column, so a consumer of the model sees a + // plain column. We therefore do NOT turn enrichers into field expressions on + // import; they are dropped with a warning. + for (const enricher of model.enrichers) { + warnings.push( + `[drop] enricher "${enricher._name}": the computed value is materialized ` + + `in the column; the field stays a plain column reference`, + ); + } + + const relationships = model.references + .map((r) => referenceToRelationship(r, entitiesByName, warnings)) + .filter((r): r is OSIRelationship => r !== undefined); + + const semanticModel: OSISemanticModel = { + name: model.name, + datasets, + }; + if (relationships.length) semanticModel.relationships = relationships; + + return { + document: { version: OSI_VERSION, semantic_model: [semanticModel] }, + warnings, + }; +} diff --git a/converters/semarchy/src/semarchy-types.ts b/converters/semarchy/src/semarchy-types.ts new file mode 100644 index 00000000..55a08b80 --- /dev/null +++ b/converters/semarchy/src/semarchy-types.ts @@ -0,0 +1,172 @@ +/** + * TypeScript types for the subset of the Semarchy xDM model this converter maps. + * + * Derived from the Semarchy model JSON Schemas (Entity.json, Reference.json, + * UniqueKey.json). Only the fields the converter reads or writes are typed here; + * `additionalProperties` on the real schemas allows more. Each object is a + * separately-serialized, `_type`-discriminated YAML document; a "model" is a + * directory of such documents (see `io.ts`). + */ + +/** Built-in Semarchy attribute data types. */ +export type SemarchyBuiltInType = + | "Binary" + | "Boolean" + | "ByteInteger" + | "Date" + | "Decimal" + | "Integer" + | "LongInteger" + | "LongText" + | "ShortInteger" + | "String" + | "Timestamp" + | "UUID"; + +/** + * Data types Semarchy treats as time/temporal (drive OSI `dimension.is_time`). + * Real models use either PascalCase (`Date`) or SCREAMING_SNAKE (`DATE`), so + * detection is case-insensitive. + */ +const TIME_DATA_TYPES: ReadonlySet = new Set(["DATE", "TIMESTAMP"]); + +/** True if a (possibly qualified) Semarchy data type is temporal. */ +export function isTimeDataType(dataType: string | undefined): boolean { + return dataType !== undefined && TIME_DATA_TYPES.has(dataType.toUpperCase()); +} + +export interface SemarchyEntityAttribute { + _type: "EntityAttribute"; + _name: string; + label: string; + /** UPPER_SNAKE physical column name. */ + physicalName: string; + dataType: SemarchyBuiltInType | string; + description?: string; + documentation?: string; + length?: number; + precision?: number; + scale?: number; + mandatory?: boolean; + [key: string]: unknown; +} + +export interface SemarchyEntity { + _type: "Entity"; + _package: string; + _name: string; + label: string; + pluralLabel: string; + physicalTableName: string; + entityType: "BASIC" | "FUZZY_MATCHED" | "ID_MATCHED"; + historizeGolden: boolean; + historizeMaster: boolean; + attributes: SemarchyEntityAttribute[]; + description?: string; + documentation?: string; + /** + * Fully-qualified reference to the primary-key attribute, e.g. + * `Pkg.entities.Item.Item.UPC`; the attribute name is the last segment. + */ + primaryKey?: string; + [key: string]: unknown; +} + +/** Root model object; carries the human model name. */ +export interface SemarchyModelObject { + _type: "Model"; + _package: string; + _name: string; + label?: string; + description?: string; + [key: string]: unknown; +} + +export interface SemarchyKeyAttribute { + attribute: string; +} + +export interface SemarchyUniqueKey { + _type: "UniqueKey"; + _package: string; + _name: string; + label: string; + /** `_name` of the owning entity. */ + entity: string; + keyAttributes: SemarchyKeyAttribute[]; + validationLabel: string; + validationScope: "NONE" | "POST_CONSO" | "PRE_CONSO" | "PRE_POST"; + description?: string; + [key: string]: unknown; +} + +export interface SemarchyReference { + _type: "Reference"; + _package: string; + _name: string; + label: string; + physicalName: string; + /** Fully-qualified many-side entity reference (`Pkg.entities.X.X`). */ + fromEntity: string; + /** Fully-qualified one-side entity reference. */ + toEntity: string; + /** Foreign attribute on the many side; its `_name` is the FK column. */ + foreignAttribute?: { _name?: string; physicalName?: string; [k: string]: unknown }; + fromRoleLabel: string; + fromRoleName: string; + fromRolePluralLabel: string; + toRoleLabel: string; + toRoleName: string; + toRolePhysicalName: string; + deletePropagation: "CASCADE" | "NULLIFY" | "RESTRICT"; + validationScope: "NONE" | "POST_CONSO" | "PRE_CONSO" | "PRE_POST"; + description?: string; + [key: string]: unknown; +} + +export interface SemarchyEnricherExpression { + /** Name of the entity attribute that takes the value of `expression`. */ + attributeName: string; + /** SemQL expression computing the attribute value. */ + expression: string; +} + +/** A SemQL enricher: computes attribute values from SemQL expressions. */ +export interface SemarchyEnricher { + _type: "SemQLEnricher"; + _package: string; + _name: string; + label: string; + /** Fully-qualified reference to the enriched entity. */ + entity: string; + enricherExecutionScope: "NONE" | "POST_CONSO" | "PRE_CONSO" | "PRE_POST"; + semQlEnricherExpressions: SemarchyEnricherExpression[]; + /** Optional SemQL filter restricting enriched records (no OSI equivalent). */ + condition?: string; + description?: string; + [key: string]: unknown; +} + +/** Any typed Semarchy object, discriminated by `_type`. */ +export type SemarchyObject = + | SemarchyEntity + | SemarchyReference + | SemarchyUniqueKey + | { _type: string; _name?: string; [key: string]: unknown }; + +/** + * A Semarchy model as consumed/produced by this converter: the mapped objects + * grouped by type, plus the model name/package (from the `Model` object). + */ +export interface SemarchyModel { + /** Human/logical model name (from the `Model` object), used as the OSI name. */ + name: string; + /** Root package (from the `Model` object), used to qualify sources. */ + pkg: string; + /** Root `Model` object (present on export; optional on import). */ + modelObject?: SemarchyModelObject; + entities: SemarchyEntity[]; + references: SemarchyReference[]; + uniqueKeys: SemarchyUniqueKey[]; + enrichers: SemarchyEnricher[]; +} diff --git a/converters/semarchy/src/validation.ts b/converters/semarchy/src/validation.ts new file mode 100644 index 00000000..62fa4df7 --- /dev/null +++ b/converters/semarchy/src/validation.ts @@ -0,0 +1,82 @@ +/** + * Schema validation helpers. + * + * - OSI output (import) is validated against `schemas/osi-schema.json` + * (JSON Schema draft 2020-12 -> Ajv2020). + * - Semarchy output (export) is validated per object against the matching + * `schemas/.json` (draft-07 -> default Ajv). + * + * Best-effort: if a schema file is missing, validation is skipped with a + * warning rather than failing. + */ +import { existsSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join, resolve } from "node:path"; +import Ajv from "ajv"; +import Ajv2020 from "ajv/dist/2020.js"; +import type { SemarchyModel, SemarchyObject } from "./semarchy-types.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SCHEMAS_DIR = resolve(__dirname, "..", "schemas"); + +export const OSI_SCHEMA_PATH = join(SCHEMAS_DIR, "osi-schema.json"); + +export interface ValidationOutcome { + valid: boolean; + errors: string[]; + skipped: boolean; +} + +function loadSchema(path: string): object | undefined { + return existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : undefined; +} + +/** Validate the OSI document against the 2020-12 OSI schema. */ +export function validateOsi(document: unknown): ValidationOutcome { + const schema = loadSchema(OSI_SCHEMA_PATH); + if (!schema) { + return { valid: true, skipped: true, errors: [`schema not found: ${OSI_SCHEMA_PATH}`] }; + } + const ajv = new Ajv2020({ allErrors: true, strict: false }); + const validate = ajv.compile(schema); + const valid = validate(document) as boolean; + const errors = (validate.errors ?? []).map( + (e) => `${e.instancePath || "/"} ${e.message ?? ""}`.trim(), + ); + return { valid, skipped: false, errors }; +} + +/** Validate every mapped Semarchy object against its per-type draft-07 schema. */ +export function validateSemarchy(model: SemarchyModel): ValidationOutcome { + const ajv = new Ajv({ allErrors: true, strict: false }); + const errors: string[] = []; + let checked = 0; + + const objects: SemarchyObject[] = [ + ...model.entities, + ...model.uniqueKeys, + ...model.references, + ...model.enrichers, + ]; + for (const obj of objects) { + const schema = loadSchema(join(SCHEMAS_DIR, `${obj._type}.json`)); + if (!schema) continue; + checked++; + const validate = ajv.compile(schema); + // Store the result: Ajv's ValidateFunction is a type guard, and letting it + // narrow `obj` in the negative branch would collapse it to `never`. + const ok: boolean = validate(obj); + if (!ok) { + for (const e of validate.errors ?? []) { + errors.push( + `${obj._type} ${obj._name ?? ""}: ${e.instancePath || "/"} ${e.message ?? ""}`.trim(), + ); + } + } + } + + if (checked === 0) { + return { valid: true, skipped: true, errors: ["no Semarchy schemas found"] }; + } + return { valid: errors.length === 0, skipped: false, errors }; +} diff --git a/converters/semarchy/tests/dialect.test.ts b/converters/semarchy/tests/dialect.test.ts new file mode 100644 index 00000000..b5e63567 --- /dev/null +++ b/converters/semarchy/tests/dialect.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { selectExpression } from "../src/dialect.js"; +import type { OSIExpression } from "../src/osi-types.js"; + +describe("selectExpression (dialect fallback chain)", () => { + it("prefers the SEMARCHY dialect when present", () => { + const expr: OSIExpression = { + dialects: [ + { dialect: "ANSI_SQL", expression: "a + b" }, + { dialect: "SEMARCHY", expression: "a ++ b" }, + ], + }; + const warnings: string[] = []; + const sel = selectExpression(expr, "field x", warnings); + expect(sel).toEqual({ dialect: "SEMARCHY", expression: "a ++ b" }); + expect(warnings).toHaveLength(0); + }); + + it("falls back to ANSI_SQL and warns when SEMARCHY is missing", () => { + const expr: OSIExpression = { + dialects: [{ dialect: "ANSI_SQL", expression: "a + b" }], + }; + const warnings: string[] = []; + const sel = selectExpression(expr, "field x", warnings); + expect(sel).toEqual({ dialect: "ANSI_SQL", expression: "a + b" }); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain("falling back to ANSI_SQL"); + }); + + it("returns undefined and warns when neither SEMARCHY nor ANSI_SQL is present", () => { + const expr: OSIExpression = { + dialects: [{ dialect: "SNOWFLAKE", expression: "a + b" }], + }; + const warnings: string[] = []; + const sel = selectExpression(expr, "metric m", warnings); + expect(sel).toBeUndefined(); + expect(warnings[0]).toContain("neither SEMARCHY nor ANSI_SQL"); + }); + + it("warns when there is no expression at all", () => { + const warnings: string[] = []; + expect(selectExpression(undefined, "field y", warnings)).toBeUndefined(); + expect(selectExpression({ dialects: [] }, "field z", warnings)).toBeUndefined(); + expect(warnings).toHaveLength(2); + }); +}); diff --git a/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/Customer.Entity.seml b/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/Customer.Entity.seml new file mode 100644 index 00000000..66322b4c --- /dev/null +++ b/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/Customer.Entity.seml @@ -0,0 +1,28 @@ +_package: retail.entities.Customer +_type: Entity +_name: Customer +label: Customer +pluralLabel: Customers +physicalTableName: CUSTOMER +entityType: BASIC +historizeGolden: false +historizeMaster: false +description: A retail customer +primaryKey: retail.entities.Customer.Customer.customerId +attributes: + - _type: EntityAttribute + _name: customerId + label: Customer ID + physicalName: CUSTOMER_ID + dataType: INTEGER + - _type: EntityAttribute + _name: fullName + label: Full Name + physicalName: FULL_NAME + dataType: STRING + description: Customer full name + - _type: EntityAttribute + _name: createdAt + label: Created At + physicalName: CREATED_AT + dataType: TIMESTAMP diff --git a/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/enrichers/CustomerEnricher.SemQLEnricher.seml b/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/enrichers/CustomerEnricher.SemQLEnricher.seml new file mode 100644 index 00000000..6a2558d0 --- /dev/null +++ b/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/enrichers/CustomerEnricher.SemQLEnricher.seml @@ -0,0 +1,10 @@ +_package: retail.entities.Customer +_type: SemQLEnricher +_name: CustomerEnricher +label: Customer enricher +entity: retail.entities.Customer.Customer +enricherExecutionScope: PRE_CONSO +condition: fullName IS NOT NULL +semQlEnricherExpressions: + - attributeName: fullName + expression: UPPER(fullName) diff --git a/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/forms/CustomerForm.Form.seml b/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/forms/CustomerForm.Form.seml new file mode 100644 index 00000000..97a614ff --- /dev/null +++ b/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/forms/CustomerForm.Form.seml @@ -0,0 +1,5 @@ +# A non-semantic (UI) object nested under an entity: dropped with a warning. +_type: Form +_package: retail.entities.Customer +_name: CustomerForm +label: Customer Form diff --git a/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/unique_keys/CustomerNaturalKey.UniqueKey.seml b/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/unique_keys/CustomerNaturalKey.UniqueKey.seml new file mode 100644 index 00000000..4146ccab --- /dev/null +++ b/converters/semarchy/tests/fixtures/semarchy-model/entities/Customer/unique_keys/CustomerNaturalKey.UniqueKey.seml @@ -0,0 +1,9 @@ +_package: retail.entities.Customer +_type: UniqueKey +_name: CustomerNaturalKey +label: Customer natural key +entity: retail.entities.Customer.Customer +validationLabel: Customer natural key must be unique +validationScope: NONE +keyAttributes: + - attribute: retail.entities.Customer.Customer.fullName diff --git a/converters/semarchy/tests/fixtures/semarchy-model/entities/SalesOrder/SalesOrder.Entity.seml b/converters/semarchy/tests/fixtures/semarchy-model/entities/SalesOrder/SalesOrder.Entity.seml new file mode 100644 index 00000000..31459ebd --- /dev/null +++ b/converters/semarchy/tests/fixtures/semarchy-model/entities/SalesOrder/SalesOrder.Entity.seml @@ -0,0 +1,26 @@ +_package: retail.entities.SalesOrder +_type: Entity +_name: SalesOrder +label: Sales Order +pluralLabel: Sales Orders +physicalTableName: SALES_ORDER +entityType: BASIC +historizeGolden: false +historizeMaster: false +primaryKey: retail.entities.SalesOrder.SalesOrder.orderId +attributes: + - _type: EntityAttribute + _name: orderId + label: Order ID + physicalName: ORDER_ID + dataType: INTEGER + - _type: EntityAttribute + _name: customerRef + label: Customer + physicalName: CUSTOMER_ID + dataType: INTEGER + - _type: EntityAttribute + _name: amount + label: Amount + physicalName: AMOUNT + dataType: DECIMAL diff --git a/converters/semarchy/tests/fixtures/semarchy-model/references/OrderToCustomer.Reference.seml b/converters/semarchy/tests/fixtures/semarchy-model/references/OrderToCustomer.Reference.seml new file mode 100644 index 00000000..404d1ccd --- /dev/null +++ b/converters/semarchy/tests/fixtures/semarchy-model/references/OrderToCustomer.Reference.seml @@ -0,0 +1,22 @@ +_package: retail.references +_type: Reference +_name: OrderToCustomer +label: Order to Customer +physicalName: ORDER_TO_CUSTOMER +deletePropagation: RESTRICT +oneToMany: true +fromEntity: retail.entities.SalesOrder.SalesOrder +toEntity: retail.entities.Customer.Customer +fromRoleLabel: Orders +fromRoleName: Orders +fromRolePluralLabel: Orders +toRoleLabel: Customer +toRoleName: Customer +toRolePhysicalName: CUSTOMER_ID +validationScope: NONE +foreignAttribute: + _type: ForeignAttribute + _name: customerRef + label: Customer + physicalName: CUSTOMER_ID + entity: retail.entities.SalesOrder.SalesOrder diff --git a/converters/semarchy/tests/fixtures/semarchy-model/retail.Model.seml b/converters/semarchy/tests/fixtures/semarchy-model/retail.Model.seml new file mode 100644 index 00000000..0a499cf0 --- /dev/null +++ b/converters/semarchy/tests/fixtures/semarchy-model/retail.Model.seml @@ -0,0 +1,7 @@ +_type: Model +_package: retail +_name: retail +label: Retail +description: Retail demo model +modelConfiguration: + type: POSTGRESQL diff --git a/converters/semarchy/tests/roundtrip.test.ts b/converters/semarchy/tests/roundtrip.test.ts new file mode 100644 index 00000000..69b4cbca --- /dev/null +++ b/converters/semarchy/tests/roundtrip.test.ts @@ -0,0 +1,185 @@ +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; +import { dirname, join, resolve } from "node:path"; +import { afterAll, describe, expect, it } from "vitest"; +import { parse as parseYaml } from "yaml"; +import { osiDocumentToSemarchy, osiToSemarchy } from "../src/osi-to-semarchy.js"; +import { semarchyToOsi } from "../src/semarchy-to-osi.js"; +import { readSemarchyDir, writeSemarchyDir } from "../src/io.js"; +import { validateOsi, validateSemarchy } from "../src/validation.js"; +import type { OSIDocument } from "../src/osi-types.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const TPCDS = resolve( + __dirname, + "..", + "..", + "..", + "examples", + "tpcds_semantic_model.yaml", +); +const FIXTURE_DIR = resolve(__dirname, "fixtures", "semarchy-model"); + +function loadTpcds(): OSIDocument { + return parseYaml(readFileSync(TPCDS, "utf8")) as OSIDocument; +} + +describe("export OSI -> Semarchy", () => { + it("maps the TPC-DS baseline and produces schema-valid Semarchy objects", () => { + const doc = loadTpcds(); + const { model, warnings } = osiDocumentToSemarchy(doc); + + expect(model.entities).toHaveLength(doc.semantic_model[0]!.datasets.length); + + const outcome = validateSemarchy(model); + expect(outcome.skipped).toBe(false); + expect(outcome.errors).toEqual([]); + expect(outcome.valid).toBe(true); + + // TPC-DS has metrics; Semarchy has no measure concept, so they are dropped. + expect(warnings.some((w) => w.startsWith("[drop] metric"))).toBe(true); + }); +}); + +describe("import Semarchy -> OSI", () => { + it("maps the fixture directory to a schema-valid OSI document", () => { + const warnings: string[] = []; + const model = readSemarchyDir(FIXTURE_DIR, warnings); + const { document } = semarchyToOsi(model, warnings); + + expect(document.semantic_model[0]!.name).toBe("retail"); + expect(document.semantic_model[0]!.datasets.map((d) => d.name).sort()).toEqual([ + "Customer", + "SalesOrder", + ]); + // Non-semantic Form object is dropped with a warning. + expect(warnings.some((w) => w.includes('unsupported Semarchy type "Form"'))).toBe(true); + + const outcome = validateOsi(document); + expect(outcome.skipped).toBe(false); + expect(outcome.errors).toEqual([]); + expect(outcome.valid).toBe(true); + }); + + it("maps a temporal attribute to a time dimension", () => { + const model = readSemarchyDir(FIXTURE_DIR); + const { document } = semarchyToOsi(model); + const customer = document.semantic_model[0]!.datasets.find( + (d) => d.name === "Customer", + )!; + const createdAt = customer.fields!.find((f) => f.name === "createdAt")!; + expect(createdAt.dimension?.is_time).toBe(true); + }); +}); + +describe("SemQL enricher handling", () => { + it("import drops enrichers: the enriched field stays a plain column", () => { + const warnings: string[] = []; + const model = readSemarchyDir(FIXTURE_DIR, warnings); + const { document } = semarchyToOsi(model, warnings); + + const fullName = document.semantic_model[0]!.datasets + .find((d) => d.name === "Customer")! + .fields!.find((f) => f.name === "fullName")!; + // Only the physical column reference — no SEMARCHY computation is attached, + // because in the MDM the value is already materialized in the column. + expect(fullName.expression.dialects).toEqual([ + { dialect: "ANSI_SQL", expression: "FULL_NAME" }, + ]); + expect(warnings.some((w) => w.includes('enricher "CustomerEnricher"'))).toBe(true); + }); + + it("export generates a SemQLEnricher from a SEMARCHY-dialect field", () => { + // A hand-authored OSI model carrying a SemQL computation. + const { model } = osiToSemarchy({ + name: "retail", + datasets: [ + { + name: "Customer", + source: "retail.CUSTOMER", + fields: [ + { + name: "fullName", + expression: { + dialects: [ + { dialect: "ANSI_SQL", expression: "FULL_NAME" }, + { dialect: "SEMARCHY", expression: "UPPER(fullName)" }, + ], + }, + }, + ], + }, + ], + }); + + expect(model.enrichers).toHaveLength(1); + expect(model.enrichers[0]!.entity).toBe("retail.entities.Customer.Customer"); + expect(model.enrichers[0]!.semQlEnricherExpressions).toEqual([ + { attributeName: "fullName", expression: "UPPER(fullName)" }, + ]); + // The attribute's physical column comes from ANSI_SQL, never the SemQL. + const customer = model.entities.find((e) => e._name === "Customer")!; + expect(customer.attributes[0]!.physicalName).toBe("FULL_NAME"); + expect(validateSemarchy(model).valid).toBe(true); + }); +}); + +describe("roundtrip Semarchy -> OSI -> Semarchy", () => { + it("preserves the core ER structure (entities, references, unique keys)", () => { + const original = readSemarchyDir(FIXTURE_DIR); + const { document } = semarchyToOsi(original); + const { model: roundtripped } = osiToSemarchy(document.semantic_model[0]!); + + expect(roundtripped.entities.map((e) => e._name).sort()).toEqual( + original.entities.map((e) => e._name).sort(), + ); + expect(roundtripped.references.map((r) => r._name)).toEqual( + original.references.map((r) => r._name), + ); + const localName = (fqn: string) => fqn.split(".").pop(); + expect(roundtripped.uniqueKeys.map((u) => localName(u.entity))).toEqual( + original.uniqueKeys.map((u) => localName(u.entity)), + ); + + // Attribute sets survive per entity. + for (const entity of original.entities) { + const rt = roundtripped.entities.find((e) => e._name === entity._name)!; + expect(rt.attributes.map((a) => a._name).sort()).toEqual( + entity.attributes.map((a) => a._name).sort(), + ); + } + + // The roundtripped Semarchy model still validates. + expect(validateSemarchy(roundtripped).valid).toBe(true); + }); +}); + +describe("export writes a re-importable Semarchy directory tree", () => { + const tmp = mkdtempSync(join(tmpdir(), "osi-semarchy-")); + afterAll(() => rmSync(tmp, { recursive: true, force: true })); + + it("emits a Model object, nested entities/, and references/ that re-import", () => { + const original = readSemarchyDir(FIXTURE_DIR); + const { document } = semarchyToOsi(original); + const { model } = osiToSemarchy(document.semantic_model[0]!); + + writeSemarchyDir(tmp, model); + const reread = readSemarchyDir(tmp); + + // The Model object round-trips (its _name is the OSI model name). + expect(reread.name).toBe(document.semantic_model[0]!.name); + expect(reread.entities.map((e) => e._name).sort()).toEqual( + original.entities.map((e) => e._name).sort(), + ); + expect(reread.references.map((r) => r._name)).toEqual( + original.references.map((r) => r._name), + ); + // FQN references survive: the reference resolves back to a relationship. + const { document: reDoc } = semarchyToOsi(reread); + expect(reDoc.semantic_model[0]!.relationships?.map((r) => `${r.from}->${r.to}`)).toEqual( + document.semantic_model[0]!.relationships?.map((r) => `${r.from}->${r.to}`), + ); + expect(validateOsi(reDoc).valid).toBe(true); + }); +}); diff --git a/converters/semarchy/tsconfig.json b/converters/semarchy/tsconfig.json new file mode 100644 index 00000000..4dcb82b9 --- /dev/null +++ b/converters/semarchy/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src"], + "exclude": ["dist", "node_modules", "tests"] +} diff --git a/converters/semarchy/vitest.config.ts b/converters/semarchy/vitest.config.ts new file mode 100644 index 00000000..ba29bd1c --- /dev/null +++ b/converters/semarchy/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts"], + environment: "node", + }, +}); diff --git a/core-spec/osi-schema.json b/core-spec/osi-schema.json index 72cb164d..8a9bd821 100644 --- a/core-spec/osi-schema.json +++ b/core-spec/osi-schema.json @@ -23,7 +23,7 @@ "$defs": { "Dialect": { "type": "string", - "enum": ["ANSI_SQL", "SNOWFLAKE", "MDX", "TABLEAU", "DATABRICKS", "MAQL"], + "enum": ["ANSI_SQL", "SNOWFLAKE", "MDX", "TABLEAU", "DATABRICKS", "MAQL", "SEMARCHY"], "description": "Supported SQL and expression language dialects" }, "Vendor": { diff --git a/core-spec/spec.md b/core-spec/spec.md index ea7e775d..a2d9ce20 100644 --- a/core-spec/spec.md +++ b/core-spec/spec.md @@ -38,6 +38,7 @@ Supported SQL and expression language dialects for metrics and field definitions | `TABLEAU` | Tableau calculations | | `DATABRICKS` | Databricks SQL | | `MAQL` | GoodData MAQL (Metric Analysis and Query Language) | +| `SEMARCHY` | Semarchy semantic model expression language | ## Semantic Model diff --git a/core-spec/spec.yaml b/core-spec/spec.yaml index ed462032..a329296d 100644 --- a/core-spec/spec.yaml +++ b/core-spec/spec.yaml @@ -19,6 +19,7 @@ dialects: - "TABLEAU" # Tableau - "DATABRICKS" # Databricks SQL - "MAQL" # GoodData MAQL (Multi-Dimensional Analytical Query Language) + - "SEMARCHY" # Semarchy semantic model expression language diff --git a/python/src/osi/models.py b/python/src/osi/models.py index 766b6e0f..bde09b45 100644 --- a/python/src/osi/models.py +++ b/python/src/osi/models.py @@ -14,6 +14,7 @@ class OSIDialect(str, Enum): MAQL = "MAQL" TABLEAU = "TABLEAU" DATABRICKS = "DATABRICKS" + SEMARCHY = "SEMARCHY" class OSIVendor(str, Enum): diff --git a/validation/validate.py b/validation/validate.py index a33cf76d..19dc5d18 100644 --- a/validation/validate.py +++ b/validation/validate.py @@ -41,10 +41,11 @@ "MDX": None, # Not supported by sqlglot, skip validation "TABLEAU": None, # Not supported by sqlglot, skip validation "MAQL": None, # Not supported by sqlglot, skip validation + "SEMARCHY": None, # Not supported by sqlglot, skip validation } # Dialects that sqlglot cannot parse -SKIP_SQL_VALIDATION = {"MDX", "TABLEAU", "MAQL"} +SKIP_SQL_VALIDATION = {"MDX", "TABLEAU", "MAQL", "SEMARCHY"} def validate_schema(data: dict, schema: dict) -> list[str]: