osscodeiq 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- osscodeiq/__init__.py +0 -0
- osscodeiq/analyzer.py +467 -0
- osscodeiq/cache/__init__.py +0 -0
- osscodeiq/cache/hasher.py +23 -0
- osscodeiq/cache/store.py +300 -0
- osscodeiq/classifiers/__init__.py +0 -0
- osscodeiq/classifiers/layer_classifier.py +69 -0
- osscodeiq/cli.py +721 -0
- osscodeiq/config.py +113 -0
- osscodeiq/detectors/__init__.py +0 -0
- osscodeiq/detectors/auth/__init__.py +0 -0
- osscodeiq/detectors/auth/certificate_auth.py +139 -0
- osscodeiq/detectors/auth/ldap_auth.py +89 -0
- osscodeiq/detectors/auth/session_header_auth.py +120 -0
- osscodeiq/detectors/base.py +41 -0
- osscodeiq/detectors/config/__init__.py +0 -0
- osscodeiq/detectors/config/batch_structure.py +128 -0
- osscodeiq/detectors/config/cloudformation.py +183 -0
- osscodeiq/detectors/config/docker_compose.py +179 -0
- osscodeiq/detectors/config/github_actions.py +150 -0
- osscodeiq/detectors/config/gitlab_ci.py +216 -0
- osscodeiq/detectors/config/helm_chart.py +187 -0
- osscodeiq/detectors/config/ini_structure.py +101 -0
- osscodeiq/detectors/config/json_structure.py +72 -0
- osscodeiq/detectors/config/kubernetes.py +305 -0
- osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
- osscodeiq/detectors/config/openapi.py +194 -0
- osscodeiq/detectors/config/package_json.py +99 -0
- osscodeiq/detectors/config/properties_detector.py +108 -0
- osscodeiq/detectors/config/pyproject_toml.py +169 -0
- osscodeiq/detectors/config/sql_structure.py +155 -0
- osscodeiq/detectors/config/toml_structure.py +93 -0
- osscodeiq/detectors/config/tsconfig_json.py +105 -0
- osscodeiq/detectors/config/yaml_structure.py +82 -0
- osscodeiq/detectors/cpp/__init__.py +0 -0
- osscodeiq/detectors/cpp/cpp_structures.py +192 -0
- osscodeiq/detectors/csharp/__init__.py +0 -0
- osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
- osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
- osscodeiq/detectors/csharp/csharp_structures.py +317 -0
- osscodeiq/detectors/docs/__init__.py +0 -0
- osscodeiq/detectors/docs/markdown_structure.py +117 -0
- osscodeiq/detectors/frontend/__init__.py +0 -0
- osscodeiq/detectors/frontend/angular_components.py +177 -0
- osscodeiq/detectors/frontend/frontend_routes.py +259 -0
- osscodeiq/detectors/frontend/react_components.py +148 -0
- osscodeiq/detectors/frontend/svelte_components.py +84 -0
- osscodeiq/detectors/frontend/vue_components.py +150 -0
- osscodeiq/detectors/generic/__init__.py +1 -0
- osscodeiq/detectors/generic/imports_detector.py +413 -0
- osscodeiq/detectors/go/__init__.py +0 -0
- osscodeiq/detectors/go/go_orm.py +202 -0
- osscodeiq/detectors/go/go_structures.py +162 -0
- osscodeiq/detectors/go/go_web.py +157 -0
- osscodeiq/detectors/iac/__init__.py +0 -0
- osscodeiq/detectors/iac/bicep.py +135 -0
- osscodeiq/detectors/iac/dockerfile.py +182 -0
- osscodeiq/detectors/iac/terraform.py +188 -0
- osscodeiq/detectors/java/__init__.py +0 -0
- osscodeiq/detectors/java/azure_functions.py +424 -0
- osscodeiq/detectors/java/azure_messaging.py +350 -0
- osscodeiq/detectors/java/class_hierarchy.py +349 -0
- osscodeiq/detectors/java/config_def.py +82 -0
- osscodeiq/detectors/java/cosmos_db.py +105 -0
- osscodeiq/detectors/java/graphql_resolver.py +188 -0
- osscodeiq/detectors/java/grpc_service.py +142 -0
- osscodeiq/detectors/java/ibm_mq.py +178 -0
- osscodeiq/detectors/java/jaxrs.py +160 -0
- osscodeiq/detectors/java/jdbc.py +196 -0
- osscodeiq/detectors/java/jms.py +116 -0
- osscodeiq/detectors/java/jpa_entity.py +143 -0
- osscodeiq/detectors/java/kafka.py +113 -0
- osscodeiq/detectors/java/kafka_protocol.py +70 -0
- osscodeiq/detectors/java/micronaut.py +248 -0
- osscodeiq/detectors/java/module_deps.py +191 -0
- osscodeiq/detectors/java/public_api.py +206 -0
- osscodeiq/detectors/java/quarkus.py +176 -0
- osscodeiq/detectors/java/rabbitmq.py +150 -0
- osscodeiq/detectors/java/raw_sql.py +136 -0
- osscodeiq/detectors/java/repository.py +131 -0
- osscodeiq/detectors/java/rmi.py +129 -0
- osscodeiq/detectors/java/spring_events.py +117 -0
- osscodeiq/detectors/java/spring_rest.py +168 -0
- osscodeiq/detectors/java/spring_security.py +212 -0
- osscodeiq/detectors/java/tibco_ems.py +193 -0
- osscodeiq/detectors/java/websocket.py +188 -0
- osscodeiq/detectors/kotlin/__init__.py +0 -0
- osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
- osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
- osscodeiq/detectors/proto/__init__.py +0 -0
- osscodeiq/detectors/proto/proto_structure.py +153 -0
- osscodeiq/detectors/python/__init__.py +0 -0
- osscodeiq/detectors/python/celery_tasks.py +88 -0
- osscodeiq/detectors/python/django_auth.py +132 -0
- osscodeiq/detectors/python/django_models.py +157 -0
- osscodeiq/detectors/python/django_views.py +74 -0
- osscodeiq/detectors/python/fastapi_auth.py +143 -0
- osscodeiq/detectors/python/fastapi_routes.py +68 -0
- osscodeiq/detectors/python/flask_routes.py +67 -0
- osscodeiq/detectors/python/kafka_python.py +175 -0
- osscodeiq/detectors/python/pydantic_models.py +115 -0
- osscodeiq/detectors/python/python_structures.py +234 -0
- osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
- osscodeiq/detectors/registry.py +100 -0
- osscodeiq/detectors/rust/__init__.py +0 -0
- osscodeiq/detectors/rust/actix_web.py +234 -0
- osscodeiq/detectors/rust/rust_structures.py +174 -0
- osscodeiq/detectors/scala/__init__.py +0 -0
- osscodeiq/detectors/scala/scala_structures.py +128 -0
- osscodeiq/detectors/shell/__init__.py +0 -0
- osscodeiq/detectors/shell/bash_detector.py +127 -0
- osscodeiq/detectors/shell/powershell_detector.py +118 -0
- osscodeiq/detectors/typescript/__init__.py +0 -0
- osscodeiq/detectors/typescript/express_routes.py +55 -0
- osscodeiq/detectors/typescript/fastify_routes.py +156 -0
- osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
- osscodeiq/detectors/typescript/kafka_js.py +164 -0
- osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
- osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
- osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
- osscodeiq/detectors/typescript/passport_jwt.py +133 -0
- osscodeiq/detectors/typescript/prisma_orm.py +96 -0
- osscodeiq/detectors/typescript/remix_routes.py +160 -0
- osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
- osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
- osscodeiq/detectors/typescript/typescript_structures.py +185 -0
- osscodeiq/detectors/utils.py +49 -0
- osscodeiq/discovery/__init__.py +11 -0
- osscodeiq/discovery/change_detector.py +97 -0
- osscodeiq/discovery/file_discovery.py +342 -0
- osscodeiq/flow/__init__.py +0 -0
- osscodeiq/flow/engine.py +78 -0
- osscodeiq/flow/models.py +72 -0
- osscodeiq/flow/renderer.py +127 -0
- osscodeiq/flow/templates/interactive.html +252 -0
- osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
- osscodeiq/flow/vendor/cytoscape.min.js +32 -0
- osscodeiq/flow/vendor/dagre.min.js +3809 -0
- osscodeiq/flow/views.py +357 -0
- osscodeiq/graph/__init__.py +0 -0
- osscodeiq/graph/backend.py +52 -0
- osscodeiq/graph/backends/__init__.py +23 -0
- osscodeiq/graph/backends/kuzu.py +576 -0
- osscodeiq/graph/backends/networkx.py +135 -0
- osscodeiq/graph/backends/sqlite_backend.py +406 -0
- osscodeiq/graph/builder.py +297 -0
- osscodeiq/graph/query.py +228 -0
- osscodeiq/graph/store.py +183 -0
- osscodeiq/graph/views.py +231 -0
- osscodeiq/models/__init__.py +17 -0
- osscodeiq/models/graph.py +116 -0
- osscodeiq/output/__init__.py +0 -0
- osscodeiq/output/dot.py +171 -0
- osscodeiq/output/mermaid.py +160 -0
- osscodeiq/output/safety.py +58 -0
- osscodeiq/output/serializers.py +42 -0
- osscodeiq/parsing/__init__.py +5 -0
- osscodeiq/parsing/languages/__init__.py +0 -0
- osscodeiq/parsing/languages/base.py +23 -0
- osscodeiq/parsing/languages/java.py +68 -0
- osscodeiq/parsing/languages/python.py +57 -0
- osscodeiq/parsing/languages/typescript.py +95 -0
- osscodeiq/parsing/parser_manager.py +125 -0
- osscodeiq/parsing/structured/__init__.py +0 -0
- osscodeiq/parsing/structured/gradle_parser.py +78 -0
- osscodeiq/parsing/structured/json_parser.py +24 -0
- osscodeiq/parsing/structured/properties_parser.py +56 -0
- osscodeiq/parsing/structured/sql_parser.py +54 -0
- osscodeiq/parsing/structured/xml_parser.py +148 -0
- osscodeiq/parsing/structured/yaml_parser.py +38 -0
- osscodeiq/server/__init__.py +7 -0
- osscodeiq/server/app.py +53 -0
- osscodeiq/server/mcp_server.py +174 -0
- osscodeiq/server/middleware.py +16 -0
- osscodeiq/server/routes.py +184 -0
- osscodeiq/server/service.py +445 -0
- osscodeiq/server/templates/welcome.html +56 -0
- osscodeiq-0.0.0.dist-info/METADATA +30 -0
- osscodeiq-0.0.0.dist-info/RECORD +183 -0
- osscodeiq-0.0.0.dist-info/WHEEL +5 -0
- osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
- osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
- osscodeiq-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Git diff-based incremental change detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from osscodeiq.discovery.file_discovery import (
|
|
10
|
+
ChangeType,
|
|
11
|
+
DiscoveredFile,
|
|
12
|
+
_compute_sha256,
|
|
13
|
+
_map_extension_to_language,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Mapping from git status letters to ChangeType.
|
|
18
|
+
_GIT_STATUS_MAP: dict[str, ChangeType] = {
|
|
19
|
+
"A": ChangeType.ADDED,
|
|
20
|
+
"M": ChangeType.MODIFIED,
|
|
21
|
+
"D": ChangeType.DELETED,
|
|
22
|
+
"R": ChangeType.MODIFIED, # Rename treated as modified
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ChangeDetector:
|
|
27
|
+
"""Detects file changes between two git commits."""
|
|
28
|
+
|
|
29
|
+
def detect_changes(
|
|
30
|
+
self, repo_path: Path, last_commit: str
|
|
31
|
+
) -> list[DiscoveredFile]:
|
|
32
|
+
"""Return files that changed between *last_commit* and HEAD.
|
|
33
|
+
|
|
34
|
+
Uses ``git diff --name-status <last_commit>..HEAD``.
|
|
35
|
+
"""
|
|
36
|
+
repo_path = repo_path.resolve()
|
|
37
|
+
|
|
38
|
+
if not re.fullmatch(r'[0-9a-fA-F]{4,40}', last_commit):
|
|
39
|
+
raise ValueError(f"Invalid commit SHA: {last_commit}")
|
|
40
|
+
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["git", "diff", "--name-status", f"{last_commit}..HEAD"],
|
|
43
|
+
cwd=repo_path,
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
check=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
discovered: list[DiscoveredFile] = []
|
|
50
|
+
for line in result.stdout.splitlines():
|
|
51
|
+
if not line.strip():
|
|
52
|
+
continue
|
|
53
|
+
parts = line.split("\t")
|
|
54
|
+
if len(parts) < 2:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
status_letter = parts[0][0] # First char handles R100 etc.
|
|
58
|
+
# For renames the *destination* path is the last element.
|
|
59
|
+
file_rel = parts[-1]
|
|
60
|
+
change_type = _GIT_STATUS_MAP.get(status_letter)
|
|
61
|
+
if change_type is None:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
rel_path = Path(file_rel)
|
|
65
|
+
lang = _map_extension_to_language(rel_path)
|
|
66
|
+
if lang is None:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
abs_path = repo_path / rel_path
|
|
70
|
+
|
|
71
|
+
if change_type is ChangeType.DELETED:
|
|
72
|
+
discovered.append(
|
|
73
|
+
DiscoveredFile(
|
|
74
|
+
path=rel_path,
|
|
75
|
+
language=lang,
|
|
76
|
+
content_hash="",
|
|
77
|
+
size_bytes=0,
|
|
78
|
+
change_type=change_type,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
try:
|
|
83
|
+
size = abs_path.stat().st_size
|
|
84
|
+
content_hash = _compute_sha256(abs_path)
|
|
85
|
+
except OSError:
|
|
86
|
+
continue
|
|
87
|
+
discovered.append(
|
|
88
|
+
DiscoveredFile(
|
|
89
|
+
path=rel_path,
|
|
90
|
+
language=lang,
|
|
91
|
+
content_hash=content_hash,
|
|
92
|
+
size_bytes=size,
|
|
93
|
+
change_type=change_type,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return discovered
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Git-aware file discovery for OSSCodeIQ."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import hashlib
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import pathspec
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
from osscodeiq.config import Config
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ChangeType(Enum):
|
|
23
|
+
"""Type of file change detected by git."""
|
|
24
|
+
|
|
25
|
+
ADDED = "added"
|
|
26
|
+
MODIFIED = "modified"
|
|
27
|
+
DELETED = "deleted"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Map file extensions to language identifiers.
|
|
31
|
+
_EXTENSION_MAP: dict[str, str] = {
|
|
32
|
+
".java": "java",
|
|
33
|
+
".py": "python",
|
|
34
|
+
".ts": "typescript",
|
|
35
|
+
".tsx": "typescript",
|
|
36
|
+
".js": "javascript",
|
|
37
|
+
".jsx": "javascript",
|
|
38
|
+
".xml": "xml",
|
|
39
|
+
".yaml": "yaml",
|
|
40
|
+
".yml": "yaml",
|
|
41
|
+
".json": "json",
|
|
42
|
+
".properties": "properties",
|
|
43
|
+
".gradle": "gradle",
|
|
44
|
+
".gradle.kts": "gradle",
|
|
45
|
+
".sql": "sql",
|
|
46
|
+
".graphql": "graphql",
|
|
47
|
+
".gql": "graphql",
|
|
48
|
+
".proto": "proto",
|
|
49
|
+
".md": "markdown",
|
|
50
|
+
".markdown": "markdown",
|
|
51
|
+
".bicep": "bicep",
|
|
52
|
+
".tf": "terraform",
|
|
53
|
+
".tfvars": "terraform",
|
|
54
|
+
".cs": "csharp",
|
|
55
|
+
".go": "go",
|
|
56
|
+
".cpp": "cpp",
|
|
57
|
+
".cc": "cpp",
|
|
58
|
+
".cxx": "cpp",
|
|
59
|
+
".hpp": "cpp",
|
|
60
|
+
".c": "c",
|
|
61
|
+
".h": "c",
|
|
62
|
+
".sh": "bash",
|
|
63
|
+
".bash": "bash",
|
|
64
|
+
".zsh": "bash",
|
|
65
|
+
".ps1": "powershell",
|
|
66
|
+
".psm1": "powershell",
|
|
67
|
+
".psd1": "powershell",
|
|
68
|
+
".bat": "batch",
|
|
69
|
+
".cmd": "batch",
|
|
70
|
+
".rb": "ruby",
|
|
71
|
+
".rs": "rust",
|
|
72
|
+
".kt": "kotlin",
|
|
73
|
+
".kts": "kotlin",
|
|
74
|
+
".scala": "scala",
|
|
75
|
+
".swift": "swift",
|
|
76
|
+
".r": "r",
|
|
77
|
+
".R": "r",
|
|
78
|
+
".pl": "perl",
|
|
79
|
+
".pm": "perl",
|
|
80
|
+
".lua": "lua",
|
|
81
|
+
".dart": "dart",
|
|
82
|
+
".hcl": "terraform",
|
|
83
|
+
".dockerfile": "dockerfile",
|
|
84
|
+
".toml": "toml",
|
|
85
|
+
".ini": "ini",
|
|
86
|
+
".cfg": "ini",
|
|
87
|
+
".conf": "ini",
|
|
88
|
+
".env": "dotenv",
|
|
89
|
+
".csv": "csv",
|
|
90
|
+
".vue": "vue",
|
|
91
|
+
".svelte": "svelte",
|
|
92
|
+
".html": "html",
|
|
93
|
+
".htm": "html",
|
|
94
|
+
".css": "css",
|
|
95
|
+
".scss": "scss",
|
|
96
|
+
".less": "less",
|
|
97
|
+
".mjs": "javascript",
|
|
98
|
+
".cjs": "javascript",
|
|
99
|
+
".mts": "typescript",
|
|
100
|
+
".cts": "typescript",
|
|
101
|
+
".jsonc": "json",
|
|
102
|
+
".groovy": "groovy",
|
|
103
|
+
".pyi": "python",
|
|
104
|
+
".razor": "razor",
|
|
105
|
+
".cshtml": "cshtml",
|
|
106
|
+
".adoc": "asciidoc",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_FILENAME_MAP: dict[str, str] = {
|
|
111
|
+
"Dockerfile": "dockerfile",
|
|
112
|
+
"Makefile": "makefile",
|
|
113
|
+
"GNUmakefile": "makefile",
|
|
114
|
+
"Jenkinsfile": "groovy",
|
|
115
|
+
"Vagrantfile": "ruby",
|
|
116
|
+
"Gemfile": "ruby",
|
|
117
|
+
"Rakefile": "ruby",
|
|
118
|
+
"go.mod": "gomod",
|
|
119
|
+
"go.sum": "gosum",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True, slots=True)
|
|
124
|
+
class DiscoveredFile:
|
|
125
|
+
"""A file discovered during repository scanning."""
|
|
126
|
+
|
|
127
|
+
path: Path
|
|
128
|
+
language: str
|
|
129
|
+
content_hash: str
|
|
130
|
+
size_bytes: int
|
|
131
|
+
change_type: ChangeType | None = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _map_extension_to_language(file_path: Path) -> str | None:
|
|
135
|
+
"""Map a file's extension to a language string.
|
|
136
|
+
|
|
137
|
+
Falls back to :data:`_FILENAME_MAP` when no extension matches, using the
|
|
138
|
+
basename of *file_path* (e.g. ``"Dockerfile"`` from ``"app/Dockerfile"``).
|
|
139
|
+
"""
|
|
140
|
+
name = file_path.name
|
|
141
|
+
# Check compound extensions first (e.g. .gradle.kts)
|
|
142
|
+
for ext, lang in _EXTENSION_MAP.items():
|
|
143
|
+
if name.endswith(ext):
|
|
144
|
+
return lang
|
|
145
|
+
# Fallback: match the full filename (extensionless files like Dockerfile)
|
|
146
|
+
return _FILENAME_MAP.get(name)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _matches_any_pattern(path_str: str, patterns: list[str]) -> bool:
|
|
150
|
+
"""Check if a path matches any of the given glob patterns."""
|
|
151
|
+
for pattern in patterns:
|
|
152
|
+
if fnmatch.fnmatch(path_str, pattern):
|
|
153
|
+
return True
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _compile_exclude_patterns(patterns: list[str]) -> re.Pattern[str] | None:
|
|
158
|
+
"""Compile a list of glob patterns into a single regex for fast matching."""
|
|
159
|
+
if not patterns:
|
|
160
|
+
return None
|
|
161
|
+
return re.compile("|".join(fnmatch.translate(p) for p in patterns))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _build_ignore_spec(repo_path: Path, config_patterns: list[str]) -> pathspec.PathSpec:
|
|
165
|
+
"""Build a combined ignore spec from config patterns + ignore files.
|
|
166
|
+
|
|
167
|
+
Reads .codeignore and .gitignore files from the repo root and any
|
|
168
|
+
subdirectory, combining them with the config exclude_patterns.
|
|
169
|
+
Uses gitignore-style matching (handles node_modules at any depth).
|
|
170
|
+
"""
|
|
171
|
+
all_patterns: list[str] = []
|
|
172
|
+
|
|
173
|
+
# 1. Config exclude patterns (convert ** glob to gitignore style)
|
|
174
|
+
for p in config_patterns:
|
|
175
|
+
# Strip leading **/ — gitignore patterns match at any depth by default
|
|
176
|
+
cleaned = p.replace("**/", "").rstrip("/**")
|
|
177
|
+
all_patterns.append(cleaned)
|
|
178
|
+
# Also keep original for explicit **/ matching
|
|
179
|
+
all_patterns.append(p)
|
|
180
|
+
|
|
181
|
+
# 2. Read .codeignore from repo root
|
|
182
|
+
codeignore = repo_path / ".codeignore"
|
|
183
|
+
if codeignore.is_file():
|
|
184
|
+
try:
|
|
185
|
+
lines = codeignore.read_text().splitlines()
|
|
186
|
+
for line in lines:
|
|
187
|
+
line = line.strip()
|
|
188
|
+
if line and not line.startswith("#"):
|
|
189
|
+
all_patterns.append(line)
|
|
190
|
+
logger.debug("Loaded %d patterns from .codeignore", len(lines))
|
|
191
|
+
except OSError:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
# 3. Read .gitignore from repo root (supplementary)
|
|
195
|
+
gitignore = repo_path / ".gitignore"
|
|
196
|
+
if gitignore.is_file():
|
|
197
|
+
try:
|
|
198
|
+
lines = gitignore.read_text().splitlines()
|
|
199
|
+
for line in lines:
|
|
200
|
+
line = line.strip()
|
|
201
|
+
if line and not line.startswith("#"):
|
|
202
|
+
all_patterns.append(line)
|
|
203
|
+
logger.debug("Loaded %d patterns from .gitignore", len(lines))
|
|
204
|
+
except OSError:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", all_patterns)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _compute_sha256(file_path: Path) -> str:
|
|
211
|
+
"""Compute SHA-256 hex digest for a file.
|
|
212
|
+
|
|
213
|
+
Delegates to :func:`osscodeiq.cache.hasher.hash_file` for
|
|
214
|
+
consistency, falling back to a local implementation if the import fails.
|
|
215
|
+
"""
|
|
216
|
+
from osscodeiq.cache.hasher import hash_file
|
|
217
|
+
|
|
218
|
+
return hash_file(file_path)
|
|
219
|
+
return h.hexdigest()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class FileDiscovery:
|
|
223
|
+
"""Discovers files in a repository using git or filesystem walk."""
|
|
224
|
+
|
|
225
|
+
def __init__(self, config: Config | None = None) -> None:
|
|
226
|
+
self._config = config or Config()
|
|
227
|
+
self._current_commit: str | None = None
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def current_commit(self) -> str | None:
|
|
231
|
+
"""The HEAD commit hash from the last discovery run."""
|
|
232
|
+
return self._current_commit
|
|
233
|
+
|
|
234
|
+
def discover(
|
|
235
|
+
self, repo_path: Path, incremental: bool = True
|
|
236
|
+
) -> list[DiscoveredFile]:
|
|
237
|
+
"""Discover tracked files in a repository.
|
|
238
|
+
|
|
239
|
+
Uses ``git ls-files`` for git repos (fast, ~50ms for large repos).
|
|
240
|
+
Falls back to ``os.walk`` for non-git directories.
|
|
241
|
+
"""
|
|
242
|
+
repo_path = repo_path.resolve()
|
|
243
|
+
discovery_cfg = self._config.discovery
|
|
244
|
+
|
|
245
|
+
if self._is_git_repo(repo_path):
|
|
246
|
+
self._current_commit = self._git_head(repo_path)
|
|
247
|
+
relative_paths = self._git_ls_files(repo_path)
|
|
248
|
+
else:
|
|
249
|
+
self._current_commit = None
|
|
250
|
+
relative_paths = self._walk_files(repo_path)
|
|
251
|
+
|
|
252
|
+
ignore_spec = _build_ignore_spec(repo_path, discovery_cfg.exclude_patterns)
|
|
253
|
+
|
|
254
|
+
result: list[DiscoveredFile] = []
|
|
255
|
+
for rel in relative_paths:
|
|
256
|
+
abs_path = repo_path / rel
|
|
257
|
+
rel_path = Path(rel)
|
|
258
|
+
|
|
259
|
+
# Check ignore patterns first (fastest rejection)
|
|
260
|
+
if ignore_spec.match_file(str(rel_path)):
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# Extension filter
|
|
264
|
+
lang = _map_extension_to_language(rel_path)
|
|
265
|
+
if lang is None:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
# Check include extensions (skip for extensionless filename matches)
|
|
269
|
+
is_filename_match = rel_path.name in _FILENAME_MAP
|
|
270
|
+
if not is_filename_match and not any(
|
|
271
|
+
rel.endswith(ext) for ext in discovery_cfg.include_extensions
|
|
272
|
+
):
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
# Size guard
|
|
276
|
+
try:
|
|
277
|
+
size = abs_path.stat().st_size
|
|
278
|
+
except OSError:
|
|
279
|
+
continue
|
|
280
|
+
if size > discovery_cfg.max_file_size_bytes:
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
content_hash = _compute_sha256(abs_path)
|
|
284
|
+
result.append(
|
|
285
|
+
DiscoveredFile(
|
|
286
|
+
path=rel_path,
|
|
287
|
+
language=lang,
|
|
288
|
+
content_hash=content_hash,
|
|
289
|
+
size_bytes=size,
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return result
|
|
294
|
+
|
|
295
|
+
# ------------------------------------------------------------------
|
|
296
|
+
# Internal helpers
|
|
297
|
+
# ------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def _is_git_repo(path: Path) -> bool:
|
|
301
|
+
try:
|
|
302
|
+
subprocess.run(
|
|
303
|
+
["git", "rev-parse", "--git-dir"],
|
|
304
|
+
cwd=path,
|
|
305
|
+
capture_output=True,
|
|
306
|
+
check=True,
|
|
307
|
+
)
|
|
308
|
+
return True
|
|
309
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _git_head(repo_path: Path) -> str:
|
|
314
|
+
result = subprocess.run(
|
|
315
|
+
["git", "rev-parse", "HEAD"],
|
|
316
|
+
cwd=repo_path,
|
|
317
|
+
capture_output=True,
|
|
318
|
+
text=True,
|
|
319
|
+
check=True,
|
|
320
|
+
)
|
|
321
|
+
return result.stdout.strip()
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def _git_ls_files(repo_path: Path) -> list[str]:
|
|
325
|
+
result = subprocess.run(
|
|
326
|
+
["git", "ls-files"],
|
|
327
|
+
cwd=repo_path,
|
|
328
|
+
capture_output=True,
|
|
329
|
+
text=True,
|
|
330
|
+
check=True,
|
|
331
|
+
)
|
|
332
|
+
return [line for line in result.stdout.splitlines() if line]
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _walk_files(root: Path) -> list[str]:
|
|
336
|
+
paths: list[str] = []
|
|
337
|
+
for dirpath, _dirnames, filenames in os.walk(root):
|
|
338
|
+
for fname in filenames:
|
|
339
|
+
abs_p = Path(dirpath) / fname
|
|
340
|
+
rel = str(abs_p.relative_to(root))
|
|
341
|
+
paths.append(rel)
|
|
342
|
+
return paths
|
|
File without changes
|
osscodeiq/flow/engine.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""FlowEngine — core library for generating architecture flow diagrams.
|
|
2
|
+
|
|
3
|
+
All consumers (CLI, HTTP API, MCP tool, HTML UI) call the same methods.
|
|
4
|
+
FlowDiagram is the single source of truth — renderers only change format, never data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from osscodeiq.flow.models import FlowDiagram
|
|
10
|
+
from osscodeiq.flow.renderer import render_html, render_json, render_mermaid
|
|
11
|
+
from osscodeiq.flow.views import (
|
|
12
|
+
build_auth_view,
|
|
13
|
+
build_ci_view,
|
|
14
|
+
build_deploy_view,
|
|
15
|
+
build_overview,
|
|
16
|
+
build_runtime_view,
|
|
17
|
+
)
|
|
18
|
+
from osscodeiq.graph.store import GraphStore
|
|
19
|
+
|
|
20
|
+
_VIEWS = {
|
|
21
|
+
"overview": build_overview,
|
|
22
|
+
"ci": build_ci_view,
|
|
23
|
+
"deploy": build_deploy_view,
|
|
24
|
+
"runtime": build_runtime_view,
|
|
25
|
+
"auth": build_auth_view,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
AVAILABLE_VIEWS = tuple(_VIEWS.keys())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FlowEngine:
|
|
32
|
+
"""Generate and render architecture flow diagrams from a OSSCodeIQ graph.
|
|
33
|
+
|
|
34
|
+
Usage::
|
|
35
|
+
|
|
36
|
+
engine = FlowEngine(store)
|
|
37
|
+
diagram = engine.generate("overview")
|
|
38
|
+
print(engine.render(diagram, "mermaid"))
|
|
39
|
+
# Or generate interactive HTML with all views:
|
|
40
|
+
html = engine.render_interactive()
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, store: GraphStore) -> None:
|
|
44
|
+
self._store = store
|
|
45
|
+
|
|
46
|
+
def generate(self, view: str = "overview") -> FlowDiagram:
|
|
47
|
+
"""Generate a single flow view diagram."""
|
|
48
|
+
builder = _VIEWS.get(view)
|
|
49
|
+
if builder is None:
|
|
50
|
+
raise ValueError(f"Unknown view: {view}. Available: {', '.join(AVAILABLE_VIEWS)}")
|
|
51
|
+
return builder(self._store)
|
|
52
|
+
|
|
53
|
+
def generate_all(self) -> dict[str, FlowDiagram]:
|
|
54
|
+
"""Generate all views. Used for HTML interactive output."""
|
|
55
|
+
return {name: self.generate(name) for name in AVAILABLE_VIEWS}
|
|
56
|
+
|
|
57
|
+
def render(self, diagram: FlowDiagram, format: str = "mermaid") -> str:
|
|
58
|
+
"""Render a diagram to string.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
diagram: The FlowDiagram to render.
|
|
62
|
+
format: Output format — "mermaid" or "json".
|
|
63
|
+
"""
|
|
64
|
+
if format == "mermaid":
|
|
65
|
+
return render_mermaid(diagram)
|
|
66
|
+
elif format == "json":
|
|
67
|
+
return render_json(diagram)
|
|
68
|
+
else:
|
|
69
|
+
raise ValueError(f"Unknown format: {format}. Available: mermaid, json")
|
|
70
|
+
|
|
71
|
+
def render_interactive(self, project_name: str = "Project") -> str:
|
|
72
|
+
"""Generate all views and bake into a self-contained interactive HTML file."""
|
|
73
|
+
all_views = self.generate_all()
|
|
74
|
+
stats = {
|
|
75
|
+
"total_nodes": self._store.node_count,
|
|
76
|
+
"total_edges": self._store.edge_count,
|
|
77
|
+
}
|
|
78
|
+
return render_html(all_views, stats, project_name=project_name)
|
osscodeiq/flow/models.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Data models for flow diagrams — the single source of truth for all renderers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class FlowNode:
|
|
10
|
+
"""A node in a flow diagram (collapsed/summarized from graph nodes)."""
|
|
11
|
+
id: str
|
|
12
|
+
label: str
|
|
13
|
+
kind: str # "trigger", "job", "service", "endpoint", "database", "guard", etc.
|
|
14
|
+
properties: dict = field(default_factory=dict)
|
|
15
|
+
style: str = "default" # "default", "success", "warning", "danger"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class FlowSubgraph:
|
|
20
|
+
"""A labeled group of nodes in a flow diagram."""
|
|
21
|
+
id: str
|
|
22
|
+
label: str
|
|
23
|
+
nodes: list[FlowNode] = field(default_factory=list)
|
|
24
|
+
drill_down_view: str | None = None # "ci", "deploy", "runtime", "auth"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class FlowEdge:
|
|
29
|
+
"""An edge in a flow diagram."""
|
|
30
|
+
source: str
|
|
31
|
+
target: str
|
|
32
|
+
label: str | None = None
|
|
33
|
+
style: str = "solid" # "solid", "dotted", "thick"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class FlowDiagram:
|
|
38
|
+
"""A complete flow diagram — the single source of truth for all renderers."""
|
|
39
|
+
title: str
|
|
40
|
+
view: str # "overview", "ci", "deploy", "runtime", "auth"
|
|
41
|
+
direction: str = "LR"
|
|
42
|
+
subgraphs: list[FlowSubgraph] = field(default_factory=list)
|
|
43
|
+
loose_nodes: list[FlowNode] = field(default_factory=list)
|
|
44
|
+
edges: list[FlowEdge] = field(default_factory=list)
|
|
45
|
+
stats: dict = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
def all_nodes(self) -> list[FlowNode]:
|
|
48
|
+
"""Return all nodes across subgraphs and loose nodes."""
|
|
49
|
+
result = list(self.loose_nodes)
|
|
50
|
+
for sg in self.subgraphs:
|
|
51
|
+
result.extend(sg.nodes)
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> dict:
|
|
55
|
+
"""Serialize to a plain dict (for JSON renderer and API responses)."""
|
|
56
|
+
return {
|
|
57
|
+
"title": self.title,
|
|
58
|
+
"view": self.view,
|
|
59
|
+
"direction": self.direction,
|
|
60
|
+
"subgraphs": [
|
|
61
|
+
{
|
|
62
|
+
"id": sg.id,
|
|
63
|
+
"label": sg.label,
|
|
64
|
+
"drill_down_view": sg.drill_down_view,
|
|
65
|
+
"nodes": [{"id": n.id, "label": n.label, "kind": n.kind, "properties": n.properties, "style": n.style} for n in sg.nodes],
|
|
66
|
+
}
|
|
67
|
+
for sg in self.subgraphs
|
|
68
|
+
],
|
|
69
|
+
"loose_nodes": [{"id": n.id, "label": n.label, "kind": n.kind, "properties": n.properties, "style": n.style} for n in self.loose_nodes],
|
|
70
|
+
"edges": [{"source": e.source, "target": e.target, "label": e.label, "style": e.style} for e in self.edges],
|
|
71
|
+
"stats": self.stats,
|
|
72
|
+
}
|