Skip to content

feat(enforcement): implement canonical enforcement pipeline (CPL)#13

Merged
kschlt merged 4 commits intomainfrom
feat/canonical-enforcement-pipeline
Mar 25, 2026
Merged

feat(enforcement): implement canonical enforcement pipeline (CPL)#13
kschlt merged 4 commits intomainfrom
feat/canonical-enforcement-pipeline

Conversation

@kschlt
Copy link
Copy Markdown
Owner

@kschlt kschlt commented Mar 25, 2026

Why

Enforcement rules were previously generated by calling adapters directly on raw ADR files (via the stub _apply_guardrails and _generate_enforcement_rules in approval.py). This bypassed the ConstraintsContract — the compiled, validated representation of all ADR constraints — meaning the enforcement layer had no knowledge of conflict resolution, clause provenance, or topological ordering of rules. The CPL makes the contract the single source of truth for all enforcement output.

Approach

A new EnforcementPipeline class in enforcement/pipeline.py accepts a ConstraintsContract and runs two stages: native fragment generation (ESLint and Ruff adapters via new generate_*_from_contract() entry points) and secondary artifact generation (scripts, hooks). The result is wrapped in an EnforcementResult envelope that carries AppliedFragment, EnforcementConflict, SkippedAdapter, and ProvenanceEntry records, plus a SHA-256 idempotency hash computed over sorted outputs — so compiling the same contract twice yields the exact same hash.

Key design decisions:

  • clause_id (SHA-256(adr_id:rule_path)[:12]) was added to PolicyProvenance across all merge methods so every enforcement output is traceable to a specific ADR clause.
  • _topological_sort() in contract/merger.py replaced a date-sort TODO with Kahn's algorithm using the supersedes field for edges, date-sort as tie-breaking, and cycle-detection fallback — this ensures superseded ADRs don't override the decisions that replaced them.
  • approval.py wiring was updated to call EnforcementPipeline.compile() instead of the old stubs.

What Was Tested

  • 27 new unit tests covering EnforcementResult construction and hash stability, EnforcementPipeline.compile() with mock adapters, generate_eslint_config_from_contract() and generate_ruff_config_from_contract() for both adapter types, clause_id derivation across merge methods, and _topological_sort() with linear chains, ties, and cycles.
  • Full suite of 313 tests passes.
  • Mypy clean on all 65 source files (two type errors surfaced and fixed in this branch: explicit default= kwarg for Pydantic Field and int() cast for dict.get() return).

Risks

The new generate_*_from_contract() adapter entry points are additive — existing generate_*_config() functions (called on raw ADRs) remain untouched. The approval.py wiring change is the highest-risk area: the old stubs were no-ops, so any regression would manifest as enforcement output where there previously was none, not as broken existing output. The idempotency hash is new observable behaviour; downstream callers that compare pipeline outputs across runs will now have a stable key to use.

kschlt added 4 commits March 24, 2026 23:04
Updated /close skill step 6 to record completed tasks via the baseline
summary line instead of the Done section (matches current task-tracking.md
format). Added CHANGELOG.md update step so user-facing changes are
captured as part of every task close.

Also added the RST module restructure entry to CHANGELOG.md under
[Unreleased] — this was missing from the RST task close.
Enforcement rules must be derived from the compiled ConstraintsContract,
not from raw ADR files directly. The old _apply_guardrails (stub) and
_generate_enforcement_rules (called adapters on raw ADRs) in approval.py
were replaced by a single EnforcementPipeline.compile() call.

Key additions:
- enforcement/pipeline.py: EnforcementResult envelope (AppliedFragment,
  EnforcementConflict, SkippedAdapter, ProvenanceEntry) with idempotency
  hash (SHA-256 of sorted outputs — same contract compiled twice yields
  identical hash)
- generate_eslint_config_from_contract() and
  generate_ruff_config_from_contract() added to both adapters as the
  canonical contract-driven path
- clause_id added to PolicyProvenance (SHA-256(adr_id:rule_path)[:12])
  for traceability; populated via _make_provenance() in all merge methods
- _topological_sort() replaced date-sort TODO with Kahn's algorithm using
  supersedes field for edges, date-sort tie-breaking, cycle-detection
  fallback
- 27 new unit tests covering all of the above; 313 tests pass
Fixes two mypy errors caught during quality gate:
- EnforcementResult.idempotency_hash Field uses explicit default= kwarg so pydantic plugin recognises the default correctly
- _count_policy_rules_applied casts dict.get() result to int to satisfy no-any-return
@kschlt kschlt merged commit b3a42b2 into main Mar 25, 2026
8 checks passed
@kschlt kschlt deleted the feat/canonical-enforcement-pipeline branch March 25, 2026 10:29
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