Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
- name: Lint
run: make lint

- name: Check typed extension contracts
- name: Check strict typing
run: make typecheck

test:
Expand Down
6 changes: 3 additions & 3 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ make setup

```bash
make test # pytest suite with branch coverage
make typecheck # typed extension contracts + external consumer fixture
make typecheck # strict type checking for package and fixtures
make lint # project-env ruff check src tests scripts
make format # project-env ruff import fixes + format src tests scripts
make format-check # project-env ruff format --check src tests scripts
Expand Down Expand Up @@ -273,8 +273,8 @@ exists on TestPyPI.
- Bug fixes must include regression tests.
- Keep tests deterministic and filesystem-local.
- Integration implementations must include contract tests under `tests/integration/architecture/` and adapter tests under `tests/integration/adapters/`.
- Public extension contracts must pass `make typecheck`; the package job also
type-checks the consumer fixture against the built wheel.
- Production code and typing fixtures must pass strict mypy via `make typecheck`;
CI also validates the built wheel's public typing.
- Tests enforce branch coverage.

## Mock Invoker Local Validation
Expand Down
25 changes: 4 additions & 21 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ help:
'Development:' \
' setup Install editable dev environment' \
' test Run pytest' \
' typecheck Check typed extension contracts and a public consumer' \
' typecheck Check strict typing for the package and fixtures' \
' lint Run ruff checks' \
' format Run ruff import fixes and formatter' \
' format-check Check formatting' \
Expand Down Expand Up @@ -96,27 +96,10 @@ uninstall:
test:
$(RUN_PYTEST) -p pytest_cov --cov=crewplane --cov-branch --cov-report=term-missing:skip-covered --cov-fail-under=$(COVERAGE_FLOOR)

# Strict mypy adoption currently covers public extension contracts and their
# direct consumers; the rest of the package is not yet strict-mypy clean.
# Keep the full production package and repository typecheck fixtures under
# strict mypy coverage.
typecheck:
$(RUN_MYPY) \
src/crewplane/architecture/contracts \
src/crewplane/architecture/ports \
src/crewplane/bootstrap/container.py \
src/crewplane/adapters/ui \
src/crewplane/adapters/invokers/mock_invoker/context.py \
src/crewplane/observability/observer.py \
src/crewplane/observability/runtime.py \
src/crewplane/observability/tmux/selection.py \
src/crewplane/observability/tmux/snapshot_types.py \
src/crewplane/observability/tmux/compact.py \
src/crewplane/observability/tmux/rendering.py \
src/crewplane/observability/tmux/refresh.py \
src/crewplane/observability/tmux/selected_invocation.py \
src/crewplane/observability/run_summary/builder.py \
src/crewplane/observability/run_summary/logger.py \
tests/typecheck/public_observer_consumer.py \
tests/typecheck/public_artifacts_consumer.py
$(RUN_MYPY) src/crewplane tests/typecheck

lint:
$(RUN_RUFF) check src tests scripts
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dev = [
"pytest-cov>=7.0",
"ruff>=0.6",
"twine>=6.0",
"types-PyYAML>=6.0.12",
]
stress = [
"pytest-randomly>=4.0.1",
Expand Down
14 changes: 5 additions & 9 deletions src/crewplane/adapters/artifacts/filesystem.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

from pathlib import Path
from typing import cast

from crewplane.architecture.contracts import (
CanonicalIntegrationConfig,
Expand Down Expand Up @@ -66,14 +65,11 @@ def create_store(

parsed_options = _parse_options(options)

return cast(
ArtifactStorePort,
OutputManager(
workflow_name,
base_dir=state_dir,
template_base_dir=project_root,
log_cli_output=parsed_options.log_cli_output,
),
return OutputManager(
workflow_name,
base_dir=state_dir,
template_base_dir=project_root,
log_cli_output=parsed_options.log_cli_output,
)

def create_terminal_history_reader(
Expand Down
2 changes: 1 addition & 1 deletion src/crewplane/adapters/artifacts/terminal_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ def _read_result_bytes(self, result_path: Path) -> TerminalHistoryRead:
payload=payload,
)

@staticmethod
def _matched_error(
self,
message: str,
path: Path | None = None,
) -> TerminalHistoryRead:
Expand Down
22 changes: 7 additions & 15 deletions src/crewplane/adapters/invokers/cli_invoker/claude_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ def extract_claude_output(
"""Extract Claude's result string into an owned temporary output file."""
extraction = _extract_claude_document(
result,
use_stderr_fallback=True,
capture_result=True,
parse_result=True,
parse_model_usage=False,
max_captured_usage_bytes=max_captured_usage_bytes,
)
if extraction.error is not None:
Expand Down Expand Up @@ -78,27 +74,23 @@ class ClaudeJsonDocument:

def _extract_claude_document(
result: CommandResult,
use_stderr_fallback: bool,
capture_result: bool,
parse_result: bool,
parse_model_usage: bool,
max_captured_usage_bytes: int,
) -> ClaudeJsonDocument:
document = _parse_claude_source(
stdout_source(result),
capture_result=capture_result,
parse_result=parse_result,
parse_model_usage=parse_model_usage,
capture_result=True,
parse_result=True,
parse_model_usage=False,
max_captured_usage_bytes=max_captured_usage_bytes,
)
if use_stderr_fallback and document.error is None and document.result_path is None:
if document.error is None and document.result_path is None:
stderr_source = stream_source(result.stderr_text, result.stderr_path)
if stderr_source is not None:
document = _parse_claude_source(
stderr_source,
capture_result=capture_result,
parse_result=parse_result,
parse_model_usage=parse_model_usage,
capture_result=True,
parse_result=True,
parse_model_usage=False,
max_captured_usage_bytes=max_captured_usage_bytes,
)
return document
Expand Down
4 changes: 2 additions & 2 deletions src/crewplane/adapters/invokers/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
class MockInvokerAdapter:
"""Create deterministic mock invokers for local orchestration runs."""

@staticmethod
def canonicalize_options(
self,
implementation: str,
resolved_identity: str,
options: JsonObject | None = None,
Expand All @@ -44,8 +44,8 @@ def canonicalize_options(
).as_dict(),
)

@staticmethod
def create_invoker(
self,
config: Config,
options: JsonObject | None = None,
) -> AgentInvoker:
Expand Down
2 changes: 2 additions & 0 deletions src/crewplane/architecture/contracts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
InvocationCostConfidence,
InvocationDiagnostic,
InvocationDiagnosticSink,
InvocationEventFields,
InvocationLogLevel,
InvocationPlan,
InvocationProcessEvent,
Expand Down Expand Up @@ -159,6 +160,7 @@
"InvocationContext",
"InvocationDiagnostic",
"InvocationDiagnosticSink",
"InvocationEventFields",
"InvocationLogLevel",
"InvocationPlan",
"InvocationProcessEvent",
Expand Down
3 changes: 1 addition & 2 deletions src/crewplane/architecture/contracts/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,11 @@ def sensitive_integration_option_keys(


def redacted_integration_option_value(
value: JsonValue = None,
value: JsonValue = None, # noqa: ARG001 - Raw secrets must not affect redaction.
fingerprint: str | None = None,
value_handle: str | None = None,
) -> JsonObject:
"""Build trusted redaction metadata without retaining the raw value."""
del value
redacted: JsonObject = {"redacted": True}
if fingerprint is not None:
redacted["fingerprint"] = fingerprint
Expand Down
3 changes: 2 additions & 1 deletion src/crewplane/architecture/contracts/integration_secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ def _should_redact(
isinstance(value, (dict, list)) and self._has_explicit_descendant(pointer)
)

def _is_sensitive_name(self, path: tuple[JsonPathSegment, ...]) -> bool:
@staticmethod
def _is_sensitive_name(path: tuple[JsonPathSegment, ...]) -> bool:
segment = path[-1]
return (
isinstance(segment, str)
Expand Down
31 changes: 23 additions & 8 deletions src/crewplane/architecture/contracts/invocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from enum import StrEnum
from pathlib import Path
from types import MappingProxyType
from typing import TYPE_CHECKING, Literal, Protocol, cast
from typing import TYPE_CHECKING, Literal, Protocol, TypedDict, cast

from crewplane.core.workflow.keywords import ProviderRole

Expand Down Expand Up @@ -127,6 +127,23 @@ def add_exact(self, additional: ProviderTokenUsage) -> ProviderTokenUsage:
)


class InvocationEventFields(TypedDict, total=False):
"""Keyword fields persisted with terminal invocation events."""

attempt_count: int
cli_captured: bool
output_extraction_status: OutputExtractionStatus
provider_usage_status: ProviderUsageStatus
provider_usage_report_count: int | None
provider_tokens: dict[str, int | None]
visible_estimate_tokens: int | None
visible_estimate_method: str | None
visible_estimate_is_lower_bound: bool
configured_cost_usd: float | None
invocation_cost_confidence: InvocationCostConfidence
usage_parse_error: str | None


@dataclass(frozen=True)
class InvocationUsage:
"""Normalized usage and cost evidence for one terminal invocation."""
Expand Down Expand Up @@ -160,7 +177,7 @@ def __post_init__(self) -> None:
MappingProxyType(dict(self.provider_tokens)),
)

def as_event_fields(self) -> JsonObject:
def as_event_fields(self) -> InvocationEventFields:
"""Return a JSON-compatible shallow copy for event persistence."""
return {
"attempt_count": self.attempt_count,
Expand Down Expand Up @@ -436,12 +453,10 @@ def iter_combined_lines(self) -> Iterator[str]:

def cleanup_stream_files(self) -> None:
"""Remove persisted stream capture files for this invocation result."""
if self.stdout_path is not None:
with suppress(OSError):
self.stdout_path.unlink(missing_ok=True)
if self.stderr_path is not None:
with suppress(OSError):
self.stderr_path.unlink(missing_ok=True)
for path in (self.stdout_path, self.stderr_path):
if path is not None:
with suppress(OSError):
path.unlink(missing_ok=True)


@dataclass(frozen=True)
Expand Down
4 changes: 4 additions & 0 deletions src/crewplane/architecture/ports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@
ProviderProcessInvocation,
ProviderProcessPublication,
ProviderProcessStorePort,
RunSummaryArtifactReaderPort,
TerminalHistoryRead,
TerminalHistoryReaderPort,
)
from .invoker import InvokerAdapterPort
from .options import IntegrationOptionsCanonicalizerPort
from .runtime import RuntimeComponents, UIRuntimePlan
from .ui import UIAdapterCapabilities, UIAdapterPort

__all__ = [
"ArtifactAdapterPort",
"ArtifactStorePort",
"IntegrationOptionsCanonicalizerPort",
"InvokerAdapterPort",
"ProviderProcessInvocation",
"ProviderProcessPublication",
"ProviderProcessStorePort",
"RunSummaryArtifactReaderPort",
"TerminalHistoryRead",
"TerminalHistoryReaderPort",
"RuntimeComponents",
Expand Down
Loading
Loading