fabgate 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.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: fabgate
3
+ Version: 0.1.0
4
+ Summary: Pre-fab PCB design review and release gate for KiCad boards
5
+ Project-URL: Homepage, https://pypi.org/project/fabgate/
6
+ Project-URL: Repository, https://github.com/coderlifevyn12/fabgate
7
+ Author: coderlifevyn12
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: dfm,drc,emc,hardware,kicad,pcb,release-gate
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: pydantic-settings>=2.3.0
23
+ Requires-Dist: pydantic>=2.7.0
24
+ Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: rich>=13.7.0
26
+ Requires-Dist: typer>=0.12.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
30
+ Provides-Extra: emcopilot
31
+ Requires-Dist: matplotlib>=3.10.8; extra == 'emcopilot'
32
+ Requires-Dist: mcp>=1.0.0; extra == 'emcopilot'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # FabGate
36
+
37
+ Pre-fab PCB design review and release gate for KiCad boards.
38
+
39
+ **v0.1** — evaluate your board, get a scored pass/hold/fail report, and a fix checklist before fabrication.
40
+
41
+ ## Prerequisites
42
+
43
+ - [Python 3.11+](https://www.python.org/downloads/)
44
+ - [KiCad 9+](https://www.kicad.org/download/) with `kicad-cli` available
45
+
46
+ ## Install
47
+
48
+ ```powershell
49
+ pip install fabgate
50
+ ```
51
+
52
+ Optional deep physics review (EMC / power / signal integrity):
53
+
54
+ ```powershell
55
+ pip install "fabgate[emcopilot]"
56
+ ```
57
+
58
+ ## Quick start
59
+
60
+ ```powershell
61
+ # Check toolchain
62
+ fabgate doctor
63
+
64
+ # Review an existing KiCad project
65
+ fabgate evaluate "C:\path\to\your-kicad-project"
66
+
67
+ # CI / release gate (exit 0=pass, 1=hold, 2=fail)
68
+ fabgate gate "C:\path\to\your-kicad-project"
69
+
70
+ # Open board in KiCad
71
+ fabgate open "C:\path\to\your-kicad-project"
72
+ ```
73
+
74
+ Reports are written to `reports/` as HTML + JSON.
75
+
76
+ ## What it checks
77
+
78
+ | Check | Engine |
79
+ |-------|--------|
80
+ | DRC violations | KiCad `kicad-cli` |
81
+ | Schematic parity | KiCad |
82
+ | EMC / power / SI physics | emcopilot (optional) |
83
+ | Scoring + pass/hold/fail | FabGate rules |
84
+
85
+ FabGate does **not** call an LLM. It uses deterministic KiCad and physics-based analyzers.
86
+
87
+ ## Commands
88
+
89
+ | Command | Purpose |
90
+ |---------|---------|
91
+ | `fabgate doctor` | Check KiCad CLI + optional emcopilot |
92
+ | `fabgate evaluate` | Full evaluation + HTML report |
93
+ | `fabgate review` | Basic review |
94
+ | `fabgate gate` | CI exit code |
95
+ | `fabgate optimize run` | Optimization plan + safe auto-fixes |
96
+ | `fabgate auto run` | Autonomous optimize loop (with rollback guardrail) |
97
+ | `fabgate design create` | Scaffold new KiCad project |
98
+ | `fabgate workflow` | Design → evaluate → optimize |
99
+ | `fabgate open` | Open in KiCad GUI |
100
+
101
+ ## Development install
102
+
103
+ ```powershell
104
+ git clone <repo-url>
105
+ cd fabgate
106
+ python -m venv .venv
107
+ .\.venv\Scripts\Activate.ps1
108
+ pip install -e ".[dev,emcopilot]"
109
+ fabgate doctor
110
+ ```
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,39 @@
1
+ kitty/__init__.py,sha256=uVMvpQyzTmqHheC7-s74JiSsg8NSqZ8d0MrVNibHk4U,81
2
+ kitty/__main__.py,sha256=7z0PGCi9Z0i8jGG6AZHMMcsd-b-tqWab0omrHvh1DMc,36
3
+ kitty/audit.py,sha256=R1qFueevHlnYZYw-jNuU6R5q7irG7ZWY1VTAunlgPxQ,3199
4
+ kitty/autonomous.py,sha256=1q_Hblsx-ApzPH9mlXlhjGhAWX87Kvw4NGMciWikYjU,6687
5
+ kitty/cli.py,sha256=Us889rflQFE7PESooMLyNh6CUtCZFLK9IMHGOCaj_So,23456
6
+ kitty/design.py,sha256=PGyhkhEB-jqK2B2kDmF7sHw9n3AX-CsP4PTxUWSKkGI,10214
7
+ kitty/emcopilot_client.py,sha256=AeH7FBFOz2zYXK5kvJhiIpvSRX9saJBnj2fl9NlOJIE,4927
8
+ kitty/evaluate.py,sha256=sjHMZiORcKea9I9VwaAvjKOeGw16ClHJpx-Qeikw0Pg,8080
9
+ kitty/export_bundle.py,sha256=D5M4BCkBja7ujmKFEJ303b9MkLV7W-xin890VBLb2Bw,1958
10
+ kitty/findings_triage.py,sha256=AEisI8wAt8tDV_NesO5icnYQhJWL4YXK0lT-D602Mvk,3624
11
+ kitty/ingest.py,sha256=tf70vdG6_q8lP0_YbMv2ZRtry2MOk0diBVrjoEdOvcU,4089
12
+ kitty/kicad_paths.py,sha256=oqTddlLvQ07Ku0JhDfaf9HJbxzMLgvxsn2cZCiImNIU,4015
13
+ kitty/kicad_project.py,sha256=uU2PlKtFWUVu-SjIzHvaZ-46HHqSRq9Thp5YeNML8xg,4576
14
+ kitty/models.py,sha256=1XVV8nw3J9t4a87yNgLOheg2Kr-eXvxXBYX03L-CSXA,2912
15
+ kitty/optimize.py,sha256=3W93pmnn-WBT9QzN1zatJubOSD2nz3xknMgM__Qr3e8,14169
16
+ kitty/pipeline.py,sha256=bjas41LpTvEb5pYZ4eH1g8KkDCwvejoMYzDnzfj2bRs,3627
17
+ kitty/pkg_paths.py,sha256=aPBE7e6et8BEHHhpxL8fPQxgiJPfPsupL-2a4CLQQRc,233
18
+ kitty/product_config.py,sha256=e8RN0wpfSY50S86CWJgg9A79vVOSvXDC7Z_XMoSzcjQ,2142
19
+ kitty/scoring.py,sha256=cEAda3s5J-w_dajqZyVQKP0epX23jkh4YlZzQhUC5nM,3624
20
+ kitty/visualization.py,sha256=cKGkSnuu3EOxqjR5Cc8AwliJ3j0adbeV3Ca5b_EOxSY,3072
21
+ kitty/workflow.py,sha256=QFXmBDwvZGQUPs1r7lvQfwH3Djj8Ax2XMsddzfxp1ug,3777
22
+ kitty/analyzers/__init__.py,sha256=uexMfi7IxYAKX1lagI71RUOXiPibaI2spRBnzZJ6Zcs,906
23
+ kitty/analyzers/base.py,sha256=mEvpb_W1DB8etZbMTePsH5VImvcUUS_49IS3b6l5wMY,494
24
+ kitty/analyzers/drc_severity.py,sha256=1OfKXXLsCcVviaiiA7Fw4wjd1v_nNKmWiaTw9zG-1ZA,1391
25
+ kitty/analyzers/emcopilot.py,sha256=h4JqP3M6Hgl-oyT55Yt8i9YSXgIfhyQ1bR5P9iKNG7k,3665
26
+ kitty/analyzers/gerber_dfm.py,sha256=VXA0BFkz6Y9KpI_eCxuhk0qovILRFrKBgTkNZ87D528,4443
27
+ kitty/analyzers/kicad_drc.py,sha256=YAvh3cQdBHNvAwH6jzY7TxyTFaUy9cTmb-jL5UWex8Y,9786
28
+ kitty/analyzers/layout_rules.py,sha256=8hMb5QoTS-0JFyBo5bCmFv_SRPbhap3dYD25eDMU48s,9377
29
+ kitty/analyzers/stub.py,sha256=JAvDuRsauXeN2Heo5hQ53s5-z71n_lVWAQrZvZWYXqE,1577
30
+ kitty/auto_fixers/__init__.py,sha256=MsuJmfkF4_aeFgcEQaHVgvvs6OUWMmIC_4_oLhTUNww,166
31
+ kitty/auto_fixers/pcb_mutations.py,sha256=pNhLc2ul0RwotobQ-gdmIkNnIDW9vXjoTB8Jam56--U,8278
32
+ kitty/config/default_rules.yaml,sha256=pbCBTPNOLhlwd2lwPwq08x3W9Z7f6V149oh4ZqKgGdM,675
33
+ kitty/config/design_spec.example.yaml,sha256=3XLz4H-K_HsJf0g9NhLEdr_BqvwDLJG-wDjdBnLY8kE,468
34
+ kitty/config/product.yaml,sha256=vV01xwi03TP4G9YMa2u6_MKcLZ_iflnyu50YHkjAWAQ,358
35
+ fabgate-0.1.0.dist-info/METADATA,sha256=TIG7RF5NDz1vb3mDlh8R_eUUA4pie0I40DMm8wDAEwo,3232
36
+ fabgate-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
37
+ fabgate-0.1.0.dist-info/entry_points.txt,sha256=pnwRtThsYjEMZndvC0GtqGNOdI9-H_vbWElfyIdxPyg,42
38
+ fabgate-0.1.0.dist-info/licenses/LICENSE,sha256=TyIag98QdFhuiq4hdeWDM6Ix52w91Pb4bO7eCRKk94I,1092
39
+ fabgate-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fabgate = kitty.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 coderlifevyn12
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
kitty/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """FabGate — pre-fab PCB review and release gate."""
2
+
3
+ __version__ = "0.1.0"
kitty/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from kitty.cli import app
2
+
3
+ app()
@@ -0,0 +1,29 @@
1
+ from kitty.analyzers.base import Analyzer
2
+ from kitty.analyzers.emcopilot import EmcopilotAnalyzer
3
+ from kitty.analyzers.gerber_dfm import GerberDfmAnalyzer
4
+ from kitty.analyzers.kicad_drc import KiCadDrcAnalyzer
5
+ from kitty.analyzers.layout_rules import LayoutRulesAnalyzer
6
+ from kitty.analyzers.stub import StubAnalyzer
7
+ from kitty.emcopilot_client import emcopilot_available
8
+
9
+ __all__ = [
10
+ "Analyzer",
11
+ "EmcopilotAnalyzer",
12
+ "GerberDfmAnalyzer",
13
+ "KiCadDrcAnalyzer",
14
+ "LayoutRulesAnalyzer",
15
+ "StubAnalyzer",
16
+ "default_analyzers",
17
+ ]
18
+
19
+
20
+ def default_analyzers() -> list[Analyzer]:
21
+ analyzers: list[Analyzer] = [
22
+ KiCadDrcAnalyzer(),
23
+ LayoutRulesAnalyzer(),
24
+ GerberDfmAnalyzer(),
25
+ EmcopilotAnalyzer(),
26
+ ]
27
+ if not emcopilot_available():
28
+ analyzers.append(StubAnalyzer("si", "Signal Integrity", "si"))
29
+ return analyzers
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+
6
+ from kitty.models import AnalyzerResult, LayoutFormat
7
+
8
+
9
+ class Analyzer(ABC):
10
+ analyzer_id: str
11
+ analyzer_name: str
12
+
13
+ @abstractmethod
14
+ def supports(self, layout_format: LayoutFormat) -> bool:
15
+ raise NotImplementedError
16
+
17
+ @abstractmethod
18
+ def run(self, layout_path: Path, layout_format: LayoutFormat) -> AnalyzerResult:
19
+ raise NotImplementedError
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from kitty.models import Severity
4
+
5
+ # KiCad DRC types that are fab-critical — always treat as blocking when KiCad says error.
6
+ DRC_CRITICAL_TYPES = {
7
+ "copper_edge_clearance",
8
+ "clearance",
9
+ "shorting_items",
10
+ "unconnected_items",
11
+ "unconnected",
12
+ "zones_intersect",
13
+ "zone_has_empty_net",
14
+ "invalid_outline",
15
+ "malformed_courtyard",
16
+ "duplicate_footprints",
17
+ "lib_footprint_mismatch",
18
+ "lib_footprint_issues",
19
+ "track_dangling",
20
+ "via_dangling",
21
+ }
22
+
23
+ # Cosmetic / documentation checks — downgrade KiCad warnings to info for release scoring.
24
+ DRC_COSMETIC_TYPES = {
25
+ "solder_mask_bridge",
26
+ "silk_overlap",
27
+ "silk_edge_clearance",
28
+ "silk_over_copper",
29
+ "text_thickness",
30
+ "text_height",
31
+ "gr_text",
32
+ "duplicate_silkscreen",
33
+ "silkscreen_over_copper",
34
+ }
35
+
36
+
37
+ def map_kicad_drc_severity(*, kicad_severity: str, violation_type: str) -> Severity:
38
+ normalized = (kicad_severity or "error").lower()
39
+ vtype = (violation_type or "").lower()
40
+
41
+ if normalized == "error":
42
+ if vtype in DRC_COSMETIC_TYPES:
43
+ return Severity.WARNING
44
+ return Severity.BLOCKING
45
+
46
+ if vtype in DRC_CRITICAL_TYPES:
47
+ return Severity.WARNING
48
+ if vtype in DRC_COSMETIC_TYPES:
49
+ return Severity.INFO
50
+ return Severity.WARNING
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from pathlib import Path
5
+
6
+ from kitty.analyzers.base import Analyzer
7
+ from kitty.emcopilot_client import (
8
+ EMCOPILOT_INSTALL,
9
+ emcopilot_available,
10
+ emcopilot_finding_to_kitty,
11
+ run_emcopilot_review,
12
+ )
13
+ from kitty.models import AnalyzerResult, Finding, LayoutFormat, Severity
14
+
15
+
16
+ class EmcopilotAnalyzer(Analyzer):
17
+ """Physics-based EMC / SI / PI review via mcp-pcb-emcopilot."""
18
+
19
+ analyzer_id = "emcopilot"
20
+ analyzer_name = "EMC / SI / PI Physics Review"
21
+
22
+ def supports(self, layout_format: LayoutFormat) -> bool:
23
+ return layout_format in {LayoutFormat.KICAD, LayoutFormat.GERBER}
24
+
25
+ def run(self, layout_path: Path, layout_format: LayoutFormat) -> AnalyzerResult:
26
+ started = time.perf_counter()
27
+
28
+ if not emcopilot_available():
29
+ return AnalyzerResult(
30
+ analyzer_id=self.analyzer_id,
31
+ analyzer_name=self.analyzer_name,
32
+ status="not_installed",
33
+ findings=[
34
+ Finding(
35
+ id="emcopilot-not-installed",
36
+ category="emc",
37
+ severity=Severity.INFO,
38
+ title="Deep physics review not enabled",
39
+ message="mcp-pcb-emcopilot is not installed in this environment.",
40
+ recommendation=EMCOPILOT_INSTALL,
41
+ )
42
+ ],
43
+ duration_ms=int((time.perf_counter() - started) * 1000),
44
+ )
45
+
46
+ session_id = f"kitty-{int(time.time())}"
47
+ try:
48
+ review = run_emcopilot_review(layout_path, layout_format, session_id)
49
+ except Exception as exc:
50
+ return AnalyzerResult(
51
+ analyzer_id=self.analyzer_id,
52
+ analyzer_name=self.analyzer_name,
53
+ status="error",
54
+ error=str(exc),
55
+ findings=[
56
+ Finding(
57
+ id="emcopilot-error",
58
+ category="emc",
59
+ severity=Severity.WARNING,
60
+ title="Emcopilot review failed",
61
+ message=str(exc),
62
+ recommendation="Verify the layout file is a valid KiCad PCB or Gerber copper layer.",
63
+ )
64
+ ],
65
+ duration_ms=int((time.perf_counter() - started) * 1000),
66
+ )
67
+
68
+ findings: list[Finding] = []
69
+ index = 0
70
+ for domain in review.domain_results:
71
+ for finding in domain.findings:
72
+ index += 1
73
+ findings.append(emcopilot_finding_to_kitty(finding, index))
74
+
75
+ status = "issues_found" if findings else "clean"
76
+ return AnalyzerResult(
77
+ analyzer_id=self.analyzer_id,
78
+ analyzer_name=self.analyzer_name,
79
+ status=status,
80
+ findings=findings,
81
+ metadata={
82
+ "executive_summary": review.executive_summary,
83
+ "detected_interfaces": review.detected_interfaces,
84
+ "design_classification": review.design_classification,
85
+ "recommendations": review.recommendations,
86
+ "domain_count": len(review.domain_results),
87
+ "finding_count": len(findings),
88
+ "schematic_sheets_loaded": getattr(review, "schematic_sheets_loaded", 0),
89
+ "schematic_warnings": getattr(review, "schematic_warnings", []),
90
+ },
91
+ duration_ms=int((time.perf_counter() - started) * 1000),
92
+ )
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from pathlib import Path
5
+
6
+ from kitty.analyzers.base import Analyzer
7
+ from kitty.ingest import classify_gerber_files
8
+ from kitty.models import AnalyzerResult, Finding, LayoutFormat, Severity
9
+
10
+
11
+ class GerberDfmAnalyzer(Analyzer):
12
+ analyzer_id = "gerber_dfm"
13
+ analyzer_name = "Gerber Fab Readiness"
14
+
15
+ def supports(self, layout_format: LayoutFormat) -> bool:
16
+ return layout_format == LayoutFormat.GERBER
17
+
18
+ def run(self, layout_path: Path, layout_format: LayoutFormat) -> AnalyzerResult:
19
+ started = time.perf_counter()
20
+ gerber_dir = layout_path if layout_path.is_dir() else layout_path.parent
21
+ layers = classify_gerber_files(gerber_dir)
22
+ findings: list[Finding] = []
23
+
24
+ if not layers["all_files"]:
25
+ return AnalyzerResult(
26
+ analyzer_id=self.analyzer_id,
27
+ analyzer_name=self.analyzer_name,
28
+ status="error",
29
+ error=f"No Gerber files found in {gerber_dir}",
30
+ duration_ms=int((time.perf_counter() - started) * 1000),
31
+ )
32
+
33
+ if not layers["copper_top"] and not layers["copper_bottom"]:
34
+ findings.append(
35
+ Finding(
36
+ id="gerber-no-copper",
37
+ category="dfm",
38
+ severity=Severity.BLOCKING,
39
+ title="No copper layers in Gerber package",
40
+ message="Could not find top or bottom copper files.",
41
+ recommendation="Include F_Cu/GTL and/or B_Cu/GBL in the fab upload.",
42
+ )
43
+ )
44
+
45
+ if not layers["outline"]:
46
+ findings.append(
47
+ Finding(
48
+ id="gerber-no-outline",
49
+ category="dfm",
50
+ severity=Severity.BLOCKING,
51
+ title="No board outline in Gerber package",
52
+ message="Could not find Edge_Cuts/GKO/GM1 outline layer.",
53
+ recommendation="Export the board outline layer for CAM processing.",
54
+ )
55
+ )
56
+
57
+ if not layers["drill"]:
58
+ findings.append(
59
+ Finding(
60
+ id="gerber-no-drill",
61
+ category="dfm",
62
+ severity=Severity.WARNING,
63
+ title="No drill files detected",
64
+ message="No Excellon/NC drill files found.",
65
+ recommendation="Include drill files if the board has plated holes.",
66
+ )
67
+ )
68
+
69
+ if not layers["mask_top"] and not layers["mask_bottom"]:
70
+ findings.append(
71
+ Finding(
72
+ id="gerber-no-mask",
73
+ category="dfm",
74
+ severity=Severity.WARNING,
75
+ title="No solder mask layers",
76
+ message="Soldermask layers were not found.",
77
+ recommendation="Export F/B soldermask for standard fab flows.",
78
+ )
79
+ )
80
+
81
+ if not layers["silk_top"] and not layers["silk_bottom"]:
82
+ findings.append(
83
+ Finding(
84
+ id="gerber-no-silk",
85
+ category="dfm",
86
+ severity=Severity.INFO,
87
+ title="No silkscreen layers",
88
+ message="Silkscreen layers were not found.",
89
+ recommendation="Add silkscreen if component reference designators are needed.",
90
+ )
91
+ )
92
+
93
+ if len(layers["all_files"]) < 4:
94
+ findings.append(
95
+ Finding(
96
+ id="gerber-thin-package",
97
+ category="dfm",
98
+ severity=Severity.INFO,
99
+ title="Minimal Gerber package",
100
+ message=f"Only {len(layers['all_files'])} Gerber/drill file(s) detected.",
101
+ recommendation="Verify the full fab package (copper, mask, silk, outline, drill).",
102
+ )
103
+ )
104
+
105
+ status = "issues_found" if findings else "clean"
106
+ return AnalyzerResult(
107
+ analyzer_id=self.analyzer_id,
108
+ analyzer_name=self.analyzer_name,
109
+ status=status,
110
+ findings=findings,
111
+ duration_ms=int((time.perf_counter() - started) * 1000),
112
+ )