DEV Community

Dakota Huang
Dakota Huang

Posted on

Freeze Path-Skip Decisions Before One Matcher Extract

Do not extract ignore logic on the first pass.
Freeze every skip decision in a fixture table.
Extract one matcher only after those rows stay green.
A larger move rewrites branches you never pinned.

Messy repos hide skip rules in helpers, loops, and comments.
Cache keys, linters, and packagers consult the same soup.
A cleanup that feels local often flips one silent path.
This article treats current skip answers as the spec.
It does not claim those answers are correct product behavior.
It only stops the refactor from drifting them.

The failure mode

Ignore functions mix prefixes, suffixes, and ad-hoc stars.
Callers pass relative paths, then absolute paths, then ./ forms.
One branch lowercases names. Another branch keeps original case.
Tests that only hit node_modules miss the trailing-slash form.
They also miss vendor/foo against the reverse layout foo/vendor.
Green unit tests then ship a silently different skip set.

Characterization tests record what the live code does today.
They do not argue with the output on the first pass.
That argument belongs in a later, explicit behavior change.

What to pin

Pin boolean skip results, not helper names or call counts.
Pin the exact path strings you feed the function under test.
Pin normalization choices if the function mutates its input.

Record at least these classes of input in the first table:

  1. Bare directory names with no slash
  2. Trailing slashes on directory forms
  3. Leading ./ prefixes on relative paths
  4. Nested vendor segments in the middle
  5. Extension suffixes such as .pyc
  6. Star characters that are not real globs
  7. Empty strings and the single-dot path
  8. Paths that look similar but differ in case

If the function also returns a reason string, pin that text.
Do not pin log lines unless a caller parses those lines.
Extra pins make the later extract harder than it needs to be.

Fixture table

The table below is a proposed characterization set only.
It is not production telemetry from any live service.
Replace the expected column with your repo's current answers.

path expected skip
node_modules true
node_modules/ true
./node_modules/pkg true
src/node_modules/pkg true
node_modules.bak false
vendor true
src/vendor/lib.c true
vendored.c false
build/ true
BUILD false
dist/app.js true
src/app.js false
.git/config true
src/.gitkeep false
tmp true
template false
`` false
. false

Run the live function against every row before editing logic.
Write the actual boolean into expected during characterization.
Do not "fix" surprising rows during that first capture pass.

Step 1: Isolate the messy function

Copy the current skip logic into a test module unchanged.
Do not rename locals on that first copy.
Do not merge the star branches on that copy either.

`python

proposed example — current messy behavior, not a design

def should_skip(path):
if not path:
return False
p = path
if p.startswith("./"):
p = p[2:]
if p.endswith("/"):
p = p[:-1]
if p == ".":
return False
lowered = p.lower()
parts = p.split("/")
if "node_modules" in parts:
return True
if p == "vendor" or p.startswith("vendor/"):
return True
if "vendor" in parts[1:]:
return True
if p == "build" or p.startswith("build/"):
return True
if p == "dist" or p.startswith("dist/"):
return True
if p == "tmp" or p.startswith("tmp/"):
return True
if lowered.startswith(".git/") or lowered == ".git":
return True
if p.endswith(".pyc"):
return True
if "*" in p:
return True
return False
`

Note the case split in the sample above.
BUILD does not skip. build does skip.
node_modules stays case-sensitive because parts uses p.
That inconsistency is the reason the table exists.

Step 2: Encode decisions as rows

Keep the table in code next to the function under test.
A list of tuples is enough for the first harness.
Do not hide rows in factories on day one of the work.

`python

proposed characterization harness

CASES = [
("node_modules", True),
("node_modules/", True),
("./node_modules/pkg", True),
("src/node_modules/pkg", True),
("node_modules.bak", False),
("vendor", True),
("src/vendor/lib.c", True),
("vendored.c", False),
("build/", True),
("BUILD", False),
("dist/app.js", True),
("src/app.js", False),
(".git/config", True),
("src/.gitkeep", False),
("tmp", True),
("template", False),
("", False),
(".", False),
("foo/*.pyc", True),
("src/app.pyc", True),
("src/app.py", False),
]

def test_should_skip_characterization():
failures = []
for path, expected in CASES:
actual = should_skip(path)
if actual is not expected:
failures.append((path, expected, actual))
assert failures == [], failures
`

One assertion at the end prints every missed row.
Per-row asserts stop at the first missed row.
You want the full drift map, not a single miss.

Dump live answers before you lock expected booleans.

`python
def dump_current_answers(paths):
for path in paths:
print(repr(path), should_skip(path))
`

`bash
python -c "from test_should_skip import CASES, dump_current_answers; dump_current_answers([c[0] for c in CASES])"
`

Paste those printed booleans into CASES without editing logic.
That dump is the contract for the later extract.

Step 3: Run until the table is green

`bash
python -m pytest test_should_skip.py -q
`

If a row disagrees with your memory, trust the function.
Update the expected boolean to match current code.
File a separate ticket for behavior you actually dislike.

Do not improve star handling while the table is still red.
Do not introduce pathlib in the same red window.
Do not add Windows separators in that same commit.
Windows \\ paths are a new specification, not a tidy rename.
Treat them as a later characterization set with new rows.
Mixing that set now doubles the extract surface without coverage.

Step 4: Extract one matcher

After the harness is green, change one mechanical thing.
Extract a helper that answers skip for a normalized path.
Keep should_skip as the adapter that strips ./ and slashes.

`python
def _normalized(path):
if path.startswith("./"):
path = path[2:]
if path.endswith("/") and path != "/":
path = path[:-1]
return path

def _skip_normalized(p):
if not p or p == ".":
return False
parts = p.split("/")
if "node_modules" in parts:
return True
if p == "vendor" or p.startswith("vendor/"):
return True
if "vendor" in parts[1:]:
return True
if p == "build" or p.startswith("build/"):
return True
if p == "dist" or p.startswith("dist/"):
return True
if p == "tmp" or p.startswith("tmp/"):
return True
lowered = p.lower()
if lowered.startswith(".git/") or lowered == ".git":
return True
if p.endswith(".pyc"):
return True
if "*" in p:
return True
return False

def should_skip(path):
if not path:
return False
return _skip_normalized(_normalized(path))
`

Re-run the same table after that split lands.
If any row flips, revert the extract immediately.
The adapter must preserve the case bugs you already pinned.
That last sentence is the method, not a style preference.
You are not blessing the bugs as product truth.
You are refusing to change them by accident during cleanup.

Decision matrix for the next change

Use this matrix after the first extract stays green.
Do not start several rows in one commit.

Next urge Do it now? Why
Unify case folding No BUILD versus build is pinned behavior
Switch to pathlib No Object identity and slash-style drift
Real glob matching No * currently means skip the whole path
Add Windows \\ New table first Unpinned inputs are new specs
Delete the star branch Only with product sign-off Callers may inject pattern strings
Merge vendor rules Maybe Only if every table cell stays identical

The smallest safe change is the extract that keeps every cell.
Anything that needs a new row is a behavior change.
Label that change as new spec, not as cleanup.

Where a free coding model fits

A model is useful after the table is already green.
It is not useful as a substitute for the table.
Unpinned suggestions rewrite skip sets without a diff you can trust.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode offers free model access and a free server option.
Those two facts are the only product claims used here.
This workflow does not depend on a named model or a quoted quota.

A practical sequence looks like this:

  1. Commit the messy function and the green table.
  2. Point the free model at that commit only.
  3. Ask for one extract that must not change CASES.
  4. Run the same pytest command on the free server.
  5. Keep the patch only if the table is still identical.

Reject patches that add fnmatch as a silent upgrade.
Reject patches that lowercase every path part for consistency.
Reject patches that drop the empty-string branch as dead code.
If the server run cannot execute your pytest file, stop there.
Paste the patch back into local pytest before you merge.
Do not accept a refactor you cannot re-run on the same table.

If you already keep a skip table, that free-model pass is optional.
Use it only to draft the adapter split against pinned rows.
Skip it entirely when the table is still red.

Limitations

This method freezes today's accidents in executable form.
It will not tell you which skips users originally wanted.
Product intent still needs a second, explicit change with new rows.

The table is only as good as the rows you bothered to write.
Missing ././node_modules means that form can still drift later.
Missing symlink paths means filesystem reality remains untested.
The sample function is not a glob engine under any reading.
A star in the path skips the whole string today.
Real glob rules would be a behavior change, not an extract.

Boolean pins also hide reason-string drift on CLI output.
If a command prints skipped vendor, pin that exact text.
If nothing reads the reason, do not invent a reason field.
Pytest order does not matter for this immutable string table.
Shared mutable path lists would matter and need extra pins.
This example uses immutable string inputs on purpose.

Who should not use this approach

Do not use characterization-first extracts for secret-gating allowlists.
A frozen skip bug can hide a path you must start scanning.
Fix that security behavior in an explicit, reviewed commit instead.

Do not use it when the function must change in the same pull request.
Ship the behavior change with new rows labeled as new spec.
Do not bury that change inside a rename or helper extract.

Do not use it as a license to copy ignore rules across languages.
Python split("/") is not Git pathspec and not fnmatch.
Go and Rust ports need their own tables, not a translated helper.

Teams without a test runner gain little from this sequence.
A markdown table that nobody executes is only documentation.
It is not a characterization harness until a command fails on drift.

Result

The useful result is a green skip table plus one adapter split.
Everything else waits until those cells still match.
That order is the refactor. The extract is just bookkeeping.

Top comments (0)