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"