v0.12 item 1: break the module cycle ARCHITECTURE §6 has recorded since v0.7 - #208
Merged
Merged
Conversation
§6 says dependencies point downward only, and from v0.7 to v0.11 that was false: `state -> receipt -> policy -> authority -> state`. Nothing broke at run time, because the two edges out of `policy.py` are function-level, so `import ctrlrun` resolved in one order and the suite passed. That is why it survived five milestones and why a reviewer had to find it by reading. **The edge broken is `receipt -> policy`.** `Decision` and `POLICY_UNAPPROVED` move to `decision.py`, which imports nothing from the package. That was the whole of what a receipt needed from the decider, and it is vocabulary rather than behaviour: an evidence type reaching up into the decider is the edge that most contradicts §6's table, which lists `receipt.py` as used by everything else. **No public name moves.** `policy.py` re-exports both, so `from ctrlrun.policy import Decision` is the same object and SPEC-v0.1 §8's frozen `__init__` block is untouched. The roadmap called this a public-surface question; with a re-export it is not one. **The rule is now a test**, and writing it was worth more than the fix. It distinguishes two graphs §6 kept conflating: import-order, which is module-level imports, and layering, which counts deferred imports and is what the table describes. The recorded cycle is invisible to the first, so a guard built only on module-level imports passes on 0.11.0's tree, which I checked rather than assumed. That guard immediately found a second cycle nobody had recorded: `jwt_identity <-> revocation`, where `revocation.py` imported `_NoRedirects` from inside a method to avoid a second copy of a security-critical redirect handler. One copy was always right and the direction was wrong. The class now lives in `revocation.py`, which `jwt_identity.py` already imports, and it keeps warning on the `ctrlrun` logger so no existing handler or filter is re-routed. **One layering cycle stays, named rather than left to a reader.** `policy <-> authority` cannot be removed by relocation: `_from_section` constructs an `Authority` and `canonical_grants` consumes one, so neither moves below `policy.py`, and moving the two callers up to `control.py` would change what `policy_hash` is taken over, which is evidence in every receipt. `RECORDED_LAYERING_CYCLES` holds that pair and nothing else, so a second one fails the suite. Signed-off-by: arpan <contact@arpanghoshal.com>
|
Warning Review limit reachedNext included review available in 59 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (6)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| # in `__init__.py`, and `adapter.py` imports `Decision` from here too. Both names moved down to | ||
| # break the module cycle §6 records, and both still resolve from this module because that block | ||
| # is a frozen public surface and a cycle is not a reason to move a published import path. | ||
| from .decision import POLICY_UNAPPROVED as POLICY_UNAPPROVED |
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.
§6 says dependencies point downward only, and from v0.7 to v0.11 that was false:
state -> receipt -> policy -> authority -> state. Nothing broke at run time, because the two edges out ofpolicy.pyare function-level, soimport ctrlrunresolved in one order and the suite passed. That is why it survived five milestones, and why a reviewer had to find it by reading instead of by a red run.The edge broken
receipt -> policy.DecisionandPOLICY_UNAPPROVEDmove todecision.py, which imports nothing from the package. That was the whole of what a receipt needed from the decider, and it is vocabulary rather than behaviour: an evidence type reaching up into the decider is the edge that most contradicts §6's table, which listsreceipt.pyas used by everything else.No public name moves.
policy.pyre-exports both, sofrom ctrlrun.policy import Decisionis the same object and SPEC-v0.1 §8's frozen__init__block is untouched. The roadmap called each candidate fix "a public-surface question of its own"; with a re-export this one is not.The other candidate was moving
policy.py's two deferred imports, and it is not available:_from_sectionconstructs anAuthorityandcanonical_grantsconsumes one, so neither moves belowpolicy.py, and moving the two callers up tocontrol.pywould change whatpolicy_hashis taken over, which is evidence in every receipt.The guard was worth more than the fix
tests/test_module_graph.pywalks every module's AST and separates two questions §6 kept conflating:import ctrlrunThe recorded cycle is invisible to the first. A guard built only on module-level imports passes on 0.11.0's tree, which I checked against a
git archivecopy rather than assumed, and my first draft did exactly that while its own docstring claimed otherwise.Writing it found two things nobody had recorded:
jwt_identity <-> revocation.revocation.pyimported_NoRedirectsfrom inside a method to avoid a second copy of a security-critical redirect handler. One copy was always right; the direction was wrong. The class now lives inrevocation.py, whichjwt_identity.pyalready imports, and it keeps warning on thectrlrunlogger so no existing handler or filter is silently re-routed.from .. import transportinsidegateway/togateway.transportinstead of the top-leveltransport, and reported a cycle that does not exist. A detector that invents edges is worse than none, because the fix for a phantom cycle is a refactor nobody needed. That resolution is now the function's main documented concern.What stays
policy <-> authority, named inRECORDED_LAYERING_CYCLESrather than left to a reader, with the reason it cannot be relocated. An allow-list of one fails the moment a second appears; a plain "no cycles" assertion softened to a skip never would.Gate
The one failure in that run was the cookbook race, which is #207 and is unrelated to this change.
Pairing
Docs are
v0.12/1-import-cycle. Do not merge beforev0.11.0is tagged.🤖 Generated with Claude Code