Skip to content
Draft
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
33 changes: 27 additions & 6 deletions apps/backend/app/intelligence/query_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class ArchitectureSnapshotFacts:

snapshot: RiSnapshot
nodes: list[RiNode]
#: Stable keys of every ``symbol`` node in the snapshot, and nothing else.
#: Stable keys of top-level ``symbol`` definitions used by module inventory.
#: The architecture consumer needs to know what each file defines, but
#: whole symbol rows are what dominate a large snapshot -- see the bound in
#: ``architecture_facts``. Keys alone are short strings and carry the file
Expand Down Expand Up @@ -391,21 +391,42 @@ def architecture_facts(self, repository_id: str) -> ArchitectureSnapshotFacts |
# that symbol is an endpoint of a relationship edge. Symbols dominate a
# large snapshot, so excluding the unreferenced ones is the bound that
# matters here — while still returning every node the consumer reads.
endpoint_keys = {key for edge in edges for key in (edge.subject_key, edge.object_key)}
node_filter = RiNode.node_kind.in_(ARCHITECTURE_NODE_KINDS)
if endpoint_keys:
node_filter = or_(node_filter, RiNode.stable_key.in_(endpoint_keys))
# Keep endpoint membership in SQL: a large graph must not become one
# bind parameter per endpoint (SQLite/PostgreSQL parameter limits).
endpoint_filter = (
RiEdge.snapshot_id == snapshot.snapshot_id,
RiEdge.predicate.in_(ARCHITECTURE_FACT_PREDICATES),
)
endpoint_keys = (
select(RiEdge.subject_key).where(*endpoint_filter).union(select(RiEdge.object_key).where(*endpoint_filter))
)
node_filter = or_(RiNode.node_kind.in_(ARCHITECTURE_NODE_KINDS), RiNode.stable_key.in_(endpoint_keys))
nodes = list(
self.db.scalars(
select(RiNode)
.where(RiNode.snapshot_id == snapshot.snapshot_id, node_filter)
.order_by(RiNode.stable_key, RiNode.id)
).all()
)
# Module inventory only counts top-level definitions. Apply the same
# qualified-name rule as ArchitectureAnalyzer before materializing keys;
# nested methods dominate many real repositories and are never rendered.
delimiter = (
func.strpos(RiNode.stable_key, "::")
if self.db.get_bind().dialect.name == "postgresql"
else func.instr(RiNode.stable_key, "::")
)
qualified_name = func.substr(RiNode.stable_key, delimiter + 2)
symbol_keys = list(
self.db.scalars(
select(RiNode.stable_key)
.where(RiNode.snapshot_id == snapshot.snapshot_id, RiNode.node_kind == "symbol")
.where(
RiNode.snapshot_id == snapshot.snapshot_id,
RiNode.node_kind == "symbol",
delimiter > 1,
qualified_name != "",
qualified_name.not_like("%.%"),
)
.order_by(RiNode.stable_key)
).all()
)
Expand Down
52 changes: 52 additions & 0 deletions apps/backend/tests/test_architecture_relationships.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,3 +582,55 @@ def test_repository_furniture_does_not_become_an_architecture_module(auth_client
# And nothing is left describing itself by its own path.
assert not any("derived from repository intelligence" in node["description"] for node in architecture["nodes"])
assert not any("Owns unknown concerns" in node["responsibilities"] for node in architecture["nodes"])


def test_architecture_endpoint_membership_uses_a_bounded_sql_subquery(auth_client):
from sqlalchemy import event
from app.core.database import SessionLocal

sources = {
"src/alpha.ts": b"import { beta } from './beta';\nexport const alpha = beta;\n",
"src/beta.ts": b"export const beta = 1;\n",
}
repository = _upload(auth_client, sources)
_persist_snapshot(repository["id"], sources)
observed = []
with SessionLocal() as session:
record = session.get(RepositoryRecord, repository["id"])
engine = session.get_bind()

def capture(connection, cursor, statement, parameters, context, executemany):
if "ri_nodes" in statement and "UNION" in statement:
observed.append((statement, len(parameters)))

event.listen(engine, "before_cursor_execute", capture)
try:
facts = SnapshotQueryService(session, record.owner_id).architecture_facts(record.id)
finally:
event.remove(engine, "before_cursor_execute", capture)
assert facts is not None
assert observed
assert max(count for _, count in observed) < 50
endpoint_keys = {key for edge in facts.edges for key in (edge.subject_key, edge.object_key)}
assert endpoint_keys <= {node.stable_key for node in facts.nodes}


def test_architecture_inventory_does_not_materialize_nested_method_keys(auth_client):
from app.core.database import SessionLocal

source = (
"export class LargeClass {\n"
+ "".join(f" method_{index}() {{ return {index}; }}\n" for index in range(500))
+ "}\n"
)
sources = {"src/large.ts": source.encode()}
repository = _upload(auth_client, sources)
_persist_snapshot(repository["id"], sources)
with SessionLocal() as session:
record = session.get(RepositoryRecord, repository["id"])
facts = SnapshotQueryService(session, record.owner_id).architecture_facts(record.id)
assert facts is not None
assert facts.symbol_keys == ["src/large.ts::LargeClass"]
response = auth_client.get(f"/analysis/{repository['id']}/architecture")
assert response.status_code == 200
assert "Defines 1 symbol: LargeClass" in response.text
Loading