atomicguard 1.2.0__py3-none-any.whl → 2.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.
- atomicguard/__init__.py +4 -3
- atomicguard/application/__init__.py +5 -0
- atomicguard/application/action_pair.py +2 -2
- atomicguard/application/agent.py +2 -3
- atomicguard/application/checkpoint_service.py +130 -0
- atomicguard/application/resume_service.py +242 -0
- atomicguard/application/workflow.py +53 -3
- atomicguard/domain/__init__.py +2 -0
- atomicguard/domain/extraction.py +257 -0
- atomicguard/domain/interfaces.py +17 -2
- atomicguard/domain/models.py +91 -13
- atomicguard/domain/multiagent.py +279 -0
- atomicguard/domain/prompts.py +12 -1
- atomicguard/domain/workflow.py +642 -0
- atomicguard/infrastructure/llm/huggingface.py +4 -22
- atomicguard/infrastructure/llm/mock.py +5 -4
- atomicguard/infrastructure/llm/ollama.py +5 -23
- atomicguard/infrastructure/persistence/filesystem.py +133 -46
- atomicguard/infrastructure/persistence/memory.py +10 -0
- {atomicguard-1.2.0.dist-info → atomicguard-2.0.0.dist-info}/METADATA +1 -1
- atomicguard-2.0.0.dist-info/RECORD +42 -0
- atomicguard-1.2.0.dist-info/RECORD +0 -37
- {atomicguard-1.2.0.dist-info → atomicguard-2.0.0.dist-info}/WHEEL +0 -0
- {atomicguard-1.2.0.dist-info → atomicguard-2.0.0.dist-info}/entry_points.txt +0 -0
- {atomicguard-1.2.0.dist-info → atomicguard-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {atomicguard-1.2.0.dist-info → atomicguard-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Artifact Extraction (Extension 02: Definitions 17-18).
|
|
3
|
+
|
|
4
|
+
Implements:
|
|
5
|
+
- Predicate base class (Φ: r → {⊤, ⊥})
|
|
6
|
+
- Concrete predicates: StatusPredicate, ActionPairPredicate, WorkflowPredicate, SourcePredicate
|
|
7
|
+
- Compound predicates: AndPredicate, OrPredicate, NotPredicate
|
|
8
|
+
- Extraction function: E: ℛ × Φ → 2^ℛ
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from atomicguard.domain.models import Artifact, ArtifactSource, ArtifactStatus
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from atomicguard.domain.interfaces import ArtifactDAGInterface
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# PREDICATE BASE CLASS (Definition 18)
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Predicate(ABC):
|
|
28
|
+
"""
|
|
29
|
+
Abstract base class for filter predicates Φ: r → {⊤, ⊥} (Definition 18).
|
|
30
|
+
|
|
31
|
+
Predicates are boolean functions over repository items that determine
|
|
32
|
+
whether an artifact should be included in an extraction result.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def matches(self, artifact: Artifact) -> bool:
|
|
37
|
+
"""Evaluate predicate on artifact.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
artifact: The artifact to test.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if artifact matches predicate, False otherwise.
|
|
44
|
+
"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def __call__(self, artifact: Artifact) -> bool:
|
|
48
|
+
"""Allow predicate(artifact) syntax.
|
|
49
|
+
|
|
50
|
+
Enables predicates to be used as Φ(r) per the formal notation.
|
|
51
|
+
"""
|
|
52
|
+
return self.matches(artifact)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# =============================================================================
|
|
56
|
+
# CONCRETE PREDICATES
|
|
57
|
+
# =============================================================================
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class StatusPredicate(Predicate):
|
|
61
|
+
"""Filter by artifact status (Φ_status).
|
|
62
|
+
|
|
63
|
+
Matches artifacts whose status is in the specified set of statuses.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, *statuses: ArtifactStatus) -> None:
|
|
67
|
+
"""Initialize with one or more statuses to match.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
*statuses: ArtifactStatus values to match.
|
|
71
|
+
"""
|
|
72
|
+
self._statuses = set(statuses)
|
|
73
|
+
|
|
74
|
+
def matches(self, artifact: Artifact) -> bool:
|
|
75
|
+
"""Check if artifact status is in the specified set."""
|
|
76
|
+
return artifact.status in self._statuses
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ActionPairPredicate(Predicate):
|
|
80
|
+
"""Filter by action pair ID (Φ_action_pair).
|
|
81
|
+
|
|
82
|
+
Matches artifacts produced by a specific action pair.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, action_pair_id: str) -> None:
|
|
86
|
+
"""Initialize with action pair ID to match.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
action_pair_id: The action_pair_id to match.
|
|
90
|
+
"""
|
|
91
|
+
self._action_pair_id = action_pair_id
|
|
92
|
+
|
|
93
|
+
def matches(self, artifact: Artifact) -> bool:
|
|
94
|
+
"""Check if artifact's action_pair_id matches."""
|
|
95
|
+
return artifact.action_pair_id == self._action_pair_id
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class WorkflowPredicate(Predicate):
|
|
99
|
+
"""Filter by workflow ID (Φ_workflow).
|
|
100
|
+
|
|
101
|
+
Matches artifacts from a specific workflow execution.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, workflow_id: str) -> None:
|
|
105
|
+
"""Initialize with workflow ID to match.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
workflow_id: The workflow_id to match.
|
|
109
|
+
"""
|
|
110
|
+
self._workflow_id = workflow_id
|
|
111
|
+
|
|
112
|
+
def matches(self, artifact: Artifact) -> bool:
|
|
113
|
+
"""Check if artifact's workflow_id matches."""
|
|
114
|
+
return artifact.workflow_id == self._workflow_id
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class SourcePredicate(Predicate):
|
|
118
|
+
"""Filter by artifact source (Φ_source).
|
|
119
|
+
|
|
120
|
+
Matches artifacts with a specific source (GENERATED, HUMAN, IMPORTED).
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, source: ArtifactSource) -> None:
|
|
124
|
+
"""Initialize with source to match.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
source: The ArtifactSource to match.
|
|
128
|
+
"""
|
|
129
|
+
self._source = source
|
|
130
|
+
|
|
131
|
+
def matches(self, artifact: Artifact) -> bool:
|
|
132
|
+
"""Check if artifact's source matches."""
|
|
133
|
+
return artifact.source == self._source
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# =============================================================================
|
|
137
|
+
# COMPOUND PREDICATES
|
|
138
|
+
# =============================================================================
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class AndPredicate(Predicate):
|
|
142
|
+
"""Logical AND of two predicates (Φ₁ ∧ Φ₂).
|
|
143
|
+
|
|
144
|
+
Matches only if both predicates match.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, p1: Predicate, p2: Predicate) -> None:
|
|
148
|
+
"""Initialize with two predicates to AND together.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
p1: First predicate.
|
|
152
|
+
p2: Second predicate.
|
|
153
|
+
"""
|
|
154
|
+
self._p1 = p1
|
|
155
|
+
self._p2 = p2
|
|
156
|
+
|
|
157
|
+
def matches(self, artifact: Artifact) -> bool:
|
|
158
|
+
"""Check if both predicates match."""
|
|
159
|
+
return self._p1.matches(artifact) and self._p2.matches(artifact)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class OrPredicate(Predicate):
|
|
163
|
+
"""Logical OR of two predicates (Φ₁ ∨ Φ₂).
|
|
164
|
+
|
|
165
|
+
Matches if either predicate matches.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(self, p1: Predicate, p2: Predicate) -> None:
|
|
169
|
+
"""Initialize with two predicates to OR together.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
p1: First predicate.
|
|
173
|
+
p2: Second predicate.
|
|
174
|
+
"""
|
|
175
|
+
self._p1 = p1
|
|
176
|
+
self._p2 = p2
|
|
177
|
+
|
|
178
|
+
def matches(self, artifact: Artifact) -> bool:
|
|
179
|
+
"""Check if either predicate matches."""
|
|
180
|
+
return self._p1.matches(artifact) or self._p2.matches(artifact)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class NotPredicate(Predicate):
|
|
184
|
+
"""Logical NOT of a predicate (¬Φ).
|
|
185
|
+
|
|
186
|
+
Inverts the result of the wrapped predicate.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def __init__(self, predicate: Predicate) -> None:
|
|
190
|
+
"""Initialize with predicate to invert.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
predicate: The predicate to negate.
|
|
194
|
+
"""
|
|
195
|
+
self._predicate = predicate
|
|
196
|
+
|
|
197
|
+
def matches(self, artifact: Artifact) -> bool:
|
|
198
|
+
"""Return inverse of wrapped predicate."""
|
|
199
|
+
return not self._predicate.matches(artifact)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# =============================================================================
|
|
203
|
+
# EXTRACTION FUNCTION (Definition 17)
|
|
204
|
+
# =============================================================================
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def extract(
|
|
208
|
+
dag: ArtifactDAGInterface,
|
|
209
|
+
predicate: Predicate | None = None,
|
|
210
|
+
limit: int | None = None,
|
|
211
|
+
offset: int | None = None,
|
|
212
|
+
order_by: str | None = None,
|
|
213
|
+
) -> list[Artifact]:
|
|
214
|
+
"""Extract artifacts from repository matching predicate (Definition 17).
|
|
215
|
+
|
|
216
|
+
E: ℛ × Φ → 2^ℛ
|
|
217
|
+
|
|
218
|
+
This is a read-only operation that does not modify the repository.
|
|
219
|
+
Implements Theorem 3: Extraction Invariance - extraction is idempotent
|
|
220
|
+
and referentially transparent.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
dag: The artifact repository to extract from.
|
|
224
|
+
predicate: Filter predicate Φ. If None, matches all artifacts.
|
|
225
|
+
limit: Maximum number of artifacts to return.
|
|
226
|
+
offset: Number of artifacts to skip before returning.
|
|
227
|
+
order_by: Field name to sort by. Prefix with '-' for descending order.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of artifacts matching the predicate, with pagination applied.
|
|
231
|
+
"""
|
|
232
|
+
# Get all artifacts from DAG via interface method
|
|
233
|
+
all_artifacts = dag.get_all()
|
|
234
|
+
|
|
235
|
+
# Apply predicate filter
|
|
236
|
+
if predicate is not None:
|
|
237
|
+
all_artifacts = [a for a in all_artifacts if predicate.matches(a)]
|
|
238
|
+
|
|
239
|
+
# Apply ordering
|
|
240
|
+
if order_by is not None:
|
|
241
|
+
descending = order_by.startswith("-")
|
|
242
|
+
field_name = order_by.lstrip("-")
|
|
243
|
+
|
|
244
|
+
def get_sort_key(artifact: Artifact) -> str:
|
|
245
|
+
return getattr(artifact, field_name, "")
|
|
246
|
+
|
|
247
|
+
all_artifacts.sort(key=get_sort_key, reverse=descending)
|
|
248
|
+
|
|
249
|
+
# Apply offset
|
|
250
|
+
if offset is not None and offset > 0:
|
|
251
|
+
all_artifacts = all_artifacts[offset:]
|
|
252
|
+
|
|
253
|
+
# Apply limit
|
|
254
|
+
if limit is not None and limit > 0:
|
|
255
|
+
all_artifacts = all_artifacts[:limit]
|
|
256
|
+
|
|
257
|
+
return all_artifacts
|
atomicguard/domain/interfaces.py
CHANGED
|
@@ -44,18 +44,20 @@ class GeneratorInterface(ABC):
|
|
|
44
44
|
def generate(
|
|
45
45
|
self,
|
|
46
46
|
context: "Context",
|
|
47
|
-
template:
|
|
47
|
+
template: "PromptTemplate",
|
|
48
48
|
action_pair_id: str = "unknown",
|
|
49
49
|
workflow_id: str = "unknown",
|
|
50
|
+
workflow_ref: str | None = None,
|
|
50
51
|
) -> "Artifact":
|
|
51
52
|
"""
|
|
52
53
|
Generate an artifact based on context.
|
|
53
54
|
|
|
54
55
|
Args:
|
|
55
56
|
context: The generation context including specification and feedback
|
|
56
|
-
template:
|
|
57
|
+
template: Prompt template for structured generation
|
|
57
58
|
action_pair_id: Identifier for the action pair requesting generation
|
|
58
59
|
workflow_id: UUID of the workflow execution instance
|
|
60
|
+
workflow_ref: Content-addressed workflow hash (Extension 01, Def 11)
|
|
59
61
|
|
|
60
62
|
Returns:
|
|
61
63
|
A new Artifact containing the generated content
|
|
@@ -152,6 +154,19 @@ class ArtifactDAGInterface(ABC):
|
|
|
152
154
|
"""
|
|
153
155
|
pass
|
|
154
156
|
|
|
157
|
+
@abstractmethod
|
|
158
|
+
def get_all(self) -> list["Artifact"]:
|
|
159
|
+
"""
|
|
160
|
+
Return all artifacts in the DAG.
|
|
161
|
+
|
|
162
|
+
Used by extraction queries that need to filter across all artifacts.
|
|
163
|
+
Implementations should return artifacts in a consistent order (e.g., by created_at).
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List of all artifacts in the repository.
|
|
167
|
+
"""
|
|
168
|
+
pass
|
|
169
|
+
|
|
155
170
|
|
|
156
171
|
class CheckpointDAGInterface(ABC):
|
|
157
172
|
"""
|
atomicguard/domain/models.py
CHANGED
|
@@ -7,12 +7,48 @@ All models are immutable (frozen dataclasses) to ensure referential transparency
|
|
|
7
7
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from enum import Enum
|
|
10
|
-
from
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
from atomicguard.domain.interfaces import ArtifactDAGInterface
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# GUARD RESULT (moved before Artifact for forward reference)
|
|
19
|
+
# =============================================================================
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class SubGuardOutcome:
|
|
24
|
+
"""Outcome from a single sub-guard within a composite (Definition 43).
|
|
25
|
+
|
|
26
|
+
Captures individual guard result for attribution in composite validations.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
guard_name: str # Class name of the sub-guard
|
|
30
|
+
passed: bool # Whether this sub-guard passed
|
|
31
|
+
feedback: str # Feedback from this sub-guard
|
|
32
|
+
execution_time_ms: float = 0.0 # Time spent in this sub-guard
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class GuardResult:
|
|
37
|
+
"""Immutable guard validation outcome (Definition 6).
|
|
38
|
+
|
|
39
|
+
G(α, C) → {v, φ} where:
|
|
40
|
+
- v = passed (⊤ or ⊥)
|
|
41
|
+
- φ = feedback signal
|
|
42
|
+
- fatal = ⊥_fatal (non-recoverable, requires escalation)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
passed: bool
|
|
46
|
+
feedback: str = ""
|
|
47
|
+
fatal: bool = False # ⊥_fatal - skip retry, escalate to human
|
|
48
|
+
guard_name: str | None = None # Name of the guard that produced this result
|
|
49
|
+
sub_results: tuple[SubGuardOutcome, ...] = () # For composite guards (Extension 08)
|
|
50
|
+
|
|
51
|
+
|
|
16
52
|
# =============================================================================
|
|
17
53
|
# ARTIFACT MODEL (Definition 4-6)
|
|
18
54
|
# =============================================================================
|
|
@@ -81,24 +117,27 @@ class Artifact:
|
|
|
81
117
|
created_at: str # ISO timestamp
|
|
82
118
|
attempt_number: int # Attempt within this action pair context
|
|
83
119
|
status: ArtifactStatus # pending/rejected/accepted/superseded
|
|
84
|
-
guard_result:
|
|
85
|
-
feedback: str # φ - guard feedback (empty if passed)
|
|
120
|
+
guard_result: GuardResult | None # Full guard result (None if pending)
|
|
86
121
|
context: ContextSnapshot # Full context snapshot at generation time
|
|
87
122
|
source: ArtifactSource = ArtifactSource.GENERATED # Origin of content
|
|
88
123
|
|
|
124
|
+
# Extension 01: Versioned Environment (Definition 10)
|
|
125
|
+
workflow_ref: str | None = None # W_ref: Content-addressed workflow hash (Def 11)
|
|
89
126
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
#
|
|
127
|
+
# Extension 07: Incremental Execution (Definition 33)
|
|
128
|
+
config_ref: str | None = (
|
|
129
|
+
None # Ψ_ref: Configuration fingerprint for change detection
|
|
130
|
+
)
|
|
93
131
|
|
|
132
|
+
metadata: MappingProxyType[str, Any] = field(
|
|
133
|
+
default_factory=lambda: MappingProxyType({})
|
|
134
|
+
) # Immutable metadata dict
|
|
94
135
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
feedback: str = ""
|
|
101
|
-
fatal: bool = False # ⊥_fatal - skip retry, escalate to human
|
|
136
|
+
def __post_init__(self) -> None:
|
|
137
|
+
"""Convert metadata dict to immutable MappingProxyType if needed."""
|
|
138
|
+
# Handle case where metadata is passed as a regular dict
|
|
139
|
+
if isinstance(object.__getattribute__(self, "metadata"), dict):
|
|
140
|
+
object.__setattr__(self, "metadata", MappingProxyType(self.metadata))
|
|
102
141
|
|
|
103
142
|
|
|
104
143
|
# =============================================================================
|
|
@@ -125,6 +164,7 @@ class Context:
|
|
|
125
164
|
dependency_artifacts: tuple[
|
|
126
165
|
tuple[str, str], ...
|
|
127
166
|
] = () # (action_pair_id, artifact_id) - matches schema
|
|
167
|
+
workflow_id: str | None = None # Extension 03: Agent workflow identifier (Def 19)
|
|
128
168
|
|
|
129
169
|
def get_dependency(self, action_pair_id: str) -> str | None:
|
|
130
170
|
"""Look up artifact_id by action_pair_id."""
|
|
@@ -133,6 +173,41 @@ class Context:
|
|
|
133
173
|
return artifact_id
|
|
134
174
|
return None
|
|
135
175
|
|
|
176
|
+
def amend(self, delta_spec: str = "", delta_constraints: str = "") -> "Context":
|
|
177
|
+
"""Monotonic configuration amendment (⊕ operator, Definition 12).
|
|
178
|
+
|
|
179
|
+
Creates a new Context with appended specification and/or constraints.
|
|
180
|
+
Original Context remains unchanged (immutability preserved).
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
delta_spec: Additional specification to append to current specification.
|
|
184
|
+
delta_constraints: Additional constraints to append to ambient constraints.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
New Context with amended specification and constraints.
|
|
188
|
+
"""
|
|
189
|
+
new_spec = self.specification
|
|
190
|
+
if delta_spec:
|
|
191
|
+
new_spec = f"{self.specification}\n{delta_spec}"
|
|
192
|
+
|
|
193
|
+
new_constraints = self.ambient.constraints
|
|
194
|
+
if delta_constraints:
|
|
195
|
+
new_constraints = f"{self.ambient.constraints}\n{delta_constraints}"
|
|
196
|
+
|
|
197
|
+
new_ambient = AmbientEnvironment(
|
|
198
|
+
repository=self.ambient.repository,
|
|
199
|
+
constraints=new_constraints,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return Context(
|
|
203
|
+
ambient=new_ambient,
|
|
204
|
+
specification=new_spec,
|
|
205
|
+
current_artifact=self.current_artifact,
|
|
206
|
+
feedback_history=self.feedback_history,
|
|
207
|
+
dependency_artifacts=self.dependency_artifacts,
|
|
208
|
+
workflow_id=self.workflow_id,
|
|
209
|
+
)
|
|
210
|
+
|
|
136
211
|
|
|
137
212
|
# =============================================================================
|
|
138
213
|
# WORKFLOW STATE
|
|
@@ -223,6 +298,9 @@ class WorkflowCheckpoint:
|
|
|
223
298
|
failure_feedback: str # Error/feedback message
|
|
224
299
|
provenance_ids: tuple[str, ...] # Artifact IDs of all failed attempts
|
|
225
300
|
|
|
301
|
+
# Extension 01: Versioned Environment (Definition 11)
|
|
302
|
+
workflow_ref: str | None = None # W_ref: Content-addressed workflow hash
|
|
303
|
+
|
|
226
304
|
|
|
227
305
|
class AmendmentType(Enum):
|
|
228
306
|
"""Type of human amendment."""
|