atomicguard 1.1.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.
@@ -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
@@ -44,18 +44,20 @@ class GeneratorInterface(ABC):
44
44
  def generate(
45
45
  self,
46
46
  context: "Context",
47
- template: Optional["PromptTemplate"] = None,
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: Optional prompt template for structured generation
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
  """
@@ -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 typing import TYPE_CHECKING
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: bool | None # or (None if pending)
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
- # GUARD RESULT
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
- @dataclass(frozen=True)
96
- class GuardResult:
97
- """Immutable guard validation outcome."""
98
-
99
- passed: bool
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."""