feat(policy): read backend's .rafter/config.yml + flat-shape compat (sable-c1c)#154
Merged
Conversation
…sable-c1c)
Backend triage (hq-4tcey, closed) confirmed rafter-backend reads policy
at .rafter/config.yml (subdir + config.yml) with exclude_paths /
custom_patterns flat at the top level, while the CLI canonical is
.rafter.yml with scan.* nested. Customers writing either shape get
honored by only one tool — the customer's .rafter.yml worked locally
(after sable-yz0) and silently no-op'd on the remote cloud scan.
Bilateral alignment plan:
- Backend adds .rafter.yml fallback with scan.* schema compat
(their preferred direction).
- CLI (this PR) reads .rafter/config.yml indefinitely and accepts
the backend flat shape alongside the canonical nested form. No
deprecation — both shapes supported permanently.
Two changes in policy-loader.ts / policy_loader.py:
1. findPolicyFile / find_policy_file walks all four candidates in
precedence order: .rafter.yml > .rafter.yaml > .rafter/config.yml
> .rafter/config.yaml. The canonical dotfile wins if both shapes
exist (matching prior CLI behavior for users who already wrote
.rafter.yml).
2. mapPolicy / _map_policy accepts top-level `exclude_paths` and
`custom_patterns` (backend flat shape) and folds them into
policy.scan.* — but only if the nested form wasn't already set.
Nested form takes precedence on collision. Top-level compat keys
are added to VALID_TOP_LEVEL_KEYS so they don't trigger
"Unknown policy key" warnings on validate.
Tests: 8 Node + 7 Python new assertions covering subdir-only discovery,
extension variants, dotfile-wins precedence, top-level schema accepted
on both file paths, nested wins on collision, no warnings for compat
keys. Broader regression sweeps (25 Python + 16 Node) still pass.
Paired with sable-yz0 (PR #152), this unblocks customers writing
either CLI-shape or backend-shape policy. Once rafter-backend adds the
.rafter.yml fallback (their side of the bilateral fix), both tools
honor either file in either shape.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced Jun 1, 2026
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.
Summary
Backend triage (hq-4tcey) confirmed
rafter-backendreads policy at.rafter/config.yml(subdir +config.yml) withexclude_paths/custom_patternsflat at the top level, while the CLI canonical is.rafter.ymlwithscan.*nested. Customers writing either shape get honored by only one tool — exactly what bit the recent customer (.rafter.ymlworked locally after #152, silently no-op'd remotely).This PR is the CLI half of the bilateral fix:
.rafter/config.ymlindefinitely (no deprecation — backend's file path stays a first-class location).exclude_paths/custom_patterns) alongside the canonical nested form, in either file.Backend pairs this with their own work to add
.rafter.ymlfallback +scan.*schema compat. Once both ship, customers can write either file with either shape and both tools honor it.Precedence
For each directory in the walk-up from cwd:
Within a single file, nested
scan.exclude_pathswins over top-levelexclude_pathsif both are present (no merge — explicit precedence keeps debugging straightforward).What's accepted in each shape
scan.exclude_paths(nested).rafter.yml,.rafter/config.ymlexclude_paths(top-level).rafter.yml,.rafter/config.ymlscan.custom_patterns(nested)custom_patterns(top-level)Top-level
exclude_pathsandcustom_patternsare added toVALID_TOP_LEVEL_KEYSso they no longer triggerWarning: Unknown policy key …on validate.Tests
node/tests/policy-loader.test.ts(new sable-c1c block)python/tests/test_policy.py::TestBackendCompatnode/tests/policy-{loader,merge,validation}.test.ts(regression)python/tests/test_policy.pyfull suite (regression)Coverage hits: subdir-only discovery, both
.ymland.yamlextensions, dotfile-wins precedence, top-level schema accepted on both file paths, nested-wins-on-collision, no warnings for compat keys.Pairs with
scan.exclude_pathshonored on both engines + multi-segment path semantics. Without fix(scan): honor .rafter.yml scan.exclude_paths on both engines (sable-yz0) #152, the policy is loaded but the scanners still ignore parts of it. With both PRs, the customer's flow works end-to-end on local scans..rafter.ymlfallback (hq-XXXX on their side) — completes the bilateral alignment for remote scans.Tracking
sable-c1c(P1 feature, claimed by me)sable-yz0(P0 bug) —scan.exclude_pathsenforcement (PR fix(scan): honor .rafter.yml scan.exclude_paths on both engines (sable-yz0) #152, pending merge)sable-s2t(P1) — rerouted: R-XXXXX hashing lives ingithub-action/juno, notrafter-backendsable-5vo(P1) — closed: backend confirmed which file/schema they honor; this PR is the answersable-r9h(P3) — follow-up: pattern-length cap for ReDoS defense-in-depth🤖 Generated with Claude Code