Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dev = [
"playwright>=1.50.0",
"fastapi>=0.115.0",
"httpx>=0.28.0",
"jsonschema>=4.23,<5",
"pyarrow>=14.0.0",
"uvicorn>=0.34.0",
# fakesnow pinned out until it supports sqlglot 30.x
Expand Down Expand Up @@ -61,6 +62,9 @@ charts = [
fast = [
"sqlglot[c]>=30.1.0",
]
ossie = [
"jsonschema>=4.23,<5",
]
serve = [
"riffq>=0.1.0",
"pyarrow>=14.0.0",
Expand Down Expand Up @@ -126,7 +130,7 @@ all-databases = [
"sidemantic[postgres,bigquery,snowflake,clickhouse,databricks,spark,adbc]",
]
full = [
"sidemantic[workbench,mcp,apps,charts,lsp,dax,lookml,malloy,metricflow,widget,api]",
"sidemantic[workbench,mcp,apps,charts,lsp,dax,lookml,malloy,metricflow,widget,api,ossie]",
]

[build-system]
Expand Down Expand Up @@ -193,6 +197,7 @@ force-exclude = true
exclude = [
"sidemantic/adapters/malloy_grammar", # ANTLR-generated files
"sidemantic/adapters/holistics_grammar", # ANTLR-generated files
"tests/ossie-fixtures/upstream/validation/validate.py", # Exact pinned Apache validator fixture
]

[tool.ruff.lint]
Expand Down
1 change: 1 addition & 0 deletions sidemantic/interchange/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Typed interchange contracts that are independent from executable semantic models."""
106 changes: 106 additions & 0 deletions sidemantic/interchange/ossie/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Typed Apache Ossie source-contract foundation."""

from sidemantic.interchange.ossie.diagnostics import (
OssieDiagnostic,
OssieDiagnosticSeverity,
OssieSchemaProvenance,
OssieSourceLocation,
diagnostic_sort_key,
sort_diagnostics,
)
from sidemantic.interchange.ossie.documents import (
FrozenJSONObject,
FrozenJSONValue,
JSONScalar,
OssieDocument,
OssieDocumentSource,
OssieLogicalDocument,
OssieOntologyDocument,
UnsupportedOssieDocument,
freeze_json,
thaw_json,
)
from sidemantic.interchange.ossie.lowering import OssieLoweringResult, lower_ossie_document
from sidemantic.interchange.ossie.parser import (
OssieParseOptions,
OssieParseResult,
parse_ossie_document,
)
from sidemantic.interchange.ossie.profiles import (
DBT_1_12_0_1_0_ALIAS,
DBT_1_12_0_1_1,
OSSIE_CORE_0_1_1,
OSSIE_CORE_0_2_0_DEV0,
OSSIE_PROFILES,
OssieConsumerProfile,
OssieImportPolicy,
OssieOptions,
OssiePreservationPolicy,
OssieProfile,
OssieProfileError,
OssieSerialization,
resolve_ossie_profile,
)
from sidemantic.interchange.ossie.semantic_validation import (
SemanticDocumentKind,
SemanticValidationResult,
validate_ossie_semantics,
)
from sidemantic.interchange.ossie.serialization import (
OssieSerializationError,
OssieSerializationResult,
serialize_ossie_document,
)
from sidemantic.interchange.ossie.synthesis import (
OssieSynthesisError,
OssieSynthesisResult,
require_synthesized_document,
synthesize_ossie_document,
)

__all__ = [
"DBT_1_12_0_1_0_ALIAS",
"DBT_1_12_0_1_1",
"OSSIE_CORE_0_1_1",
"OSSIE_CORE_0_2_0_DEV0",
"OSSIE_PROFILES",
"FrozenJSONValue",
"FrozenJSONObject",
"JSONScalar",
"OssieConsumerProfile",
"OssieDiagnostic",
"OssieDiagnosticSeverity",
"OssieDocument",
"OssieDocumentSource",
"OssieImportPolicy",
"OssieLogicalDocument",
"OssieLoweringResult",
"OssieOntologyDocument",
"OssieOptions",
"OssieParseOptions",
"OssieParseResult",
"OssiePreservationPolicy",
"OssieProfile",
"OssieProfileError",
"OssieSchemaProvenance",
"OssieSerialization",
"OssieSerializationError",
"OssieSerializationResult",
"OssieSourceLocation",
"OssieSynthesisError",
"OssieSynthesisResult",
"SemanticDocumentKind",
"SemanticValidationResult",
"UnsupportedOssieDocument",
"diagnostic_sort_key",
"freeze_json",
"lower_ossie_document",
"parse_ossie_document",
"resolve_ossie_profile",
"require_synthesized_document",
"serialize_ossie_document",
"sort_diagnostics",
"synthesize_ossie_document",
"thaw_json",
"validate_ossie_semantics",
]
112 changes: 112 additions & 0 deletions sidemantic/interchange/ossie/diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Structured diagnostics for Apache Ossie parsing, validation, and lowering."""

from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass
from enum import Enum

from sidemantic.interchange.ossie.profiles import OssieProfile


class OssieDiagnosticSeverity(str, Enum):
ERROR = "error"
WARNING = "warning"
INFO = "info"


@dataclass(frozen=True, slots=True)
class OssieSourceLocation:
"""Source identity and optional one-based text coordinates."""

identifier: str
line: int | None = None
column: int | None = None
end_line: int | None = None
end_column: int | None = None

def __post_init__(self) -> None:
if not self.identifier:
raise ValueError("source identifier must not be empty")
for field_name in ("line", "column", "end_line", "end_column"):
value = getattr(self, field_name)
if value is not None and value < 1:
raise ValueError(f"{field_name} must be one-based")
if self.column is not None and self.line is None:
raise ValueError("column requires line")
if self.end_column is not None and self.end_line is None:
raise ValueError("end_column requires end_line")


@dataclass(frozen=True, slots=True)
class OssieSchemaProvenance:
"""Identity of the pinned schema used to produce a diagnostic."""

schema_id: str
version: str | None = None
commit: str | None = None
sha256: str | None = None

def __post_init__(self) -> None:
if not self.schema_id:
raise ValueError("schema_id must not be empty")


@dataclass(frozen=True, slots=True)
class OssieDiagnostic:
"""One stable, source-addressable Ossie diagnostic."""

severity: OssieDiagnosticSeverity
code: str
message: str
json_pointer: str = ""
source: OssieSourceLocation | None = None
scope: str | None = None
profile: OssieProfile | None = None
schema: OssieSchemaProvenance | None = None

def __post_init__(self) -> None:
object.__setattr__(self, "severity", OssieDiagnosticSeverity(self.severity))
if not self.code or any(character.isspace() for character in self.code):
raise ValueError("diagnostic code must be non-empty and contain no whitespace")
if not self.message:
raise ValueError("diagnostic message must not be empty")
if self.json_pointer and not self.json_pointer.startswith("/"):
raise ValueError("json_pointer must be empty or start with '/'")


_SEVERITY_ORDER = {
OssieDiagnosticSeverity.ERROR: 0,
OssieDiagnosticSeverity.WARNING: 1,
OssieDiagnosticSeverity.INFO: 2,
}


def diagnostic_sort_key(diagnostic: OssieDiagnostic) -> tuple[object, ...]:
"""Return a stable ordering by source position, object path, severity, and identity."""

source = diagnostic.source
schema = diagnostic.schema
return (
source.identifier if source else "",
source.line if source and source.line is not None else 0,
source.column if source and source.column is not None else 0,
source.end_line if source and source.end_line is not None else 0,
source.end_column if source and source.end_column is not None else 0,
diagnostic.json_pointer,
_SEVERITY_ORDER[diagnostic.severity],
diagnostic.code,
diagnostic.message,
diagnostic.scope or "",
diagnostic.profile.identifier if diagnostic.profile else "",
schema.schema_id if schema else "",
schema.version or "" if schema else "",
schema.commit or "" if schema else "",
schema.sha256 or "" if schema else "",
)


def sort_diagnostics(diagnostics: Iterable[OssieDiagnostic]) -> tuple[OssieDiagnostic, ...]:
"""Materialize diagnostics in deterministic presentation order."""

return tuple(sorted(diagnostics, key=diagnostic_sort_key))
Loading
Loading