se-theory-reference-kit 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.
- se_theory_reference_kit/__init__.py +1 -0
- se_theory_reference_kit/_version.py +24 -0
- se_theory_reference_kit/base/__init__.py +51 -0
- se_theory_reference_kit/base/errors.py +25 -0
- se_theory_reference_kit/base/io.py +67 -0
- se_theory_reference_kit/base/json_utils.py +78 -0
- se_theory_reference_kit/base/paths.py +168 -0
- se_theory_reference_kit/base/results.py +183 -0
- se_theory_reference_kit/cli.py +5 -0
- se_theory_reference_kit/commands/__init__.py +1 -0
- se_theory_reference_kit/commands/_context.py +115 -0
- se_theory_reference_kit/commands/catalog.py +87 -0
- se_theory_reference_kit/commands/export.py +68 -0
- se_theory_reference_kit/commands/inspect.py +66 -0
- se_theory_reference_kit/commands/root.py +48 -0
- se_theory_reference_kit/commands/scaffold.py +35 -0
- se_theory_reference_kit/commands/validate.py +50 -0
- se_theory_reference_kit/declarations/__init__.py +13 -0
- se_theory_reference_kit/declarations/config.py +24 -0
- se_theory_reference_kit/declarations/export_spec.py +18 -0
- se_theory_reference_kit/declarations/index.py +86 -0
- se_theory_reference_kit/declarations/surface.py +53 -0
- se_theory_reference_kit/export/__init__.py +21 -0
- se_theory_reference_kit/export/catalog.py +66 -0
- se_theory_reference_kit/export/engine.py +170 -0
- se_theory_reference_kit/lean/__init__.py +34 -0
- se_theory_reference_kit/lean/declarations.py +99 -0
- se_theory_reference_kit/lean/modules.py +88 -0
- se_theory_reference_kit/lean/spec.py +52 -0
- se_theory_reference_kit/lean/surface.py +41 -0
- se_theory_reference_kit/load.py +18 -0
- se_theory_reference_kit/py.typed +0 -0
- se_theory_reference_kit/reference/__init__.py +47 -0
- se_theory_reference_kit/reference/artifacts.py +161 -0
- se_theory_reference_kit/reference/registry.py +164 -0
- se_theory_reference_kit/reference/stubs.py +63 -0
- se_theory_reference_kit/reference/validation.py +85 -0
- se_theory_reference_kit/validation/__init__.py +32 -0
- se_theory_reference_kit/validation/checks/__init__.py +1 -0
- se_theory_reference_kit/validation/checks/export.py +66 -0
- se_theory_reference_kit/validation/checks/lean_surface.py +64 -0
- se_theory_reference_kit/validation/checks/reference_artifacts.py +82 -0
- se_theory_reference_kit/validation/checks/reference_index.py +43 -0
- se_theory_reference_kit/validation/checks/strict.py +85 -0
- se_theory_reference_kit/validation/context.py +54 -0
- se_theory_reference_kit/validation/defaults.py +64 -0
- se_theory_reference_kit/validation/registry.py +101 -0
- se_theory_reference_kit/validation/runner.py +110 -0
- se_theory_reference_kit-0.1.0.dist-info/METADATA +172 -0
- se_theory_reference_kit-0.1.0.dist-info/RECORD +53 -0
- se_theory_reference_kit-0.1.0.dist-info/WHEEL +4 -0
- se_theory_reference_kit-0.1.0.dist-info/entry_points.txt +2 -0
- se_theory_reference_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""se_theory_reference_kit package."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""base/__init__.py - Shared base utilities for theory-reference tooling."""
|
|
2
|
+
|
|
3
|
+
from se_theory_reference_kit.base.errors import (
|
|
4
|
+
ArtifactLoadError,
|
|
5
|
+
ConfigurationError,
|
|
6
|
+
ReferenceKitError,
|
|
7
|
+
RepositoryRootError,
|
|
8
|
+
)
|
|
9
|
+
from se_theory_reference_kit.base.paths import (
|
|
10
|
+
find_repository_root,
|
|
11
|
+
lean_module_to_path,
|
|
12
|
+
reference_artifact_path,
|
|
13
|
+
reference_dir,
|
|
14
|
+
reference_index_path,
|
|
15
|
+
resolve_repo_path,
|
|
16
|
+
)
|
|
17
|
+
from se_theory_reference_kit.base.results import (
|
|
18
|
+
CheckResult,
|
|
19
|
+
CheckSeverity,
|
|
20
|
+
CheckStatus,
|
|
21
|
+
JsonDetail,
|
|
22
|
+
cannot_verify,
|
|
23
|
+
failure,
|
|
24
|
+
ok,
|
|
25
|
+
partial,
|
|
26
|
+
warning,
|
|
27
|
+
worst_status,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ArtifactLoadError",
|
|
32
|
+
"ConfigurationError",
|
|
33
|
+
"ReferenceKitError",
|
|
34
|
+
"RepositoryRootError",
|
|
35
|
+
"find_repository_root",
|
|
36
|
+
"lean_module_to_path",
|
|
37
|
+
"reference_artifact_path",
|
|
38
|
+
"reference_dir",
|
|
39
|
+
"reference_index_path",
|
|
40
|
+
"resolve_repo_path",
|
|
41
|
+
"CheckResult",
|
|
42
|
+
"CheckSeverity",
|
|
43
|
+
"CheckStatus",
|
|
44
|
+
"JsonDetail",
|
|
45
|
+
"cannot_verify",
|
|
46
|
+
"failure",
|
|
47
|
+
"ok",
|
|
48
|
+
"partial",
|
|
49
|
+
"warning",
|
|
50
|
+
"worst_status",
|
|
51
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""base/errors.py - Exception types for theory-reference tooling."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ReferenceKitError(Exception):
|
|
5
|
+
"""Base exception for theory-reference-kit failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RepositoryRootError(ReferenceKitError):
|
|
9
|
+
"""Raised when a repository root cannot be resolved."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigurationError(ReferenceKitError):
|
|
13
|
+
"""Raised when repo-provided reference configuration is invalid."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ArtifactLoadError(ReferenceKitError):
|
|
17
|
+
"""Raised when a reference artifact cannot be loaded."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ArtifactWriteError(ReferenceKitError):
|
|
21
|
+
"""Raised when a reference artifact cannot be written."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PathResolutionError(ReferenceKitError):
|
|
25
|
+
"""Raised when a repository-relative path is invalid."""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""base/io.py - UTF-8 text and TOML loading helpers."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import tomllib
|
|
5
|
+
|
|
6
|
+
from se_theory_reference_kit.base.errors import ArtifactLoadError, ArtifactWriteError
|
|
7
|
+
|
|
8
|
+
TomlDocument = dict[str, object]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read_text(path: Path) -> str:
|
|
12
|
+
"""Read a UTF-8 text file.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
path: File path.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
File contents.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ArtifactLoadError: If the file cannot be read.
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
return path.read_text(encoding="utf-8")
|
|
25
|
+
except OSError as exc:
|
|
26
|
+
msg = f"Unable to read text file: {path}"
|
|
27
|
+
raise ArtifactLoadError(msg) from exc
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_text(path: Path, content: str) -> None:
|
|
31
|
+
"""Write a UTF-8 text file, creating parent directories if needed.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
path: Output path.
|
|
35
|
+
content: Text content.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ArtifactWriteError: If the file cannot be written.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
path.write_text(content, encoding="utf-8")
|
|
43
|
+
except OSError as exc:
|
|
44
|
+
msg = f"Unable to write text file: {path}"
|
|
45
|
+
raise ArtifactWriteError(msg) from exc
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_toml(path: Path) -> TomlDocument:
|
|
49
|
+
"""Load a TOML file.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
path: TOML file path.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Parsed TOML document.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ArtifactLoadError: If the file cannot be read or parsed.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
with path.open("rb") as file_obj:
|
|
62
|
+
data = tomllib.load(file_obj)
|
|
63
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
64
|
+
msg = f"Unable to read TOML file: {path}"
|
|
65
|
+
raise ArtifactLoadError(msg) from exc
|
|
66
|
+
|
|
67
|
+
return data
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""base/json_utils.py - Deterministic JSON helpers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from se_theory_reference_kit.base.io import write_text
|
|
8
|
+
|
|
9
|
+
JsonObject = dict[str, Any]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def encode_json(payload: JsonObject) -> str:
|
|
13
|
+
"""Encode a JSON payload deterministically.
|
|
14
|
+
|
|
15
|
+
The payload builder owns ordering. This encoder preserves insertion order
|
|
16
|
+
rather than sorting keys.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
payload: JSON-compatible object.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Encoded JSON text ending with a newline.
|
|
23
|
+
"""
|
|
24
|
+
return (
|
|
25
|
+
json.dumps(
|
|
26
|
+
payload,
|
|
27
|
+
indent=2,
|
|
28
|
+
sort_keys=False,
|
|
29
|
+
ensure_ascii=True,
|
|
30
|
+
)
|
|
31
|
+
+ "\n"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def write_or_check_text(path: Path, content: str, *, check: bool) -> bool:
|
|
36
|
+
"""Write a file or check whether it is current.
|
|
37
|
+
|
|
38
|
+
Returns true when the file is current or was written.
|
|
39
|
+
Returns false when check mode finds stale content.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
path: Output path.
|
|
43
|
+
content: Expected file content.
|
|
44
|
+
check: If true, check freshness without writing.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True when current or written, otherwise false.
|
|
48
|
+
"""
|
|
49
|
+
if check:
|
|
50
|
+
if not path.exists():
|
|
51
|
+
print(f"[stale] {path.as_posix()} is missing")
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
current = path.read_text(encoding="utf-8")
|
|
55
|
+
if current != content:
|
|
56
|
+
print(f"[stale] {path.as_posix()} is out of date")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
print(f"[ok ] {path.as_posix()}")
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
write_text(path, content)
|
|
63
|
+
print(f"[write] {path.as_posix()}")
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def write_or_check_json(path: Path, payload: JsonObject, *, check: bool) -> bool:
|
|
68
|
+
"""Write a JSON payload or check whether the file is current.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
path: Output path.
|
|
72
|
+
payload: JSON payload.
|
|
73
|
+
check: If true, check freshness without writing.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True when current or written, otherwise false.
|
|
77
|
+
"""
|
|
78
|
+
return write_or_check_text(path, encode_json(payload), check=check)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""base/paths.py - Repository-relative path helpers."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from se_theory_reference_kit.base.errors import (
|
|
6
|
+
PathResolutionError,
|
|
7
|
+
RepositoryRootError,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
ROOT_MARKERS: tuple[str, ...] = ("pyproject.toml", "SE_MANIFEST.toml", ".git")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_repository_root(start: Path | None = None) -> Path:
|
|
14
|
+
"""Find the nearest repository root from a starting path.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
start: Starting path. Defaults to the current working directory.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Resolved repository root path.
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
RepositoryRootError: If no repository root marker is found.
|
|
24
|
+
"""
|
|
25
|
+
current = (start or Path.cwd()).resolve()
|
|
26
|
+
|
|
27
|
+
if current.is_file():
|
|
28
|
+
current = current.parent
|
|
29
|
+
|
|
30
|
+
for candidate in (current, *current.parents):
|
|
31
|
+
if any((candidate / marker).exists() for marker in ROOT_MARKERS):
|
|
32
|
+
return candidate
|
|
33
|
+
|
|
34
|
+
msg = f"Could not resolve repository root from {current}"
|
|
35
|
+
raise RepositoryRootError(msg)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_repo_path(path: str | Path, *, root: Path | None = None) -> Path:
|
|
39
|
+
"""Resolve a path as repository-relative and contained within the repository.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
path: Repository-relative path.
|
|
43
|
+
root: Repository root. Defaults to nearest detected repository root.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Resolved absolute path.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
PathResolutionError: If the path escapes the repository root.
|
|
50
|
+
"""
|
|
51
|
+
repo_root = find_repository_root(root)
|
|
52
|
+
resolved = (repo_root / path).resolve()
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
resolved.relative_to(repo_root)
|
|
56
|
+
except ValueError as exc:
|
|
57
|
+
msg = f"Path escapes repository root: {path}"
|
|
58
|
+
raise PathResolutionError(msg) from exc
|
|
59
|
+
|
|
60
|
+
return resolved
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def reference_dir(
|
|
64
|
+
*,
|
|
65
|
+
root: Path | None = None,
|
|
66
|
+
reference_dir_name: str = "reference",
|
|
67
|
+
) -> Path:
|
|
68
|
+
"""Return the repository reference directory."""
|
|
69
|
+
return resolve_repo_path(reference_dir_name, root=root)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def reference_index_path(
|
|
73
|
+
*,
|
|
74
|
+
root: Path | None = None,
|
|
75
|
+
reference_dir_name: str = "reference",
|
|
76
|
+
reference_index_name: str = "index.toml",
|
|
77
|
+
) -> Path:
|
|
78
|
+
"""Return the repository reference index path."""
|
|
79
|
+
return (
|
|
80
|
+
reference_dir(
|
|
81
|
+
root=root,
|
|
82
|
+
reference_dir_name=reference_dir_name,
|
|
83
|
+
)
|
|
84
|
+
/ reference_index_name
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def reference_artifact_path(
|
|
89
|
+
path: str | Path,
|
|
90
|
+
*,
|
|
91
|
+
root: Path | None = None,
|
|
92
|
+
reference_dir_name: str = "reference",
|
|
93
|
+
) -> Path:
|
|
94
|
+
"""Resolve a declared reference artifact path.
|
|
95
|
+
|
|
96
|
+
The declared path must be repository-relative and under the reference
|
|
97
|
+
directory.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
path: Declared repository-relative artifact path.
|
|
101
|
+
root: Repository root.
|
|
102
|
+
reference_dir_name: Name of the reference artifact directory.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Resolved reference artifact path.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
PathResolutionError: If the path is outside the reference directory.
|
|
109
|
+
"""
|
|
110
|
+
resolved = resolve_repo_path(path, root=root)
|
|
111
|
+
reference_root = reference_dir(
|
|
112
|
+
root=root,
|
|
113
|
+
reference_dir_name=reference_dir_name,
|
|
114
|
+
).resolve()
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
resolved.relative_to(reference_root)
|
|
118
|
+
except ValueError as exc:
|
|
119
|
+
msg = f"Reference artifact path is not under {reference_dir_name}/: {path}"
|
|
120
|
+
raise PathResolutionError(msg) from exc
|
|
121
|
+
|
|
122
|
+
return resolved
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def lean_module_to_path(
|
|
126
|
+
module: str,
|
|
127
|
+
*,
|
|
128
|
+
root: Path | None = None,
|
|
129
|
+
lean_public_root: str,
|
|
130
|
+
) -> Path:
|
|
131
|
+
"""Resolve a Lean module name to its repository source path.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
module: Lean module name.
|
|
135
|
+
root: Repository root.
|
|
136
|
+
lean_public_root: Expected public Lean root for the owning repository.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Repository-contained Lean source path.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
PathResolutionError: If the module name is empty, path-like, malformed,
|
|
143
|
+
or outside the declared public Lean root.
|
|
144
|
+
"""
|
|
145
|
+
module_name = module.strip()
|
|
146
|
+
|
|
147
|
+
if not module_name:
|
|
148
|
+
msg = "Lean module name must be nonempty."
|
|
149
|
+
raise PathResolutionError(msg)
|
|
150
|
+
|
|
151
|
+
if "/" in module_name or "\\" in module_name:
|
|
152
|
+
msg = f"Expected Lean module name, got path-like value: {module}"
|
|
153
|
+
raise PathResolutionError(msg)
|
|
154
|
+
|
|
155
|
+
parts = module_name.split(".")
|
|
156
|
+
|
|
157
|
+
if any(not part for part in parts):
|
|
158
|
+
msg = f"Malformed Lean module name: {module}"
|
|
159
|
+
raise PathResolutionError(msg)
|
|
160
|
+
|
|
161
|
+
if module_name != lean_public_root and not module_name.startswith(
|
|
162
|
+
f"{lean_public_root}."
|
|
163
|
+
):
|
|
164
|
+
msg = f"Expected Lean module under {lean_public_root}, got: {module_name}"
|
|
165
|
+
raise PathResolutionError(msg)
|
|
166
|
+
|
|
167
|
+
relative_path = Path(*parts).with_suffix(".lean")
|
|
168
|
+
return resolve_repo_path(relative_path, root=root)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""validation/results.py - Result vocabulary for theory-reference checks."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
type JsonDetail = dict[str, object]
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CheckResult",
|
|
12
|
+
"CheckSeverity",
|
|
13
|
+
"CheckStatus",
|
|
14
|
+
"JsonDetail",
|
|
15
|
+
"cannot_verify",
|
|
16
|
+
"failure",
|
|
17
|
+
"ok",
|
|
18
|
+
"partial",
|
|
19
|
+
"warning",
|
|
20
|
+
"worst_status",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def empty_detail() -> JsonDetail:
|
|
25
|
+
"""Return an empty result detail dictionary."""
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CheckStatus(StrEnum):
|
|
30
|
+
"""Status vocabulary for one validation finding."""
|
|
31
|
+
|
|
32
|
+
OK = "ok"
|
|
33
|
+
PARTIAL = "partial"
|
|
34
|
+
FAIL = "fail"
|
|
35
|
+
CANNOT_VERIFY = "cannot-verify"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CheckSeverity(StrEnum):
|
|
39
|
+
"""Severity vocabulary for one validation finding."""
|
|
40
|
+
|
|
41
|
+
INFO = "info"
|
|
42
|
+
WARNING = "warning"
|
|
43
|
+
ERROR = "error"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class CheckResult:
|
|
48
|
+
"""One validation finding emitted by one check.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
check_id: Stable id of the check that emitted the finding.
|
|
52
|
+
status: Check status.
|
|
53
|
+
severity: Finding severity.
|
|
54
|
+
message: Human-readable finding message.
|
|
55
|
+
artifact_id: Optional artifact id associated with the finding.
|
|
56
|
+
path: Optional path associated with the finding.
|
|
57
|
+
detail: Optional structured detail for reports or downstream tooling.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
check_id: str
|
|
61
|
+
status: CheckStatus
|
|
62
|
+
severity: CheckSeverity
|
|
63
|
+
message: str
|
|
64
|
+
artifact_id: str | None = None
|
|
65
|
+
path: Path | None = None
|
|
66
|
+
detail: JsonDetail = field(default_factory=empty_detail)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def ok(
|
|
70
|
+
check_id: str,
|
|
71
|
+
message: str,
|
|
72
|
+
*,
|
|
73
|
+
artifact_id: str | None = None,
|
|
74
|
+
path: Path | None = None,
|
|
75
|
+
detail: JsonDetail | None = None,
|
|
76
|
+
) -> CheckResult:
|
|
77
|
+
"""Create an ok result."""
|
|
78
|
+
return CheckResult(
|
|
79
|
+
check_id=check_id,
|
|
80
|
+
status=CheckStatus.OK,
|
|
81
|
+
severity=CheckSeverity.INFO,
|
|
82
|
+
message=message,
|
|
83
|
+
artifact_id=artifact_id,
|
|
84
|
+
path=path,
|
|
85
|
+
detail={} if detail is None else detail,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def partial(
|
|
90
|
+
check_id: str,
|
|
91
|
+
message: str,
|
|
92
|
+
*,
|
|
93
|
+
artifact_id: str | None = None,
|
|
94
|
+
path: Path | None = None,
|
|
95
|
+
detail: JsonDetail | None = None,
|
|
96
|
+
) -> CheckResult:
|
|
97
|
+
"""Create a partial result."""
|
|
98
|
+
return CheckResult(
|
|
99
|
+
check_id=check_id,
|
|
100
|
+
status=CheckStatus.PARTIAL,
|
|
101
|
+
severity=CheckSeverity.WARNING,
|
|
102
|
+
message=message,
|
|
103
|
+
artifact_id=artifact_id,
|
|
104
|
+
path=path,
|
|
105
|
+
detail={} if detail is None else detail,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def warning(
|
|
110
|
+
check_id: str,
|
|
111
|
+
message: str,
|
|
112
|
+
*,
|
|
113
|
+
artifact_id: str | None = None,
|
|
114
|
+
path: Path | None = None,
|
|
115
|
+
detail: JsonDetail | None = None,
|
|
116
|
+
) -> CheckResult:
|
|
117
|
+
"""Create a warning failure result."""
|
|
118
|
+
return CheckResult(
|
|
119
|
+
check_id=check_id,
|
|
120
|
+
status=CheckStatus.FAIL,
|
|
121
|
+
severity=CheckSeverity.WARNING,
|
|
122
|
+
message=message,
|
|
123
|
+
artifact_id=artifact_id,
|
|
124
|
+
path=path,
|
|
125
|
+
detail={} if detail is None else detail,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def failure(
|
|
130
|
+
check_id: str,
|
|
131
|
+
message: str,
|
|
132
|
+
*,
|
|
133
|
+
artifact_id: str | None = None,
|
|
134
|
+
path: Path | None = None,
|
|
135
|
+
detail: JsonDetail | None = None,
|
|
136
|
+
) -> CheckResult:
|
|
137
|
+
"""Create an error failure result."""
|
|
138
|
+
return CheckResult(
|
|
139
|
+
check_id=check_id,
|
|
140
|
+
status=CheckStatus.FAIL,
|
|
141
|
+
severity=CheckSeverity.ERROR,
|
|
142
|
+
message=message,
|
|
143
|
+
artifact_id=artifact_id,
|
|
144
|
+
path=path,
|
|
145
|
+
detail={} if detail is None else detail,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def cannot_verify(
|
|
150
|
+
check_id: str,
|
|
151
|
+
message: str,
|
|
152
|
+
*,
|
|
153
|
+
artifact_id: str | None = None,
|
|
154
|
+
path: Path | None = None,
|
|
155
|
+
detail: JsonDetail | None = None,
|
|
156
|
+
) -> CheckResult:
|
|
157
|
+
"""Create a cannot-verify result."""
|
|
158
|
+
return CheckResult(
|
|
159
|
+
check_id=check_id,
|
|
160
|
+
status=CheckStatus.CANNOT_VERIFY,
|
|
161
|
+
severity=CheckSeverity.ERROR,
|
|
162
|
+
message=message,
|
|
163
|
+
artifact_id=artifact_id,
|
|
164
|
+
path=path,
|
|
165
|
+
detail={} if detail is None else detail,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def worst_status(results: Iterable[CheckResult]) -> CheckStatus:
|
|
170
|
+
"""Return the worst status across validation results."""
|
|
171
|
+
rank = {
|
|
172
|
+
CheckStatus.OK: 0,
|
|
173
|
+
CheckStatus.PARTIAL: 1,
|
|
174
|
+
CheckStatus.FAIL: 2,
|
|
175
|
+
CheckStatus.CANNOT_VERIFY: 3,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
worst = CheckStatus.OK
|
|
179
|
+
for result in results:
|
|
180
|
+
if rank[result.status] > rank[worst]:
|
|
181
|
+
worst = result.status
|
|
182
|
+
|
|
183
|
+
return worst
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command implementations for cli."""
|