opencomplai-cli 0.1.0__tar.gz
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.
- opencomplai_cli-0.1.0/.gitignore +69 -0
- opencomplai_cli-0.1.0/PKG-INFO +99 -0
- opencomplai_cli-0.1.0/README.md +81 -0
- opencomplai_cli-0.1.0/pyproject.toml +37 -0
- opencomplai_cli-0.1.0/src/opencomplai_cli/__init__.py +22 -0
- opencomplai_cli-0.1.0/src/opencomplai_cli/commands/__init__.py +0 -0
- opencomplai_cli-0.1.0/src/opencomplai_cli/commands/checker.py +342 -0
- opencomplai_cli-0.1.0/src/opencomplai_cli/commands/dashboard.py +379 -0
- opencomplai_cli-0.1.0/src/opencomplai_cli/commands/interactive_init.py +91 -0
- opencomplai_cli-0.1.0/src/opencomplai_cli/connectors/__init__.py +1 -0
- opencomplai_cli-0.1.0/src/opencomplai_cli/connectors/github_actions.py +247 -0
- opencomplai_cli-0.1.0/src/opencomplai_cli/connectors/gitlab_ci.py +253 -0
- opencomplai_cli-0.1.0/src/opencomplai_cli/data/checker-local.html +391 -0
- opencomplai_cli-0.1.0/src/opencomplai_cli/main.py +2684 -0
- opencomplai_cli-0.1.0/tests/__init__.py +1 -0
- opencomplai_cli-0.1.0/tests/test_checker_cmd.py +76 -0
- opencomplai_cli-0.1.0/tests/test_cli.py +171 -0
- opencomplai_cli-0.1.0/tests/test_connectors.py +198 -0
- opencomplai_cli-0.1.0/tests/test_dashboard_cmd.py +402 -0
- opencomplai_cli-0.1.0/tests/test_eu_ai_scan_golden.py +193 -0
- opencomplai_cli-0.1.0/tests/test_scan_cmd.py +340 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
.coverage
|
|
10
|
+
htmlcov/
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
|
|
13
|
+
# uv — DO NOT ignore uv.lock
|
|
14
|
+
.venv/
|
|
15
|
+
|
|
16
|
+
# Node.js
|
|
17
|
+
node_modules/
|
|
18
|
+
dist/
|
|
19
|
+
*.js.map
|
|
20
|
+
.pnpm-store/
|
|
21
|
+
|
|
22
|
+
# Editors
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
*.swp
|
|
26
|
+
.DS_Store
|
|
27
|
+
.cursor/*
|
|
28
|
+
.autopilot.json
|
|
29
|
+
|
|
30
|
+
# Secrets
|
|
31
|
+
.env
|
|
32
|
+
*.pem
|
|
33
|
+
*.key
|
|
34
|
+
|
|
35
|
+
# Docs build output
|
|
36
|
+
site/
|
|
37
|
+
docs/site/
|
|
38
|
+
docs-env/
|
|
39
|
+
.mkdocs/
|
|
40
|
+
|
|
41
|
+
# Build artifacts
|
|
42
|
+
.mintlify/
|
|
43
|
+
|
|
44
|
+
# Opencomplai generated artifacts (output of `opencomplai init` / `opencomplai check`)
|
|
45
|
+
compliance-artifact.json
|
|
46
|
+
system-manifest.json
|
|
47
|
+
# ...but keep the committed example fixtures used by the scanner docs
|
|
48
|
+
!examples/sample-system/**/system-manifest.json
|
|
49
|
+
!tests/fixtures/**/system-manifest.json
|
|
50
|
+
.venv-ci-check/*
|
|
51
|
+
# GitHub issues sync output (local mirror; may contain PII from issues)
|
|
52
|
+
github-issues/
|
|
53
|
+
|
|
54
|
+
docs/src/assets/js/checker-widget.js
|
|
55
|
+
docs/checker-widget/src/data/
|
|
56
|
+
docs/checker-widget/node_modules/
|
|
57
|
+
packages/cli/src/opencomplai_cli/data/checker-local.html
|
|
58
|
+
|
|
59
|
+
# --- Public-repo hardening (keep AI-tooling and internal artifacts out) ---
|
|
60
|
+
.claude/
|
|
61
|
+
.cursor/
|
|
62
|
+
.autopilot.json
|
|
63
|
+
.selfaudit/
|
|
64
|
+
compliance-reports/
|
|
65
|
+
*.plan.md
|
|
66
|
+
x.key
|
|
67
|
+
.tmp-*.db
|
|
68
|
+
ci-logs/
|
|
69
|
+
tmp/
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opencomplai-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Opencomplai command-line interface for EU AI Act compliance assessment
|
|
5
|
+
Project-URL: Homepage, https://opencomplai.com
|
|
6
|
+
Project-URL: Documentation, https://docs.opencomplai.com
|
|
7
|
+
Author-email: Opencomplai <hello@opencomplai.com>
|
|
8
|
+
License: AGPL-3.0-only
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: opencomplai-core[reports]>=0.1.0
|
|
11
|
+
Requires-Dist: questionary>=2.0.0
|
|
12
|
+
Requires-Dist: rich>=13.0.0
|
|
13
|
+
Requires-Dist: typer>=0.9.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# opencomplai-cli
|
|
20
|
+
|
|
21
|
+
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
22
|
+
[](https://pypi.org/project/opencomplai-cli/)
|
|
23
|
+
[](https://www.python.org/)
|
|
24
|
+
|
|
25
|
+
The `opencomplai` command-line tool for EU AI Act compliance assessment. It scans your
|
|
26
|
+
repository, classifies your AI system against the EU AI Act, and produces an auditable,
|
|
27
|
+
CI-gateable compliance artifact.
|
|
28
|
+
|
|
29
|
+
Built on [`opencomplai-core`](https://pypi.org/project/opencomplai-core/) — the same
|
|
30
|
+
deterministic, rule-based risk engine, with a rich terminal UX.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install opencomplai-cli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This pulls in `opencomplai-core[reports]` automatically. To install the full suite
|
|
39
|
+
(engine + CLI) in one step, use the [`opencomplai`](https://pypi.org/project/opencomplai/)
|
|
40
|
+
meta-package instead.
|
|
41
|
+
|
|
42
|
+
## Core commands
|
|
43
|
+
|
|
44
|
+
| Command | What it does |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `opencomplai init` | Scaffold a `system-manifest.json` for your project |
|
|
47
|
+
| `opencomplai scan` | Corroborate the manifest against your code and report discrepancies |
|
|
48
|
+
| `opencomplai check` | Run the compliance gate and write `compliance-artifact.json` |
|
|
49
|
+
| `opencomplai dashboard` | Manage premium dashboard enrollment and sync |
|
|
50
|
+
|
|
51
|
+
Run `opencomplai --help` for the full command list, or `opencomplai <command> --help` for
|
|
52
|
+
options.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# 1. Scaffold a manifest for your project
|
|
58
|
+
opencomplai init
|
|
59
|
+
|
|
60
|
+
# 2. Cross-check the manifest against your source tree
|
|
61
|
+
opencomplai scan --manifest system-manifest.json --repo-root .
|
|
62
|
+
|
|
63
|
+
# 3. Run the compliance gate (writes compliance-artifact.json)
|
|
64
|
+
opencomplai check
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`opencomplai check` is the canonical CI gate. Its exit code is contractual:
|
|
68
|
+
|
|
69
|
+
| Exit code | Meaning |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `0` | PASS |
|
|
72
|
+
| `1` | CONTROL_FAIL |
|
|
73
|
+
| `2` | VALIDATION_FAIL |
|
|
74
|
+
| `3` | POLICY_BLOCK |
|
|
75
|
+
| `4` | TRAP_DETECTED |
|
|
76
|
+
|
|
77
|
+
So you can wire it straight into CI:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
opencomplai check || exit $?
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Optional: AI intent analysis
|
|
84
|
+
|
|
85
|
+
Install the [`opencomplai-ai`](https://pypi.org/project/opencomplai-ai/) plugin to unlock
|
|
86
|
+
the `--ai-intent` flag, which classifies how each AI callsite is actually used:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install opencomplai-ai
|
|
90
|
+
opencomplai scan --ai-intent
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
Full CLI reference and guides at **[docs.opencomplai.com](https://docs.opencomplai.com)**.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
AGPL-3.0-only. See [LICENSE](https://www.gnu.org/licenses/agpl-3.0).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# opencomplai-cli
|
|
2
|
+
|
|
3
|
+
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
4
|
+
[](https://pypi.org/project/opencomplai-cli/)
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
|
|
7
|
+
The `opencomplai` command-line tool for EU AI Act compliance assessment. It scans your
|
|
8
|
+
repository, classifies your AI system against the EU AI Act, and produces an auditable,
|
|
9
|
+
CI-gateable compliance artifact.
|
|
10
|
+
|
|
11
|
+
Built on [`opencomplai-core`](https://pypi.org/project/opencomplai-core/) — the same
|
|
12
|
+
deterministic, rule-based risk engine, with a rich terminal UX.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install opencomplai-cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This pulls in `opencomplai-core[reports]` automatically. To install the full suite
|
|
21
|
+
(engine + CLI) in one step, use the [`opencomplai`](https://pypi.org/project/opencomplai/)
|
|
22
|
+
meta-package instead.
|
|
23
|
+
|
|
24
|
+
## Core commands
|
|
25
|
+
|
|
26
|
+
| Command | What it does |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `opencomplai init` | Scaffold a `system-manifest.json` for your project |
|
|
29
|
+
| `opencomplai scan` | Corroborate the manifest against your code and report discrepancies |
|
|
30
|
+
| `opencomplai check` | Run the compliance gate and write `compliance-artifact.json` |
|
|
31
|
+
| `opencomplai dashboard` | Manage premium dashboard enrollment and sync |
|
|
32
|
+
|
|
33
|
+
Run `opencomplai --help` for the full command list, or `opencomplai <command> --help` for
|
|
34
|
+
options.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# 1. Scaffold a manifest for your project
|
|
40
|
+
opencomplai init
|
|
41
|
+
|
|
42
|
+
# 2. Cross-check the manifest against your source tree
|
|
43
|
+
opencomplai scan --manifest system-manifest.json --repo-root .
|
|
44
|
+
|
|
45
|
+
# 3. Run the compliance gate (writes compliance-artifact.json)
|
|
46
|
+
opencomplai check
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`opencomplai check` is the canonical CI gate. Its exit code is contractual:
|
|
50
|
+
|
|
51
|
+
| Exit code | Meaning |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `0` | PASS |
|
|
54
|
+
| `1` | CONTROL_FAIL |
|
|
55
|
+
| `2` | VALIDATION_FAIL |
|
|
56
|
+
| `3` | POLICY_BLOCK |
|
|
57
|
+
| `4` | TRAP_DETECTED |
|
|
58
|
+
|
|
59
|
+
So you can wire it straight into CI:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
opencomplai check || exit $?
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Optional: AI intent analysis
|
|
66
|
+
|
|
67
|
+
Install the [`opencomplai-ai`](https://pypi.org/project/opencomplai-ai/) plugin to unlock
|
|
68
|
+
the `--ai-intent` flag, which classifies how each AI callsite is actually used:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install opencomplai-ai
|
|
72
|
+
opencomplai scan --ai-intent
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
Full CLI reference and guides at **[docs.opencomplai.com](https://docs.opencomplai.com)**.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
AGPL-3.0-only. See [LICENSE](https://www.gnu.org/licenses/agpl-3.0).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "opencomplai-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Opencomplai command-line interface for EU AI Act compliance assessment"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "AGPL-3.0-only" }
|
|
7
|
+
authors = [{ name = "Opencomplai", email = "hello@opencomplai.com" }]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"opencomplai-core[reports]>=0.1.0",
|
|
11
|
+
"typer>=0.9.0",
|
|
12
|
+
"rich>=13.0.0",
|
|
13
|
+
"questionary>=2.0.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://opencomplai.com"
|
|
18
|
+
Documentation = "https://docs.opencomplai.com"
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
opencomplai = "opencomplai_cli.main:app"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=7.0",
|
|
26
|
+
"ruff>=0.1.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/opencomplai_cli"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
33
|
+
"src/opencomplai_cli/data/checker-local.html" = "opencomplai_cli/data/checker-local.html"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Opencomplai CLI."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
# Canonical project metadata — single source of truth for the `--version`
|
|
6
|
+
# flag and the `opencomplai info` command. Mirrors the [project] table in
|
|
7
|
+
# each package's pyproject.toml so the CLI never depends on `pip show`.
|
|
8
|
+
PROJECT_NAME = "opencomplai"
|
|
9
|
+
PROJECT_TAGLINE = "Open-source AI compliance for a trustworthy future."
|
|
10
|
+
PROJECT_HOMEPAGE = "https://opencomplai.com"
|
|
11
|
+
PROJECT_DOCS_URL = "https://docs.opencomplai.com"
|
|
12
|
+
PROJECT_AUTHOR = "Opencomplai"
|
|
13
|
+
PROJECT_AUTHOR_EMAIL = "hello@opencomplai.com"
|
|
14
|
+
PROJECT_LICENSE = "AGPL-3.0-only"
|
|
15
|
+
|
|
16
|
+
# The three distributions that make up the Opencomplai suite, in dependency
|
|
17
|
+
# order (umbrella SDK → CLI → core engine), with a short role description.
|
|
18
|
+
SUITE_PACKAGES: tuple[tuple[str, str], ...] = (
|
|
19
|
+
("opencomplai", "SDK (umbrella)"),
|
|
20
|
+
("opencomplai-cli", "command-line interface"),
|
|
21
|
+
("opencomplai-core", "risk assessment engine"),
|
|
22
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""EU AI Act compliance checker CLI wizard and results display."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from opencomplai_core.compliance_checker import (
|
|
13
|
+
CHECKER_VERSION,
|
|
14
|
+
CheckerSession,
|
|
15
|
+
ComplianceCheckerResult,
|
|
16
|
+
EntityType,
|
|
17
|
+
evaluate,
|
|
18
|
+
export_all,
|
|
19
|
+
render_json,
|
|
20
|
+
render_markdown,
|
|
21
|
+
render_pdf,
|
|
22
|
+
)
|
|
23
|
+
from opencomplai_core.compliance_checker.catalog import load_help_content
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
from rich.panel import Panel
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
err_console = Console(stderr=True)
|
|
30
|
+
|
|
31
|
+
DISCLAIMER = (
|
|
32
|
+
"Opencomplai is not affiliated with the Future of Life Institute or the European Union. "
|
|
33
|
+
"Results are informational only — not legal advice. Seek professional legal counsel."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
ENTITY_LABELS = {
|
|
37
|
+
EntityType.PROVIDER.value: "Provider",
|
|
38
|
+
EntityType.DEPLOYER.value: "Deployer",
|
|
39
|
+
EntityType.DISTRIBUTOR.value: "Distributor",
|
|
40
|
+
EntityType.IMPORTER.value: "Importer",
|
|
41
|
+
EntityType.PRODUCT_MANUFACTURER.value: "Product manufacturer",
|
|
42
|
+
EntityType.AUTHORISED_REP.value: "Authorised representative",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _confirm(label: str, *, default: bool = False) -> bool:
|
|
47
|
+
try:
|
|
48
|
+
import questionary
|
|
49
|
+
|
|
50
|
+
return questionary.confirm(label, default=default).ask() or False
|
|
51
|
+
except ImportError:
|
|
52
|
+
return typer.confirm(label, default=default)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _select(label: str, choices: list[tuple[str, str]]) -> str:
|
|
56
|
+
"""Return the choice value (first element of tuple)."""
|
|
57
|
+
try:
|
|
58
|
+
import questionary
|
|
59
|
+
|
|
60
|
+
options = [
|
|
61
|
+
questionary.Choice(title=title, value=value) for value, title in choices
|
|
62
|
+
]
|
|
63
|
+
result = questionary.select(label, choices=options).ask()
|
|
64
|
+
if result is None:
|
|
65
|
+
raise typer.Abort()
|
|
66
|
+
return result
|
|
67
|
+
except ImportError:
|
|
68
|
+
console.print(f"\n[bold]{label}[/bold]")
|
|
69
|
+
for idx, (_value, title) in enumerate(choices, start=1):
|
|
70
|
+
console.print(f" {idx}. {title}")
|
|
71
|
+
raw = typer.prompt("Enter number", type=int)
|
|
72
|
+
if raw < 1 or raw > len(choices):
|
|
73
|
+
raise typer.BadParameter("Invalid selection") from None
|
|
74
|
+
return choices[raw - 1][0]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _show_help(section_key: str) -> None:
|
|
78
|
+
help_data = load_help_content()
|
|
79
|
+
section = help_data.get(section_key)
|
|
80
|
+
if not section:
|
|
81
|
+
return
|
|
82
|
+
console.print(
|
|
83
|
+
Panel(
|
|
84
|
+
section.get("body", ""),
|
|
85
|
+
title=section.get("title", section_key),
|
|
86
|
+
border_style="blue",
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _show_entity_guide() -> None:
|
|
92
|
+
help_data = load_help_content()
|
|
93
|
+
body = help_data.get("entity_definitions", {}).get("body", "")
|
|
94
|
+
console.print(Panel(body, title="Operator roles (Article 3)", border_style="cyan"))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_interactive_wizard(*, skip_allowed: bool = True) -> CheckerSession | None:
|
|
98
|
+
"""Collect answers via terminal prompts. Returns None if skipped."""
|
|
99
|
+
console.print(
|
|
100
|
+
Panel(
|
|
101
|
+
"This wizard determines how the EU AI Act may apply to your AI system.\n"
|
|
102
|
+
"Press Ctrl+C to abort. Type '?' at any prompt for help (where supported).",
|
|
103
|
+
title="EU AI Act Compliance Checker",
|
|
104
|
+
border_style="green",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
if skip_allowed and _confirm(
|
|
108
|
+
"Skip applicability check and continue without checker?", default=False
|
|
109
|
+
):
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
answers: dict[str, Any] = {}
|
|
113
|
+
|
|
114
|
+
_show_help("ai_system_definition")
|
|
115
|
+
answers["gate_is_ai_system"] = _confirm(
|
|
116
|
+
"Is your system an 'AI system' under the EU AI Act (Article 3(1))?",
|
|
117
|
+
default=True,
|
|
118
|
+
)
|
|
119
|
+
if not answers["gate_is_ai_system"]:
|
|
120
|
+
return CheckerSession(answers=answers)
|
|
121
|
+
|
|
122
|
+
_show_entity_guide()
|
|
123
|
+
entity = _select(
|
|
124
|
+
"Which kind of entity is your organisation?",
|
|
125
|
+
[(e.value, ENTITY_LABELS[e.value]) for e in EntityType],
|
|
126
|
+
)
|
|
127
|
+
answers["e1_entity_type"] = entity
|
|
128
|
+
|
|
129
|
+
if entity == EntityType.AUTHORISED_REP.value:
|
|
130
|
+
return CheckerSession(answers=answers)
|
|
131
|
+
|
|
132
|
+
if entity == EntityType.PRODUCT_MANUFACTURER.value:
|
|
133
|
+
integration = _select(
|
|
134
|
+
"Does your product integrate an AI system under your manufacturer name?",
|
|
135
|
+
[
|
|
136
|
+
(
|
|
137
|
+
"integrated",
|
|
138
|
+
"Yes — placed on market or put into service with my product",
|
|
139
|
+
),
|
|
140
|
+
("none", "No — none of the above"),
|
|
141
|
+
],
|
|
142
|
+
)
|
|
143
|
+
answers["e3_product_integration"] = integration
|
|
144
|
+
if integration == "none":
|
|
145
|
+
return CheckerSession(answers=answers)
|
|
146
|
+
else:
|
|
147
|
+
_show_help("modifications_overview")
|
|
148
|
+
answers["e2_modifications"] = _confirm(
|
|
149
|
+
"Do you (or a downstream operator) make substantial modifications to the system?",
|
|
150
|
+
default=False,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
_show_help("high_risk_overview")
|
|
154
|
+
answers["hr1_annex_i"] = _confirm(
|
|
155
|
+
"Is the system a product with AI as safety component under Annex I harmonisation law?",
|
|
156
|
+
default=False,
|
|
157
|
+
)
|
|
158
|
+
answers["hr2_annex_iii"] = _confirm(
|
|
159
|
+
"Does the system fall within an Annex III high-risk use case?",
|
|
160
|
+
default=False,
|
|
161
|
+
)
|
|
162
|
+
if answers["hr1_annex_i"] or answers["hr2_annex_iii"]:
|
|
163
|
+
answers["hr3_art_6_3"] = _confirm(
|
|
164
|
+
"Does Article 6(3) apply (safety component required for product conformity)?",
|
|
165
|
+
default=False,
|
|
166
|
+
)
|
|
167
|
+
answers["hr4_narrow_task"] = _confirm(
|
|
168
|
+
"Is the AI intended only for a narrow procedural task (Art 6(3) exception)?",
|
|
169
|
+
default=False,
|
|
170
|
+
)
|
|
171
|
+
answers["hr5_no_significant_risk"] = _confirm(
|
|
172
|
+
"Does the system NOT pose significant risk to health, safety, or fundamental rights?",
|
|
173
|
+
default=False,
|
|
174
|
+
)
|
|
175
|
+
answers["hr6_accessory"] = _confirm(
|
|
176
|
+
"Is the system purely accessory to the relevant human decision (Art 6(3))?",
|
|
177
|
+
default=False,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
_show_help("scope_overview")
|
|
181
|
+
answers["s1_in_scope"] = _confirm(
|
|
182
|
+
"Are you placing, deploying, or using the system's output in the EU?",
|
|
183
|
+
default=True,
|
|
184
|
+
)
|
|
185
|
+
if not answers["s1_in_scope"]:
|
|
186
|
+
return CheckerSession(answers=answers)
|
|
187
|
+
|
|
188
|
+
answers["s1_scope_region"] = _select(
|
|
189
|
+
"Where is your organisation established?",
|
|
190
|
+
[
|
|
191
|
+
("eu", "Established or located in the EU"),
|
|
192
|
+
("third_country", "Outside the EU"),
|
|
193
|
+
],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
_show_help("gpai_overview")
|
|
197
|
+
answers["s1_gpai"] = _confirm(
|
|
198
|
+
"Are you placing a General-Purpose AI model on the EU market?",
|
|
199
|
+
default=False,
|
|
200
|
+
)
|
|
201
|
+
if answers["s1_gpai"]:
|
|
202
|
+
answers["s1_gpai_systemic_risk"] = _confirm(
|
|
203
|
+
"Does the GPAI model have systemic risk (high impact capabilities)?",
|
|
204
|
+
default=False,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
answers["r2_excluded"] = _confirm(
|
|
208
|
+
"Is the system excluded (military, R&D only, open-source not yet placed, personal use)?",
|
|
209
|
+
default=False,
|
|
210
|
+
)
|
|
211
|
+
if answers["r2_excluded"]:
|
|
212
|
+
return CheckerSession(answers=answers)
|
|
213
|
+
|
|
214
|
+
answers["r3_prohibited"] = _confirm(
|
|
215
|
+
"Does the system perform prohibited practices under Article 5?",
|
|
216
|
+
default=False,
|
|
217
|
+
)
|
|
218
|
+
if answers["r3_prohibited"]:
|
|
219
|
+
return CheckerSession(answers=answers)
|
|
220
|
+
|
|
221
|
+
answers["r4_transparency"] = _confirm(
|
|
222
|
+
"Does the system require transparency obligations (chatbot, deepfake, synthetic content)?",
|
|
223
|
+
default=False,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if entity == EntityType.DEPLOYER.value:
|
|
227
|
+
answers["r5_fria"] = _confirm(
|
|
228
|
+
"Are you a public body or private entity providing public services (FRIA under Art 27)?",
|
|
229
|
+
default=False,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return CheckerSession(answers=answers)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def display_results(result: ComplianceCheckerResult) -> None:
|
|
236
|
+
"""Render human-readable results with glossary."""
|
|
237
|
+
headline_parts: list[str] = []
|
|
238
|
+
if result.is_prohibited:
|
|
239
|
+
headline_parts.append("[red]Prohibited[/red]")
|
|
240
|
+
elif result.is_high_risk:
|
|
241
|
+
headline_parts.append("[yellow]High risk[/yellow]")
|
|
242
|
+
elif result.in_scope:
|
|
243
|
+
headline_parts.append("[green]In scope[/green]")
|
|
244
|
+
else:
|
|
245
|
+
headline_parts.append("[dim]Out of scope[/dim]")
|
|
246
|
+
if result.effective_entity:
|
|
247
|
+
headline_parts.append(
|
|
248
|
+
f"— {ENTITY_LABELS.get(result.effective_entity.value, result.effective_entity.value)}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
console.print(
|
|
252
|
+
Panel(
|
|
253
|
+
" ".join(headline_parts),
|
|
254
|
+
title="EU AI Act Applicability Result",
|
|
255
|
+
border_style="bold",
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if result.status_changes:
|
|
260
|
+
st = Table(title="Status changes")
|
|
261
|
+
st.add_column("Status")
|
|
262
|
+
st.add_column("Description")
|
|
263
|
+
for item in result.status_changes:
|
|
264
|
+
st.add_row(f"[bold]{item.title}[/bold]", item.body)
|
|
265
|
+
console.print(st)
|
|
266
|
+
|
|
267
|
+
if result.obligations:
|
|
268
|
+
ot = Table(title="Your obligations")
|
|
269
|
+
ot.add_column("Obligation")
|
|
270
|
+
ot.add_column("Reference")
|
|
271
|
+
ot.add_column("Summary")
|
|
272
|
+
for item in result.obligations:
|
|
273
|
+
summary = item.body[:120] + ("…" if len(item.body) > 120 else "")
|
|
274
|
+
ot.add_row(item.title, item.article_ref, summary)
|
|
275
|
+
console.print(ot)
|
|
276
|
+
|
|
277
|
+
help_data = load_help_content()
|
|
278
|
+
glossary = help_data.get("entity_definitions", {})
|
|
279
|
+
if glossary:
|
|
280
|
+
console.print(
|
|
281
|
+
Panel(
|
|
282
|
+
glossary.get("body", ""),
|
|
283
|
+
title="What these terms mean",
|
|
284
|
+
border_style="dim",
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
console.print(f"\n[dim]{DISCLAIMER}[/dim]\n")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def evaluate_and_finalize(session: CheckerSession) -> ComplianceCheckerResult:
|
|
292
|
+
result = evaluate(session)
|
|
293
|
+
result.answers = dict(session.answers)
|
|
294
|
+
result.session_id = str(uuid.uuid4())
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def write_exports(
|
|
299
|
+
result: ComplianceCheckerResult,
|
|
300
|
+
*,
|
|
301
|
+
export_json: Path | None = None,
|
|
302
|
+
export_md: Path | None = None,
|
|
303
|
+
export_pdf: Path | None = None,
|
|
304
|
+
export_all_base: Path | None = None,
|
|
305
|
+
) -> None:
|
|
306
|
+
if export_all_base is not None:
|
|
307
|
+
parent = export_all_base.parent
|
|
308
|
+
base = export_all_base.stem
|
|
309
|
+
paths = export_all(result, parent, basename=base)
|
|
310
|
+
console.print(f"Exported: {paths['json']}, {paths['markdown']}, {paths['pdf']}")
|
|
311
|
+
return
|
|
312
|
+
if export_json:
|
|
313
|
+
export_json.write_text(render_json(result), encoding="utf-8")
|
|
314
|
+
console.print(f"Wrote JSON: {export_json}")
|
|
315
|
+
if export_md:
|
|
316
|
+
export_md.write_text(render_markdown(result), encoding="utf-8")
|
|
317
|
+
console.print(f"Wrote Markdown: {export_md}")
|
|
318
|
+
if export_pdf:
|
|
319
|
+
try:
|
|
320
|
+
export_pdf.write_bytes(render_pdf(result))
|
|
321
|
+
console.print(f"Wrote PDF: {export_pdf}")
|
|
322
|
+
except ImportError as exc:
|
|
323
|
+
err_console.print(f"[yellow]PDF export skipped:[/yellow] {exc}")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def load_answers_file(path: Path) -> CheckerSession:
|
|
327
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
328
|
+
if "answers" in raw:
|
|
329
|
+
return CheckerSession(answers=raw["answers"])
|
|
330
|
+
return CheckerSession(answers=raw)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def build_checker_session_ref(
|
|
334
|
+
result: ComplianceCheckerResult,
|
|
335
|
+
report_json_path: Path | None = None,
|
|
336
|
+
) -> dict[str, str]:
|
|
337
|
+
return {
|
|
338
|
+
"checker_version": CHECKER_VERSION,
|
|
339
|
+
"session_id": result.session_id or str(uuid.uuid4()),
|
|
340
|
+
"completed_at": datetime.now(UTC).isoformat(),
|
|
341
|
+
"report_json_path": str(report_json_path) if report_json_path else "",
|
|
342
|
+
}
|