Skip to content

feat(enforcement): typed OutputMode and EnforcementStage enum taxonomy (ENF-MODE)#20

Merged
kschlt merged 7 commits into
mainfrom
feat/enforcement-output-modes
Apr 3, 2026
Merged

feat(enforcement): typed OutputMode and EnforcementStage enum taxonomy (ENF-MODE)#20
kschlt merged 7 commits into
mainfrom
feat/enforcement-output-modes

Conversation

@kschlt
Copy link
Copy Markdown
Owner

@kschlt kschlt commented Apr 3, 2026

Why

The enforcement plane lacked a formal vocabulary for what kind of artifact an adapter emits and at what gate it runs. Downstream code (pipeline, reporter, routing) was using plain strings with no type safety, making it easy to introduce typos or drift from the spec taxonomy silently.

Approach

Introduced two str,Enum subclasses — OutputMode (5 values: native_config, native_rules, ci_config, git_hook, script_fallback) and EnforcementStage (3 values: pre_commit, ci, runtime) — in the existing clause_kinds.py module so all enforcement code imports from one place.

All five concrete adapters (ESLint, Ruff, Mypy, Tsconfig, ImportLinter) now return typed lists instead of list[str]. ConfigFragment and AppliedFragment carry output_mode at the fragment level so the output type flows through the pipeline into results. RoutingDecision closes the chain at the routing layer.

FallbackAdapter is extracted as a proper BaseAdapter subclass (replacing an inline private method) so the SCRIPT_FALLBACK output mode is fully represented in the taxonomy and testable in isolation. The _build_fallback_promptlet private path is kept for backward compat.

str,Enum inheritance means all existing string comparisons remain valid without modification — this is purely additive typing.

One correctness fix: ImportLinterAdapter.output_modes was returning NATIVE_CONFIG; corrected to NATIVE_RULES (import-linter appends contracts to config rather than owning a whole file).

What Was Tested

  • 12 new assertions covering typed returns from all 5 adapters and confirming enum values remain str-compatible for serialization
  • ConfigFragment and AppliedFragment tested for default construction, explicit NATIVE_RULES, and script_fallback
  • FallbackAdapter covered in isolation via the existing adapter test classes
  • RoutingDecision verified to carry OutputMode/EnforcementStage instances for ESLint and ImportLinter
  • Full suite: 551 tests passed, mypy clean, ruff clean

Risks

Additive changes only — no existing public API modified. The str,Enum approach guarantees no downstream breakage. The ImportLinterAdapter.output_modes correction is technically a behavior change but was previously wrong per the spec; no code depended on the incorrect value.

kschlt added 7 commits April 3, 2026 10:07
…e_kinds

Extends the enforcement-plane enum taxonomy with two new str enums:
OutputMode (5 values) to classify what artifact family an adapter emits,
and EnforcementStage (3 values) to identify the gate at which enforcement
runs. Placed alongside ClauseKind in clause_kinds.py — the shared enum
module for the enforcement plane — so all pipeline and reporting code can
import from one location. str,Enum idiom preserves backward-compatible
string comparisons throughout the codebase.
The two optional properties on BaseAdapter previously returned list[str]
with a comment noting ENF-MODE would formalise the values. Now that
OutputMode and EnforcementStage exist in clause_kinds, replace the
provisional string returns with the proper enum types.

Because both are str-enum subclasses, all downstream string comparisons
remain valid without modification.
…enums

Replace list[str] returns with list[OutputMode] / list[EnforcementStage]
across all 5 concrete adapters (ESLint, Ruff, Mypy, Tsconfig, ImportLinter)
to surface strong types at the adapter boundary instead of raw strings.

Also corrects ImportLinterAdapter.output_modes from NATIVE_CONFIG to
NATIVE_RULES: import-linter appends contract entries to existing config
rather than owning a whole config file, matching the spec table definition
of native_rules.

Adds TestAdapterOutputModes and TestAdapterEnforcementStages test classes
(12 new assertions) covering the typed returns and confirming enum values
remain str-compatible for downstream serialisation.
…dFragment

ConfigFragment (dataclass in base.py) gains output_mode: OutputMode with
default NATIVE_CONFIG. AppliedFragment (Pydantic model in pipeline.py)
gains output_mode: str with default "native_config" — kept as str to avoid
importing enforcement enums into the result model and to keep JSON
serialization trivial. _write_fragment bridges them via fragment.output_mode.value.

Tests cover default construction, explicit NATIVE_RULES, and
script_fallback for both classes (453 passed, 5 new).
Extracts fallback promptlet generation into a proper BaseAdapter subclass
(output_modes=[OutputMode.SCRIPT_FALLBACK]) so the output mode taxonomy is
complete and the logic is testable in isolation. Pipeline Stage 4.5 now
delegates to FallbackAdapter.generate_fragments instead of the private
_build_fallback_promptlet side path. Backward compat preserved:
result.fallback_promptlets is still populated; unroutable keys also appear
in result.fragments_applied with output_mode='script_fallback'.
_build_fallback_promptlet is kept in place for backward compat.
…d_stages with enum types

Closes the type chain started in earlier steps: EnforcementStage and OutputMode
are now reflected as typed lists in RoutingDecision rather than list[str]. No
runtime behavior changes since both enums inherit from str. Tests verify that
ESLintAdapter decisions carry OutputMode/EnforcementStage instances and that
ImportLinterAdapter returns NATIVE_RULES in its output_modes.
@kschlt kschlt merged commit ef9c9e3 into main Apr 3, 2026
8 checks passed
@kschlt kschlt deleted the feat/enforcement-output-modes branch April 3, 2026 17:57
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