embtrace-check 0.1.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.
Files changed (37) hide show
  1. embtrace_check/__init__.py +3 -0
  2. embtrace_check/analyzer/__init__.py +1 -0
  3. embtrace_check/analyzer/models.py +148 -0
  4. embtrace_check/analyzer/normalize.py +118 -0
  5. embtrace_check/analyzer/parsers/__init__.py +40 -0
  6. embtrace_check/analyzer/parsers/autotools.py +101 -0
  7. embtrace_check/analyzer/parsers/cargo.py +102 -0
  8. embtrace_check/analyzer/parsers/cmake.py +242 -0
  9. embtrace_check/analyzer/parsers/configure.py +139 -0
  10. embtrace_check/analyzer/parsers/gomod.py +54 -0
  11. embtrace_check/analyzer/parsers/gradle.py +97 -0
  12. embtrace_check/analyzer/parsers/makefile.py +86 -0
  13. embtrace_check/analyzer/parsers/maven.py +81 -0
  14. embtrace_check/analyzer/parsers/meson.py +125 -0
  15. embtrace_check/analyzer/parsers/npm.py +32 -0
  16. embtrace_check/analyzer/parsers/python.py +252 -0
  17. embtrace_check/analyzer/pipeline/__init__.py +162 -0
  18. embtrace_check/analyzer/pipeline/base.py +55 -0
  19. embtrace_check/analyzer/pipeline/merge.py +107 -0
  20. embtrace_check/analyzer/pipeline/tier1_cli.py +749 -0
  21. embtrace_check/analyzer/pipeline/tier2_structured.py +717 -0
  22. embtrace_check/analyzer/pipeline/tier4_regex.py +231 -0
  23. embtrace_check/analyzer/scanner.py +215 -0
  24. embtrace_check/cli.py +198 -0
  25. embtrace_check/collector.py +100 -0
  26. embtrace_check/core/__init__.py +1 -0
  27. embtrace_check/core/exceptions.py +228 -0
  28. embtrace_check/core/log.py +72 -0
  29. embtrace_check/payload.py +99 -0
  30. embtrace_check/sbom/__init__.py +1 -0
  31. embtrace_check/sbom/scanner.py +984 -0
  32. embtrace_check/upload.py +86 -0
  33. embtrace_check-0.1.0.dist-info/METADATA +123 -0
  34. embtrace_check-0.1.0.dist-info/RECORD +37 -0
  35. embtrace_check-0.1.0.dist-info/WHEEL +4 -0
  36. embtrace_check-0.1.0.dist-info/entry_points.txt +2 -0
  37. embtrace_check-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """embtrace-check — standalone collector for the embtrace CRA Readiness Check."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """Vendored subset of the embtrace analyzer (deterministic tiers only)."""
@@ -0,0 +1,148 @@
1
+ """Pydantic models for AI project analyzer.
2
+
3
+ Covers LLM analysis output, reconciliation results, and the decisions.yaml
4
+ persistence format.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pydantic import BaseModel
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # LLM output models
13
+ # ---------------------------------------------------------------------------
14
+
15
+ class BuildFileDependency(BaseModel):
16
+ """A dependency detected by LLM analysis of build files."""
17
+
18
+ name: str
19
+ version: str = ""
20
+ ecosystem: str = ""
21
+ source_file: str = ""
22
+ source_line: int = 0
23
+ context: str = "" # LLM context, e.g. "inside if(USE_LIBRESSL)"
24
+ confidence: float = 1.0
25
+ tier: int = 0 # 0=LLM, 1-5=Pipeline tier
26
+ detection_method: str = "" # e.g. "cargo-metadata", "regex-cmake", "treesitter-meson"
27
+
28
+
29
+ class BuildFileArtifact(BaseModel):
30
+ """An artifact (binary, library, etc.) detected in build files."""
31
+
32
+ path: str
33
+ artifact_type: str = "binary" # binary | library | config | script
34
+ source_file: str = ""
35
+ source_line: int = 0
36
+
37
+
38
+ class BuildFileInternalDep(BaseModel):
39
+ """An internal project dependency detected in build files."""
40
+
41
+ project: str
42
+ source_file: str = ""
43
+ source_line: int = 0
44
+ context: str = ""
45
+
46
+
47
+ class LLMAnalysisResult(BaseModel):
48
+ """Result of analyzing a single build file with the LLM."""
49
+
50
+ file_path: str
51
+ dependencies: list[BuildFileDependency] = []
52
+ artifacts: list[BuildFileArtifact] = []
53
+ internal_deps: list[BuildFileInternalDep] = []
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Reconciliation result models
58
+ # ---------------------------------------------------------------------------
59
+
60
+ class ConfirmedEntry(BaseModel):
61
+ """Both lockfile scanner and build-file analysis agree on this dependency."""
62
+
63
+ name: str
64
+ version: str
65
+ ecosystem: str = ""
66
+ source_file: str = ""
67
+
68
+
69
+ class LockfileOnlyEntry(BaseModel):
70
+ """Dependency found only in the lockfile scanner output."""
71
+
72
+ name: str
73
+ version: str
74
+ ecosystem: str = ""
75
+ accept: bool | None = None
76
+
77
+
78
+ class BuildFileOnlyEntry(BaseModel):
79
+ """Dependency found only in LLM build-file analysis."""
80
+
81
+ name: str
82
+ version: str = ""
83
+ ecosystem: str = ""
84
+ source_file: str = ""
85
+ confidence: float = 1.0
86
+ context: str = ""
87
+ accept: bool | None = None
88
+
89
+
90
+ class ConflictEntry(BaseModel):
91
+ """Version mismatch between lockfile and build-file analysis."""
92
+
93
+ name: str
94
+ lockfile_version: str
95
+ build_file_version: str
96
+ ecosystem: str = ""
97
+ source_file: str = ""
98
+ accept: bool | None = None
99
+ use_version: str = ""
100
+
101
+
102
+ class ArtifactEntry(BaseModel):
103
+ """A produced artifact detected in build files."""
104
+
105
+ path: str
106
+ artifact_type: str = "binary"
107
+ source_file: str = ""
108
+
109
+
110
+ class InternalDepEntry(BaseModel):
111
+ """An internal dependency detected in build files."""
112
+
113
+ project: str
114
+ source_file: str = ""
115
+ context: str = ""
116
+ accept: bool | None = None
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # decisions.yaml root model
121
+ # ---------------------------------------------------------------------------
122
+
123
+ class DecisionsFile(BaseModel):
124
+ """Root model for .embtrace/proposal/decisions.yaml."""
125
+
126
+ scan_date: str
127
+ model: str = ""
128
+ confirmed: list[ConfirmedEntry] = []
129
+ lockfile_only: list[LockfileOnlyEntry] = []
130
+ build_file_only: list[BuildFileOnlyEntry] = []
131
+ conflicts: list[ConflictEntry] = []
132
+ artifacts: list[ArtifactEntry] = []
133
+ internal_dependencies: list[InternalDepEntry] = []
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Reconciliation container
138
+ # ---------------------------------------------------------------------------
139
+
140
+ class ReconciliationResult(BaseModel):
141
+ """Container for reconciliation output — input to proposal generation."""
142
+
143
+ confirmed: list[ConfirmedEntry] = []
144
+ lockfile_only: list[LockfileOnlyEntry] = []
145
+ build_file_only: list[BuildFileOnlyEntry] = []
146
+ conflicts: list[ConflictEntry] = []
147
+ artifacts: list[ArtifactEntry] = []
148
+ internal_dependencies: list[InternalDepEntry] = []
@@ -0,0 +1,118 @@
1
+ """Shared dependency name normalization.
2
+
3
+ Consolidated from reconciler._normalize_name() and eval.normalize_dep_name()
4
+ to provide a single source of truth for name matching across the analyzer.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+ # Version suffix pattern: _0_3, _1_0, _2, _3_0, _2_11, etc.
12
+ # Applied AFTER separator normalization (- → _), so dots become _.
13
+ _VERSION_SUFFIX = re.compile(r"(?:_\d+)+$")
14
+
15
+ # Component suffixes commonly appended to pkg-config names
16
+ _COMPONENT_SUFFIXES = (
17
+ "_client", "_server", "_base", "_core", "_common",
18
+ "_egl", "_cursor", "_protocols", "_generic",
19
+ "_video", "_app", "_riff", "_pbutils", "_audio",
20
+ "_simple", "_allocators",
21
+ )
22
+
23
+ # CMake Find-module names often differ from pkg-config / canonical names.
24
+ # Map normalized aliases → canonical normalized name.
25
+ _ALIASES: dict[str, str] = {
26
+ # pkg-config → canonical
27
+ "cares": "c_ares",
28
+ "pulse": "pulseaudio",
29
+ "pulse_simple": "pulseaudio",
30
+ "avcodec": "ffmpeg",
31
+ "avformat": "ffmpeg",
32
+ "avutil": "ffmpeg",
33
+ "swscale": "ffmpeg",
34
+ "swresample": "ffmpeg",
35
+ "avdevice": "ffmpeg",
36
+ "avfilter": "ffmpeg",
37
+ "postproc": "ffmpeg",
38
+ "gtk2": "gtk",
39
+ "gtk3": "gtk",
40
+ "gtk4": "gtk",
41
+ "gtk_3": "gtk",
42
+ "gstreamer": "gstreamer",
43
+ "gstreamer_base": "gstreamer",
44
+ "gstreamer_video": "gstreamer",
45
+ "gstreamer_app": "gstreamer",
46
+ "gstreamer_riff": "gstreamer",
47
+ "gstreamer_pbutils": "gstreamer",
48
+ # CMake module variants
49
+ "dbus1": "dbus",
50
+ "dbus_1": "dbus",
51
+ "sdl2": "sdl",
52
+ "sdl3": "sdl",
53
+ "qt6widgets": "qt",
54
+ "qt5widgets": "qt",
55
+ "qt6core": "qt",
56
+ "qt5core": "qt",
57
+ "qt6gui": "qt",
58
+ "qt5gui": "qt",
59
+ "qt6": "qt",
60
+ "qt5": "qt",
61
+ "gtest": "googletest",
62
+ "gmock": "googletest",
63
+ "pythoninterp": "python",
64
+ "pythonlibs": "python",
65
+ "python3": "python",
66
+ "cudnn": "cudnn",
67
+ "cudatoolkit": "cuda",
68
+ "threads": "pthread",
69
+ "pthreads": "pthread",
70
+ # -l flag names → package names
71
+ "ssl": "openssl",
72
+ "crypto": "openssl",
73
+ "z": "zlib",
74
+ # OpenCV-specific
75
+ "onnxruntime": "onnx_runtime",
76
+ # Wayland components
77
+ "wayland_scanner": "wayland",
78
+ # glib variants (with and without version suffix)
79
+ "glib_2": "glib",
80
+ "gobject_2": "glib",
81
+ "gobject": "glib",
82
+ "gmodule": "glib",
83
+ "gmodule_2": "glib",
84
+ "gio_2": "glib",
85
+ "gio": "glib",
86
+ # mesa / graphics
87
+ "glslangvalidator": "glslang",
88
+ "sensors": "lmsensors",
89
+ }
90
+
91
+
92
+ def normalize_dep_name(name: str) -> str:
93
+ """Normalize a dependency name for matching.
94
+
95
+ Lowercases, strips common prefixes (lib, python-, py-),
96
+ strips version suffixes and component suffixes from pkg-config names,
97
+ normalizes separators (-, ., +, space → _), and applies alias mappings.
98
+ """
99
+ n = name.lower().strip()
100
+ # Strip common prefixes
101
+ for prefix in ("lib", "python-", "py-"):
102
+ if n.startswith(prefix) and len(n) > len(prefix) + 1:
103
+ n = n[len(prefix):]
104
+ break
105
+ # Remove + (gtk+ → gtk)
106
+ n = n.replace("+", "")
107
+ # Normalize separators
108
+ n = n.replace("-", "_").replace(".", "_").replace(" ", "_")
109
+ # Strip version suffixes (e.g. pipewire_0_3 → pipewire)
110
+ n = _VERSION_SUFFIX.sub("", n)
111
+ # Strip component suffixes (e.g. wayland_client → wayland)
112
+ for suffix in _COMPONENT_SUFFIXES:
113
+ if n.endswith(suffix) and len(n) > len(suffix):
114
+ n = n[: -len(suffix)]
115
+ break
116
+ # Apply alias mappings
117
+ n = _ALIASES.get(n, n)
118
+ return n
@@ -0,0 +1,40 @@
1
+ """Deterministic build-file parsers.
2
+
3
+ Each parser module exposes a ``parse(content: str) -> list[str]`` function
4
+ that extracts dependency names from a specific build-file format.
5
+
6
+ The ``PARSERS`` dict maps file_type → parse function, matching the types
7
+ used by ``scanner.BUILD_FILE_PATTERNS``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Callable
13
+
14
+ from embtrace_check.analyzer.parsers.autotools import parse as parse_autotools
15
+ from embtrace_check.analyzer.parsers.cargo import parse as parse_cargo
16
+ from embtrace_check.analyzer.parsers.cmake import parse as parse_cmake
17
+ from embtrace_check.analyzer.parsers.configure import parse as parse_configure
18
+ from embtrace_check.analyzer.parsers.gomod import parse as parse_go
19
+ from embtrace_check.analyzer.parsers.gradle import parse as parse_gradle
20
+ from embtrace_check.analyzer.parsers.makefile import parse as parse_make
21
+ from embtrace_check.analyzer.parsers.maven import parse as parse_maven
22
+ from embtrace_check.analyzer.parsers.meson import parse as parse_meson
23
+ from embtrace_check.analyzer.parsers.npm import parse as parse_npm
24
+ from embtrace_check.analyzer.parsers.python import parse as parse_python
25
+
26
+ # Maps file_type (from scanner.BUILD_FILE_PATTERNS) → parser function
27
+ PARSERS: dict[str, Callable[[str], list[str]]] = {
28
+ "cmake": parse_cmake,
29
+ "meson": parse_meson,
30
+ "cargo": parse_cargo,
31
+ "go": parse_go,
32
+ "python": parse_python,
33
+ "npm": parse_npm,
34
+ "gradle": parse_gradle,
35
+ "maven": parse_maven,
36
+ "autotools": parse_autotools,
37
+ "configure": parse_configure,
38
+ "make": parse_make,
39
+ "conan": parse_cmake, # conanfile.py/txt handled separately; fallback to cmake
40
+ }
@@ -0,0 +1,101 @@
1
+ """Deterministic Autotools parser (configure.ac / configure.in).
2
+
3
+ Extracts dependency names from GNU Autotools build files:
4
+ - AC_CHECK_LIB([name], [func])
5
+ - AC_CHECK_HEADER([header.h])
6
+ - PKG_CHECK_MODULES([VAR], [name >= version])
7
+ - AC_SEARCH_LIBS([func], [lib1 lib2 ...])
8
+ - AX_CHECK_OPENSSL / AX_CHECK_ZLIB etc.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+
15
+ # AC_CHECK_LIB([crypto], [SHA1_Init], ...)
16
+ # AC_CHECK_LIB(curl, curl_global_init, ...)
17
+ _AC_CHECK_LIB = re.compile(
18
+ r"AC_CHECK_LIB\s*\(\s*\[?(\w+)\]?",
19
+ re.IGNORECASE,
20
+ )
21
+
22
+ # PKG_CHECK_MODULES([DLT], ['automotive-dlt >= 2.11'])
23
+ # PKG_CHECK_MODULES(DLT, automotive-dlt >= 2.11)
24
+ _PKG_CHECK_MODULES = re.compile(
25
+ r"PKG_CHECK_MODULES\s*\(\s*\[?\w+\]?\s*,\s*\[?'?\"?([a-zA-Z0-9_][a-zA-Z0-9_.+-]*)",
26
+ )
27
+
28
+ # AC_SEARCH_LIBS([clock_gettime], [rt posix4])
29
+ # Also handles unbracketed: AC_SEARCH_LIBS(fmod, m)
30
+ _AC_SEARCH_LIBS = re.compile(
31
+ r"AC_SEARCH_LIBS\s*\(\s*\[?\w+\]?\s*,\s*\[?([^\],\)]+)",
32
+ )
33
+
34
+ # AC_CHECK_HEADER([header.h]) — not always a dependency, but useful
35
+ _AC_CHECK_HEADER = re.compile(
36
+ r"AC_CHECK_HEADER\s*\(\s*\[?([a-zA-Z0-9_/.+-]+\.h)\]?",
37
+ )
38
+
39
+ # AX_CHECK_OPENSSL, AX_CHECK_ZLIB, etc.
40
+ _AX_CHECK = re.compile(
41
+ r"AX_CHECK_(\w+)",
42
+ )
43
+
44
+ # Header → library name mapping (common cases)
45
+ _HEADER_TO_LIB: dict[str, str] = {
46
+ "openssl/ssl.h": "openssl",
47
+ "openssl/crypto.h": "openssl",
48
+ "curl/curl.h": "libcurl",
49
+ "zlib.h": "zlib",
50
+ "expat.h": "libexpat",
51
+ "pcre2.h": "libpcre2",
52
+ "pcre.h": "libpcre",
53
+ "iconv.h": "libiconv",
54
+ "libintl.h": "libintl",
55
+ "pthread.h": "pthread",
56
+ }
57
+
58
+
59
+ def _strip_comments(content: str) -> str:
60
+ """Remove autotools comments (dnl ... and # ...)."""
61
+ content = re.sub(r"dnl\s+[^\n]*", "", content)
62
+ content = re.sub(r"#[^\n]*", "", content)
63
+ return content
64
+
65
+
66
+ def parse(content: str) -> list[str]:
67
+ """Extract dependency names from configure.ac content."""
68
+ content = _strip_comments(content)
69
+ deps: set[str] = set()
70
+
71
+ # AC_CHECK_LIB
72
+ for m in _AC_CHECK_LIB.finditer(content):
73
+ name = m.group(1)
74
+ # Prefix with lib if it's a short name (convention)
75
+ if len(name) > 1:
76
+ deps.add(name)
77
+
78
+ # PKG_CHECK_MODULES
79
+ for m in _PKG_CHECK_MODULES.finditer(content):
80
+ deps.add(m.group(1))
81
+
82
+ # AC_SEARCH_LIBS — extract each library name
83
+ for m in _AC_SEARCH_LIBS.finditer(content):
84
+ libs = m.group(1).split()
85
+ for lib in libs:
86
+ lib = lib.strip()
87
+ if lib:
88
+ deps.add(lib)
89
+
90
+ # AX_CHECK_*
91
+ for m in _AX_CHECK.finditer(content):
92
+ name = m.group(1).lower()
93
+ deps.add(name)
94
+
95
+ # AC_CHECK_HEADER — map known headers to library names
96
+ for m in _AC_CHECK_HEADER.finditer(content):
97
+ header = m.group(1)
98
+ if header in _HEADER_TO_LIB:
99
+ deps.add(_HEADER_TO_LIB[header])
100
+
101
+ return sorted(deps)
@@ -0,0 +1,102 @@
1
+ """Deterministic Cargo.toml parser.
2
+
3
+ Extracts dependency names from Rust Cargo.toml by parsing:
4
+ - [dependencies]
5
+ - [dev-dependencies]
6
+ - [build-dependencies]
7
+ - [target.'cfg(...)'.dependencies]
8
+
9
+ Filters out path-only dependencies (workspace members).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+
16
+ # Section headers
17
+ _SECTION = re.compile(r"^\[([^\]]+)\]", re.MULTILINE)
18
+
19
+ # Simple dependency: name = "version"
20
+ _SIMPLE_DEP = re.compile(r"^(\w[\w-]*)\s*=\s*\"([^\"]+)\"", re.MULTILINE)
21
+
22
+ # Table dependency: name = { version = "...", ... }
23
+ # Also matches: name = { path = "..." } (internal — filter later)
24
+ _TABLE_DEP = re.compile(
25
+ r"^(\w[\w-]*)\s*=\s*\{([^}]+)\}",
26
+ re.MULTILINE,
27
+ )
28
+
29
+ # Workspace members in [workspace]
30
+ _WORKSPACE_MEMBERS = re.compile(
31
+ r"members\s*=\s*\[([^\]]+)\]",
32
+ re.DOTALL,
33
+ )
34
+
35
+
36
+ def _is_dep_section(section: str) -> bool:
37
+ """Check if a TOML section header is a dependency section."""
38
+ s = section.lower().strip()
39
+ return s.endswith("dependencies") and not s.startswith("workspace")
40
+
41
+
42
+ _PACKAGE_FIELD = re.compile(r'package\s*=\s*"([^"]+)"')
43
+
44
+
45
+ def _extract_package_name(table_content: str) -> str | None:
46
+ """Extract the actual package name from a ``package = "..."`` field."""
47
+ m = _PACKAGE_FIELD.search(table_content)
48
+ return m.group(1) if m else None
49
+
50
+
51
+ def _has_path_only(table_content: str) -> bool:
52
+ """Check if a table-style dependency is path-only (internal)."""
53
+ has_path = "path" in table_content
54
+ has_version = "version" in table_content
55
+ has_git = "git" in table_content
56
+ has_workspace = re.search(r"workspace\s*=\s*true", table_content) is not None
57
+ # workspace = true without external source = workspace/internal dep
58
+ if has_workspace and not has_git:
59
+ return True
60
+ # path-only without version = workspace/internal dep
61
+ return has_path and not has_version and not has_git
62
+
63
+
64
+ def parse(content: str) -> list[str]:
65
+ """Extract dependency names from Cargo.toml content."""
66
+ deps: set[str] = set()
67
+
68
+ # Find all sections and their content ranges
69
+ sections: list[tuple[str, int, int]] = []
70
+ for m in _SECTION.finditer(content):
71
+ sections.append((m.group(1), m.end(), 0)) # end will be filled below
72
+
73
+ # Fill section end positions
74
+ filled: list[tuple[str, int, int]] = []
75
+ for i, (name, start, _) in enumerate(sections):
76
+ end = (
77
+ sections[i + 1][1] - len(sections[i + 1][0]) - 2
78
+ if i + 1 < len(sections) else len(content)
79
+ )
80
+ filled.append((name, start, end))
81
+
82
+ # Process each dependency section
83
+ for section_name, start, end in filled:
84
+ if not _is_dep_section(section_name):
85
+ continue
86
+
87
+ section_content = content[start:end]
88
+
89
+ # Simple deps: name = "version"
90
+ for m in _SIMPLE_DEP.finditer(section_content):
91
+ deps.add(m.group(1))
92
+
93
+ # Table deps: name = { version = "...", ... }
94
+ for m in _TABLE_DEP.finditer(section_content):
95
+ name = m.group(1)
96
+ table = m.group(2)
97
+ if not _has_path_only(table):
98
+ # Use actual package name if renamed (package = "real_name")
99
+ pkg = _extract_package_name(table)
100
+ deps.add(pkg if pkg else name)
101
+
102
+ return sorted(deps)