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
@@ -0,0 +1,262 @@
from __future__ import annotations
from pydantic import BaseModel, Field
from integration_topology import IntegrationEndpoint, build_integration_topology
from projection_engine import InMemoryProjection
from sir import EdgeKind, NodeKind, SemanticNode, SirSnapshot
class RoutineImpactReport(BaseModel):
routine_name: str
callers: list[SemanticNode] = Field(default_factory=list)
callees: list[SemanticNode] = Field(default_factory=list)
query_tables: list[SemanticNode] = Field(default_factory=list)
writes: list[SemanticNode] = Field(default_factory=list)
class RoleAccessGrant(BaseModel):
role: SemanticNode
permissions: dict = Field(default_factory=dict)
class ObjectImpactReport(BaseModel):
object_name: str
object: SemanticNode
modules: list[SemanticNode] = Field(default_factory=list)
routines: list[SemanticNode] = Field(default_factory=list)
forms: list[SemanticNode] = Field(default_factory=list)
commands: list[SemanticNode] = Field(default_factory=list)
attributes: list[SemanticNode] = Field(default_factory=list)
tabular_sections: list[SemanticNode] = Field(default_factory=list)
tabular_section_columns: dict[str, list[SemanticNode]] = Field(default_factory=dict)
roles: list[SemanticNode] = Field(default_factory=list)
role_access: list[RoleAccessGrant] = Field(default_factory=list)
jobs: list[SemanticNode] = Field(default_factory=list)
callees: list[SemanticNode] = Field(default_factory=list)
query_tables: list[SemanticNode] = Field(default_factory=list)
writes: list[SemanticNode] = Field(default_factory=list)
integrations: list[IntegrationEndpoint] = Field(default_factory=list)
def routine_impact(graph: InMemoryProjection, routine_name: str) -> RoutineImpactReport:
return RoutineImpactReport(
routine_name=routine_name,
callers=graph.find_callers(routine_name),
callees=graph.find_callees(routine_name),
query_tables=graph.find_query_tables(routine_name),
writes=graph.find_writes(routine_name),
)
def object_impact(graph: InMemoryProjection, object_name: str) -> ObjectImpactReport | None:
owner = _find_object(graph, object_name)
if owner is None:
return None
modules = _targets(graph, owner, EdgeKind.CONTAINS, {NodeKind.MODULE})
attributes = _targets(graph, owner, EdgeKind.HAS_ATTRIBUTE, {NodeKind.ATTRIBUTE})
tabular_sections = _targets(graph, owner, EdgeKind.HAS_TABULAR_SECTION, {NodeKind.TABULAR_SECTION})
tabular_section_columns = {
section.lineage_id: _targets(graph, section, EdgeKind.HAS_ATTRIBUTE, {NodeKind.ATTRIBUTE})
for section in tabular_sections
}
forms = _targets(graph, owner, EdgeKind.HAS_FORM, {NodeKind.FORM})
commands = _dedupe_nodes(
[
*_targets(graph, owner, EdgeKind.HAS_COMMAND, {NodeKind.COMMAND}),
*[
command
for form in forms
for command in _targets(graph, form, EdgeKind.HAS_COMMAND, {NodeKind.COMMAND})
],
]
)
command_handlers = _dedupe_nodes(
[
handler
for command in commands
for handler in _targets(graph, command, EdgeKind.HANDLES, {NodeKind.PROCEDURE, NodeKind.FUNCTION})
]
)
form_handlers = _dedupe_nodes(
[
handler
for form in forms
for handler in _targets(graph, form, EdgeKind.HANDLES, {NodeKind.PROCEDURE, NodeKind.FUNCTION})
]
)
role_access = _role_access_grants(graph, owner)
roles = _dedupe_nodes([grant.role for grant in role_access])
job_routines = _targets(graph, owner, EdgeKind.RUNS, {NodeKind.PROCEDURE, NodeKind.FUNCTION})
routines = _dedupe_nodes(
job_routines
+ command_handlers
+ form_handlers
+ [
routine
for module in modules
for routine in _targets(graph, module, EdgeKind.DECLARES, {NodeKind.PROCEDURE, NodeKind.FUNCTION})
]
)
routine_modules = _dedupe_nodes(
[
module
for routine in routines
for module in _sources(graph, routine, EdgeKind.DECLARES, {NodeKind.MODULE})
]
)
modules = _dedupe_nodes([*modules, *routine_modules])
callees = _dedupe_nodes(
[
callee
for routine in routines
for callee in _targets(graph, routine, EdgeKind.CALLS, {NodeKind.PROCEDURE, NodeKind.FUNCTION})
]
)
query_nodes = [
query
for routine in routines
for query in _targets(graph, routine, EdgeKind.OWNS_QUERY, {NodeKind.QUERY})
]
query_tables = _dedupe_nodes(
[
table
for query in query_nodes
for table in _targets(graph, query, EdgeKind.READS_TABLE, {NodeKind.TABLE, NodeKind.REGISTER})
]
)
writes = _dedupe_nodes(
[
target
for routine in routines
for target in _targets(graph, routine, EdgeKind.WRITES, {NodeKind.REGISTER, NodeKind.TABLE})
]
)
return ObjectImpactReport(
object_name=object_name,
object=owner,
modules=modules,
routines=routines,
forms=forms,
commands=commands,
attributes=attributes,
tabular_sections=tabular_sections,
tabular_section_columns=tabular_section_columns,
roles=roles,
role_access=role_access,
jobs=[owner] if owner.kind == NodeKind.SCHEDULED_JOB else [],
callees=callees,
query_tables=query_tables,
writes=writes,
)
def object_impact_from_snapshot(snapshot: SirSnapshot, object_name: str) -> ObjectImpactReport | None:
graph = InMemoryProjection()
graph.project_snapshot(snapshot)
report = object_impact(graph, object_name)
if report is None:
return None
routine_names = {routine.qualified_name for routine in report.routines}
module_names = {module.qualified_name for module in report.modules}
report.integrations = [
endpoint
for endpoint in build_integration_topology(snapshot).endpoints
if endpoint.owner in routine_names or endpoint.owner in module_names
]
return report
def _find_object(graph: InMemoryProjection, object_name: str) -> SemanticNode | None:
wanted = object_name.lower()
object_kinds = {
NodeKind.CATALOG,
NodeKind.DOCUMENT,
NodeKind.CONSTANT,
NodeKind.DOCUMENT_JOURNAL,
NodeKind.ENUM,
NodeKind.REPORT,
NodeKind.DATA_PROCESSOR,
NodeKind.CHART_OF_CHARACTERISTIC_TYPES,
NodeKind.CHART_OF_ACCOUNTS,
NodeKind.CHART_OF_CALCULATION_TYPES,
NodeKind.REGISTER,
NodeKind.COMMON_MODULE,
NodeKind.EXCHANGE_PLAN,
NodeKind.EXTERNAL_DATA_SOURCE,
NodeKind.SCHEDULED_JOB,
NodeKind.BUSINESS_PROCESS,
NodeKind.TASK,
}
return next(
(
node
for node in graph.nodes.values()
if node.kind in object_kinds
and (node.name.lower() == wanted or node.qualified_name.lower() == wanted)
),
None,
)
def _targets(
graph: InMemoryProjection,
source: SemanticNode,
kind: EdgeKind,
target_kinds: set[NodeKind],
) -> list[SemanticNode]:
return [
graph.nodes[edge.target_lineage]
for edge in graph.edges.values()
if edge.kind == kind
and edge.source_lineage == source.lineage_id
and edge.target_lineage in graph.nodes
and graph.nodes[edge.target_lineage].kind in target_kinds
]
def _sources(
graph: InMemoryProjection,
target: SemanticNode,
kind: EdgeKind,
source_kinds: set[NodeKind],
) -> list[SemanticNode]:
return [
graph.nodes[edge.source_lineage]
for edge in graph.edges.values()
if edge.kind == kind
and edge.target_lineage == target.lineage_id
and edge.source_lineage in graph.nodes
and graph.nodes[edge.source_lineage].kind in source_kinds
]
def _role_access_grants(graph: InMemoryProjection, target: SemanticNode) -> list[RoleAccessGrant]:
grants: list[RoleAccessGrant] = []
for edge in graph.edges.values():
if edge.kind != EdgeKind.GRANTS_ACCESS or edge.target_lineage != target.lineage_id:
continue
role = graph.nodes.get(edge.source_lineage)
if role is None or role.kind != NodeKind.ROLE:
continue
grants.append(RoleAccessGrant(role=role, permissions=edge.attributes))
return grants
def _dedupe_nodes(nodes: list[SemanticNode]) -> list[SemanticNode]:
seen: dict[str, SemanticNode] = {}
for node in nodes:
seen.setdefault(node.lineage_id, node)
return list(seen.values())
__all__ = [
"ObjectImpactReport",
"RoleAccessGrant",
"RoutineImpactReport",
"object_impact",
"object_impact_from_snapshot",
"routine_impact",
]