Repository navigation
Make v2 file-layer exceptions expire and cover whole files - #4205
Merged
Merged
Conversation
- A files-rule exception that sets category, removal or match, or has no expires date, is reported: the guard reads none of them, so such an exception could last forever or claim to cover one site. - A database_connectors module can't be excepted, because rule api.v2.crud-checks-cover-every-resource also checks connector names, and an unassigned one is told to take its operation's name. - The services fix message says where each name must sit. - The files rule's proof also has an unassigned file under fastapi_routes/v2. Refs #4204 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A files-rule exception that sets category, removal or match, or has no expires date, is reported and not applied; one for a module in a database_connectors package is rejected, because api.v2.crud-checks-cover-every-resource needs its operation name. - The services fix message says where each name must sit, and a misnamed connector module is told to take its operation's name. - The guard reached 834 lines, over the 800-line limit, so the per-file import and call checks move to scripts/rules/v2_layer_checks.py. scripts/rules/v2_layers.py keeps the layer assignment, exceptions, scanning and baselines, and the names it exported. - v2-code-organization.md describes the exception constraints. Refs #4204 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…failing) - A file in a database_connectors package that already has an operation's name but sits one level too deep is told to move directly into the package, not to take a name it already has. The exception refusal no longer says the CRUD rule checks such a file, which it doesn't. - An exception path with a '.', '..' or empty segment is reported: the guard compares paths as written, so it would match no scanned file. - An exception with no expires, category or removal, or an unreadable date, is reported. This test already passes; it covers a branch no test reached. - The match subtest's flag is named has_expires_date. Refs #4204 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on paths - The fix for an unassigned file in a database_connectors package, and the refusal of an exception for one, say to put it directly in the package as well as to name it after its operation. The refusal's reason is that the package holds only connector modules; the CRUD rule is cited only for the files it checks. - An exception whose path has a '.', '..' or empty segment is reported and not applied, because assign_layer compares paths as git ls-files writes them. - v2-code-organization.md and the changelog fragment say the same. Refs #4204 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion (failing) - An exception path that names a real file under another spelling is reported with one message, whatever the spelling. The '..' case now has its middle directory, so it reaches the real file as the bug needs. - Connector fix messages name services/v2/<resource>/database_connectors, the location the CRUD checks cover, rather than any database_connectors package. - The connector-exception test removes each subtest's file and checks the refused module is still reported as having no layer. - The expiry test is named for what the guard checks: that an expires date is required. The registry's expiry check ends the exception. Refs #4204 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or location - A files-rule exception must name a path the guard scans, as git ls-files writes it. Any other spelling of a real file (a '.', '..' or empty segment, a symlinked directory, or different letter case on macOS) is reported instead of being accepted and never applied. run_check lists the files once and passes them to both steps. - The connector fix messages say to put the module directly in services/v2/<resource>/database_connectors, where the CRUD checks find it, rather than in any database_connectors package. - The changelog fragment says the same. Refs #4204 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iling) - An exception that spells an untracked file's path exactly, in a git checkout, is told to git add the file as well as to check the spelling. This test runs the guard against git ls-files, which the other exception tests, in plain directories, don't. - A module in a database_connectors package is told to move out of the package if it isn't a connector, as well as how to name and place a connector. - The refusal of a connector-package exception says the CRUD rule fails a module there whose name doesn't start with an operation; its globs pass names such as createsfoo.py. Refs #4204 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…D claim - The fix for an exception path the guard doesn't scan also says to git add the file if git doesn't track it. - A module in a database_connectors package is told to move out of the package if it isn't a connector, or else to take an operation's name directly in services/v2/<resource>/database_connectors. - The refusal of a connector-package exception, and v2-code-organization.md, say the CRUD rule fails a module there whose name doesn't start with an operation, which is what its globs check. - The files rule's statement says the exception is for a file outside a database_connectors package; docs/generated/rules.md is regenerated. Refs #4204 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
marked this pull request as ready for review
November 5, 2026 18:32
anth-volk
added a commit
that referenced
this pull request
Nov 5, 2026
Brings in #4196's Codecov flags, #4200's diff-coverage job and #4205's v2 file-layer exceptions. pr.yml merged cleanly: CI complete needs every job, including diff-coverage and the coverage uploads, and the quality-guards job still runs make quality, which runs #4205's v2-layers guard. No rule refers to the removed lint or typecheck-v2 jobs. docs/generated/rules.md and the Rules block in AGENTS.md were rendered again, keeping this branch's make quality instruction in AGENTS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Nov 5, 2026
Brings in master (#4196, #4200, #4205) through #4203's branch. The generated docs/generated/rules.md and the Rules block in AGENTS.md were rendered again, keeping this branch's make install and hook instructions in AGENTS.md, which #4203's branch doesn't change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was previously deployed
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.
Fixes #4204
Summary
#4191 added the
v2-layersquality guard and ruleapi.v2.files-assigned-to-layers: every file underpolicyengine_api/services/v2andpolicyengine_api/fastapi_routes/v2must have a name that gives it a layer, or a[[rule.exception]]for that rule inrules/registry.toml. #4191 merged at e19f0d5, before the fixes from its last self-review round were pushed. This PR brings those fixes onto the latestmaster.What
masteraccepts that it shouldn'tThe guard on
masterapplies every files-rule exception the registry loader accepts, as long as it names one existing v2 file whose name gives no layer. The loader requires anexpiresdate only when an exception sets neithercategorynorremoval, and the guard reads no other field. Somaster:category = "forever"orremoval = "never"and noexpiresloads, passes the registry checks and is applied. The file then keeps a name with no layer permanently.match. An exception withmatch = "session.get"reads as if it covered one call, but the guard applies it to the whole file.database_connectorspackage, such aspolicies/database_connectors/helpers.py. Ruleapi.v2.crud-checks-cover-every-resource(pytest) still fails a module directly inservices/v2/<resource>/database_connectorswhose name doesn't start with an operation, so for such a module the exception can't work.services.py,validators.py,transformations.py,types.py,process_state.py,database_session.pyanddatabase_connectors/<operation>[_name].py, but not where each must sit.database_session.pyis only accepted directly in a resource package, so following the message can fail again..,..or empty segment (household_calculations/./client.py), a path through a symlinked directory, or different letter case on macOS. The guard compares exceptions with the pathsgit ls-fileslists, so the exception covers nothing, and the file still fails with a fix that asks for the exception it already has.api.v2.files-assigned-to-layershas no unassigned file underfastapi_routes/v2, so the proof doesn't show the check working there. Other unit tests do cover it.Changes
scripts/rules/v2_layers.pyreports, and doesn't apply, a files-rule exception that:expiresdate (or one the loader couldn't read);category,removalormatch;database_connectorspackage, at any depth;git ls-fileslists; any other spelling of a real file, or a file git doesn't track, is reported, and the fix says to check the spelling and togit addan untracked file.run_checklists the files once and uses the list for both the exception check and the scan.services/v2,database_session.pydirectly in one, and connector modules directly in theirdatabase_connectorspackage.database_connectorspackage gets its own message: if it reads or writes rows, name it after its operation and put it directly inservices/v2/<resource>/database_connectors; otherwise move it out of the package and name it after its layer. An exception can't stand in. This covers a file that already has an operation's name but sits one level too deep, such asdatabase_connectors/legacy/creates.py. The message names that location, rather than anydatabase_connectorspackage, because it is the only one the CRUD checks find.database_connectorspackage holds only connector modules", and adds thatapi.v2.crud-checks-cover-every-resourcefails a module directly inservices/v2/<resource>/database_connectorswhose name doesn't start with an operation. That is what the rule's test checks: it finds only that location, and its patterns (creates*.pyand so on) pass a name such ascreatesfoo.py, which this guard gives no layer.fastapi_routes/v2/reports/helpers.py, which has no layer.scripts/rules/v2_layers.py(463 lines) keeps the layer assignment, exceptions, scanning, baselines and the names other modules import;scripts/rules/v2_layer_checks.py(438 lines) holds the per-file import and call checks.master,v2_layers.pyis 786 lines; this change would have taken it past 800.docs/engineering/skills/v2-code-organization.mdsays an exception's path is written asgit ls-fileslists it, for a tracked file, that an exception covers the whole file and sets nomatch,categoryorremoval, and that it can't name a connector-package module, which moves out of the package if it isn't a connector.api.v2.files-assigned-to-layersinrules/registry.tomlsays the exception is for a file outside adatabase_connectorspackage, anddocs/generated/rules.mdis regenerated.No exception exists for this rule today, so nothing in the repository is newly reported.
CI and environment variables
quality-guardsjob, and its tests in the "Test the rules registry tool" step.Known limits, not changed here
expiresdate far ahead, such as9999-12-30, is accepted. No rule in the registry sets a maximum.9999-12-31is reported as "no expires date", because the loader uses that date for a missing or unreadable one.database_connectorspackage, includingservices/v2/database_connectors/andservices/v2/<group>/<resource>/database_connectors/. The CRUD checks find onlyservices/v2/<resource>/database_connectors/. This was already so onmaster; the fix messages now point to the checked location.api.v2.files-assigned-to-layers. A[[rule.exception]]for one of its other five rules is neither applied nor reported, so the finding it targets still fails. This was already so onmaster.run_checkreturns the files rule's exception problems but not the registry loader's own; therules-registryguard reports those. This was already so onmaster.expires = 2099-01-01T00:00:00, because a datetime is a date.scripts/rules/validate.pythen raisesTypeErrorcomparing it with today's date. This affects every rule's exceptions, not only this one.Testing
Test first:
cb01ffa adds the tests, with "(failing)" in its message. Against
master's guard, 4 of the 5 touched tests fail:test_an_unassigned_file_fails_and_names_the_fix(fix message placement);test_an_unassigned_connector_file_must_be_renamed;test_an_exception_ends_on_its_expires_date_and_covers_the_whole_file, with one subtest each forcategory,removalandmatch;test_a_connector_module_cannot_be_excepted.test_rule_proof_files_assigned_to_layerspasses onmastertoo: its new route-side file closes a coverage gap rather than showing a behaviour change.f651b4a makes the change, and the 5 tests pass.
These two commits were first made on #4191's branch after it merged (0dc5686 and 12889c2) and were cherry-picked here without conflicts. cbe0c87 then merges
masterwith #4183 (the suppressions guard), also without conflicts.Three self-review rounds on this PR found more. Round 1:
c991814 adds or changes the tests, with "(failing)" in its message. Three fail before the fix:
test_an_unassigned_connector_file_is_told_its_name_and_place(renamed fromtest_an_unassigned_connector_file_must_be_renamed; adds thelegacy/creates.pycase);test_a_connector_module_cannot_be_excepted(new wording, and the nested case);test_an_exception_path_must_be_written_as_git_lists_it.test_an_exception_with_no_readable_expires_date_is_reportedpasses before and after. It covers the guard's check for an exception with noexpires,categoryorremoval, or an unreadable date, which no test reached: with that check disabled, every other exception test still passed.14b1d9c makes the change, and the tests pass.
Round 2:
services/v2/<resource>/database_connectors), andtest_an_exception_path_must_be_written_as_git_lists_it(one message for every unscanned spelling). The..subtest now creates its middle directory, so the path reaches the real file as the bug needs. The connector-exception test removes each subtest's file and checks that the refused module is still reported as having no layer.test_an_exception_ends_on_its_expires_date_and_covers_the_whole_fileis renamedtest_an_exception_needs_an_expires_date_and_covers_the_whole_file, because the guard checks that a date is set, and the registry's expiry check ends the exception.Round 3:
test_an_exception_path_must_be_written_as_git_lists_it(thegit addfix), and the newtest_an_exception_for_a_file_git_does_not_track_is_reported. The new test runs the guard in agit initdirectory, so the exception is checked againstgit ls-filesrather than a directory listing; the other exception tests use plain directories.4c64934 merges
masterwith #4188 (the asserting-tests guard), without conflicts;docs/generated/rules.mdandAGENTS.mdregenerate unchanged.After these changes:
scripts/rules/tests/test_v2_layers.py, run individually by node ID, pass;python scripts/run_quality_guards.py(including the newsuppressionsand asserting-tests guards),python -m scripts.rules guard --base-ref origin/master, andruff checkandruff format --checkonscripts/rulespass.🤖 Generated with Claude Code