Initial SFERA platform baseline

This commit is contained in:
2026-05-16 19:03:49 +03:00
commit 3b845c8fce
282 changed files with 55045 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# sfera-operations-core
Operations primitives for jobs, observability, reports, marketplace inventory, and licensing.
+12
View File
@@ -0,0 +1,12 @@
[project]
name = "sfera-operations-core"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"pydantic>=2.0",
"sfera-integration-topology",
"sfera-sir",
]
[tool.uv]
package = true
@@ -0,0 +1,308 @@
from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from pydantic import BaseModel, Field, computed_field
from integration_topology import IntegrationKind, build_integration_topology
from sir import EdgeKind, NodeKind, SirSnapshot
class OperationJobStatus(str, Enum):
QUEUED = "QUEUED"
RUNNING = "RUNNING"
SUCCEEDED = "SUCCEEDED"
FAILED = "FAILED"
CANCELED = "CANCELED"
class OperationJob(BaseModel):
job_id: str
kind: str
status: OperationJobStatus = OperationJobStatus.QUEUED
payload: dict = Field(default_factory=dict)
result: dict = Field(default_factory=dict)
error: str | None = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class MetricSample(BaseModel):
metric_id: str
name: str
value: float
unit: str | None = None
observed_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
labels: dict[str, str] = Field(default_factory=dict)
class AiUsageRecord(BaseModel):
usage_id: str
project_id: str
user_id: str
model: str
operation: str
prompt_tokens: int = 0
completion_tokens: int = 0
cost: float = 0.0
currency: str = "USD"
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
attributes: dict = Field(default_factory=dict)
@computed_field
@property
def total_tokens(self) -> int:
return self.prompt_tokens + self.completion_tokens
class AiUsageSummary(BaseModel):
project_id: str | None = None
user_id: str | None = None
model: str | None = None
operation: str | None = None
request_count: int = 0
prompt_tokens: int = 0
completion_tokens: int = 0
total_tokens: int = 0
cost: float = 0.0
currency: str = "USD"
class ProjectReport(BaseModel):
project_id: str
snapshot_id: str
node_count: int
edge_count: int
procedure_count: int
query_count: int
write_count: int
role_count: int = 0
access_grant_count: int = 0
attribute_count: int = 0
object_attribute_count: int = 0
tabular_section_count: int = 0
tabular_section_column_count: int = 0
empty_tabular_section_count: int = 0
empty_tabular_sections: list[str] = Field(default_factory=list)
unsecured_object_count: int = 0
unsecured_objects: list[str] = Field(default_factory=list)
integration_count: int = 0
outbound_integration_count: int = 0
integration_kinds: dict[str, int] = Field(default_factory=dict)
diagnostic_count: int
generated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class MarketplacePackage(BaseModel):
package_id: str
name: str
version: str
description: str = ""
enabled: bool = True
attributes: dict = Field(default_factory=dict)
class LicenseState(BaseModel):
license_id: str = "community"
plan: str = "community"
valid: bool = True
seats: int | None = None
expires_at: datetime | None = None
class InMemoryOperationsStore:
def __init__(self) -> None:
self.jobs: dict[str, OperationJob] = {}
self.metrics: list[MetricSample] = []
self.ai_usage: dict[str, AiUsageRecord] = {}
self.marketplace: dict[str, MarketplacePackage] = {}
self.license_state = LicenseState()
def enqueue(self, job: OperationJob) -> OperationJob:
self.jobs[job.job_id] = job
return job
def update_job(
self,
job_id: str,
status: OperationJobStatus,
*,
result: dict | None = None,
error: str | None = None,
) -> OperationJob:
job = self.jobs[job_id]
job.status = status
job.result = result or job.result
job.error = error
job.updated_at = datetime.now(timezone.utc)
return job
def list_jobs(self, status: OperationJobStatus | None = None) -> list[OperationJob]:
jobs = list(self.jobs.values())
if status is not None:
jobs = [job for job in jobs if job.status == status]
return sorted(jobs, key=lambda job: job.created_at, reverse=True)
def record_metric(self, metric: MetricSample) -> MetricSample:
self.metrics.append(metric)
return metric
def list_metrics(self, name: str | None = None) -> list[MetricSample]:
samples = self.metrics
if name is not None:
samples = [sample for sample in samples if sample.name == name]
return sorted(samples, key=lambda sample: sample.observed_at, reverse=True)
def record_ai_usage(self, usage: AiUsageRecord) -> AiUsageRecord:
self.ai_usage[usage.usage_id] = usage
return usage
def list_ai_usage(
self,
*,
project_id: str | None = None,
user_id: str | None = None,
model: str | None = None,
) -> list[AiUsageRecord]:
records = list(self.ai_usage.values())
if project_id is not None:
records = [record for record in records if record.project_id == project_id]
if user_id is not None:
records = [record for record in records if record.user_id == user_id]
if model is not None:
records = [record for record in records if record.model == model]
return sorted(records, key=lambda record: record.created_at, reverse=True)
def summarize_ai_usage(
self,
*,
project_id: str | None = None,
user_id: str | None = None,
model: str | None = None,
operation: str | None = None,
) -> AiUsageSummary:
records = self.list_ai_usage(project_id=project_id, user_id=user_id, model=model)
if operation is not None:
records = [record for record in records if record.operation == operation]
prompt_tokens = sum(record.prompt_tokens for record in records)
completion_tokens = sum(record.completion_tokens for record in records)
return AiUsageSummary(
project_id=project_id,
user_id=user_id,
model=model,
operation=operation,
request_count=len(records),
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
total_tokens=prompt_tokens + completion_tokens,
cost=sum(record.cost for record in records),
)
def upsert_marketplace_package(self, package: MarketplacePackage) -> MarketplacePackage:
self.marketplace[package.package_id] = package
return package
def list_marketplace_packages(self, enabled_only: bool = False) -> list[MarketplacePackage]:
packages = list(self.marketplace.values())
if enabled_only:
packages = [package for package in packages if package.enabled]
return sorted(packages, key=lambda package: package.name)
def set_license(self, license_state: LicenseState) -> LicenseState:
self.license_state = license_state
return license_state
def build_project_report(snapshot: SirSnapshot) -> ProjectReport:
integration_topology = build_integration_topology(snapshot)
integration_kinds = {
kind.value: len(integration_topology.by_kind(kind))
for kind in IntegrationKind
if integration_topology.by_kind(kind)
}
access_target_kinds = {
NodeKind.CATALOG,
NodeKind.DOCUMENT,
NodeKind.REGISTER,
NodeKind.COMMON_MODULE,
NodeKind.EXCHANGE_PLAN,
NodeKind.SCHEDULED_JOB,
NodeKind.BUSINESS_PROCESS,
NodeKind.TASK,
}
secured_lineages = {
edge.target_lineage
for edge in snapshot.edges
if edge.kind == EdgeKind.GRANTS_ACCESS
}
unsecured_objects = sorted(
node.qualified_name
for node in snapshot.nodes
if node.kind in access_target_kinds and node.lineage_id not in secured_lineages
)
nodes = {node.lineage_id: node for node in snapshot.nodes}
object_attribute_edges = [
edge
for edge in snapshot.edges
if edge.kind == EdgeKind.HAS_ATTRIBUTE
and (source := nodes.get(edge.source_lineage)) is not None
and source.kind in access_target_kinds
]
tabular_section_lineages = {
node.lineage_id
for node in snapshot.nodes
if node.kind == NodeKind.TABULAR_SECTION
}
tabular_section_column_edges = [
edge
for edge in snapshot.edges
if edge.kind == EdgeKind.HAS_ATTRIBUTE and edge.source_lineage in tabular_section_lineages
]
tabular_sections_with_columns = {edge.source_lineage for edge in tabular_section_column_edges}
empty_tabular_sections = sorted(
nodes[lineage].qualified_name
for lineage in tabular_section_lineages
if lineage not in tabular_sections_with_columns
)
return ProjectReport(
project_id=snapshot.project_id,
snapshot_id=snapshot.snapshot_id,
node_count=len(snapshot.nodes),
edge_count=len(snapshot.edges),
procedure_count=sum(
1 for node in snapshot.nodes if node.kind in {NodeKind.PROCEDURE, NodeKind.FUNCTION}
),
query_count=sum(1 for node in snapshot.nodes if node.kind == NodeKind.QUERY),
write_count=sum(1 for edge in snapshot.edges if edge.kind == EdgeKind.WRITES),
role_count=sum(1 for node in snapshot.nodes if node.kind == NodeKind.ROLE),
access_grant_count=sum(1 for edge in snapshot.edges if edge.kind == EdgeKind.GRANTS_ACCESS),
attribute_count=sum(1 for node in snapshot.nodes if node.kind == NodeKind.ATTRIBUTE),
object_attribute_count=len(object_attribute_edges),
tabular_section_count=len(tabular_section_lineages),
tabular_section_column_count=len(tabular_section_column_edges),
empty_tabular_section_count=len(empty_tabular_sections),
empty_tabular_sections=empty_tabular_sections,
unsecured_object_count=len(unsecured_objects),
unsecured_objects=unsecured_objects,
integration_count=len(integration_topology.endpoints),
outbound_integration_count=sum(
1 for endpoint in integration_topology.endpoints if endpoint.direction == "OUTBOUND"
),
integration_kinds=integration_kinds,
diagnostic_count=len(snapshot.diagnostics),
)
__all__ = [
"AiUsageRecord",
"AiUsageSummary",
"InMemoryOperationsStore",
"LicenseState",
"MarketplacePackage",
"MetricSample",
"OperationJob",
"OperationJobStatus",
"ProjectReport",
"build_project_report",
]
@@ -0,0 +1,148 @@
from pathlib import Path
from operations_core import (
AiUsageRecord,
InMemoryOperationsStore,
MarketplacePackage,
OperationJob,
OperationJobStatus,
build_project_report,
)
from semantic_kernel import index_project
def test_operations_store_jobs_and_marketplace():
store = InMemoryOperationsStore()
job = store.enqueue(OperationJob(job_id="job.1", kind="INDEX_PROJECT"))
store.update_job(job.job_id, OperationJobStatus.SUCCEEDED, result={"ok": True})
store.upsert_marketplace_package(
MarketplacePackage(package_id="pack.1", name="BSP Knowledge", version="1.0.0")
)
assert store.list_jobs()[0].status == OperationJobStatus.SUCCEEDED
assert store.list_marketplace_packages()[0].name == "BSP Knowledge"
def test_operations_store_ai_usage_summary():
store = InMemoryOperationsStore()
store.record_ai_usage(
AiUsageRecord(
usage_id="usage.1",
project_id="demo",
user_id="user.1",
model="gpt-test",
operation="review",
prompt_tokens=100,
completion_tokens=40,
cost=0.01,
)
)
store.record_ai_usage(
AiUsageRecord(
usage_id="usage.2",
project_id="demo",
user_id="user.1",
model="gpt-test",
operation="impact",
prompt_tokens=50,
completion_tokens=10,
cost=0.005,
)
)
summary = store.summarize_ai_usage(project_id="demo", user_id="user.1", model="gpt-test")
assert summary.request_count == 2
assert summary.total_tokens == 200
assert summary.cost == 0.015
def test_build_project_report_counts_snapshot_parts(tmp_path: Path):
module = tmp_path / "demo_module.bsl"
module.write_text(
"""
Процедура Проведение()
Движения.ОстаткиТоваров.Записать();
КонецПроцедуры
""",
encoding="utf-8",
)
snapshot = index_project(tmp_path, project_id="demo")
report = build_project_report(snapshot)
assert report.procedure_count == 1
assert report.write_count == 1
def test_build_project_report_counts_1c_role_access(tmp_path: Path):
xml = tmp_path / "metadata.xml"
xml.write_text(
"""
<Configuration>
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя" />
<Catalog name="Номенклатура" qualifiedName="Справочник.Номенклатура" />
<Role name="Менеджер" qualifiedName="Роль.Менеджер">
<Right object="Документ.ЗаказПокупателя" read="true" write="true" />
</Role>
</Configuration>
""",
encoding="utf-8",
)
snapshot = index_project(tmp_path, project_id="security-report")
report = build_project_report(snapshot)
assert report.role_count == 1
assert report.access_grant_count == 1
assert report.unsecured_object_count == 1
assert report.unsecured_objects == ["Справочник.Номенклатура"]
def test_build_project_report_counts_1c_schema(tmp_path: Path):
xml = tmp_path / "metadata.xml"
xml.write_text(
"""
<Configuration>
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
<Attribute name="Контрагент" qualifiedName="Документ.ЗаказПокупателя.Контрагент" />
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
<Attribute name="Номенклатура" qualifiedName="Документ.ЗаказПокупателя.Товары.Номенклатура" />
</TabularSection>
<TabularSection name="Услуги" qualifiedName="Документ.ЗаказПокупателя.Услуги" />
</Document>
</Configuration>
""",
encoding="utf-8",
)
snapshot = index_project(tmp_path, project_id="schema-report")
report = build_project_report(snapshot)
assert report.attribute_count == 2
assert report.object_attribute_count == 1
assert report.tabular_section_count == 2
assert report.tabular_section_column_count == 1
assert report.empty_tabular_section_count == 1
assert report.empty_tabular_sections == ["Документ.ЗаказПокупателя.Услуги"]
def test_build_project_report_counts_integrations(tmp_path: Path):
module = tmp_path / "integration.bsl"
module.write_text(
"""
Процедура Отправить()
Адрес = "https://api.example.local/orders";
Объект = Новый COMОбъект("V83.Application");
КонецПроцедуры
""",
encoding="utf-8",
)
snapshot = index_project(tmp_path, project_id="integration-report")
report = build_project_report(snapshot)
assert report.integration_count == 2
assert report.outbound_integration_count == 2
assert report.integration_kinds["HTTP_SERVICE"] == 1
assert report.integration_kinds["COM_CONNECTOR"] == 1