From c66d792e8ca6d718d43b88ab2bef22fb022ffeb5 Mon Sep 17 00:00:00 2001 From: hardikuppal04 <262667963+hardikuppal04@users.noreply.github.com> Date: Tue, 28 Jul 2026 02:28:12 +0530 Subject: [PATCH 1/4] style(backend): format Python source with Ruff --- apps/backend/app/ai/orchestrator.py | 4 +- apps/backend/app/ai/prompt_builder.py | 10 +- apps/backend/app/ai/providers/base.py | 3 +- apps/backend/app/ai/providers/config_store.py | 7 +- apps/backend/app/ai/repository_context.py | 4 +- apps/backend/app/analysis/architecture.py | 203 ++++++++++++++---- apps/backend/app/analysis/authentication.py | 10 +- apps/backend/app/analysis/manifest.py | 7 +- apps/backend/app/api/openapi.py | 4 +- apps/backend/app/api/routes/ai.py | 9 +- apps/backend/app/api/routes/analysis.py | 9 +- apps/backend/app/api/routes/intelligence.py | 129 +++++++++-- apps/backend/app/api/routes/reports.py | 2 +- apps/backend/app/auth/service.py | 4 +- apps/backend/app/core/config.py | 4 +- apps/backend/app/core/rate_limit.py | 4 +- apps/backend/app/core/schema_sync.py | 4 +- apps/backend/app/core/security_headers.py | 4 +- apps/backend/app/extraction/base.py | 8 +- apps/backend/app/extraction/manifests.py | 2 +- apps/backend/app/extraction/pipeline.py | 15 +- apps/backend/app/extraction/python.py | 76 +++---- apps/backend/app/extraction/support_matrix.py | 14 +- apps/backend/app/extraction/typescript.py | 144 +++++++------ apps/backend/app/graph/dependency_graph.py | 4 +- apps/backend/app/insights/service.py | 7 +- apps/backend/app/intelligence/canonical.py | 28 +-- .../app/intelligence/classification.py | 6 +- apps/backend/app/intelligence/models.py | 4 +- .../backend/app/intelligence/query_service.py | 56 +++-- apps/backend/app/intelligence/resolution.py | 72 +++---- .../app/intelligence/snapshot_store.py | 20 +- apps/backend/app/models/snapshot.py | 4 +- apps/backend/app/parsers/repository_parser.py | 4 +- apps/backend/app/reports/builders.py | 10 +- apps/backend/app/reports/renderers.py | 3 +- apps/backend/app/review/review_service.py | 28 +-- apps/backend/app/schemas/architecture.py | 4 +- .../app/services/analysis_job_service.py | 20 +- .../app/services/documentation_service.py | 11 +- apps/backend/app/services/evidence_service.py | 4 +- .../app/services/repository_service.py | 4 +- apps/backend/app/workers/analysis_worker.py | 48 ++--- 43 files changed, 576 insertions(+), 441 deletions(-) diff --git a/apps/backend/app/ai/orchestrator.py b/apps/backend/app/ai/orchestrator.py index 3f351df..d77dde3 100644 --- a/apps/backend/app/ai/orchestrator.py +++ b/apps/backend/app/ai/orchestrator.py @@ -46,7 +46,9 @@ async def test_connection(self, request: AiProviderTestRequest) -> AiProviderTes provider = self.provider_factory.resolve(config) prompt = PromptBundle(system_prompt="Reply with the single word: ok", user_prompt="Connection test.") await provider.complete(config, prompt) - return AiProviderTestResponse(ok=True, message=f"{config.provider} connection succeeded.", checked_at=datetime.now(UTC)) + return AiProviderTestResponse( + ok=True, message=f"{config.provider} connection succeeded.", checked_at=datetime.now(UTC) + ) async def query(self, request: AiQueryRequest) -> AiQueryResponse: # Owner-scoped: another user's repository id resolves to None and gets diff --git a/apps/backend/app/ai/prompt_builder.py b/apps/backend/app/ai/prompt_builder.py index 49dd4b7..fbd9a34 100644 --- a/apps/backend/app/ai/prompt_builder.py +++ b/apps/backend/app/ai/prompt_builder.py @@ -33,12 +33,14 @@ def render_repository_context(self, repository_context: RepositoryContext) -> st f"Frameworks: {', '.join(architecture.frameworks) if architecture.frameworks else 'Not detected'}", f"Entry points: {', '.join(architecture.entry_points) if architecture.entry_points else 'Not found'}", "Modules (roles are heuristic classifications):", + *[f"- {module.name} ({module.role}, {module.file_count} files)" for module in architecture.modules], + "Dependencies:", *[ - f"- {module.name} ({module.role}, {module.file_count} files)" - for module in architecture.modules + self._render_dependency( + dependency.name, dependency.version, dependency.declared_versions, dependency.has_version_conflict + ) + for dependency in repository_context.dependencies ], - "Dependencies:", - *[self._render_dependency(dependency.name, dependency.version, dependency.declared_versions, dependency.has_version_conflict) for dependency in repository_context.dependencies], "Files:", *[f"- {file.path}" for file in repository_context.selected_files], ] diff --git a/apps/backend/app/ai/providers/base.py b/apps/backend/app/ai/providers/base.py index 702a3b7..2ca5d02 100644 --- a/apps/backend/app/ai/providers/base.py +++ b/apps/backend/app/ai/providers/base.py @@ -4,5 +4,4 @@ class AiProvider(Protocol): - async def complete(self, config: AiProviderConfig, prompt: PromptBundle) -> AiProviderResponse: - ... + async def complete(self, config: AiProviderConfig, prompt: PromptBundle) -> AiProviderResponse: ... diff --git a/apps/backend/app/ai/providers/config_store.py b/apps/backend/app/ai/providers/config_store.py index 5b76b0c..e11ef25 100644 --- a/apps/backend/app/ai/providers/config_store.py +++ b/apps/backend/app/ai/providers/config_store.py @@ -145,7 +145,12 @@ def _resolve_key( """ if config.api_key: return self.cipher.encrypt(config.api_key), config.api_key[-4:] - if config.provider != "ollama" and record is not None and record.provider == config.provider and record.encrypted_api_key: + if ( + config.provider != "ollama" + and record is not None + and record.provider == config.provider + and record.encrypted_api_key + ): return record.encrypted_api_key, record.api_key_last4 if config.provider == "ollama": return None, None diff --git a/apps/backend/app/ai/repository_context.py b/apps/backend/app/ai/repository_context.py index 9404dfd..8f5d12b 100644 --- a/apps/backend/app/ai/repository_context.py +++ b/apps/backend/app/ai/repository_context.py @@ -38,9 +38,7 @@ def build(self, record: RepositoryRecord, selected_file: str | None = None) -> R ) dependencies: list[DependencyContext] = [] for dependency in projection.dependencies[:20]: - declared_versions = tuple( - dict.fromkeys(declaration.version for declaration in dependency.declarations) - ) + declared_versions = tuple(dict.fromkeys(declaration.version for declaration in dependency.declarations)) has_conflict = len(declared_versions) > 1 dependencies.append( DependencyContext( diff --git a/apps/backend/app/analysis/architecture.py b/apps/backend/app/analysis/architecture.py index 3279006..de40ca4 100644 --- a/apps/backend/app/analysis/architecture.py +++ b/apps/backend/app/analysis/architecture.py @@ -52,7 +52,6 @@ } - class ArchitectureAnalyzer: """Builds the Architecture read model exclusively from sealed ri.v1 snapshots. @@ -70,21 +69,39 @@ def __init__(self, snapshots: SnapshotQueryService | None = None) -> None: def build_architecture(self, record: RepositoryRecord) -> ArchitectureResponse: if self.snapshots is not None: self.snapshots.require_sealed_snapshot_for_current_revision(record.id) - facts = self.snapshots.architecture_facts(record.id) if self.snapshots is not None else None + facts = ( + self.snapshots.architecture_facts(record.id) + if self.snapshots is not None + else None + ) modules = self._modules_from_facts(facts) frameworks = self._frameworks_from_facts(facts) primary_language = self._primary_language_from_facts(facts) entry_points = self._entry_points_from_facts(facts) nodes = self._nodes_for_modules(modules) nodes.extend(self._dependency_nodes(facts)) - edges, diagnostics, unresolved_node_ids, covered_paths = self._edges_for_modules(modules, nodes, facts) - edge_endpoint_ids = {node_id for edge in edges for node_id in (edge.source, edge.target)} - nodes = [node for node in nodes if node.layer != "external" or node.id in edge_endpoint_ids] + edges, diagnostics, unresolved_node_ids, covered_paths = ( + self._edges_for_modules(modules, nodes, facts) + ) + edge_endpoint_ids = { + node_id for edge in edges for node_id in (edge.source, edge.target) + } + nodes = [ + node + for node in nodes + if node.layer != "external" or node.id in edge_endpoint_ids + ] remaining_node_ids = {node.id for node in nodes} for diagnostic in diagnostics: if diagnostic.node_ids is not None: - diagnostic.node_ids = [node_id for node_id in diagnostic.node_ids if node_id in remaining_node_ids] or None - self._set_relationship_states(modules, nodes, edges, unresolved_node_ids, covered_paths, facts is not None) + diagnostic.node_ids = [ + node_id + for node_id in diagnostic.node_ids + if node_id in remaining_node_ids + ] or None + self._set_relationship_states( + modules, nodes, edges, unresolved_node_ids, covered_paths, facts is not None + ) layers = self._layers_for_nodes(nodes) arch_modules = [ ArchModule( @@ -114,7 +131,9 @@ def build_architecture(self, record: RepositoryRecord) -> ArchitectureResponse: entry_point=entry_points[0] if entry_points else "/", architecture_pattern=self._architecture_type(frameworks), ), - relationship_snapshot_id=facts.snapshot.snapshot_id if facts is not None else None, + relationship_snapshot_id=facts.snapshot.snapshot_id + if facts is not None + else None, diagnostics=diagnostics, ) @@ -166,7 +185,10 @@ def _file_roles(self, facts: ArchitectureSnapshotFacts) -> dict[str, str]: roles: dict[str, str] = {} for assertion in facts.assertions: - if assertion.predicate != "classified_as" or assertion.subject_kind != "file": + if ( + assertion.predicate != "classified_as" + or assertion.subject_kind != "file" + ): continue classification = str((assertion.value or {}).get("classification", "")) if not classification: @@ -174,7 +196,9 @@ def _file_roles(self, facts: ArchitectureSnapshotFacts) -> dict[str, str]: roles[assertion.subject_key.removeprefix("file:")] = classification return roles - def _modules_from_facts(self, facts: ArchitectureSnapshotFacts | None) -> list[RepositoryModule]: + def _modules_from_facts( + self, facts: ArchitectureSnapshotFacts | None + ) -> list[RepositoryModule]: if facts is None: # Defensive only: build_architecture requires a sealed snapshot # before calling this whenever self.snapshots is configured, so a @@ -198,7 +222,8 @@ def _modules_from_facts(self, facts: ArchitectureSnapshotFacts | None) -> list[R candidate_roles = [ role_by_path[path] for path in paths - if role_by_path.get(path) not in (None, "unknown", "documentation", "test") + if role_by_path.get(path) + not in (None, "unknown", "documentation", "test") ] dominant = ( Counter(candidate_roles).most_common(1)[0][0] @@ -238,7 +263,11 @@ def _module_id(path: str, role: str | None) -> str: return "module:tests" if role == "documentation": return "module:documentation" - if parts and parts[0] in {"app", "src", "backend", "frontend", "apps"} and len(parts) > 1: + if ( + parts + and parts[0] in {"app", "src", "backend", "frontend", "apps"} + and len(parts) > 1 + ): return f"module:{parts[1].lower()}" return f"module:{parts[0].lower() if parts else 'repository'}" @@ -255,10 +284,16 @@ def _path_prefix(paths: list[str]) -> str: break return "/" + "/".join(prefix) if prefix else "/" - def _frameworks_from_facts(self, facts: ArchitectureSnapshotFacts | None) -> list[str]: + def _frameworks_from_facts( + self, facts: ArchitectureSnapshotFacts | None + ) -> list[str]: if facts is None: return [] - names = {node.name.lower() for node in facts.nodes if node.node_kind == "dependency" and node.name} + names = { + node.name.lower() + for node in facts.nodes + if node.node_kind == "dependency" and node.name + } return sorted( { framework @@ -267,26 +302,40 @@ def _frameworks_from_facts(self, facts: ArchitectureSnapshotFacts | None) -> lis } ) - def _primary_language_from_facts(self, facts: ArchitectureSnapshotFacts | None) -> str: + def _primary_language_from_facts( + self, facts: ArchitectureSnapshotFacts | None + ) -> str: if facts is None: return "Unknown" # Count persisted file facts, not symbols. Symbol counts make a file # with many declarations (and synthetic route symbols) outweigh other # files, and report "Unknown" for a valid script containing only # top-level statements. - counts = Counter(node.language for node in facts.nodes if node.node_kind == "file" and node.language) + counts = Counter( + node.language + for node in facts.nodes + if node.node_kind == "file" and node.language + ) if not counts: return "Unknown" dominant = counts.most_common(1)[0][0] - return {"python": "Python", "typescript": "TypeScript"}.get(dominant, dominant.title()) + return {"python": "Python", "typescript": "TypeScript"}.get( + dominant, dominant.title() + ) - def _entry_points_from_facts(self, facts: ArchitectureSnapshotFacts | None) -> list[str]: + def _entry_points_from_facts( + self, facts: ArchitectureSnapshotFacts | None + ) -> list[str]: if facts is None: return [] role_by_path = self._file_roles(facts) - return sorted(path for path, role in role_by_path.items() if role == "entrypoint") + return sorted( + path for path, role in role_by_path.items() if role == "entrypoint" + ) - def _dependency_nodes(self, facts: ArchitectureSnapshotFacts | None) -> list[ArchNode]: + def _dependency_nodes( + self, facts: ArchitectureSnapshotFacts | None + ) -> list[ArchNode]: if facts is None: return [] relationship_keys = { @@ -297,7 +346,10 @@ def _dependency_nodes(self, facts: ArchitectureSnapshotFacts | None) -> list[Arc } result: list[ArchNode] = [] for item in facts.nodes: - if item.node_kind != "dependency" or item.stable_key not in relationship_keys: + if ( + item.node_kind != "dependency" + or item.stable_key not in relationship_keys + ): continue evidence = facts.node_evidence.get(item.id, []) result.append( @@ -306,7 +358,9 @@ def _dependency_nodes(self, facts: ArchitectureSnapshotFacts | None) -> list[Arc name=item.name or item.stable_key, type="shared-library", description="External dependency from resolved repository evidence.", - responsibilities=["Provides an externally declared or imported capability"], + responsibilities=[ + "Provides an externally declared or imported capability" + ], files=sorted({entry.path for entry in evidence}), dependencies=[], dependents=[], @@ -343,7 +397,9 @@ def _edges_for_modules( modules_by_file: dict[str, list[str]] = {} for module in modules: for path in module.files: - modules_by_file.setdefault(self._normalize_path(path), []).append(module.id) + modules_by_file.setdefault(self._normalize_path(path), []).append( + module.id + ) snapshot_node_by_key = {item.stable_key: item for item in facts.nodes} node_ids = {node.id for node in nodes} diagnostics = [ @@ -361,7 +417,9 @@ def _edges_for_modules( if item.code not in ARCHITECTURE_DIAGNOSTIC_CODES: continue if item.path: - unresolved_node_ids.update(modules_by_file.get(self._normalize_path(item.path), [])) + unresolved_node_ids.update( + modules_by_file.get(self._normalize_path(item.path), []) + ) for key in (item.subject_key, item.object_key): if key in node_ids: unresolved_node_ids.add(key) @@ -411,7 +469,9 @@ def _edges_for_modules( subject_key=fact.subject_key, object_key=fact.object_key, details={"factId": fact.edge_id, "predicate": fact.predicate}, - node_ids=[fact.object_key] if fact.object_key in node_ids else None, + node_ids=[fact.object_key] + if fact.object_key in node_ids + else None, ) ) if len(root_scope_evidence) == len(evidence_rows): @@ -422,7 +482,9 @@ def _edges_for_modules( for target in target_ids if source in node_ids and target in node_ids ) - non_self_pairs = [(source, target) for source, target in pairs if source != target] + non_self_pairs = [ + (source, target) for source, target in pairs if source != target + ] if pairs and not non_self_pairs: # A resolved fact wholly inside one architecture module remains # extraction evidence, but it is not a module-to-module edge. @@ -435,7 +497,9 @@ def _edges_for_modules( severity="warning", message="A resolved relationship could not be mapped to architecture nodes without guessing.", path=evidence_rows[0].path if evidence_rows else None, - start_line=evidence_rows[0].start_line if evidence_rows else None, + start_line=evidence_rows[0].start_line + if evidence_rows + else None, end_line=evidence_rows[0].end_line if evidence_rows else None, subject_key=fact.subject_key, object_key=fact.object_key, @@ -456,7 +520,11 @@ def _edges_for_modules( for item in evidence_rows ] for index, (source, target) in enumerate(non_self_pairs, start=1): - edge_id = fact.edge_id if len(non_self_pairs) == 1 else f"{fact.edge_id}:{index}" + edge_id = ( + fact.edge_id + if len(non_self_pairs) == 1 + else f"{fact.edge_id}:{index}" + ) edges.append( ArchEdge( id=edge_id, @@ -519,7 +587,10 @@ def _architecture_endpoint_ids( return { module.id for module in module_by_id.values() - if any(self._path_is_within(self._normalize_path(file_path), path) for file_path in module.files) + if any( + self._path_is_within(self._normalize_path(file_path), path) + for file_path in module.files + ) } return set() @@ -551,15 +622,23 @@ def _set_relationship_states( covered_paths: set[str], snapshot_available: bool, ) -> None: - connected = {node_id for edge in edges for node_id in (edge.source, edge.target)} + connected = { + node_id for edge in edges for node_id in (edge.source, edge.target) + } module_by_id = {module.id: module for module in modules} for node in nodes: if node.id in connected: node.relationship_state = "connected" elif node.id in unresolved_node_ids: node.relationship_state = "unresolved" - elif node.id in module_by_id and snapshot_available and module_by_id[node.id].files and all( - self._normalize_path(path) in covered_paths for path in module_by_id[node.id].files + elif ( + node.id in module_by_id + and snapshot_available + and module_by_id[node.id].files + and all( + self._normalize_path(path) in covered_paths + for path in module_by_id[node.id].files + ) ): node.relationship_state = "no-observed-relationships" else: @@ -573,14 +652,18 @@ def _architecture_diagnostic( ) -> ArchitectureDiagnostic: attributed_node_ids: set[str] = set() if item.path: - attributed_node_ids.update(modules_by_file.get(self._normalize_path(item.path), [])) + attributed_node_ids.update( + modules_by_file.get(self._normalize_path(item.path), []) + ) for key in (item.subject_key, item.object_key): if key is None: continue if key in node_ids: attributed_node_ids.add(key) continue - path = self._path_for_stable_key("file" if key.startswith("file:") else "symbol", key) + path = self._path_for_stable_key( + "file" if key.startswith("file:") else "symbol", key + ) if path is not None: attributed_node_ids.update(modules_by_file.get(path, [])) return ArchitectureDiagnostic( @@ -620,8 +703,15 @@ def _layers_for_nodes(self, nodes: list[ArchNode]) -> list[ArchLayer]: for node in nodes: layers.setdefault(node.layer, []).append(node.id) return [ - ArchLayer(id=layer, name=layer.replace("-", " ").title(), order=LAYER_ORDER.get(layer, 99), nodes=node_ids) - for layer, node_ids in sorted(layers.items(), key=lambda item: LAYER_ORDER.get(item[0], 99)) + ArchLayer( + id=layer, + name=layer.replace("-", " ").title(), + order=LAYER_ORDER.get(layer, 99), + nodes=node_ids, + ) + for layer, node_ids in sorted( + layers.items(), key=lambda item: LAYER_ORDER.get(item[0], 99) + ) ] def _architecture_type(self, frameworks: list[str]) -> str: @@ -634,12 +724,45 @@ def _architecture_type(self, frameworks: list[str]) -> str: def _request_flow(self, modules: list[RepositoryModule]) -> list[RequestFlowStep]: module_roles = {module.role for module in modules} steps = [ - RequestFlowStep(id="client", name="Client", type="frontend", description="Request enters the system.", details=["Browser or API client sends a request."]), + RequestFlowStep( + id="client", + name="Client", + type="frontend", + description="Request enters the system.", + details=["Browser or API client sends a request."], + ), ] if "route" in module_roles or "controller" in module_roles: - steps.append(RequestFlowStep(id="api", name="API Layer", type="controller", description="Route/controller handles input.", details=["Validate request", "Call service"])) + steps.append( + RequestFlowStep( + id="api", + name="API Layer", + type="controller", + description="Route/controller handles input.", + details=["Validate request", "Call service"], + ) + ) if "service" in module_roles: - steps.append(RequestFlowStep(id="service", name="Service Layer", type="service", description="Business logic executes.", details=["Coordinate repository intelligence consumers", "Transform data"])) + steps.append( + RequestFlowStep( + id="service", + name="Service Layer", + type="service", + description="Business logic executes.", + details=[ + "Coordinate repository intelligence consumers", + "Transform data", + ], + ) + ) if "repository" in module_roles: - steps.append(RequestFlowStep(id="repository", name="Repository Layer", type="repository", description="Persistence or source files are accessed.", details=["Read or write data"] )) + steps.append( + RequestFlowStep( + id="repository", + name="Repository Layer", + type="repository", + description="Persistence or source files are accessed.", + details=["Read or write data"], + ) + ) return steps diff --git a/apps/backend/app/analysis/authentication.py b/apps/backend/app/analysis/authentication.py index de557b5..d6108c8 100644 --- a/apps/backend/app/analysis/authentication.py +++ b/apps/backend/app/analysis/authentication.py @@ -41,9 +41,7 @@ _GUARD_CLASSIFICATION = "auth_dependency" _SERVICE_CLASSIFICATION = "service" _MODEL_CLASSIFICATION = "model" -_DIAGNOSTIC_CODES = frozenset( - {"RI-RES-UNRESOLVED", "RI-RES-AMBIGUOUS", "RI-EXT-UNSUPPORTED", "RI-SRC-MALFORMED"} -) +_DIAGNOSTIC_CODES = frozenset({"RI-RES-UNRESOLVED", "RI-RES-AMBIGUOUS", "RI-EXT-UNSUPPORTED", "RI-SRC-MALFORMED"}) _MAX_DIAGNOSTICS = 50 @@ -274,11 +272,7 @@ def _reachable_service_model_edges(self, guard_node: RiNode) -> list[tuple[str, used_nodes.add(walk_key) walk_key = parent[walk_key][0] - return [ - (edge.subject_key, edge.object_key, edge) - for edge in discovered_edges - if edge.object_key in used_nodes - ] + return [(edge.subject_key, edge.object_key, edge) for edge in discovered_edges if edge.object_key in used_nodes] def _role_of(self, stable_key: str) -> AuthRelationshipNodeKind: roles = self.classifications.get(stable_key, ()) diff --git a/apps/backend/app/analysis/manifest.py b/apps/backend/app/analysis/manifest.py index f2fe9a7..85bea08 100644 --- a/apps/backend/app/analysis/manifest.py +++ b/apps/backend/app/analysis/manifest.py @@ -154,8 +154,7 @@ def verify( matches_stored_snapshot=False, mismatched_fields=mismatched, detail=( - "The manifest is internally consistent but does not match the " - "snapshot stored for this repository." + "The manifest is internally consistent but does not match the snapshot stored for this repository." ), ) if named.snapshot_id != current.snapshot_id: @@ -180,6 +179,4 @@ def verify( def _mismatched_fields(stored: RevisionManifest, submitted: RevisionManifest) -> list[str]: stored_fields = stored.model_dump(mode="json", by_alias=True) submitted_fields = submitted.model_dump(mode="json", by_alias=True) - return sorted( - key for key in stored_fields if stored_fields[key] != submitted_fields.get(key) - ) + return sorted(key for key in stored_fields if stored_fields[key] != submitted_fields.get(key)) diff --git a/apps/backend/app/api/openapi.py b/apps/backend/app/api/openapi.py index 497c38d..73810e0 100644 --- a/apps/backend/app/api/openapi.py +++ b/apps/backend/app/api/openapi.py @@ -122,9 +122,7 @@ def remove_suppressed_automatic_validation_errors(document: dict[str, Any]) -> N """Remove FastAPI's synthetic 422 response from explicitly marked operations.""" for path_item in document.get("paths", {}).values(): for operation in path_item.values(): - if not isinstance(operation, dict) or not operation.pop( - _SUPPRESS_AUTOMATIC_422_MARKER, False - ): + if not isinstance(operation, dict) or not operation.pop(_SUPPRESS_AUTOMATIC_422_MARKER, False): continue operation.get("responses", {}).pop("422", None) diff --git a/apps/backend/app/api/routes/ai.py b/apps/backend/app/api/routes/ai.py index a317e91..2108ce1 100644 --- a/apps/backend/app/api/routes/ai.py +++ b/apps/backend/app/api/routes/ai.py @@ -4,7 +4,14 @@ from app.api.deps import get_ai_service, get_current_user from app.api.openapi import documented_responses -from app.schemas.ai import AiProviderConfig, AiProviderPublicConfig, AiProviderTestRequest, AiProviderTestResponse, AiQueryRequest, AiQueryResponse +from app.schemas.ai import ( + AiProviderConfig, + AiProviderPublicConfig, + AiProviderTestRequest, + AiProviderTestResponse, + AiQueryRequest, + AiQueryResponse, +) from app.services.ai_service import AiService # Every AI route requires auth; the repository and the provider config are both diff --git a/apps/backend/app/api/routes/analysis.py b/apps/backend/app/api/routes/analysis.py index 65d58ab..fde0225 100644 --- a/apps/backend/app/api/routes/analysis.py +++ b/apps/backend/app/api/routes/analysis.py @@ -226,8 +226,7 @@ def get_authentication_explanation( dependencies=[Depends(require_repository_owner)], responses=documented_responses( 200, - "Source text for one evidence citation, verified against the exact " - "snapshot revision it was cited from.", + "Source text for one evidence citation, verified against the exact snapshot revision it was cited from.", { "schemaVersion": "evidence-source.v1", "repositoryId": _REPOSITORY_ID, @@ -240,7 +239,7 @@ def get_authentication_explanation( "endLine": 10, "status": "ready", "reason": None, - "content": "@app.get(\"/me\")\n", + "content": '@app.get("/me")\n', "truncated": False, "size": 16, }, @@ -390,9 +389,7 @@ def verify_revision_manifest( ], } ], - "edges": [ - {"id": "edge_example", "source": "repo:root", "target": "dep:npm:react", "type": "depends-on"} - ], + "edges": [{"id": "edge_example", "source": "repo:root", "target": "dep:npm:react", "type": "depends-on"}], "totalDependencies": 1, "manifestCount": 1, "diagnostics": [], diff --git a/apps/backend/app/api/routes/intelligence.py b/apps/backend/app/api/routes/intelligence.py index cce9e58..faef278 100644 --- a/apps/backend/app/api/routes/intelligence.py +++ b/apps/backend/app/api/routes/intelligence.py @@ -87,7 +87,9 @@ def _node(snapshot, node, evidence) -> RiNodeResponse: language=node.language, truth_class=node.truth_class, properties=node.properties, - evidence=[_evidence(snapshot, item, fact_kind="node", fact_id=node.stable_key) for item in evidence.get(node.id, [])], + evidence=[ + _evidence(snapshot, item, fact_kind="node", fact_id=node.stable_key) for item in evidence.get(node.id, []) + ], ) @@ -102,7 +104,9 @@ def _edge(snapshot, edge, evidence, derivations) -> RiEdgeResponse: truth_class=edge.truth_class, producer=edge.producer, producer_version=edge.producer_version, - evidence=[_evidence(snapshot, item, fact_kind="edge", fact_id=edge.edge_id) for item in evidence.get(edge.id, [])], + evidence=[ + _evidence(snapshot, item, fact_kind="edge", fact_id=edge.edge_id) for item in evidence.get(edge.id, []) + ], derived_from=[{"kind": item.ref_kind, "identity": item.ref_identity} for item in derivations.get(edge.id, [])], ) @@ -117,7 +121,9 @@ def _assertion(assertion, derivations) -> RiAssertionResponse: truth_class=assertion.truth_class, producer=assertion.producer, producer_version=assertion.producer_version, - derived_from=[{"kind": item.ref_kind, "identity": item.ref_identity} for item in derivations.get(assertion.id, [])], + derived_from=[ + {"kind": item.ref_kind, "identity": item.ref_identity} for item in derivations.get(assertion.id, []) + ], ) @@ -142,16 +148,29 @@ def _impact_direction(snapshot, direction, evidence, derivations) -> RiImpactDir @router.get( "/{snapshot_id}", response_model=RiSnapshotMetadataResponse, - responses=documented_responses(200, "Sealed ri.v1 snapshot metadata.", {"schemaVersion": "ri.v1"}, 401, 404, 422, 429, 500), + responses=documented_responses( + 200, "Sealed ri.v1 snapshot metadata.", {"schemaVersion": "ri.v1"}, 401, 404, 422, 429, 500 + ), ) -def get_snapshot(snapshot_id: str, service: SnapshotQueryService = Depends(get_snapshot_query_service)) -> RiSnapshotMetadataResponse: +def get_snapshot( + snapshot_id: str, service: SnapshotQueryService = Depends(get_snapshot_query_service) +) -> RiSnapshotMetadataResponse: return _metadata(service.metadata(snapshot_id)) @router.get( "/{snapshot_id}/symbols", response_model=RiSymbolsResponse, - responses=documented_responses(200, "Observed symbol nodes with stored evidence.", {"schemaVersion": "ri.v1", "data": [], "pagination": {"offset": 0, "limit": 50, "total": 0}}, 401, 404, 422, 429, 500), + responses=documented_responses( + 200, + "Observed symbol nodes with stored evidence.", + {"schemaVersion": "ri.v1", "data": [], "pagination": {"offset": 0, "limit": 50, "total": 0}}, + 401, + 404, + 422, + 429, + 500, + ), ) def list_symbols( snapshot_id: str, @@ -161,13 +180,31 @@ def list_symbols( ) -> RiSymbolsResponse: snapshot, nodes, total = service.symbols(snapshot_id, offset=offset, limit=limit) evidence = service.evidence_for_nodes(snapshot, nodes) - return RiSymbolsResponse(schema_version=snapshot.schema_version, data=[_node(snapshot, node, evidence) for node in nodes], pagination=_pagination(offset, limit, total)) + return RiSymbolsResponse( + schema_version=snapshot.schema_version, + data=[_node(snapshot, node, evidence) for node in nodes], + pagination=_pagination(offset, limit, total), + ) @router.get( "/{snapshot_id}/neighbours", response_model=RiNeighboursResponse, - responses=documented_responses(200, "Stored resolved edges incident to a node.", {"schemaVersion": "ri.v1", "nodeKey": "repo:root", "data": [], "pagination": {"offset": 0, "limit": 50, "total": 0}}, 401, 404, 422, 429, 500), + responses=documented_responses( + 200, + "Stored resolved edges incident to a node.", + { + "schemaVersion": "ri.v1", + "nodeKey": "repo:root", + "data": [], + "pagination": {"offset": 0, "limit": 50, "total": 0}, + }, + 401, + 404, + 422, + 429, + 500, + ), ) def list_neighbours( snapshot_id: str, @@ -179,7 +216,12 @@ def list_neighbours( snapshot, edges, total = service.neighbours(snapshot_id, node_key=node_key, offset=offset, limit=limit) evidence = service.evidence_for_edges(snapshot, edges) derivations = service.derivations_for_edges(snapshot, edges) - return RiNeighboursResponse(schema_version=snapshot.schema_version, node_key=node_key, data=[_edge(snapshot, edge, evidence, derivations) for edge in edges], pagination=_pagination(offset, limit, total)) + return RiNeighboursResponse( + schema_version=snapshot.schema_version, + node_key=node_key, + data=[_edge(snapshot, edge, evidence, derivations) for edge in edges], + pagination=_pagination(offset, limit, total), + ) @router.get( @@ -228,7 +270,16 @@ def get_impact( @router.get( "/{snapshot_id}/references", response_model=RiReferencesResponse, - responses=documented_responses(200, "Stored resolved relationship facts only.", {"schemaVersion": "ri.v1", "data": [], "pagination": {"offset": 0, "limit": 50, "total": 0}}, 401, 404, 422, 429, 500), + responses=documented_responses( + 200, + "Stored resolved relationship facts only.", + {"schemaVersion": "ri.v1", "data": [], "pagination": {"offset": 0, "limit": 50, "total": 0}}, + 401, + 404, + 422, + 429, + 500, + ), ) def list_references( snapshot_id: str, @@ -239,13 +290,26 @@ def list_references( snapshot, edges, total = service.references(snapshot_id, offset=offset, limit=limit) evidence = service.evidence_for_edges(snapshot, edges) derivations = service.derivations_for_edges(snapshot, edges) - return RiReferencesResponse(schema_version=snapshot.schema_version, data=[_edge(snapshot, edge, evidence, derivations) for edge in edges], pagination=_pagination(offset, limit, total)) + return RiReferencesResponse( + schema_version=snapshot.schema_version, + data=[_edge(snapshot, edge, evidence, derivations) for edge in edges], + pagination=_pagination(offset, limit, total), + ) @router.get( "/{snapshot_id}/assertions", response_model=RiAssertionsResponse, - responses=documented_responses(200, "Stored inferred assertions with their derivations.", {"schemaVersion": "ri.v1", "data": [], "pagination": {"offset": 0, "limit": 50, "total": 0}}, 401, 404, 422, 429, 500), + responses=documented_responses( + 200, + "Stored inferred assertions with their derivations.", + {"schemaVersion": "ri.v1", "data": [], "pagination": {"offset": 0, "limit": 50, "total": 0}}, + 401, + 404, + 422, + 429, + 500, + ), ) def list_assertions( snapshot_id: str, @@ -255,13 +319,26 @@ def list_assertions( ) -> RiAssertionsResponse: snapshot, assertions, total = service.assertions(snapshot_id, offset=offset, limit=limit) derivations = service.derivations_for_assertions(snapshot, assertions) - return RiAssertionsResponse(schema_version=snapshot.schema_version, data=[_assertion(assertion, derivations) for assertion in assertions], pagination=_pagination(offset, limit, total)) + return RiAssertionsResponse( + schema_version=snapshot.schema_version, + data=[_assertion(assertion, derivations) for assertion in assertions], + pagination=_pagination(offset, limit, total), + ) @router.get( "/{snapshot_id}/paths", response_model=RiPathsResponse, - responses=documented_responses(200, "Stored file facts with provenance.", {"schemaVersion": "ri.v1", "data": [], "pagination": {"offset": 0, "limit": 50, "total": 0}}, 401, 404, 422, 429, 500), + responses=documented_responses( + 200, + "Stored file facts with provenance.", + {"schemaVersion": "ri.v1", "data": [], "pagination": {"offset": 0, "limit": 50, "total": 0}}, + 401, + 404, + 422, + 429, + 500, + ), ) def list_paths( snapshot_id: str, @@ -271,13 +348,29 @@ def list_paths( ) -> RiPathsResponse: snapshot, nodes, total = service.paths(snapshot_id, offset=offset, limit=limit) evidence = service.evidence_for_nodes(snapshot, nodes) - return RiPathsResponse(schema_version=snapshot.schema_version, data=[RiPathResponse(path=node.stable_key.removeprefix("file:"), node=_node(snapshot, node, evidence)) for node in nodes], pagination=_pagination(offset, limit, total)) + return RiPathsResponse( + schema_version=snapshot.schema_version, + data=[ + RiPathResponse(path=node.stable_key.removeprefix("file:"), node=_node(snapshot, node, evidence)) + for node in nodes + ], + pagination=_pagination(offset, limit, total), + ) @router.get( "/{snapshot_id}/evidence", response_model=RiEvidenceResponsePage, - responses=documented_responses(200, "Stored evidence spans only.", {"schemaVersion": "ri.v1", "data": [], "pagination": {"offset": 0, "limit": 50, "total": 0}}, 401, 404, 422, 429, 500), + responses=documented_responses( + 200, + "Stored evidence spans only.", + {"schemaVersion": "ri.v1", "data": [], "pagination": {"offset": 0, "limit": 50, "total": 0}}, + 401, + 404, + 422, + 429, + 500, + ), ) def list_evidence( snapshot_id: str, @@ -297,4 +390,6 @@ def list_evidence( fact_kind, parent_id = "observation", item.observation_ref fact_id = identities[(fact_kind, parent_id)] data.append(_evidence(snapshot, item, fact_kind=fact_kind, fact_id=fact_id)) - return RiEvidenceResponsePage(schema_version=snapshot.schema_version, data=data, pagination=_pagination(offset, limit, total)) + return RiEvidenceResponsePage( + schema_version=snapshot.schema_version, data=data, pagination=_pagination(offset, limit, total) + ) diff --git a/apps/backend/app/api/routes/reports.py b/apps/backend/app/api/routes/reports.py index 8e13ca5..9860960 100644 --- a/apps/backend/app/api/routes/reports.py +++ b/apps/backend/app/api/routes/reports.py @@ -23,7 +23,7 @@ "filename": "engineering-review.json", "mediaType": "application/json", "encoding": "utf-8", - "content": "{\\n \\\"repositoryId\\\": \\\"11111111-1111-1111-1111-111111111111\\\"\\n}", + "content": '{\\n \\"repositoryId\\": \\"11111111-1111-1111-1111-111111111111\\"\\n}', } diff --git a/apps/backend/app/auth/service.py b/apps/backend/app/auth/service.py index b272fad..8c748d2 100644 --- a/apps/backend/app/auth/service.py +++ b/apps/backend/app/auth/service.py @@ -123,9 +123,7 @@ def _claim_token(self, token_id: str, now: datetime) -> bool: rather than the earlier read-then-write which two racers could both pass. """ result = self.db.execute( - update(RefreshToken) - .where(RefreshToken.id == token_id, RefreshToken.used_at.is_(None)) - .values(used_at=now) + update(RefreshToken).where(RefreshToken.id == token_id, RefreshToken.used_at.is_(None)).values(used_at=now) ) return result.rowcount == 1 diff --git a/apps/backend/app/core/config.py b/apps/backend/app/core/config.py index 050d841..151df2d 100644 --- a/apps/backend/app/core/config.py +++ b/apps/backend/app/core/config.py @@ -249,9 +249,7 @@ def resolve_ai_encryption_key(self) -> "Settings": except (binascii.Error, ValueError) as exc: raise ValueError("AI_ENCRYPTION_KEY must be a URL-safe base64-encoded 32-byte Fernet key.") from exc if len(decoded) != FERNET_KEY_BYTES: - raise ValueError( - f"AI_ENCRYPTION_KEY must decode to exactly {FERNET_KEY_BYTES} bytes (a Fernet key)." - ) + raise ValueError(f"AI_ENCRYPTION_KEY must decode to exactly {FERNET_KEY_BYTES} bytes (a Fernet key).") return self @model_validator(mode="after") diff --git a/apps/backend/app/core/rate_limit.py b/apps/backend/app/core/rate_limit.py index c2734d9..8a1082c 100644 --- a/apps/backend/app/core/rate_limit.py +++ b/apps/backend/app/core/rate_limit.py @@ -188,9 +188,7 @@ class RateLimitMiddleware(BaseHTTPMiddleware): condition is impossible to miss. """ - async def dispatch( - self, request: Request, call_next: Callable[[Request], Awaitable[Response]] - ) -> Response: + async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response: settings: Settings = request.app.state.rate_limit_settings if not settings.rate_limit_enabled: return await call_next(request) diff --git a/apps/backend/app/core/schema_sync.py b/apps/backend/app/core/schema_sync.py index 50cc093..431d737 100644 --- a/apps/backend/app/core/schema_sync.py +++ b/apps/backend/app/core/schema_sync.py @@ -150,9 +150,7 @@ def ensure_schema_in_sync(engine: Engine, *, app_env: str) -> None: lower = current or "base" existing_tables = set(sa_inspect(engine).get_table_names()) tables_by_revision = _tables_created_between(script, head, lower) - conflicts = { - table: revision for table, revision in tables_by_revision.items() if table in existing_tables - } + conflicts = {table: revision for table, revision in tables_by_revision.items() if table in existing_tables} if conflicts: recommended = _recommended_stamp(script, head, lower, existing_tables) conflict_desc = ", ".join(f"{table!r} (from {revision})" for table, revision in sorted(conflicts.items())) diff --git a/apps/backend/app/core/security_headers.py b/apps/backend/app/core/security_headers.py index 2295c87..233c9d2 100644 --- a/apps/backend/app/core/security_headers.py +++ b/apps/backend/app/core/security_headers.py @@ -28,9 +28,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): (e.g. a route needing a looser CSP) is never overridden. """ - async def dispatch( - self, request: Request, call_next: Callable[[Request], Awaitable[Response]] - ) -> Response: + async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response: response = await call_next(request) for header, value in SECURITY_HEADERS.items(): response.headers.setdefault(header, value) diff --git a/apps/backend/app/extraction/base.py b/apps/backend/app/extraction/base.py index 2da8bc0..0bf40fc 100644 --- a/apps/backend/app/extraction/base.py +++ b/apps/backend/app/extraction/base.py @@ -126,9 +126,7 @@ def logical_line_count(text: str) -> int: return 1 + text.count("\n") -def decode_source( - path: str, source: bytes, *, producer: str -) -> tuple[str | None, ExtractedDiagnostic | None]: +def decode_source(path: str, source: bytes, *, producer: str) -> tuple[str | None, ExtractedDiagnostic | None]: """Strict UTF-8 decode with RFC §6.2 binary/malformed handling. Returns ``(text, None)`` for decodable text (a zero-byte file decodes to @@ -197,9 +195,7 @@ def build_evidence( code=RI_SPAN_INVALID, category=_CATEGORY[RI_SPAN_INVALID], severity="error", - message=( - f"span {start_line}..{end_line} is not within 1..{logical_line_count}" - ), + message=(f"span {start_line}..{end_line} is not within 1..{logical_line_count}"), path=normalized, span=(start_line, end_line), ) diff --git a/apps/backend/app/extraction/manifests.py b/apps/backend/app/extraction/manifests.py index 2716e98..8b6a8bb 100644 --- a/apps/backend/app/extraction/manifests.py +++ b/apps/backend/app/extraction/manifests.py @@ -340,7 +340,7 @@ def _json_tokens(text: str) -> list[tuple[str, object, int]]: i += 1 else: start = i - while i < n and text[i] not in " \t\r\n{}[]:,\"": + while i < n and text[i] not in ' \t\r\n{}[]:,"': i += 1 tokens.append(("other", text[start:i], line)) return tokens diff --git a/apps/backend/app/extraction/pipeline.py b/apps/backend/app/extraction/pipeline.py index 3275876..4802f7c 100644 --- a/apps/backend/app/extraction/pipeline.py +++ b/apps/backend/app/extraction/pipeline.py @@ -125,21 +125,16 @@ def run( result = extractor.extract(path, source) if check_cancelled is not None: check_cancelled() - produced.append( - ProducedExtraction(extractor.name, extractor.version, result) - ) + produced.append(ProducedExtraction(extractor.name, extractor.version, result)) # Python emits a directory module but no file node. Inventory # supplies that shared entity only after successful extraction; # TypeScript already emits its own file node. if result.nodes and not any( - node.node_kind == "file" and node.stable_key == file_key - for node in result.nodes + node.node_kind == "file" and node.stable_key == file_key for node in result.nodes ): evidence = self._whole_file_evidence(path, source) if evidence is not None: - inventory_nodes.append( - self._file_node(path, evidence, self._language(path), source) - ) + inventory_nodes.append(self._file_node(path, evidence, self._language(path), source)) continue text, diagnostic = decode_source( @@ -197,9 +192,7 @@ def run( ) return tuple(produced) - def _whole_file_evidence( - self, path: str, source: bytes - ) -> ExtractedEvidence | None: + def _whole_file_evidence(self, path: str, source: bytes) -> ExtractedEvidence | None: text, _ = decode_source( path, source, diff --git a/apps/backend/app/extraction/python.py b/apps/backend/app/extraction/python.py index 1dbbc18..93bdcb3 100644 --- a/apps/backend/app/extraction/python.py +++ b/apps/backend/app/extraction/python.py @@ -173,9 +173,7 @@ def extract(self, path: str, source: bytes) -> ExtractionResult: severity="error", message="file could not be parsed as Python", path=canonical.normalize_repo_path(path), - subject=canonical.normalize_stable_key( - "file", f"file:{canonical.normalize_repo_path(path)}" - ), + subject=canonical.normalize_stable_key("file", f"file:{canonical.normalize_repo_path(path)}"), ), ) ) @@ -205,12 +203,8 @@ def extract(self, path: str, source: bytes) -> ExtractionResult: elif module_ev_diag is not None: diagnostics.append(module_ev_diag) - self._collect_imports( - tree, path, line_count, module_key, observations, diagnostics - ) - self._collect_symbols( - tree, path, line_count, nodes, observations, diagnostics - ) + self._collect_imports(tree, path, line_count, module_key, observations, diagnostics) + self._collect_symbols(tree, path, line_count, nodes, observations, diagnostics) self._collect_calls(tree, path, line_count, module_key, observations, diagnostics) self._collect_blind_spots(tree, path, line_count, diagnostics) @@ -220,16 +214,12 @@ def extract(self, path: str, source: bytes) -> ExtractionResult: diagnostics=tuple(diagnostics), ) - def _collect_imports( - self, tree, path, line_count, module_key, observations, diagnostics - ) -> None: + def _collect_imports(self, tree, path, line_count, module_key, observations, diagnostics) -> None: # Only direct module-level imports can safely act as file-wide name # bindings. A function- or block-local binding must not leak into an # unrelated call site during downstream resolution. module_binding_statements = { - id(statement) - for statement in tree.body - if isinstance(statement, (ast.Import, ast.ImportFrom)) + id(statement) for statement in tree.body if isinstance(statement, (ast.Import, ast.ImportFrom)) } for node in ast.walk(tree): names: list[str] = [] @@ -255,7 +245,10 @@ def _collect_imports( continue for name in names: ev, diag = build_evidence( - path, node.lineno, node.end_lineno or node.lineno, line_count, + path, + node.lineno, + node.end_lineno or node.lineno, + line_count, producer=self.producer, ) if ev is None: @@ -274,7 +267,10 @@ def _collect_imports( ) for specifier, imported, local in bindings: ev, diag = build_evidence( - path, node.lineno, node.end_lineno or node.lineno, line_count, + path, + node.lineno, + node.end_lineno or node.lineno, + line_count, producer=self.producer, ) if ev is None: @@ -294,9 +290,7 @@ def _collect_imports( _DEF_TYPES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef) - def _collect_symbols( - self, tree, path, line_count, nodes, observations, diagnostics - ) -> None: + def _collect_symbols(self, tree, path, line_count, nodes, observations, diagnostics) -> None: assigner = DiscriminatorAssigner() route_ordinal = 0 @@ -308,17 +302,17 @@ def visit(scope: list[str], body) -> None: base_key = symbol_stable_key(path, scope, child.name) final_key, duplicate = assigner.key(base_key) ev, diag = build_evidence( - path, child.lineno, child.end_lineno or child.lineno, - line_count, producer=self.producer, + path, + child.lineno, + child.end_lineno or child.lineno, + line_count, + producer=self.producer, ) if ev is None: if diag is not None: diagnostics.append(diag) else: - decorator_nodes = [ - (self._decorator_name(d), d) - for d in getattr(child, "decorator_list", []) - ] + decorator_nodes = [(self._decorator_name(d), d) for d in getattr(child, "decorator_list", [])] decorator_nodes = [(n, d) for n, d in decorator_nodes if n] decorators = [n for n, _ in decorator_nodes] properties = {"decorators": decorators} if decorators else None @@ -347,9 +341,11 @@ def visit(scope: list[str], body) -> None: # lines rather than only a name in `properties` (#90). for decorator_name, decorator_node in decorator_nodes: dec_ev, dec_diag = build_evidence( - path, decorator_node.lineno, + path, + decorator_node.lineno, decorator_node.end_lineno or decorator_node.lineno, - line_count, producer=self.producer, + line_count, + producer=self.producer, ) if dec_ev is None: if dec_diag is not None: @@ -367,8 +363,11 @@ def visit(scope: list[str], body) -> None: ) for route_path, route_node in self._route_paths(child): route_ev, route_diag = build_evidence( - path, route_node.lineno, route_node.end_lineno or route_node.lineno, - line_count, producer=self.producer, + path, + route_node.lineno, + route_node.end_lineno or route_node.lineno, + line_count, + producer=self.producer, ) if route_ev is None: if route_diag is not None: @@ -458,8 +457,11 @@ def emit(node: ast.Call, scope: _BindingScope) -> None: if node.func.id in {"dir", "getattr", "hasattr", "setattr", "vars"}: return evidence, diagnostic = build_evidence( - path, node.lineno, node.end_lineno or node.lineno, - line_count, producer=self.producer, + path, + node.lineno, + node.end_lineno or node.lineno, + line_count, + producer=self.producer, ) if evidence is None: if diagnostic is not None: @@ -737,9 +739,7 @@ def scan(node, scope: _BindingScope) -> None: elif isinstance(node, ast.Import): bind_import(node, scope) elif isinstance(node, ast.ClassDef): - if any( - keyword.arg == "metaclass" for keyword in node.keywords - ): + if any(keyword.arg == "metaclass" for keyword in node.keywords): # The class itself is still extracted; what a metaclass does to it # at runtime is not modelled, so say so rather than imply we know. flag(node, "metaclass is unsupported") @@ -843,5 +843,9 @@ def _route_paths(self, symbol): continue if decorator.func.attr not in _ROUTE_METHODS: continue - if decorator.args and isinstance(decorator.args[0], ast.Constant) and isinstance(decorator.args[0].value, str): + if ( + decorator.args + and isinstance(decorator.args[0], ast.Constant) + and isinstance(decorator.args[0].value, str) + ): yield decorator.args[0].value, decorator diff --git a/apps/backend/app/extraction/support_matrix.py b/apps/backend/app/extraction/support_matrix.py index d50cf41..7f44fc7 100644 --- a/apps/backend/app/extraction/support_matrix.py +++ b/apps/backend/app/extraction/support_matrix.py @@ -16,8 +16,18 @@ class LanguageSupport: ), "typescript": LanguageSupport( supported=( - "file", "module", "import", "export", "function", "class", "method", - "interface", "type", "enum", "const", "route", + "file", + "module", + "import", + "export", + "function", + "class", + "method", + "interface", + "type", + "enum", + "const", + "route", ), unsupported=("dynamic-import", "decorator", "namespace", "commonjs-require", "ambient-module"), ), diff --git a/apps/backend/app/extraction/typescript.py b/apps/backend/app/extraction/typescript.py index 26ea5df..f292c39 100644 --- a/apps/backend/app/extraction/typescript.py +++ b/apps/backend/app/extraction/typescript.py @@ -41,14 +41,14 @@ _NAMED_DECLARATIONS = { "function_declaration": "name", - "function_signature": "name", # ambient/overload signatures (no body) + "function_signature": "name", # ambient/overload signatures (no body) "generator_function_declaration": "name", "class_declaration": "name", "abstract_class_declaration": "name", "interface_declaration": "name", "type_alias_declaration": "name", "enum_declaration": "name", - "method_definition": "name", # emitted via the unified path, in class scope + "method_definition": "name", # emitted via the unified path, in class scope } @@ -107,9 +107,7 @@ def extract(self, path: str, source: bytes) -> ExtractionResult: ) ) - file_ev, file_diag = build_evidence( - path, 1, line_count, line_count, producer=self.producer, granularity="file" - ) + file_ev, file_diag = build_evidence(path, 1, line_count, line_count, producer=self.producer, granularity="file") if file_ev is not None: nodes.append( ExtractedNode( @@ -138,14 +136,10 @@ def extract(self, path: str, source: bytes) -> ExtractionResult: diagnostics.append(file_diag) observations: list[ExtractedObservation] = [] - self._collect_symbols( - tree.root_node, path, line_count, file_key, nodes, observations, diagnostics - ) + self._collect_symbols(tree.root_node, path, line_count, file_key, nodes, observations, diagnostics) self._collect_implements(tree.root_node, path, line_count, nodes, observations) self._collect_imports(tree.root_node, path, line_count, file_key, observations) - self._collect_routes( - tree.root_node, path, line_count, file_key, nodes, observations, diagnostics - ) + self._collect_routes(tree.root_node, path, line_count, file_key, nodes, observations, diagnostics) self._collect_calls(tree.root_node, path, line_count, file_key, observations) self._collect_blind_spots(tree.root_node, path, line_count, diagnostics) return ExtractionResult( @@ -163,15 +157,21 @@ def walk(node): if source_node is not None: literal = self._node_text(source_node, source).strip("'\"`") ev, _ = build_evidence( - path, node.start_point[0] + 1, node.end_point[0] + 1, - line_count, producer=self.producer, + path, + node.start_point[0] + 1, + node.end_point[0] + 1, + line_count, + producer=self.producer, ) if ev is not None: observations.append( ExtractedObservation( - observed_kind="import", subject_kind="file", - subject_key=file_key, referent_text=literal, - ordinal=_UNASSIGNED_ORDINAL, evidence=ev, + observed_kind="import", + subject_kind="file", + subject_key=file_key, + referent_text=literal, + ordinal=_UNASSIGNED_ORDINAL, + evidence=ev, ) ) if node.type == "import_statement": @@ -206,8 +206,7 @@ def walk(node): for symbol in symbols.values() if symbol.name == class_name and any( - evidence.start_line == start_line - and evidence.end_line == end_line + evidence.start_line == start_line and evidence.end_line == end_line for evidence in symbol.evidence ) ] @@ -246,9 +245,7 @@ def walk(node): walk(root) - def _collect_import_bindings( - self, statement, specifier, path, line_count, file_key, source, observations - ) -> None: + def _collect_import_bindings(self, statement, specifier, path, line_count, file_key, source, observations) -> None: """Record direct import aliases as resolver inputs without resolving them.""" clause = next((child for child in statement.named_children if child.type == "import_clause"), None) @@ -257,8 +254,11 @@ def _collect_import_bindings( def emit(imported, local, node): evidence, _ = build_evidence( - path, node.start_point[0] + 1, node.end_point[0] + 1, - line_count, producer=self.producer, + path, + node.start_point[0] + 1, + node.end_point[0] + 1, + line_count, + producer=self.producer, ) if evidence is not None: observations.append( @@ -291,9 +291,7 @@ def emit(imported, local, node): binding, ) - def _collect_routes( - self, root, path, line_count, file_key, nodes, observations, diagnostics - ) -> None: + def _collect_routes(self, root, path, line_count, file_key, nodes, observations, diagnostics) -> None: """Emit a ``route`` observation per confirmed react-router path literal. Only two contexts count: a ``path`` key inside an argument to a router @@ -335,8 +333,11 @@ def emit(node, literal, handler_referent=None, handler_node=None): return seen.add(node.id) ev, _ = build_evidence( - path, node.start_point[0] + 1, node.end_point[0] + 1, - line_count, producer=self.producer, + path, + node.start_point[0] + 1, + node.end_point[0] + 1, + line_count, + producer=self.producer, ) if ev is not None: route_ordinal += 1 @@ -356,9 +357,12 @@ def emit(node, literal, handler_referent=None, handler_node=None): ) observations.append( ExtractedObservation( - observed_kind="route", subject_kind="symbol", - subject_key=route_key, referent_text=literal, - ordinal=_UNASSIGNED_ORDINAL, evidence=ev, + observed_kind="route", + subject_kind="symbol", + subject_key=route_key, + referent_text=literal, + ordinal=_UNASSIGNED_ORDINAL, + evidence=ev, ) ) if handler_referent: @@ -447,8 +451,7 @@ def walk(node): if node.type == "call_expression": fn = node.child_by_field_name("function") arguments = node.child_by_field_name("arguments") - if (fn is not None and arguments is not None - and self._node_text(fn, source) in _ROUTER_FACTORIES): + if fn is not None and arguments is not None and self._node_text(fn, source) in _ROUTER_FACTORIES: # createBrowserRouter(routes, opts?) — only the first argument # is the route table. The options object also accepts a `path` # (e.g. basename config), which is not a route. @@ -457,8 +460,7 @@ def walk(node): collect_route_table(route_table) elif node.type in ("jsx_self_closing_element", "jsx_opening_element"): name_node = node.child_by_field_name("name") - if (name_node is not None - and self._node_text(name_node, source) in _ROUTE_ELEMENTS): + if name_node is not None and self._node_text(name_node, source) in _ROUTE_ELEMENTS: path_attribute = None handler_referent = None handler_node = None @@ -466,15 +468,13 @@ def walk(node): if child.type != "jsx_attribute": continue parts = child.named_children - if (len(parts) > 1 - and self._node_text(parts[0], source) == "path"): + if len(parts) > 1 and self._node_text(parts[0], source) == "path": path_literal = jsx_path_literal(parts[1]) if path_literal is None: flag_dynamic_path(parts[1]) else: path_attribute = child - elif (len(parts) > 1 - and self._node_text(parts[0], source) == "element"): + elif len(parts) > 1 and self._node_text(parts[0], source) == "element": jsx_handler = self._jsx_handler(parts[1], source) if jsx_handler is not None: handler_referent, handler_node = jsx_handler @@ -561,8 +561,11 @@ def walk(node, shadowed: frozenset[str] = frozenset()): # construct, so its diagnostic is the only emitted fact. if function_name != "require": evidence, _ = build_evidence( - path, node.start_point[0] + 1, node.end_point[0] + 1, - line_count, producer=self.producer, + path, + node.start_point[0] + 1, + node.end_point[0] + 1, + line_count, + producer=self.producer, ) if evidence is not None: observations.append( @@ -599,8 +602,11 @@ def _collect_blind_spots(self, root, path, line_count, diagnostics) -> None: def flag(node, message): diagnostics.append( ExtractedDiagnostic( - code=RI_EXT_UNSUPPORTED, category="unsupported construct", - severity="info", message=message, path=normalized, + code=RI_EXT_UNSUPPORTED, + category="unsupported construct", + severity="info", + message=message, + path=normalized, span=(node.start_point[0] + 1, node.end_point[0] + 1), subject=file_subject, ) @@ -630,7 +636,7 @@ def walk(node): walk(root) def _node_text(self, node, source: bytes) -> str: - return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace") + return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace") def _is_exported(self, node) -> bool: return node.parent is not None and node.parent.type == "export_statement" @@ -643,10 +649,7 @@ def _default_export_names(self, root, source: bytes) -> set[str]: if statement.type != "export_statement": continue if any(child.type == "default" for child in statement.children): - target = ( - statement.child_by_field_name("declaration") - or statement.child_by_field_name("value") - ) + target = statement.child_by_field_name("declaration") or statement.child_by_field_name("value") if target is not None: name = target.child_by_field_name("name") or target if name.type in ("identifier", "type_identifier"): @@ -659,11 +662,7 @@ def _default_export_names(self, root, source: bytes) -> set[str]: continue name = specifier.child_by_field_name("name") alias = specifier.child_by_field_name("alias") - if ( - name is not None - and alias is not None - and self._node_text(alias, source) == "default" - ): + if name is not None and alias is not None and self._node_text(alias, source) == "default": names.add(self._node_text(name, source)) return names @@ -673,15 +672,9 @@ def _is_top_level(self, node) -> bool: return False if parent.type == "program": return True - return ( - parent.type == "export_statement" - and parent.parent is not None - and parent.parent.type == "program" - ) + return parent.type == "export_statement" and parent.parent is not None and parent.parent.type == "program" - def _collect_symbols( - self, root, path, line_count, file_key, nodes, observations, diagnostics - ) -> None: + def _collect_symbols(self, root, path, line_count, file_key, nodes, observations, diagnostics) -> None: assigner = DiscriminatorAssigner() source = root.text # bytes of the whole tree default_export_names = self._default_export_names(root, source) @@ -694,8 +687,11 @@ def emit(name_node, decl_node, scope, exported): key = canonical.normalize_stable_key("symbol", final_key) # tree-sitter rows are 0-based; RFC spans are 1-based inclusive. ev, diag = build_evidence( - path, decl_node.start_point[0] + 1, decl_node.end_point[0] + 1, - line_count, producer=self.producer, + path, + decl_node.start_point[0] + 1, + decl_node.end_point[0] + 1, + line_count, + producer=self.producer, ) if ev is None: if diag is not None: @@ -709,28 +705,36 @@ def emit(name_node, decl_node, scope, exported): properties["default_export"] = True nodes.append( ExtractedNode( - node_kind="symbol", stable_key=key, name=name, - language="typescript", evidence=(ev,), + node_kind="symbol", + stable_key=key, + name=name, + language="typescript", + evidence=(ev,), properties=properties, ) ) observations.append( ExtractedObservation( - observed_kind="definition", subject_kind="symbol", - subject_key=key, referent_text=None, - ordinal=_UNASSIGNED_ORDINAL, evidence=ev, + observed_kind="definition", + subject_kind="symbol", + subject_key=key, + referent_text=None, + ordinal=_UNASSIGNED_ORDINAL, + evidence=ev, ) ) if duplicate: diagnostics.append( ExtractedDiagnostic( - code=RI_KEY_DUP_SYMBOL, category="duplicate symbol", + code=RI_KEY_DUP_SYMBOL, + category="duplicate symbol", severity="info", # The key lives in `subject`, the field meant for it; # repeating it here would put source-derived text in # `message`, which RFC §13 reserves from content. message="duplicate symbol name resolved with a discriminator", - path=canonical.normalize_repo_path(path), subject=key, + path=canonical.normalize_repo_path(path), + subject=key, ) ) return name diff --git a/apps/backend/app/graph/dependency_graph.py b/apps/backend/app/graph/dependency_graph.py index 6876476..2140aa9 100644 --- a/apps/backend/app/graph/dependency_graph.py +++ b/apps/backend/app/graph/dependency_graph.py @@ -73,7 +73,9 @@ def build(self, record: RepositoryRecord) -> DependencyGraphResponse: item for item in self.snapshots.db.scalars( select(RiDiagnostic) - .where(RiDiagnostic.snapshot_id == snapshot.snapshot_id, RiDiagnostic.code.in_(_RELEVANT_DIAGNOSTIC_CODES)) + .where( + RiDiagnostic.snapshot_id == snapshot.snapshot_id, RiDiagnostic.code.in_(_RELEVANT_DIAGNOSTIC_CODES) + ) .order_by(RiDiagnostic.path, RiDiagnostic.code, RiDiagnostic.id) ) if item.path and posixpath.basename(item.path) in _MANIFEST_FILENAMES diff --git a/apps/backend/app/insights/service.py b/apps/backend/app/insights/service.py index 4b0115c..5e78ea9 100644 --- a/apps/backend/app/insights/service.py +++ b/apps/backend/app/insights/service.py @@ -84,9 +84,7 @@ def build(self, record: RepositoryRecord) -> RepositoryInsightsResponse: if language is not None } evidence_count = ( - self.snapshots.db.scalar( - select(func.count(RiEvidence.id)).where(RiEvidence.snapshot_id == snapshot_id) - ) + self.snapshots.db.scalar(select(func.count(RiEvidence.id)).where(RiEvidence.snapshot_id == snapshot_id)) or 0 ) evidence_extractor_counts = { @@ -295,7 +293,6 @@ def metric( for key, value in sorted(diagnostic_code_counts.items()) ], languages=[ - InsightBreakdown(key=key, label=key, value=value) - for key, value in sorted(language_counts.items()) + InsightBreakdown(key=key, label=key, value=value) for key, value in sorted(language_counts.items()) ], ) diff --git a/apps/backend/app/intelligence/canonical.py b/apps/backend/app/intelligence/canonical.py index 327f6e0..1a0f7f1 100644 --- a/apps/backend/app/intelligence/canonical.py +++ b/apps/backend/app/intelligence/canonical.py @@ -85,9 +85,7 @@ def _normalize(value: Any) -> Any: for key, item in value.items(): normalized_key = nfc(str(key)) if normalized_key in normalized: - raise CanonicalizationError( - f"object keys collide after Unicode normalization: {normalized_key!r}" - ) + raise CanonicalizationError(f"object keys collide after Unicode normalization: {normalized_key!r}") normalized[normalized_key] = _normalize(item) return normalized if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): @@ -162,9 +160,7 @@ def prepare(node: Any, key: str | None = None) -> Any: return prepared if isinstance(node, Sequence) and not isinstance(node, (str, bytes, bytearray)): if key not in set_array_keys and key not in ordered_array_keys: - raise CanonicalizationError( - f"{context} array {key!r} has no declared set/order semantics" - ) + raise CanonicalizationError(f"{context} array {key!r} has no declared set/order semantics") elements = [prepare(item) for item in node] if key in set_array_keys: by_bytes = {canonical_json_bytes(element): element for element in elements} @@ -420,9 +416,7 @@ def prepare(node: Any, key: str | None) -> Any: for raw_key, value in node.items(): normalized_key = nfc(str(raw_key)) if normalized_key in prepared: - raise CanonicalizationError( - f"config keys collide after Unicode normalization: {normalized_key!r}" - ) + raise CanonicalizationError(f"config keys collide after Unicode normalization: {normalized_key!r}") prepared[normalized_key] = prepare(value, normalized_key) return prepared if isinstance(node, Sequence) and not isinstance(node, (str, bytes, bytearray)): @@ -504,9 +498,7 @@ def order_evidence(evidence_records: Sequence[Mapping[str, Any]]) -> list[dict[s ) -def canonical_node_record( - node: Mapping[str, Any], *, schema_version: str = SCHEMA_VERSION -) -> dict[str, Any]: +def canonical_node_record(node: Mapping[str, Any], *, schema_version: str = SCHEMA_VERSION) -> dict[str, Any]: record: dict[str, Any] = { "kind": "node", "node_kind": node["node_kind"], @@ -524,9 +516,7 @@ def canonical_node_record( return record -def canonical_edge_record( - edge: Mapping[str, Any], *, schema_version: str = SCHEMA_VERSION -) -> dict[str, Any]: +def canonical_edge_record(edge: Mapping[str, Any], *, schema_version: str = SCHEMA_VERSION) -> dict[str, Any]: return { "kind": "edge", "edge_id": edge["edge_id"], @@ -542,9 +532,7 @@ def canonical_edge_record( } -def canonical_assertion_record( - assertion: Mapping[str, Any], *, schema_version: str = SCHEMA_VERSION -) -> dict[str, Any]: +def canonical_assertion_record(assertion: Mapping[str, Any], *, schema_version: str = SCHEMA_VERSION) -> dict[str, Any]: return { "kind": "assertion", "assertion_id": assertion["assertion_id"], @@ -610,9 +598,7 @@ def _consolidate_edges( existing = grouped[triple] for field in ("edge_id", "truth_class", "producer", "producer_version"): if existing[field] != record[field]: - raise CanonicalizationError( - f"inconsistent edge field {field!r} for duplicate triple {triple}" - ) + raise CanonicalizationError(f"inconsistent edge field {field!r} for duplicate triple {triple}") existing["evidence"] = order_evidence(existing["evidence"] + record["evidence"]) existing["derived_from"] = sort_derived_from(existing["derived_from"] + record["derived_from"]) return list(grouped.values()) diff --git a/apps/backend/app/intelligence/classification.py b/apps/backend/app/intelligence/classification.py index 9f0da5c..c2cf02e 100644 --- a/apps/backend/app/intelligence/classification.py +++ b/apps/backend/app/intelligence/classification.py @@ -138,11 +138,7 @@ def classify( if self.producer not in set(snapshot.producer_version_set): raise ValueError(f"snapshot producer_version_set is missing {self.producer!r}") - nodes = list( - self.store.db.scalars( - select(RiNode).where(RiNode.snapshot_id == snapshot.snapshot_id) - ) - ) + nodes = list(self.store.db.scalars(select(RiNode).where(RiNode.snapshot_id == snapshot.snapshot_id))) injected_keys = { edge.object_key for edge in self.store.db.scalars( diff --git a/apps/backend/app/intelligence/models.py b/apps/backend/app/intelligence/models.py index 11490e8..dcc1023 100644 --- a/apps/backend/app/intelligence/models.py +++ b/apps/backend/app/intelligence/models.py @@ -25,7 +25,9 @@ ] SymbolKind = Literal["function", "class", "interface", "type", "enum", "constant", "route"] GraphNodeType = Literal["repository", "module", "file", "symbol", "dependency"] -RelationshipType = Literal["imports", "calls", "extends", "implements", "depends_on", "contains", "references", "exports"] +RelationshipType = Literal[ + "imports", "calls", "extends", "implements", "depends_on", "contains", "references", "exports" +] DependencyType = Literal["production", "development", "peer", "optional", "multiple"] diff --git a/apps/backend/app/intelligence/query_service.py b/apps/backend/app/intelligence/query_service.py index ef4b4f7..f97677c 100644 --- a/apps/backend/app/intelligence/query_service.py +++ b/apps/backend/app/intelligence/query_service.py @@ -38,9 +38,7 @@ #: What ``architecture_facts`` actually loads: the union of both consumers. #: Each consumer still filters again in Python for what it renders, so widening #: this set never widens a response — it only prevents starving a consumer. -ARCHITECTURE_FACT_PREDICATES = ( - frozenset(ARCHITECTURE_RELATIONSHIP_EDGE_TYPES) | AUTHENTICATION_EXPLANATION_PREDICATES -) +ARCHITECTURE_FACT_PREDICATES = frozenset(ARCHITECTURE_RELATIONSHIP_EDGE_TYPES) | AUTHENTICATION_EXPLANATION_PREDICATES ARCHITECTURE_DIAGNOSTIC_CODES = frozenset({"RI-RES-UNRESOLVED", "RI-RES-AMBIGUOUS"}) #: Diagnostic codes the authentication explanation renders. AUTHENTICATION_DIAGNOSTIC_CODES = frozenset( @@ -485,11 +483,7 @@ def product_projection(self, repository_id: str) -> ProductSnapshotProjection: for node in nodes if node.node_kind == "file" and node.stable_key.startswith("file:") ) - dependencies = tuple( - self._project_dependency(node) - for node in nodes - if node.node_kind == "dependency" - ) + dependencies = tuple(self._project_dependency(node) for node in nodes if node.node_kind == "dependency") language_counts = Counter(file.language for file in files if file.language) primary_language = ( {"python": "Python", "typescript": "TypeScript"}.get( @@ -509,11 +503,7 @@ def product_projection(self, repository_id: str) -> ProductSnapshotProjection: } dependency_names = {item.name.lower() for item in dependencies} frameworks = tuple( - sorted( - framework - for name, framework in framework_by_dependency.items() - if name in dependency_names - ) + sorted(framework for name, framework in framework_by_dependency.items() if name in dependency_names) ) entry_points = tuple(sorted(file.path for file in files if file.role == "entrypoint")) modules = self._project_modules(files) @@ -541,9 +531,7 @@ def product_projection(self, repository_id: str) -> ProductSnapshotProjection: routes = tuple( SnapshotRouteFact( path=observation.referent_text or "Not available", - evidence_paths=tuple( - sorted({item.path for item in route_evidence.get(observation.id, [])}) - ), + evidence_paths=tuple(sorted({item.path for item in route_evidence.get(observation.id, [])})), ) for observation in route_observations ) @@ -618,14 +606,10 @@ def _project_modules(files: tuple[SnapshotFileFact, ...]) -> tuple[SnapshotModul result: list[SnapshotModuleFact] = [] for key, members in sorted(grouped.items()): meaningful_roles = [ - item.role - for item in members - if item.role not in (None, "unknown", "documentation", "test") + item.role for item in members if item.role not in (None, "unknown", "documentation", "test") ] role = ( - Counter(meaningful_roles).most_common(1)[0][0] - if meaningful_roles - else (members[0].role or "unknown") + Counter(meaningful_roles).most_common(1)[0][0] if meaningful_roles else (members[0].role or "unknown") ) result.append( SnapshotModuleFact( @@ -656,7 +640,9 @@ def _project_dependency(node: RiNode) -> SnapshotDependencyFact: start_line=raw.get("start_line") if isinstance(raw.get("start_line"), int) else None, end_line=raw.get("end_line") if isinstance(raw.get("end_line"), int) else None, extractor=raw.get("extractor") if isinstance(raw.get("extractor"), str) else None, - extractor_version=raw.get("extractor_version") if isinstance(raw.get("extractor_version"), str) else None, + extractor_version=raw.get("extractor_version") + if isinstance(raw.get("extractor_version"), str) + else None, ) ) return SnapshotDependencyFact( @@ -777,7 +763,9 @@ def derivations_for_edges(self, snapshot: RiSnapshot, edges: list[RiEdge]) -> di return {} rows = self.db.scalars( select(RiDerivation) - .where(RiDerivation.snapshot_id == snapshot.snapshot_id, RiDerivation.edge_ref.in_([edge.id for edge in edges])) + .where( + RiDerivation.snapshot_id == snapshot.snapshot_id, RiDerivation.edge_ref.in_([edge.id for edge in edges]) + ) .order_by(RiDerivation.ref_kind, RiDerivation.ref_identity, RiDerivation.id) ).all() grouped: dict[int, list[RiDerivation]] = defaultdict(list) @@ -786,7 +774,9 @@ def derivations_for_edges(self, snapshot: RiSnapshot, edges: list[RiEdge]) -> di grouped[row.edge_ref].append(row) return grouped - def derivations_for_assertions(self, snapshot: RiSnapshot, assertions: list[RiAssertion]) -> dict[int, list[RiDerivation]]: + def derivations_for_assertions( + self, snapshot: RiSnapshot, assertions: list[RiAssertion] + ) -> dict[int, list[RiDerivation]]: if not assertions: return {} rows = self.db.scalars( @@ -803,22 +793,30 @@ def derivations_for_assertions(self, snapshot: RiSnapshot, assertions: list[RiAs grouped[row.assertion_ref].append(row) return grouped - def fact_identity_for_evidence(self, snapshot: RiSnapshot, evidence: list[RiEvidence]) -> dict[tuple[str, int], str]: + def fact_identity_for_evidence( + self, snapshot: RiSnapshot, evidence: list[RiEvidence] + ) -> dict[tuple[str, int], str]: node_refs = [item.node_ref for item in evidence if item.node_ref is not None] edge_refs = [item.edge_ref for item in evidence if item.edge_ref is not None] observation_refs = [item.observation_ref for item in evidence if item.observation_ref is not None] identities: dict[tuple[str, int], str] = {} if node_refs: - for node in self.db.scalars(select(RiNode).where(RiNode.snapshot_id == snapshot.snapshot_id, RiNode.id.in_(node_refs))): + for node in self.db.scalars( + select(RiNode).where(RiNode.snapshot_id == snapshot.snapshot_id, RiNode.id.in_(node_refs)) + ): identities[("node", node.id)] = node.stable_key if edge_refs: - for edge in self.db.scalars(select(RiEdge).where(RiEdge.snapshot_id == snapshot.snapshot_id, RiEdge.id.in_(edge_refs))): + for edge in self.db.scalars( + select(RiEdge).where(RiEdge.snapshot_id == snapshot.snapshot_id, RiEdge.id.in_(edge_refs)) + ): identities[("edge", edge.id)] = edge.edge_id if observation_refs: from app.models.snapshot import RiObservation for observation in self.db.scalars( - select(RiObservation).where(RiObservation.snapshot_id == snapshot.snapshot_id, RiObservation.id.in_(observation_refs)) + select(RiObservation).where( + RiObservation.snapshot_id == snapshot.snapshot_id, RiObservation.id.in_(observation_refs) + ) ): identities[("observation", observation.id)] = observation.observation_id return identities diff --git a/apps/backend/app/intelligence/resolution.py b/apps/backend/app/intelligence/resolution.py index afd2581..10eed11 100644 --- a/apps/backend/app/intelligence/resolution.py +++ b/apps/backend/app/intelligence/resolution.py @@ -105,23 +105,15 @@ def resolve( raise ValueError(f"snapshot producer_version_set is missing {self.producer!r}") self._snapshot = snapshot - nodes = list( - self.store.db.scalars( - select(RiNode).where(RiNode.snapshot_id == snapshot.snapshot_id) - ) - ) + nodes = list(self.store.db.scalars(select(RiNode).where(RiNode.snapshot_id == snapshot.snapshot_id))) nodes_by_key = {node.stable_key: node for node in nodes} observations = sorted( - self.store.db.scalars( - select(RiObservation).where(RiObservation.snapshot_id == snapshot.snapshot_id) - ), + self.store.db.scalars(select(RiObservation).where(RiObservation.snapshot_id == snapshot.snapshot_id)), key=lambda item: item.observation_id, ) evidence_by_observation: dict[int, list[RiEvidence]] = defaultdict(list) evidence_by_node: dict[int, list[RiEvidence]] = defaultdict(list) - for evidence in self.store.db.scalars( - select(RiEvidence).where(RiEvidence.snapshot_id == snapshot.snapshot_id) - ): + for evidence in self.store.db.scalars(select(RiEvidence).where(RiEvidence.snapshot_id == snapshot.snapshot_id)): self._check_cancelled(check_cancelled) if evidence.observation_ref is not None: evidence_by_observation[evidence.observation_ref].append(evidence) @@ -173,9 +165,7 @@ def resolve( for input_ in inputs_by_kind["call"]: self._check_cancelled(check_cancelled) if self._reference_input_key(input_) in shadowed_calls: - added, diagnosed = self._unresolved( - input_, "calls target is shadowed by a local binding" - ) + added, diagnosed = self._unresolved(input_, "calls target is shadowed by a local binding") else: added, diagnosed = self._resolve_reference( input_, nodes_by_key, evidence_by_node, bindings_by_file, predicate="calls" @@ -227,9 +217,7 @@ def _check_cancelled(check_cancelled: Callable[[], None] | None) -> None: if check_cancelled is not None: check_cancelled() - def _resolve_definition( - self, input_: _ObservedInput, nodes_by_key: dict[str, RiNode] - ) -> tuple[int, int]: + def _resolve_definition(self, input_: _ObservedInput, nodes_by_key: dict[str, RiNode]) -> tuple[int, int]: observation = input_.observation symbol = nodes_by_key.get(observation.subject_key) if symbol is None or symbol.node_kind != "symbol" or "::" not in symbol.stable_key: @@ -280,10 +268,15 @@ def _resolve_reference( subject = observed_subject if subject is None or subject.node_kind != "symbol": subject = self._containing_symbol(input_, nodes_by_key, evidence_by_node) - if subject is None and observed_subject is not None and observed_subject.node_kind in { - "file", - "module", - }: + if ( + subject is None + and observed_subject is not None + and observed_subject.node_kind + in { + "file", + "module", + } + ): subject = observed_subject if subject is None: return self._unresolved(input_, f"{predicate} source symbol is absent from the snapshot") @@ -302,9 +295,7 @@ def _resolve_reference( ), ) - def _resolve_dependency( - self, input_: _ObservedInput, nodes_by_key: dict[str, RiNode] - ) -> tuple[int, int]: + def _resolve_dependency(self, input_: _ObservedInput, nodes_by_key: dict[str, RiNode]) -> tuple[int, int]: dependency = nodes_by_key.get(input_.observation.subject_key) repository = nodes_by_key.get("repo:root") if dependency is None or dependency.node_kind != "dependency" or repository is None: @@ -377,11 +368,10 @@ def _import_candidates( # so the module file resolves without reparsing the source. for binding_specifier, imported, _local in bindings: if binding_specifier and f"{binding_specifier}.{imported}" == specifier: - file_candidates.update( - self._python_absolute_file_candidates(binding_specifier) - ) + file_candidates.update(self._python_absolute_file_candidates(binding_specifier)) files = [ - node for key, node in nodes_by_key.items() + node + for key, node in nodes_by_key.items() if key in {f"file:{path}" for path in file_candidates} and node.node_kind == "file" ] if files: @@ -393,10 +383,7 @@ def _import_candidates( node for node in nodes_by_key.values() if node.node_kind == "dependency" - and ( - node.stable_key == f"dep:npm:{package}" - or node.stable_key == f"dep:pypi:{normalized_python}" - ) + and (node.stable_key == f"dep:npm:{package}" or node.stable_key == f"dep:pypi:{normalized_python}") ] @staticmethod @@ -425,10 +412,16 @@ def _relative_file_candidates(specifier: str, source_path: str) -> set[str]: if root.endswith((".ts", ".tsx", ".py")): paths.add(root) else: - paths.update({ - f"{root}.ts", f"{root}.tsx", f"{root}.py", - f"{root}/index.ts", f"{root}/index.tsx", f"{root}/__init__.py", - }) + paths.update( + { + f"{root}.ts", + f"{root}.tsx", + f"{root}.py", + f"{root}/index.ts", + f"{root}/index.tsx", + f"{root}/__init__.py", + } + ) return paths @staticmethod @@ -494,7 +487,8 @@ def _reference_candidates( bound_candidates.append(direct_target) else: bound_candidates.extend( - node for node in nodes_by_key.values() + node + for node in nodes_by_key.values() if node.node_kind == "symbol" and node.name == imported and node.stable_key.startswith(f"{path}::") @@ -560,9 +554,7 @@ def _unresolved(self, input_: _ObservedInput, message: str) -> tuple[int, int]: self._diagnostic(input_, RI_RES_UNRESOLVED, "unresolved reference", message) return 0, 1 - def _ambiguous( - self, input_: _ObservedInput, message: str, candidates: tuple[str, ...] - ) -> tuple[int, int]: + def _ambiguous(self, input_: _ObservedInput, message: str, candidates: tuple[str, ...]) -> tuple[int, int]: self._diagnostic(input_, RI_RES_AMBIGUOUS, "ambiguous resolution", message, candidates) return 0, 1 diff --git a/apps/backend/app/intelligence/snapshot_store.py b/apps/backend/app/intelligence/snapshot_store.py index ceb07d2..ceb4d66 100644 --- a/apps/backend/app/intelligence/snapshot_store.py +++ b/apps/backend/app/intelligence/snapshot_store.py @@ -123,9 +123,11 @@ def _stored_snapshot_state(session: Session, snapshot_id: str | None) -> str | N return state if snapshot in session.new: return snapshot.state - return session.connection().execute( - select(RiSnapshot.state).where(RiSnapshot.snapshot_id == snapshot_id) - ).scalar_one_or_none() + return ( + session.connection() + .execute(select(RiSnapshot.state).where(RiSnapshot.snapshot_id == snapshot_id)) + .scalar_one_or_none() + ) @event.listens_for(Session, "before_flush") @@ -134,9 +136,11 @@ def _guard_completed_snapshots(session: Session, _flush_context, _instances) -> if isinstance(obj, RiSnapshot): persisted_state = _persisted_state(obj) if persisted_state is None and obj not in session.new: - persisted_state = session.connection().execute( - select(RiSnapshot.state).where(RiSnapshot.snapshot_id == obj.snapshot_id) - ).scalar_one_or_none() + persisted_state = ( + session.connection() + .execute(select(RiSnapshot.state).where(RiSnapshot.snapshot_id == obj.snapshot_id)) + .scalar_one_or_none() + ) if persisted_state == "completed": raise SnapshotImmutableError("A completed snapshot is immutable and cannot be modified.") if persisted_state is not None and obj.state != persisted_state: @@ -1016,7 +1020,9 @@ def _validate(self, snapshot: RiSnapshot, facts: "_Facts") -> None: # Every observation has exactly one evidence record (RFC §6.4). for observation in facts.observations: if len(evidence_by_observation.get(observation.id, [])) != 1: - raise SnapshotSealError(f"observation {observation.observation_id} must have exactly one evidence record") + raise SnapshotSealError( + f"observation {observation.observation_id} must have exactly one evidence record" + ) if node_kind_by_key.get(observation.subject_key) != observation.subject_kind: raise SnapshotSealError(f"observation {observation.observation_id} has an invalid subject") evidence = evidence_by_observation[observation.id][0] diff --git a/apps/backend/app/models/snapshot.py b/apps/backend/app/models/snapshot.py index 98305c9..c5de115 100644 --- a/apps/backend/app/models/snapshot.py +++ b/apps/backend/app/models/snapshot.py @@ -49,6 +49,7 @@ from app.models.base import Base from app.models.repository import _hex_only_sql + def _utcnow() -> datetime: return datetime.now(UTC) @@ -325,8 +326,7 @@ class RiEvidence(Base): # (RFC §6.2). The upper bound is what stops a post-insert mutation from # stretching a span past the file it cites. CheckConstraint( - "start_line >= 1 AND end_line >= start_line AND " - "logical_line_count >= 1 AND end_line <= logical_line_count", + "start_line >= 1 AND end_line >= start_line AND logical_line_count >= 1 AND end_line <= logical_line_count", name="ck_ri_evidence_span", ), CheckConstraint("granularity IN ('span', 'file')", name="ck_ri_evidence_granularity"), diff --git a/apps/backend/app/parsers/repository_parser.py b/apps/backend/app/parsers/repository_parser.py index 4d226fe..4c23dec 100644 --- a/apps/backend/app/parsers/repository_parser.py +++ b/apps/backend/app/parsers/repository_parser.py @@ -73,9 +73,7 @@ def __init__(self, max_file_count: int, file_count: int) -> None: class RepositoryParser: - def parse( - self, root: Path, *, max_file_count: int | None = None - ) -> tuple[list[FileTreeNode], RepositoryMeta, int]: + def parse(self, root: Path, *, max_file_count: int | None = None) -> tuple[list[FileTreeNode], RepositoryMeta, int]: if max_file_count is not None: self._enforce_file_count(root, max_file_count, [0]) tree = self._build_tree(root, root) diff --git a/apps/backend/app/reports/builders.py b/apps/backend/app/reports/builders.py index 52b92d1..29330b3 100644 --- a/apps/backend/app/reports/builders.py +++ b/apps/backend/app/reports/builders.py @@ -121,10 +121,7 @@ def build_architecture_document(architecture: ArchitectureResponse) -> ReportDoc heading="Layers", table=Table( headers=["Layer", "Order", "Components"], - rows=[ - [layer.name, str(layer.order), str(len(layer.nodes))] - for layer in architecture.detected_layers - ], + rows=[[layer.name, str(layer.order), str(len(layer.nodes))] for layer in architecture.detected_layers], ), ), Section( @@ -207,10 +204,7 @@ def build_dependencies_document(dependencies: DependencyGraphResponse, repositor paragraphs=[] if dependencies.nodes else ["No dependencies were detected."], table=Table( headers=["Name", "Version", "Type"], - rows=[ - [node.name, node.version or "unknown", node.type] - for node in dependencies.nodes - ], + rows=[[node.name, node.version or "unknown", node.type] for node in dependencies.nodes], ) if dependencies.nodes else None, diff --git a/apps/backend/app/reports/renderers.py b/apps/backend/app/reports/renderers.py index 0df4379..904b664 100644 --- a/apps/backend/app/reports/renderers.py +++ b/apps/backend/app/reports/renderers.py @@ -86,8 +86,7 @@ def _render_section_html(section: Section) -> str: if section.table: head = "".join(f"{escape(header)}" for header in section.table.headers) body = "".join( - "" + "".join(f"{escape(cell)}" for cell in row) + "" - for row in section.table.rows + "" + "".join(f"{escape(cell)}" for cell in row) + "" for row in section.table.rows ) parts.append(f"{head}{body}
") if section.bullets: diff --git a/apps/backend/app/review/review_service.py b/apps/backend/app/review/review_service.py index 65ffe3d..1831448 100644 --- a/apps/backend/app/review/review_service.py +++ b/apps/backend/app/review/review_service.py @@ -73,9 +73,7 @@ #: Codes whose category is source extraction, used to state that category's #: assessment from extraction evidence rather than from unrelated diagnostics. -_SOURCE_EXTRACTION_CODES = frozenset( - code for code, rule in _RULES.items() if rule[0] == "source_extraction" -) +_SOURCE_EXTRACTION_CODES = frozenset(code for code, rule in _RULES.items() if rule[0] == "source_extraction") _CATEGORY_LABELS: dict[ReviewCategoryId, str] = { "architecture_boundaries": "Architecture and boundaries", @@ -362,9 +360,7 @@ def _evidence_by_fact( ).all() for evidence in rows: fact_id = identity.get( - evidence.observation_ref - if column is RiEvidence.observation_ref - else evidence.node_ref + evidence.observation_ref if column is RiEvidence.observation_ref else evidence.node_ref ) if fact_id is not None: grouped[fact_id].append(evidence) @@ -431,14 +427,10 @@ def _support_for( or evidence.end_line != diagnostic.span_end_line ): continue - return _SupportedEvidence( - fact_id=fact_id, evidence=evidence, support_status="supported" - ) + return _SupportedEvidence(fact_id=fact_id, evidence=evidence, support_status="supported") if evidence.granularity != "file": continue - return _SupportedEvidence( - fact_id=fact_id, evidence=evidence, support_status="file_scoped" - ) + return _SupportedEvidence(fact_id=fact_id, evidence=evidence, support_status="file_scoped") return None def _categories( @@ -448,22 +440,16 @@ def _categories( diagnostics: list[RiDiagnostic], ) -> list[ReviewCategoryAssessment]: finding_counts = Counter(finding.category for finding in findings) - has_extraction_diagnostics = any( - diagnostic.code in _SOURCE_EXTRACTION_CODES for diagnostic in diagnostics - ) + has_extraction_diagnostics = any(diagnostic.code in _SOURCE_EXTRACTION_CODES for diagnostic in diagnostics) has_dependency_nodes = ( self.snapshots.db.scalar( - select(RiNode.id) - .where(RiNode.snapshot_id == snapshot_id, RiNode.node_kind == "dependency") - .limit(1) + select(RiNode.id).where(RiNode.snapshot_id == snapshot_id, RiNode.node_kind == "dependency").limit(1) ) is not None ) has_file_nodes = ( self.snapshots.db.scalar( - select(RiNode.id) - .where(RiNode.snapshot_id == snapshot_id, RiNode.node_kind == "file") - .limit(1) + select(RiNode.id).where(RiNode.snapshot_id == snapshot_id, RiNode.node_kind == "file").limit(1) ) is not None ) diff --git a/apps/backend/app/schemas/architecture.py b/apps/backend/app/schemas/architecture.py index d65db57..5c9f04b 100644 --- a/apps/backend/app/schemas/architecture.py +++ b/apps/backend/app/schemas/architecture.py @@ -23,7 +23,9 @@ "queue", "cache", ] -ArchEdgeType = Literal["dependency", "import", "api-call", "data-flow", "event", "reads", "writes", "calls", "config-usage"] +ArchEdgeType = Literal[ + "dependency", "import", "api-call", "data-flow", "event", "reads", "writes", "calls", "config-usage" +] RelationshipState = Literal["connected", "no-observed-relationships", "unresolved", "not-extracted"] TruthClass = Literal["resolved", "inferred"] diff --git a/apps/backend/app/services/analysis_job_service.py b/apps/backend/app/services/analysis_job_service.py index b2ce882..382efda 100644 --- a/apps/backend/app/services/analysis_job_service.py +++ b/apps/backend/app/services/analysis_job_service.py @@ -150,9 +150,7 @@ def cancel(self, repository_id: str) -> AnalysisJob: self._get_record(repository_id) job = self._latest_job(repository_id) if job is None: - raise ConflictServiceError( - "No analysis job to cancel.", {"repositoryId": repository_id} - ) + raise ConflictServiceError("No analysis job to cancel.", {"repositoryId": repository_id}) return self._cancel_job(job) def _cancel_job(self, job: AnalysisJob) -> AnalysisJob: @@ -224,9 +222,7 @@ def _get_record(self, repository_id: str) -> RepositoryRecord: @staticmethod def _require_revision(record: RepositoryRecord) -> str: if record.revision_kind is None or record.revision_value is None: - raise ServiceError( - "Repository has no analyzable revision.", {"repositoryId": record.id} - ) + raise ServiceError("Repository has no analyzable revision.", {"repositoryId": record.id}) return record.revision_value def _latest_job(self, repository_id: str) -> AnalysisJob | None: @@ -316,9 +312,7 @@ def _insert_queued_job(self, record: RepositoryRecord, config_hash: str) -> Anal self.db.refresh(job) return job - def _reconcile_completed_job( - self, record: RepositoryRecord, job: AnalysisJob, snapshot: RiSnapshot - ) -> AnalysisJob: + def _reconcile_completed_job(self, record: RepositoryRecord, job: AnalysisJob, snapshot: RiSnapshot) -> AnalysisJob: """Make a sealed snapshot authoritative over any non-completed job row.""" if ( @@ -360,9 +354,7 @@ def _reconcile_completed_job( self.db.refresh(job) return job - def _insert_completed_job( - self, record: RepositoryRecord, snapshot: RiSnapshot - ) -> AnalysisJob: + def _insert_completed_job(self, record: RepositoryRecord, snapshot: RiSnapshot) -> AnalysisJob: now = _utcnow() completed_at = snapshot.sealed_at or now job = AnalysisJob( @@ -407,9 +399,7 @@ def _complete_repository(record: RepositoryRecord, completed_at: datetime) -> No record.error_message = None record.analysed_at = completed_at - def _completed_snapshot( - self, record: RepositoryRecord, revision_value: str - ) -> RiSnapshot | None: + def _completed_snapshot(self, record: RepositoryRecord, revision_value: str) -> RiSnapshot | None: return self.snapshots.find_completed_for_owner( owner_id=self.owner_id, repository_id=record.id, diff --git a/apps/backend/app/services/documentation_service.py b/apps/backend/app/services/documentation_service.py index 16aa3ae..733b93d 100644 --- a/apps/backend/app/services/documentation_service.py +++ b/apps/backend/app/services/documentation_service.py @@ -73,7 +73,10 @@ def _document( ("Frameworks", ", ".join(projection.frameworks) if projection.frameworks else "Not detected"), ("Observed files", str(len(projection.files))), ("Observed supported-language files", str(source_files)), - ("Entry point", ", ".join(projection.entry_points) if projection.entry_points else "Not detected"), + ( + "Entry point", + ", ".join(projection.entry_points) if projection.entry_points else "Not detected", + ), ], ) ) @@ -86,11 +89,7 @@ def _document( if "api" in selected: api_files = sorted( - { - file.path - for file in projection.files - if file.role in {"route", "controller"} - } + {file.path for file in projection.files if file.role in {"route", "controller"}} | {path for route in projection.routes for path in route.evidence_paths} ) report_sections.append( diff --git a/apps/backend/app/services/evidence_service.py b/apps/backend/app/services/evidence_service.py index be4bb6f..b4a8e48 100644 --- a/apps/backend/app/services/evidence_service.py +++ b/apps/backend/app/services/evidence_service.py @@ -107,9 +107,7 @@ def unavailable(reason: str) -> EvidenceSourceResponse: sealed_content_hash = self.snapshots.source_content_hash(snapshot, path) current_content_hash = canonical.sha256_prefixed(file_response.content.encode("utf-8")) if sealed_content_hash is None or current_content_hash != sealed_content_hash: - return unavailable( - "The available source bytes do not match the content sealed into this snapshot." - ) + return unavailable("The available source bytes do not match the content sealed into this snapshot.") return EvidenceSourceResponse( schema_version="evidence-source.v1", diff --git a/apps/backend/app/services/repository_service.py b/apps/backend/app/services/repository_service.py index 81d904b..831c909 100644 --- a/apps/backend/app/services/repository_service.py +++ b/apps/backend/app/services/repository_service.py @@ -291,9 +291,7 @@ def _validate_parsed_repository(self, total_files: int) -> None: if total_files == 0: raise ValidationServiceError("Repository archive does not contain any readable files.") - def _parse_repository( - self, root: Path - ) -> tuple[list[FileTreeNode], RepositoryMeta, int]: + def _parse_repository(self, root: Path) -> tuple[list[FileTreeNode], RepositoryMeta, int]: try: return self.parser.parse(root, max_file_count=self.settings.max_file_count) except RepositoryFileLimitExceeded as exc: diff --git a/apps/backend/app/workers/analysis_worker.py b/apps/backend/app/workers/analysis_worker.py index 1665105..a5637cf 100644 --- a/apps/backend/app/workers/analysis_worker.py +++ b/apps/backend/app/workers/analysis_worker.py @@ -119,9 +119,7 @@ def __init__( self.lease_seconds = lease_seconds self.max_source_bytes = max_source_bytes self._clock = clock - self._heartbeat_interval_seconds = heartbeat_interval_seconds or min( - max(lease_seconds / 3, 0.1), 5.0 - ) + self._heartbeat_interval_seconds = heartbeat_interval_seconds or min(max(lease_seconds / 3, 0.1), 5.0) self._shutdown = threading.Event() # -- public API ---------------------------------------------------------- @@ -412,12 +410,7 @@ def _complete(self, ctx: _StageContext) -> None: # before this completion CAS. If this worker still owns that running # row, honour the accepted request instead of abandoning/completing. job = ctx.session.get(AnalysisJob, job_id) - if ( - job is not None - and job.status == "running" - and job.worker_id == self.worker_id - and job.cancel_requested - ): + if job is not None and job.status == "running" and job.worker_id == self.worker_id and job.cancel_requested: ctx.job = job ctx.record = ctx.session.get(RepositoryRecord, job.repository_id) self._cancel(ctx) @@ -435,11 +428,7 @@ def _cancel(self, ctx: _StageContext) -> None: """Honour a cooperative cancel: fail any open snapshot, cancel the job.""" self._lock_owned_job(ctx) - snapshot = ( - ctx.session.get(RiSnapshot, ctx.job.snapshot_id) - if ctx.job.snapshot_id is not None - else None - ) + snapshot = ctx.session.get(RiSnapshot, ctx.job.snapshot_id) if ctx.job.snapshot_id is not None else None if snapshot is not None and snapshot.state == "completed": self._complete_sealed_snapshot(ctx, snapshot) return @@ -668,18 +657,13 @@ def _update_owned_job( ownership.append(AnalysisJob.cancel_requested.is_(False)) with ctx.session.no_autoflush: result = ctx.session.execute( - update(AnalysisJob) - .where(*ownership) - .values(**values) - .execution_options(synchronize_session="fetch") + update(AnalysisJob).where(*ownership).values(**values).execution_options(synchronize_session="fetch") ) if result.rowcount == 0: ctx.session.rollback() raise _LeaseLostError(job_id) - def _lock_owned_job( - self, ctx: _StageContext, *, require_cancel_not_requested: bool = False - ) -> None: + def _lock_owned_job(self, ctx: _StageContext, *, require_cancel_not_requested: bool = False) -> None: """Lock the running row after atomically confirming this worker owns it.""" try: @@ -702,11 +686,7 @@ def _lock_owned_job( raise def _cancel_requested(self, ctx: _StageContext) -> bool: - return bool( - ctx.session.scalar( - select(AnalysisJob.cancel_requested).where(AnalysisJob.id == ctx.job.id) - ) - ) + return bool(ctx.session.scalar(select(AnalysisJob.cancel_requested).where(AnalysisJob.id == ctx.job.id))) def _fail_open_snapshot(self, ctx: _StageContext, *, code: str) -> None: """Mark an opened-but-unsealed snapshot ``failed`` (no-op if reused/sealed).""" @@ -820,11 +800,7 @@ def _restore_sqlite_timeout() -> None: # A SQLite writer prevents every other SQLite writer, including a # stale sweeper. Skipping that transient pulse avoids a false retry; # PostgreSQL heartbeats remain fully independent and guarded. - if ( - session.bind is not None - and session.bind.dialect.name == "sqlite" - and "locked" in str(exc).lower() - ): + if session.bind is not None and session.bind.dialect.name == "sqlite" and "locked" in str(exc).lower(): logger.debug("SQLite analysis heartbeat skipped while the stage held the write lock") else: state.failure = exc @@ -871,9 +847,7 @@ def _complete_sealed_snapshot(self, ctx: _StageContext, snapshot: RiSnapshot) -> ctx.record.analysed_at = completed_at ctx.session.commit() - def _read_sources( - self, record: RepositoryRecord, ctx: _StageContext | None = None - ) -> Mapping[str, bytes]: + def _read_sources(self, record: RepositoryRecord, ctx: _StageContext | None = None) -> Mapping[str, bytes]: """Read repository source bytes keyed by normalized repo-relative path. Paths come from ``record.file_tree`` (already computed at ingestion) rather @@ -983,7 +957,11 @@ def _merge_dependency_declarations( for node in item.result.nodes if not (node.node_kind == "dependency" and node.stable_key in mergeable) ) - rewritten.append(item if kept_nodes == item.result.nodes else replace(item, result=replace(item.result, nodes=kept_nodes))) + rewritten.append( + item + if kept_nodes == item.result.nodes + else replace(item, result=replace(item.result, nodes=kept_nodes)) + ) return tuple(merged_entries) + tuple(rewritten) @staticmethod From 38534e49a1cb1ee187a37e385d5daa622a380783 Mon Sep 17 00:00:00 2001 From: hardikuppal04 <262667963+hardikuppal04@users.noreply.github.com> Date: Tue, 28 Jul 2026 02:32:36 +0530 Subject: [PATCH 2/4] chore(ci): add backend Ruff and Mypy checks (#188) --- .github/workflows/ci.yml | 7 ++++ apps/backend/README.md | 18 ++++++++++ apps/backend/pyproject.toml | 58 +++++++++++++++++++++++++++++++ apps/backend/requirements-dev.txt | 6 ++++ 4 files changed, 89 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b5dc12..6731e0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,6 +129,13 @@ jobs: python -m pip install -r apps/backend/requirements-dev.txt python -m pip install -e apps/backend --no-deps + - name: Static analysis + working-directory: apps/backend + run: | + ruff check app + ruff format --check app + mypy app + - name: Test working-directory: apps/backend env: diff --git a/apps/backend/README.md b/apps/backend/README.md index e8434e0..1f091cf 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -49,6 +49,24 @@ npm run test:backend # from the repository root Tests use per-test SQLite and the in-memory rate limiter by default. When `PARTHA_TEST_PG_URL` is set, the migration round trip runs against a fresh temporary PostgreSQL database and the PostgreSQL refresh-token concurrency test is enabled; without it, migrations fall back to SQLite and the concurrency test skips. Redis integration tests skip unless `PARTHA_TEST_REDIS_URL` is set. CI provides both services. +## Static analysis + +The runtime lockfile deliberately excludes development tooling. Install the +pinned development toolchain before running the backend static-analysis gates: + +```bash +cd apps/backend +python -m pip install -r requirements-dev.txt +python -m pip install -e . --no-deps +ruff check app +ruff format --check app +mypy app +``` + +Ruff targets the production package and Python 3.12, the project's minimum +supported Python version. Run `ruff format app` to apply the repository's +formatter, then review the resulting diff before committing. + ## Migrations ```bash diff --git a/apps/backend/pyproject.toml b/apps/backend/pyproject.toml index 8287e97..8f5d5e9 100644 --- a/apps/backend/pyproject.toml +++ b/apps/backend/pyproject.toml @@ -34,6 +34,10 @@ dependencies = [ test = [ "pytest>=8.3.0", ] +dev = [ + "mypy==2.3.0", + "ruff==0.16.0", +] [tool.setuptools.packages.find] include = ["app*"] @@ -42,3 +46,57 @@ include = ["app*"] testpaths = ["tests"] pythonpath = ["."] addopts = "-q" + +[tool.ruff] +target-version = "py312" +line-length = 120 +src = ["app"] + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F"] +# AiProviderConfig is a backwards-compatible re-export from app.ai.types. +per-file-ignores = { "app/ai/types.py" = ["F401"] } + +[tool.mypy] +files = ["app"] +python_version = "3.12" +show_error_codes = true +warn_unused_configs = true +warn_unused_ignores = true + +# Existing type debt is isolated to the listed modules. The rest of the +# production package remains checked, so each entry can be removed as its +# annotations are repaired without weakening the global baseline. +[[tool.mypy.overrides]] +module = [ + "app.ai.providers.config_store", + "app.ai.providers.http", + "app.analysis.architecture", + "app.analysis.authentication", + "app.api.routes.ai", + "app.api.routes.analysis", + "app.api.routes.auth", + "app.api.routes.documentation", + "app.api.routes.intelligence", + "app.api.routes.repositories", + "app.api.routes.reports", + "app.auth.service", + "app.core.ai_egress", + "app.core.observability", + "app.extraction.manifests", + "app.extraction.python", + "app.extraction.typescript", + "app.graph.dependency_graph", + "app.insights.service", + "app.intelligence.query_service", + "app.intelligence.resolution", + "app.intelligence.snapshot_store", + "app.main", + "app.reports.renderers", + "app.review.review_service", + "app.services.analysis_job_service", + "app.services.documentation_service", + "app.services.repository_service", + "app.workers.analysis_worker", +] +ignore_errors = true diff --git a/apps/backend/requirements-dev.txt b/apps/backend/requirements-dev.txt index 825163a..ff09706 100644 --- a/apps/backend/requirements-dev.txt +++ b/apps/backend/requirements-dev.txt @@ -3,6 +3,12 @@ # toolchain so CI and local dev can run pytest. The runtime image installs # only requirements.txt; pytest must never ship in the container (issue #185). -r requirements.txt +ast-serialize==0.6.0 iniconfig==2.3.0 +librt==0.13.0 +mypy==2.3.0 +mypy_extensions==1.1.0 +pathspec==1.1.1 pluggy==1.6.0 pytest==9.1.1 +ruff==0.16.0 From 7338922e19bbf53ca26e62cd4eaa61b8c6f73fd6 Mon Sep 17 00:00:00 2001 From: hardikuppal04 <262667963+hardikuppal04@users.noreply.github.com> Date: Tue, 28 Jul 2026 02:50:10 +0530 Subject: [PATCH 3/4] style(backend): format rebased architecture source --- apps/backend/app/analysis/architecture.py | 156 +++++----------------- 1 file changed, 35 insertions(+), 121 deletions(-) diff --git a/apps/backend/app/analysis/architecture.py b/apps/backend/app/analysis/architecture.py index de40ca4..abf2f46 100644 --- a/apps/backend/app/analysis/architecture.py +++ b/apps/backend/app/analysis/architecture.py @@ -69,39 +69,23 @@ def __init__(self, snapshots: SnapshotQueryService | None = None) -> None: def build_architecture(self, record: RepositoryRecord) -> ArchitectureResponse: if self.snapshots is not None: self.snapshots.require_sealed_snapshot_for_current_revision(record.id) - facts = ( - self.snapshots.architecture_facts(record.id) - if self.snapshots is not None - else None - ) + facts = self.snapshots.architecture_facts(record.id) if self.snapshots is not None else None modules = self._modules_from_facts(facts) frameworks = self._frameworks_from_facts(facts) primary_language = self._primary_language_from_facts(facts) entry_points = self._entry_points_from_facts(facts) nodes = self._nodes_for_modules(modules) nodes.extend(self._dependency_nodes(facts)) - edges, diagnostics, unresolved_node_ids, covered_paths = ( - self._edges_for_modules(modules, nodes, facts) - ) - edge_endpoint_ids = { - node_id for edge in edges for node_id in (edge.source, edge.target) - } - nodes = [ - node - for node in nodes - if node.layer != "external" or node.id in edge_endpoint_ids - ] + edges, diagnostics, unresolved_node_ids, covered_paths = self._edges_for_modules(modules, nodes, facts) + edge_endpoint_ids = {node_id for edge in edges for node_id in (edge.source, edge.target)} + nodes = [node for node in nodes if node.layer != "external" or node.id in edge_endpoint_ids] remaining_node_ids = {node.id for node in nodes} for diagnostic in diagnostics: if diagnostic.node_ids is not None: diagnostic.node_ids = [ - node_id - for node_id in diagnostic.node_ids - if node_id in remaining_node_ids + node_id for node_id in diagnostic.node_ids if node_id in remaining_node_ids ] or None - self._set_relationship_states( - modules, nodes, edges, unresolved_node_ids, covered_paths, facts is not None - ) + self._set_relationship_states(modules, nodes, edges, unresolved_node_ids, covered_paths, facts is not None) layers = self._layers_for_nodes(nodes) arch_modules = [ ArchModule( @@ -131,9 +115,7 @@ def build_architecture(self, record: RepositoryRecord) -> ArchitectureResponse: entry_point=entry_points[0] if entry_points else "/", architecture_pattern=self._architecture_type(frameworks), ), - relationship_snapshot_id=facts.snapshot.snapshot_id - if facts is not None - else None, + relationship_snapshot_id=facts.snapshot.snapshot_id if facts is not None else None, diagnostics=diagnostics, ) @@ -185,10 +167,7 @@ def _file_roles(self, facts: ArchitectureSnapshotFacts) -> dict[str, str]: roles: dict[str, str] = {} for assertion in facts.assertions: - if ( - assertion.predicate != "classified_as" - or assertion.subject_kind != "file" - ): + if assertion.predicate != "classified_as" or assertion.subject_kind != "file": continue classification = str((assertion.value or {}).get("classification", "")) if not classification: @@ -196,9 +175,7 @@ def _file_roles(self, facts: ArchitectureSnapshotFacts) -> dict[str, str]: roles[assertion.subject_key.removeprefix("file:")] = classification return roles - def _modules_from_facts( - self, facts: ArchitectureSnapshotFacts | None - ) -> list[RepositoryModule]: + def _modules_from_facts(self, facts: ArchitectureSnapshotFacts | None) -> list[RepositoryModule]: if facts is None: # Defensive only: build_architecture requires a sealed snapshot # before calling this whenever self.snapshots is configured, so a @@ -222,8 +199,7 @@ def _modules_from_facts( candidate_roles = [ role_by_path[path] for path in paths - if role_by_path.get(path) - not in (None, "unknown", "documentation", "test") + if role_by_path.get(path) not in (None, "unknown", "documentation", "test") ] dominant = ( Counter(candidate_roles).most_common(1)[0][0] @@ -263,11 +239,7 @@ def _module_id(path: str, role: str | None) -> str: return "module:tests" if role == "documentation": return "module:documentation" - if ( - parts - and parts[0] in {"app", "src", "backend", "frontend", "apps"} - and len(parts) > 1 - ): + if parts and parts[0] in {"app", "src", "backend", "frontend", "apps"} and len(parts) > 1: return f"module:{parts[1].lower()}" return f"module:{parts[0].lower() if parts else 'repository'}" @@ -284,16 +256,10 @@ def _path_prefix(paths: list[str]) -> str: break return "/" + "/".join(prefix) if prefix else "/" - def _frameworks_from_facts( - self, facts: ArchitectureSnapshotFacts | None - ) -> list[str]: + def _frameworks_from_facts(self, facts: ArchitectureSnapshotFacts | None) -> list[str]: if facts is None: return [] - names = { - node.name.lower() - for node in facts.nodes - if node.node_kind == "dependency" and node.name - } + names = {node.name.lower() for node in facts.nodes if node.node_kind == "dependency" and node.name} return sorted( { framework @@ -302,40 +268,26 @@ def _frameworks_from_facts( } ) - def _primary_language_from_facts( - self, facts: ArchitectureSnapshotFacts | None - ) -> str: + def _primary_language_from_facts(self, facts: ArchitectureSnapshotFacts | None) -> str: if facts is None: return "Unknown" # Count persisted file facts, not symbols. Symbol counts make a file # with many declarations (and synthetic route symbols) outweigh other # files, and report "Unknown" for a valid script containing only # top-level statements. - counts = Counter( - node.language - for node in facts.nodes - if node.node_kind == "file" and node.language - ) + counts = Counter(node.language for node in facts.nodes if node.node_kind == "file" and node.language) if not counts: return "Unknown" dominant = counts.most_common(1)[0][0] - return {"python": "Python", "typescript": "TypeScript"}.get( - dominant, dominant.title() - ) + return {"python": "Python", "typescript": "TypeScript"}.get(dominant, dominant.title()) - def _entry_points_from_facts( - self, facts: ArchitectureSnapshotFacts | None - ) -> list[str]: + def _entry_points_from_facts(self, facts: ArchitectureSnapshotFacts | None) -> list[str]: if facts is None: return [] role_by_path = self._file_roles(facts) - return sorted( - path for path, role in role_by_path.items() if role == "entrypoint" - ) + return sorted(path for path, role in role_by_path.items() if role == "entrypoint") - def _dependency_nodes( - self, facts: ArchitectureSnapshotFacts | None - ) -> list[ArchNode]: + def _dependency_nodes(self, facts: ArchitectureSnapshotFacts | None) -> list[ArchNode]: if facts is None: return [] relationship_keys = { @@ -346,10 +298,7 @@ def _dependency_nodes( } result: list[ArchNode] = [] for item in facts.nodes: - if ( - item.node_kind != "dependency" - or item.stable_key not in relationship_keys - ): + if item.node_kind != "dependency" or item.stable_key not in relationship_keys: continue evidence = facts.node_evidence.get(item.id, []) result.append( @@ -358,9 +307,7 @@ def _dependency_nodes( name=item.name or item.stable_key, type="shared-library", description="External dependency from resolved repository evidence.", - responsibilities=[ - "Provides an externally declared or imported capability" - ], + responsibilities=["Provides an externally declared or imported capability"], files=sorted({entry.path for entry in evidence}), dependencies=[], dependents=[], @@ -397,9 +344,7 @@ def _edges_for_modules( modules_by_file: dict[str, list[str]] = {} for module in modules: for path in module.files: - modules_by_file.setdefault(self._normalize_path(path), []).append( - module.id - ) + modules_by_file.setdefault(self._normalize_path(path), []).append(module.id) snapshot_node_by_key = {item.stable_key: item for item in facts.nodes} node_ids = {node.id for node in nodes} diagnostics = [ @@ -417,9 +362,7 @@ def _edges_for_modules( if item.code not in ARCHITECTURE_DIAGNOSTIC_CODES: continue if item.path: - unresolved_node_ids.update( - modules_by_file.get(self._normalize_path(item.path), []) - ) + unresolved_node_ids.update(modules_by_file.get(self._normalize_path(item.path), [])) for key in (item.subject_key, item.object_key): if key in node_ids: unresolved_node_ids.add(key) @@ -469,9 +412,7 @@ def _edges_for_modules( subject_key=fact.subject_key, object_key=fact.object_key, details={"factId": fact.edge_id, "predicate": fact.predicate}, - node_ids=[fact.object_key] - if fact.object_key in node_ids - else None, + node_ids=[fact.object_key] if fact.object_key in node_ids else None, ) ) if len(root_scope_evidence) == len(evidence_rows): @@ -482,9 +423,7 @@ def _edges_for_modules( for target in target_ids if source in node_ids and target in node_ids ) - non_self_pairs = [ - (source, target) for source, target in pairs if source != target - ] + non_self_pairs = [(source, target) for source, target in pairs if source != target] if pairs and not non_self_pairs: # A resolved fact wholly inside one architecture module remains # extraction evidence, but it is not a module-to-module edge. @@ -497,9 +436,7 @@ def _edges_for_modules( severity="warning", message="A resolved relationship could not be mapped to architecture nodes without guessing.", path=evidence_rows[0].path if evidence_rows else None, - start_line=evidence_rows[0].start_line - if evidence_rows - else None, + start_line=evidence_rows[0].start_line if evidence_rows else None, end_line=evidence_rows[0].end_line if evidence_rows else None, subject_key=fact.subject_key, object_key=fact.object_key, @@ -520,11 +457,7 @@ def _edges_for_modules( for item in evidence_rows ] for index, (source, target) in enumerate(non_self_pairs, start=1): - edge_id = ( - fact.edge_id - if len(non_self_pairs) == 1 - else f"{fact.edge_id}:{index}" - ) + edge_id = fact.edge_id if len(non_self_pairs) == 1 else f"{fact.edge_id}:{index}" edges.append( ArchEdge( id=edge_id, @@ -577,9 +510,7 @@ def _architecture_endpoint_ids( if node is not None and not evidence: evidence = facts.node_evidence.get(node.id, []) exact = { - module_id - for item in evidence - for module_id in modules_by_file.get(self._normalize_path(item.path), []) + module_id for item in evidence for module_id in modules_by_file.get(self._normalize_path(item.path), []) } if exact: return exact @@ -587,10 +518,7 @@ def _architecture_endpoint_ids( return { module.id for module in module_by_id.values() - if any( - self._path_is_within(self._normalize_path(file_path), path) - for file_path in module.files - ) + if any(self._path_is_within(self._normalize_path(file_path), path) for file_path in module.files) } return set() @@ -606,10 +534,7 @@ def _modules_for_evidence_scope( if not directory: continue for module in module_by_id.values(): - if any( - self._path_is_within(self._normalize_path(file_path), directory) - for file_path in module.files - ): + if any(self._path_is_within(self._normalize_path(file_path), directory) for file_path in module.files): result.add(module.id) return result @@ -622,9 +547,7 @@ def _set_relationship_states( covered_paths: set[str], snapshot_available: bool, ) -> None: - connected = { - node_id for edge in edges for node_id in (edge.source, edge.target) - } + connected = {node_id for edge in edges for node_id in (edge.source, edge.target)} module_by_id = {module.id: module for module in modules} for node in nodes: if node.id in connected: @@ -635,10 +558,7 @@ def _set_relationship_states( node.id in module_by_id and snapshot_available and module_by_id[node.id].files - and all( - self._normalize_path(path) in covered_paths - for path in module_by_id[node.id].files - ) + and all(self._normalize_path(path) in covered_paths for path in module_by_id[node.id].files) ): node.relationship_state = "no-observed-relationships" else: @@ -652,18 +572,14 @@ def _architecture_diagnostic( ) -> ArchitectureDiagnostic: attributed_node_ids: set[str] = set() if item.path: - attributed_node_ids.update( - modules_by_file.get(self._normalize_path(item.path), []) - ) + attributed_node_ids.update(modules_by_file.get(self._normalize_path(item.path), [])) for key in (item.subject_key, item.object_key): if key is None: continue if key in node_ids: attributed_node_ids.add(key) continue - path = self._path_for_stable_key( - "file" if key.startswith("file:") else "symbol", key - ) + path = self._path_for_stable_key("file" if key.startswith("file:") else "symbol", key) if path is not None: attributed_node_ids.update(modules_by_file.get(path, [])) return ArchitectureDiagnostic( @@ -709,9 +625,7 @@ def _layers_for_nodes(self, nodes: list[ArchNode]) -> list[ArchLayer]: order=LAYER_ORDER.get(layer, 99), nodes=node_ids, ) - for layer, node_ids in sorted( - layers.items(), key=lambda item: LAYER_ORDER.get(item[0], 99) - ) + for layer, node_ids in sorted(layers.items(), key=lambda item: LAYER_ORDER.get(item[0], 99)) ] def _architecture_type(self, frameworks: list[str]) -> str: From c9e899d7be6e231a1e6e10217ef13a0f19469ef6 Mon Sep 17 00:00:00 2001 From: hardikuppal04 <262667963+hardikuppal04@users.noreply.github.com> Date: Tue, 28 Jul 2026 02:58:10 +0530 Subject: [PATCH 4/4] style(backend): format rebased intelligence source --- apps/backend/app/api/routes/intelligence.py | 6 +---- .../backend/app/intelligence/query_service.py | 24 +++++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/backend/app/api/routes/intelligence.py b/apps/backend/app/api/routes/intelligence.py index faef278..d3f982c 100644 --- a/apps/backend/app/api/routes/intelligence.py +++ b/apps/backend/app/api/routes/intelligence.py @@ -251,11 +251,7 @@ def get_impact( depth: ImpactDepth = 1, ) -> RiImpactResponse: impact = service.impact(snapshot_id, node_key=node_key, depth=depth) - edges = [ - step.edge - for direction in (impact.dependents, impact.dependencies) - for step in direction.steps - ] + edges = [step.edge for direction in (impact.dependents, impact.dependencies) for step in direction.steps] evidence = service.evidence_for_edges(impact.snapshot, edges) derivations = service.derivations_for_edges(impact.snapshot, edges) return RiImpactResponse( diff --git a/apps/backend/app/intelligence/query_service.py b/apps/backend/app/intelligence/query_service.py index f97677c..13df5a6 100644 --- a/apps/backend/app/intelligence/query_service.py +++ b/apps/backend/app/intelligence/query_service.py @@ -890,16 +890,20 @@ def _impact_direction( # Ranking one canonical edge per adjacent node *before* the cap is # essential: limiting raw edges could otherwise exhaust the query # budget on duplicate paths and falsely claim the result complete. - canonical_rank = func.row_number().over( - partition_by=adjacent, - order_by=( - RiEdge.subject_key, - RiEdge.predicate, - RiEdge.object_key, - RiEdge.edge_id, - RiEdge.id, - ), - ).label("canonical_rank") + canonical_rank = ( + func.row_number() + .over( + partition_by=adjacent, + order_by=( + RiEdge.subject_key, + RiEdge.predicate, + RiEdge.object_key, + RiEdge.edge_id, + RiEdge.id, + ), + ) + .label("canonical_rank") + ) ranked_edges = ( select(RiEdge.id.label("edge_row_id"), canonical_rank) .where(