feat(enforcement): implement canonical enforcement pipeline (CPL)#13
Merged
feat(enforcement): implement canonical enforcement pipeline (CPL)#13
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Enforcement rules were previously generated by calling adapters directly on raw ADR files (via the stub
_apply_guardrailsand_generate_enforcement_rulesinapproval.py). This bypassed theConstraintsContract— 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
EnforcementPipelineclass inenforcement/pipeline.pyaccepts aConstraintsContractand runs two stages: native fragment generation (ESLint and Ruff adapters via newgenerate_*_from_contract()entry points) and secondary artifact generation (scripts, hooks). The result is wrapped in anEnforcementResultenvelope that carriesAppliedFragment,EnforcementConflict,SkippedAdapter, andProvenanceEntryrecords, 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 toPolicyProvenanceacross all merge methods so every enforcement output is traceable to a specific ADR clause._topological_sort()incontract/merger.pyreplaced a date-sort TODO with Kahn's algorithm using thesupersedesfield for edges, date-sort as tie-breaking, and cycle-detection fallback — this ensures superseded ADRs don't override the decisions that replaced them.approval.pywiring was updated to callEnforcementPipeline.compile()instead of the old stubs.What Was Tested
EnforcementResultconstruction and hash stability,EnforcementPipeline.compile()with mock adapters,generate_eslint_config_from_contract()andgenerate_ruff_config_from_contract()for both adapter types,clause_idderivation across merge methods, and_topological_sort()with linear chains, ties, and cycles.default=kwarg for Pydantic Field andint()cast fordict.get()return).Risks
The new
generate_*_from_contract()adapter entry points are additive — existinggenerate_*_config()functions (called on raw ADRs) remain untouched. Theapproval.pywiring 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.