Skip to content

refactor(domain): replace literal string types with typed enums - #152

Open
morluto wants to merge 8 commits into
mainfrom
refactor/typed-enums-unrepresentable-states
Open

refactor(domain): replace literal string types with typed enums#152
morluto wants to merge 8 commits into
mainfrom
refactor/typed-enums-unrepresentable-states

Conversation

@morluto

@morluto morluto commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary

Promotes loose Literal[...] and str status fields across the domain, application, adapters, MCP, and storage layers to proper StrEnum types and discriminated unions, continuing the direction set by #128 ("Make invalid evidence and MCP states unrepresentable").

  • Adds 15 new enums in src/flameox/domain/models.py: ExperimentOutcomeMethod, ExperimentOutcomeGoal, ExperimentOutcomeDisposition, TrialFailureClass, OracleStatus, FindingConfidence, EvidenceReferenceType, EvidenceRelation, MetricPolarity, PreflightMode, PreflightDisposition, ProbeKind, ProcessCancellationCause, CaptureContainment, CapabilityPermissionStatus, CapabilitySetupVerification.
  • Replaces the single EvidenceAvailability model in src/flameox/evidence_status.py with a discriminated union (AvailableEvidence | EmptyEvidence | PartialEvidence | UnknownEvidence | UnavailableEvidence | RecoverableUnavailableEvidence) so next_tool/next_arguments are only representable on recoverable-unavailable states; adds parse_evidence_availability for typed validation.
  • Adds ContainmentPolicy and NetworkPolicy enums in src/flameox/config.py.
  • CLI and MCP receipts (CaptureReceipt, ExperimentReceipt, tool parameters) now carry the enum types instead of str, and Literal parameter aliases are replaced by the exported enums.
  • Updates call sites and tests to construct enums directly; new evidence-status coverage lives in tests/evidence/test_evidence_status.py (renamed from test_status.py to avoid a module-name collision with tests/application/test_status.py), and tests/ownership.toml is updated accordingly.

No behavioral contracts changed; this is a type-safety refactor that makes previously invalid states unrepresentable at the schema boundary.

Test plan

  • uv run ruff check src tests
  • uv run mypy src tests
  • uv run pytest -q — 907 passed, 216 deselected

Generated with Devin


Open in Devin Review

@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

Open in Devin Review

Comment on lines +172 to +188
@model_validator(mode="after")
def tool_readiness_is_coherent(self) -> _InferenceReplayPlan:
if self.argv and self.tool_executable is None:
raise ValueError("an available tool requires an executable")
if self.argv and (self.tool_compatibility_reason is not None or self.tool_remediation):
raise ValueError("an available tool cannot carry incompatibility recovery")
return self

@computed_field # type: ignore[prop-decorator]
@property
def tool_available(self) -> bool:
return bool(self.argv)

@computed_field # type: ignore[prop-decorator]
@property
def tool_compatible(self) -> bool:
return self.tool_available

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Planning an inference replay crashes when the benchmarking tool is installed but the wrong version

The replay plan now decides that the tool is usable purely from the presence of a command line (tool_available computed at src/flameox/application/inference.py:180-188) and then refuses any plan that also carries an incompatibility explanation, so planning against an installed-but-unsupported tool aborts with an internal error instead of returning the plan that explains the problem.
Impact: A user with, for example, an out-of-range AIPerf or SGLang installed can no longer plan a replay at all; instead of a clear "tool unsupported, install version X" answer they get an opaque internal validation failure.

How an unavailable-but-present discovery reaches the new coherence validator

discover_inference_tool returns UnavailableInferenceToolDiscovery with a non-None executable plus compatibility_reason/remediation when the executable exists but the version is out of range (src/flameox/application/inference_providers.py:404-431); discover_sglang does the same when benchmark_python exists but does not provide sglang 0.5.16 (src/flameox/application/inference_providers.py:363-373).

In InferenceReplayService.plan, the argv-producing request is built whenever discovery.executable is not None (src/flameox/application/inference.py:371-379), so argv is non-empty for those unavailable discoveries. Previously the plan recorded tool_available=discovery.available (False) and tool_compatible=discovery.compatible (False) while preserving tool_compatibility_reason and tool_remediation, and execution later raised a structured CAPABILITY_UNAVAILABLE DomainError with remediation.

Now tool_available is derived from bool(self.argv) and the after-validator rejects the combination:

if self.argv and (self.tool_compatibility_reason is not None or self.tool_remediation):
    raise ValueError("an available tool cannot carry incompatibility recovery")

so parse_inference_replay_plan raises ValidationError out of plan(), which callers only translate for DomainError. Either the plan should omit argv when the discovery is unavailable, or the model should keep an explicit availability field rather than inferring it from argv.

Prompt for agents
In src/flameox/application/inference.py, `_InferenceReplayPlan` now derives `tool_available`/`tool_compatible` from whether `argv` is non-empty, and `tool_readiness_is_coherent` rejects any plan that has argv together with `tool_compatibility_reason` or `tool_remediation`. However `InferenceReplayService.plan` builds the execution request (and therefore argv) whenever `discovery.executable is not None`, and `discover_inference_tool` / `discover_sglang` in src/flameox/application/inference_providers.py return UnavailableInferenceToolDiscovery objects that carry BOTH a real executable and a compatibility_reason/remediation (e.g. AIPerf installed but outside >=0.12,<0.13, or benchmark_python present without sglang 0.5.16). In that case parse_inference_replay_plan raises a pydantic ValidationError out of plan(), replacing the previous behaviour where a plan was returned with tool_available=False plus the remediation text (and execution later raised a structured CAPABILITY_UNAVAILABLE DomainError). Fix so that an incompatible-but-present tool still yields an actionable outcome: either do not build argv when the discovery is unavailable, or stop inferring availability from argv and keep it as an explicit field driven by discovery.available. Add a regression test covering plan() with an unavailable discovery whose executable is not None.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/flameox/application/summaries.py Outdated
Comment thread src/flameox/application/capabilities.py
Comment thread src/flameox/analysis/recipe_models.py
morluto and others added 7 commits August 10, 2026 18:04
Promote loose `Literal[...]` and `str` status fields in the domain models
to proper `StrEnum` types so invalid evidence, experiment, oracle, finding,
capability, preflight, and capture states cannot be represented.

New enums in `domain/models.py`: ExperimentOutcomeMethod, ExperimentOutcomeGoal,
ExperimentOutcomeDisposition, TrialFailureClass, OracleStatus, FindingConfidence,
EvidenceReferenceType, EvidenceRelation, MetricPolarity, PreflightMode,
PreflightDisposition, ProbeKind, ProcessCancellationCause, CaptureContainment,
CapabilityPermissionStatus, CapabilitySetupVerification.

`evidence_status.py` replaces the single `EvidenceAvailability` model with a
discriminated union (AvailableEvidence | EmptyEvidence | PartialEvidence |
UnknownEvidence | UnavailableEvidence | RecoverableUnavailableEvidence) so
`next_tool`/`next_arguments` are only representable on recoverable-unavailable
states; adds `parse_evidence_availability` for typed validation.

`config.py` adds `ContainmentPolicy` and `NetworkPolicy` enums.
`execution.py` adopts `ProcessCancellationCause` from the domain.
`models.py` adds `Mapping`/`Self` imports for the new typed helpers.

Includes direct unit tests for the new domain enums and the evidence-status
union, and registers the new test path in `tests/ownership.toml`.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Update the storage layer to construct and persist the new domain `StrEnum`
types instead of plain strings for execution status, validation status,
evidence level, finding confidence/assessment, and experiment outcome
fields.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Update all application services (capture, experiments, comparisons,
capabilities, preflight, recovery, integrity, records, summaries,
drilldown, imports, inference, viewers, workloads, etc.) to construct
and accept the new domain `StrEnum` types instead of plain strings.

Covers status fields for execution, validation, evidence level, finding
confidence/assessment/lifecycle, experiment outcome, oracle status,
preflight mode/disposition, probe kind, capture containment, capability
permission/setup, and evidence availability with recovery routing.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…erence protocol

Update the analysis layer to use the new domain `StrEnum` types for
evidence level, oracle status, experiment outcome, metric polarity,
and comparison validity fields across recipe models, comparison logic,
and the inference protocol.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Update adapter modules (kernel build, kernel validation, pytest,
setup runtime) and their JSON schemas to use the new domain `StrEnum`
types for validation status, execution status, and probe kind fields.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Update CLI and MCP server receipts and tool parameters to carry the
new domain `StrEnum` types (ExecutionStatus, ValidationStatus,
ExperimentOutcomeDisposition/Method, PreflightMode, EvidenceReferenceType)
instead of `str` and inline `Literal` aliases.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Update golden, evidence publication, and shared test support code to
construct the new domain `StrEnum` types (FindingConfidence,
FindingAssessment, EvidenceLevel, ExecutionStatus, ValidationStatus)
instead of plain strings.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@morluto
morluto force-pushed the refactor/typed-enums-unrepresentable-states branch from c03684f to 8e9aed7 Compare August 10, 2026 10:08

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

Open in Devin Review

Comment on lines +1158 to +1165
if initial_phase is None:
self._record_setup_completed(requested)
else:
self._record_setup_progress(
requested,
completed=already_available,
phase=initial_phase,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Capability setup is durably recorded as finished for every requested tool before any of them are installed

The durable setup record is written as fully finished for all requested tools (_record_setup_completed(requested) at src/flameox/application/capabilities.py:1158-1159) before the tools have actually been prepared, so anyone polling setup progress can be told the work succeeded while it is still running or about to fail.
Impact: An agent polling capability setup can see a "completed" receipt naming tools that were never actually made available.

Why the pre-work receipt now claims every requested adapter

This code path is reached only when pending is non-empty (the if not pending: early return is above at src/flameox/application/capabilities.py:1126-1136), i.e. some requested adapters are still unavailable. Previously the receipt written here used completed=already_available with phase="completed", so it never claimed pending adapters were done. CapabilitySetupCompletedReceipt requires completed == requested (src/flameox/application/capabilities.py:94-98), so _record_setup_completed necessarily overstates progress.

The same overstatement occurs mid-flow after package installation at src/flameox/application/capabilities.py:1229-1230, where the previous code recorded completed=self._available_requested(requested); the flow can still fail afterwards via the not_ready check, which then rewrites a failure receipt.

Prompt for agents
In `CapabilityService` (src/flameox/application/capabilities.py), the refactor replaced the intermediate `_record_setup_receipt(..., phase="completed")` calls (which recorded `completed=already_available` / `self._available_requested(requested)`) with `_record_setup_completed(requested)`, which by construction records every requested adapter as completed. Both occurrences (the initial-phase branch and the post-install branch) happen before the final availability verification, so a concurrent reader can observe a completed receipt listing adapters that are not yet available. Consider only emitting a completed receipt once the adapters are verified available (i.e. at the end of the flow), and emitting a progress/failed receipt otherwise, or model an intermediate phase that carries the truthful `completed` subset.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +73 to +80
@model_validator(mode="after")
def availability_is_a_partition(self) -> WorkloadDependencySetupResult:
if len(set(self.requested)) != len(self.requested):
raise ValueError("requested requirements must be unique")
available = set(self.already_available)
if tuple(item for item in self.requested if item in available) != self.already_available:
raise ValueError("already-available requirements must be an ordered requested subset")
return self

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 New uniqueness invariants can reject caller-supplied duplicates

WorkloadDependencySetupResult.availability_is_a_partition raises when requested contains duplicates. requested is built from config.requirements.python_distributions without deduplication (src/flameox/application/dependencies.py:141-144), so a workload that lists the same distribution twice in flameox.toml would now fail the whole prepare_workload_dependencies call with a validation error rather than installing it once. The capability path is safe because it dedupes via dict.fromkeys before building the result.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +107 to +121
@staticmethod
def _canonical(manifest: RunManifest) -> RunManifest:
try:
canonical = parse_run_manifest(manifest.model_dump(mode="python"))
except ValueError as exc:
raise DomainError(
ErrorCode.WORKSPACE_INVALID,
"Run manifest is invalid and cannot be persisted.",
) from exc
if type(canonical) is not type(manifest):
raise DomainError(
ErrorCode.WORKSPACE_INVALID,
"A run revision cannot change its run type.",
)
return canonical

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Run-store canonicalization changes the error surface for invalid manifests

_canonical re-parses every manifest before persistence and converts any validation failure into ErrorCode.WORKSPACE_INVALID. Combined with the new ExecutionRunManifest.lifecycle_is_coherent invariants (planned/running/terminal coupling of started_at, finished_at, process, and capture_status), any existing call site that produces a transiently inconsistent revision — for example writing a terminal execution status without finished_at — will now fail at append time instead of silently persisting. This is a broad behavioral tightening across every capture, import, inference, and recovery path; the added tests cover only the direct model paths, so it would be worth confirming the full capture lifecycle (including the timeout, resource-policy, and pipeline-failure branches in capture.py) always sets a finish timestamp.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

…ent tools

Address three review findings on the typed-enum refactor:

1. Inference replay planning crashed with ValidationError when the tool was
   installed but incompatible (e.g. AIPerf wrong version). The plan built argv
   from `discovery.executable is not None`, but the coherence validator rejects
   argv + compatibility_reason. Fix: build argv only when `discovery.available`
   so the plan returns with empty argv, remediation, and compatibility_reason;
   execution later raises CAPABILITY_UNAVAILABLE DomainError.

2. Summary trial row parsing dropped the NULL fallback for failure_class.
   `TrialFailureClass(str(row[2]))` raised ValueError when row[2] was None
   (str(None) is "None", not a valid enum member). Fix: fall back to
   TrialFailureClass.NONE for NULL column values from externally written rows.

3. Legacy CapabilityList payloads with next_tool="list_capabilities" became
   unparseable because the computed field no longer includes that value.
   Fix: the legacy validator treats "list_capabilities" as None (no setup
   action needed) before comparing against the computed projection.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant