Skip to content

Commit 910b645

Browse files
committed
feat: add minimal execution strategy and implement step dependency validation via requires/provides metadata
1 parent 9070612 commit 910b645

4 files changed

Lines changed: 79 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.17.1] - 2026-04-06
9+
10+
### Added
11+
12+
- **`build_minimal_strategy()`** — 4-step pipeline (context → lookup → execute → return) for pre-validated internal hot paths. Registered as `"minimal"` in Executor preset builders.
13+
- **`requires` / `provides` on `BaseStep`** — Optional advisory fields declaring step dependencies. `ExecutionStrategy` validates dependency chains at construction and insertion, emitting warnings for unmet `requires`.
14+
15+
### Fixed
16+
17+
- **`"minimal"` added to preset builders**`Executor(strategy="minimal")` now works. Previously missing from `_resolve_strategy_name()` preset dict.
18+
- **Executor docstrings updated** — Constructor and `_resolve_strategy_name` docstrings now list all 5 presets (was missing `"minimal"`).
19+
20+
---
21+
822
## [0.17.0] - 2026-04-05
923

1024
### Added

src/apcore/builtin_steps.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"build_internal_strategy",
5555
"build_testing_strategy",
5656
"build_performance_strategy",
57+
"build_minimal_strategy",
5758
]
5859

5960
_logger = logging.getLogger(__name__)
@@ -91,6 +92,7 @@ def __init__(
9192
removable=False,
9293
replaceable=False,
9394
pure=True,
95+
provides=("context",),
9496
)
9597
self._config = config
9698
self._executor = executor
@@ -129,6 +131,7 @@ def __init__(self, *, config: Any | None = None) -> None:
129131
removable=True,
130132
replaceable=True,
131133
pure=True,
134+
requires=("context",),
132135
)
133136
self._config = config
134137
if config is not None:
@@ -166,6 +169,7 @@ def __init__(self, *, registry: Any) -> None:
166169
removable=False,
167170
replaceable=False,
168171
pure=True,
172+
provides=("module",),
169173
)
170174
self._registry = registry
171175

@@ -196,6 +200,7 @@ def __init__(self, *, acl: Any | None = None) -> None:
196200
removable=True,
197201
replaceable=True,
198202
pure=True,
203+
requires=("context", "module"),
199204
)
200205
self._acl = acl
201206

@@ -236,6 +241,7 @@ def __init__(
236241
removable=True,
237242
replaceable=True,
238243
pure=False,
244+
requires=("context", "module"),
239245
)
240246
self._handler = handler
241247
self._executor = executor
@@ -404,6 +410,8 @@ def __init__(self) -> None:
404410
removable=True,
405411
replaceable=True,
406412
pure=True,
413+
requires=("module",),
414+
provides=("validated_inputs",),
407415
)
408416

409417
async def execute(self, ctx: PipelineContext) -> StepResult:
@@ -455,6 +463,8 @@ def __init__(self, *, config: Any | None = None) -> None:
455463
removable=False,
456464
replaceable=True,
457465
pure=False,
466+
requires=("module",),
467+
provides=("output",),
458468
)
459469
self._config = config
460470
if config is not None:
@@ -553,6 +563,8 @@ def __init__(self) -> None:
553563
removable=True,
554564
replaceable=True,
555565
pure=True,
566+
requires=("module", "output"),
567+
provides=("validated_output",),
556568
)
557569

558570
async def execute(self, ctx: PipelineContext) -> StepResult:
@@ -665,6 +677,7 @@ def __init__(self) -> None:
665677
removable=False,
666678
replaceable=False,
667679
pure=True,
680+
requires=("output",),
668681
)
669682

670683
async def execute(self, ctx: PipelineContext) -> StepResult:
@@ -777,3 +790,28 @@ def build_performance_strategy(**kwargs: Any) -> ExecutionStrategy:
777790
s.remove("middleware_after")
778791
object.__setattr__(s, "name", "performance")
779792
return s
793+
794+
795+
def build_minimal_strategy(**kwargs: Any) -> ExecutionStrategy:
796+
"""Build a minimal strategy: context → lookup → execute → return only.
797+
798+
Suitable for pre-validated internal hot paths where ACL, approval,
799+
middleware, and schema validation are unnecessary. Use with caution —
800+
no safety checks, no input/output validation, no middleware.
801+
802+
Args:
803+
**kwargs: Forwarded to build_standard_strategy().
804+
805+
Returns:
806+
An ExecutionStrategy named "minimal" with 4 steps.
807+
"""
808+
s = build_standard_strategy(**kwargs)
809+
s.remove("call_chain_guard")
810+
s.remove("acl_check")
811+
s.remove("approval_gate")
812+
s.remove("middleware_before")
813+
s.remove("input_validation")
814+
s.remove("output_validation")
815+
s.remove("middleware_after")
816+
object.__setattr__(s, "name", "minimal")
817+
return s

src/apcore/executor.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ def __init__(
154154
registry: Module registry for looking up modules by ID.
155155
strategy: Optional execution strategy. Can be an ExecutionStrategy
156156
instance, a preset name string ("standard", "internal",
157-
"testing", "performance"), or None (defaults to standard).
157+
"testing", "performance", "minimal"), or None (defaults to
158+
standard).
158159
middlewares: Optional list of middleware instances to register.
159160
acl: Optional ACL for access control enforcement.
160161
config: Optional configuration for timeout/depth settings.
@@ -875,7 +876,7 @@ def _resolve_strategy_name(name: str, **kwargs: Any) -> ExecutionStrategy:
875876
876877
Args:
877878
name: Strategy name ("standard", "internal", "testing", "performance",
878-
or a previously registered name).
879+
"minimal", or a previously registered name).
879880
**kwargs: Forwarded to preset builder functions.
880881
881882
Returns:
@@ -886,6 +887,7 @@ def _resolve_strategy_name(name: str, **kwargs: Any) -> ExecutionStrategy:
886887
"""
887888
from apcore.builtin_steps import (
888889
build_internal_strategy,
890+
build_minimal_strategy,
889891
build_performance_strategy,
890892
build_standard_strategy,
891893
build_testing_strategy,
@@ -896,6 +898,7 @@ def _resolve_strategy_name(name: str, **kwargs: Any) -> ExecutionStrategy:
896898
"internal": build_internal_strategy,
897899
"testing": build_testing_strategy,
898900
"performance": build_performance_strategy,
901+
"minimal": build_minimal_strategy,
899902
}
900903

901904
if name in preset_builders:

src/apcore/pipeline.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def __init__(
5353
ignore_errors: bool = False,
5454
pure: bool = False,
5555
timeout_ms: int = 0,
56+
requires: tuple[str, ...] = (),
57+
provides: tuple[str, ...] = (),
5658
) -> None:
5759
self.name = name
5860
self.description = description
@@ -62,6 +64,8 @@ def __init__(
6264
self.ignore_errors = ignore_errors
6365
self.pure = pure
6466
self.timeout_ms = timeout_ms
67+
self.requires = requires
68+
self.provides = provides
6569

6670
@abstractmethod
6771
async def execute(self, ctx: PipelineContext) -> StepResult: ...
@@ -143,6 +147,22 @@ def __init__(self, name: str, steps: list[Step]) -> None:
143147
if len(names) != len(set(names)):
144148
dupes = [n for n in names if names.count(n) > 1]
145149
raise StepNameDuplicateError(f"Duplicate step names: {set(dupes)}")
150+
self._validate_dependencies()
151+
152+
def _validate_dependencies(self) -> None:
153+
"""Warn if any step's requires are not provided by a preceding step."""
154+
provided: set[str] = set()
155+
for step in self.steps:
156+
requires = getattr(step, "requires", ())
157+
missing = set(requires) - provided
158+
if missing:
159+
_logger.warning(
160+
"Step '%s' requires %s, but no preceding step provides them. " "This may cause runtime errors.",
161+
step.name,
162+
missing,
163+
)
164+
provides = getattr(step, "provides", ())
165+
provided.update(provides)
146166

147167
def insert_after(self, anchor: str, step: Step) -> None:
148168
"""Insert a step after the named anchor step."""
@@ -151,6 +171,7 @@ def insert_after(self, anchor: str, step: Step) -> None:
151171
for i, s in enumerate(self.steps):
152172
if s.name == anchor:
153173
self.steps.insert(i + 1, step)
174+
self._validate_dependencies()
154175
return
155176
raise StepNotFoundError(f"Anchor step '{anchor}' not found")
156177

@@ -161,6 +182,7 @@ def insert_before(self, anchor: str, step: Step) -> None:
161182
for i, s in enumerate(self.steps):
162183
if s.name == anchor:
163184
self.steps.insert(i, step)
185+
self._validate_dependencies()
164186
return
165187
raise StepNotFoundError(f"Anchor step '{anchor}' not found")
166188

0 commit comments

Comments
 (0)