Split one decision table only after observed outputs are frozen. A model diff is not a safety net. Tests must pin allow, deny, ask, and audit order first.
A messy access helper hides three contracts in one function. Callers depend on the return token and the audit list. They also depend on whether the input map changes.
Move the decision table too early and those contracts drift. Style cleanup is not the same as a safe split. Freeze those rows first, then move exactly one function.
Name the failure before you edit
The helper below is a proposed example, not a production trace. It mixes normalization, a module audit list, and a branching decision. Empty role and missing role take different paths.
That difference is the bug you must not clean up by accident. Read the function as a contract list, not as style debt. Four outcomes matter more than the nested branches.
AUDIT = []
def grant_access(user, action, resource):
role = user.get("role")
if role is None:
AUDIT.append(("missing", action, resource))
return "ask"
role = str(role).strip().lower()
if role == "":
AUDIT.append(("blank", action, resource))
return "deny"
if action == "read" and role in {"guest", "member", "admin"}:
AUDIT.append(("allow", action, resource))
return "allow"
if action == "write" and role == "admin":
user["elevated"] = True
AUDIT.append(("allow", action, resource))
return "allow"
AUDIT.append(("deny", action, resource))
return "deny"
The return token is the first contract callers already observe. The audit tuple order is the second contract. The input map mutates only on admin write, and that is the third.
Step 1: Freeze the rows you already observe
Do not rename helpers or extract a pure function yet. Record the rows that current callers already see. A later edit is safe only when every recorded row stays green.
- Reset the module audit list inside every test setup.
- Call grant_access once with a single input row.
- Assert the return token and the full audit list.
- Assert whether the input map gained an elevated key.
- Keep each observed case in its own test function.
These five steps are the gate for the later edit. One red row means you changed behavior, not structure. Fix the recording before you touch the helper.
Step 2: Lock a visible case table
Use a table so the pins stay easy to review. The table is the artifact for this refactor. It is not a benchmark and it has no production metric.
| role | action | return | audit head | elevated |
|---|---|---|---|---|
| missing | read | ask | missing | no |
| blank | read | deny | blank | no |
| guest | read | allow | allow | no |
| guest | write | deny | deny | no |
| admin | write | allow | allow | yes |
| member | write | deny | deny | no |
Missing role is not the same as a blank role. Guest read is not the same as guest write. Admin write mutates the map, and member write does not.
Those three distinctions are the contract you must hold. A prettier branch that collapses them is a behavior change. Leave that policy question outside this structural split.
Step 3: Turn the table into tests
The tests below are a proposed harness for this example. Run them against the messy function before any split. They should pass before you trust a generated diff.
import unittest
class GrantContractTest(unittest.TestCase):
def setUp(self):
AUDIT.clear()
def test_missing_role_asks(self):
user = {}
token = grant_access(user, "read", "doc")
self.assertEqual(token, "ask")
self.assertEqual(AUDIT, [("missing", "read", "doc")])
self.assertNotIn("elevated", user)
def test_blank_role_denies(self):
user = {"role": " "}
token = grant_access(user, "read", "doc")
self.assertEqual(token, "deny")
self.assertEqual(AUDIT, [("blank", "read", "doc")])
self.assertNotIn("elevated", user)
def test_guest_read_allows(self):
user = {"role": "Guest"}
token = grant_access(user, "read", "doc")
self.assertEqual(token, "allow")
self.assertEqual(AUDIT, [("allow", "read", "doc")])
self.assertNotIn("elevated", user)
def test_guest_write_denies(self):
user = {"role": "guest"}
token = grant_access(user, "write", "doc")
self.assertEqual(token, "deny")
self.assertEqual(AUDIT, [("deny", "write", "doc")])
self.assertNotIn("elevated", user)
def test_admin_write_mutates(self):
user = {"role": "Admin"}
token = grant_access(user, "write", "doc")
self.assertEqual(token, "allow")
self.assertEqual(AUDIT, [("allow", "write", "doc")])
self.assertTrue(user["elevated"])
def test_member_write_denies(self):
user = {"role": "member"}
token = grant_access(user, "write", "doc")
self.assertEqual(token, "deny")
self.assertEqual(AUDIT, [("deny", "write", "doc")])
self.assertNotIn("elevated", user)
if __name__ == "__main__":
unittest.main()
Add both functions to one module before you run this file. Do not parameterize yet if a failure becomes harder to read. A failed test name should name the exact row.
Run the harness with one local command before you edit. Expect six passing tests before you open any diff. If a test fails now, the pin is wrong, so fix the pin.
python -m unittest grant_contract.py -v
Step 4: Allow only the smallest split
The safe change moves the branch logic into decide. grant_access still appends the audit tuple in order. grant_access still sets elevated only on admin write.
decide returns a token and a reason string only. It must not touch AUDIT or the user map. That boundary is the whole point of this split.
def decide(role, action):
if role is None:
return "ask", "missing"
role = str(role).strip().lower()
if role == "":
return "deny", "blank"
if action == "read" and role in {"guest", "member", "admin"}:
return "allow", "allow"
if action == "write" and role == "admin":
return "allow", "allow"
return "deny", "deny"
def grant_access(user, action, resource):
token, reason = decide(user.get("role"), action)
if token == "allow" and action == "write":
user["elevated"] = True
AUDIT.append((reason, action, resource))
return token
That split stays a proposal until the same tests pass. Do not also rename AUDIT in this same diff. Do not also stop mutating the user map yet.
Those edits are later changes with their own pins. Batching them hides which edit broke a row. Keep this round limited to one function move.
Step 5: Reject diffs that do two jobs
Hold a written reject list next to the tests. Use it when a generated diff looks tidy but wide. Width is a risk even when the suite is green.
- Reject a diff that deletes the blank-role branch.
- Reject a diff that treats missing role as deny.
- Reject a diff that drops the elevated write.
- Reject a diff that reorders audit tuple fields.
- Reject a diff that adds network, clock, or file calls.
A free coding model can draft the split for you. It cannot waive the reject list or the tests. You still run the unittest command and read the diff.
Where a free model may draft the split
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode free model access can draft the decide split. Use it only after the six characterization tests exist.
The free server option can run unittest away from your dirty tree. Both claims here are availability options supplied for this draft. This article does not state quotas, model names, or hardware.
It also does not state duration or benchmark numbers. Use the model only as a diff proposer in this loop. Paste the frozen table and the reject list into the prompt.
Ask for one function move and no token changes. Apply the patch in the isolated run, not on caller code. If that run is red, discard the diff without debate.
Do not ask the model for a broader rewrite next. Tighten the prompt with the one failing row only. Repeat that attempt once, then stop if it is still red.
Edit the split by hand after two red runs. A useful prompt stays short, closed, and specific. It names the frozen contracts and forbids extra behavior.
Tests already pin the return token, the audit tuples, and elevated.
Move only the branch logic into decide(role, action).
Keep the audit append and the elevated write inside grant_access.
Do not merge a missing role with a blank role.
Return one unified diff and then stop writing.
Use a decision matrix after the run
Count the diff shape before you praise the result. A green suite can still hide a second change. Accept only the row that matches the smallest split.
| Diff shape | Suite | Decision |
|---|---|---|
| decide added, wrapper behavior unchanged | green | accept |
| missing role merged into blank role | any | reject |
| elevated write removed | red | reject |
| audit field order swapped | red | reject |
| extra log or clock call added | green | reject |
| rename bundled with the split | green | reject this round |
Six table rows must pass before any structural split. One function moves in the first accepted diff. Two red model runs is the hard stop line.
Zero new side effects may appear in that first diff. Those counts are gates, not performance results from a lab. They do not measure a product or a model score.
Limitations you should keep visible
Characterization tests pin observed behavior, including old bugs. If blank role should later become ask, write a new test. Do not hide that policy fix inside the structural split.
The harness does not cover concurrent callers at all. AUDIT is a shared list, so threads can interleave tuples. This split does not make that shared list thread safe.
The example ignores resource ownership and external stores. A real helper may call a database or a cache. Pin that call before you move it into decide.
Generated diffs can pass tests and still be unclear. Unclear code remains a review defect after a green run. Passing tests are necessary here, but they are not sufficient.
Role matching here is exact after strip and lower. It does not handle aliases, nested groups, or inherited roles. Do not treat this sample as an access-control design.
Who should not use this path
Skip this path if you have no caller you can execute. A table you invented from memory is not characterization. It is a guess, and a guess cannot gate a split.
Skip this path if behavior must change in this same patch. Write a red test that states the new rule first. Do not hide that fix inside a rename or a move.
Skip the model step if you cannot read a unified diff. An isolated run does not replace your own review. A green run can still drop a branch the tests forgot.
Skip the free server step when tests need secrets or private rows. Do not upload credentials just to try a generated draft. Use local fixtures with fake roles and fake resource names.
Close the loop before you merge
Re-run the six tests after you apply the split. Confirm elevated still flips only on an admin write. Confirm missing and blank still log different audit heads.
Review the diff stat before you merge the branch. Two changed definitions are the expected diff shape. Ten files changed means this round is no longer small.
git diff --stat
python -m unittest grant_contract.py -v
If those rows stay green, merge that one split only. Queue the next change behind a new written pin. Do not batch a rename, a policy fix, and a log change.
A free model can draft the next pinned split after that pin exists. MonkeyCode's free model access and free server option can host that trial. Keep the tests on fixtures, and keep the reject list in the prompt.
Top comments (0)