Skip to content
VLSI Mentor

Python Part II · Module 24 · EDA Environment Engineering

Artefacts, Temporary Data and What to Keep

Chapter 23.7 decided what a single run should delete, and Chapter 24.3 moved the bulk of it somewhere it could be deleted wholesale. What neither settled is how long the survivors live. A regression that keeps everything is not making a careful choice — it is making the same choice as a regression with no policy at all, only more slowly, and the day the volume fills somebody deletes things in a hurry with no rule at all. This chapter writes the rule down, and it starts by showing that the obvious form of it throws away the waveform.

Advanced18 min readPythonPart IIAutomationEDA ToolsEnvironment

Part II · Module 24 · Chapter 24.4 · Artefacts, Temporary Data and What to Keep

1. The Obvious Policy

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
KEEP = {"sim.log", "compile.log", "cmd.sh", "waves.fsdb"}
# everything else in the run dir is disposable

A set of names and one comment. It is readable, it is reviewable, it puts the decision in one place a team can argue about, and it is the right shape — Chapter 23.7 Section 6 argued for exactly this over a cleanup spread through a script.

It is also, applied literally, a bug.

2. What a Real Run Directory Contains

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
KEEP = {"sim.log", "compile.log", "cmd.sh", "waves.fsdb"}

ACTUAL = ["sim.log", "compile.log", "cmd.sh", "waves_axi_smoke_1839274.fsdb",
          "axi_smoke.vcd", "record.json", "simv", "csrc", "ucli.key"]

print(f"{'file in the run directory':<32} kept?")
for name in ACTUAL:
    print(f"{name:<32} {name in KEEP}")

kept = sum(1 for n in ACTUAL if n in KEEP)
print()
print(f"kept {kept} of {len(ACTUAL)}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file in the run directory        kept?
sim.log                          True
compile.log                      True
cmd.sh                           True
waves_axi_smoke_1839274.fsdb     False
axi_smoke.vcd                    False
record.json                      False
simv                             False
csrc                             False
ucli.key                         False

kept 3 of 9

The waveform is deleted. The annotation names waves.fsdb and the file on disk is waves_axi_smoke_1839274.fsdb, because Chapter 24.3 Section 2 required the test and seed in every name that can collide — and a wave file absolutely can. The one artefact a failing run exists to produce is the one the policy throws away.

record.json is lost too, which is the file Chapter 21.1 built the whole reporting chain on.

This is worth dwelling on because of how it fails. The policy is readable and reviewable, somebody did write it down, and it looks right when you read it. It is wrong about the data, and nothing in a code review surfaces that — you would have to have a real run directory in front of you.

A keep-list is a claim about filenames. Test it against real ones.

3. Patterns, Not Names

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import fnmatch

KEEP_PATTERNS = ("*.log", "cmd.sh", "record.json", "*.fsdb", "*.vcd", "env.json")

ACTUAL = ["sim.log", "compile.log", "cmd.sh", "waves_axi_smoke_1839274.fsdb",
          "axi_smoke.vcd", "record.json", "simv", "csrc", "ucli.key"]


def keep_reason(name):
    return next((p for p in KEEP_PATTERNS if fnmatch.fnmatch(name, p)), None)


print(f"{'file':<32} kept by")
for name in ACTUAL:
    print(f"{name:<32} {keep_reason(name)}")

print()
print("kept    :", sum(1 for n in ACTUAL if keep_reason(n)))
print("disposed:", [n for n in ACTUAL if not keep_reason(n)])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file                             kept by
sim.log                          *.log
compile.log                      *.log
cmd.sh                           cmd.sh
waves_axi_smoke_1839274.fsdb     *.fsdb
axi_smoke.vcd                    *.vcd
record.json                      record.json
simv                             None
csrc                             None
ucli.key                         None

kept    : 6
disposed: ['simv', 'csrc', 'ucli.key']

Six kept, three disposed, and the three disposed are the right three: the simulator binary, the generated C tree, and a lock file.

Two properties of this form earn their keep.

keep_reason returns which pattern matched, not just a boolean. A sweeper that logs the reason is one you can debug when it keeps something it should not, and the reason is the line of policy to change.

fnmatch.fnmatch matches a name against a shell-style pattern, which is the same syntax everyone already reads in a .gitignore or a Makefile. Chapter 19 would do this with a regular expression; a regular expression is more powerful and, for a list of file extensions a team maintains, Chapter 19.12's advice applies — the simpler tool that everyone can edit is the better one.

4. Three Questions, Not One

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
POLICY_QUESTIONS = [
    ("what", "which files are worth keeping at all", "Chapter 23.7 §6"),
    ("when", "does the answer depend on how the run ended", "Chapter 23.7 §6"),
    ("how long", "for how many days do the survivors survive", "this chapter"),
]

for name, question, where in POLICY_QUESTIONS:
    print(f"{name:<9} {question:<48} {where}")

print()
print("a policy that answers only the first two:")
print("   keeps every passing run's log forever")
print("   keeps every failing run's waveform forever")
print("   fills the volume at a slower rate, and then fills it")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
what      which files are worth keeping at all             Chapter 23.7 §6
when      does the answer depend on how the run ended      Chapter 23.7 §6
how long  for how many days do the survivors survive       this chapter

a policy that answers only the first two:
   keeps every passing run's log forever
   keeps every failing run's waveform forever
   fills the volume at a slower rate, and then fills it

Chapter 23.7 Section 6 answered what and when, and that is a genuine improvement over deleting by habit. It is still unbounded. A nightly producing four hundred runs keeps four hundred logs a day whether or not anybody reads them, and the difference between that and no policy is a multiplier, not a limit.

The third question is what turns it into a policy:

Keep this, in this circumstance, for this long.

And the horizon differs per artefact, which is the point of asking separately:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RETENTION_DAYS = {
    "*.log": 90,          # small; the evidence trail, worth months
    "cmd.sh": 90,         # tiny; how to reproduce anything above
    "record.json": 365,   # tiny; the data a trend report reads
    "*.fsdb": 7,          # large; debugged within a week or not at all
    "*.vcd": 7,
    "env.json": 365,      # tiny; which tools ran
}

print(f"{'pattern':<14} {'days':>5}   rationale")
for pattern, days in sorted(RETENTION_DAYS.items(), key=lambda kv: kv[1]):
    size = "large" if days <= 7 else "small"
    print(f"{pattern:<14} {days:>5}   {size}, kept {'briefly' if days <= 7 else 'long'}")

print()
print("longest horizon :", max(RETENTION_DAYS.values()), "days")
print("shortest horizon:", min(RETENTION_DAYS.values()), "days")
print("the short ones are the large ones:",
      all(d <= 7 for p, d in RETENTION_DAYS.items() if p.endswith(("fsdb", "vcd"))))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pattern         days   rationale
*.fsdb             7   large, kept briefly
*.vcd              7   large, kept briefly
*.log             90   small, kept long
cmd.sh            90   small, kept long
record.json      365   small, kept long
env.json         365   small, kept long

longest horizon : 365 days
shortest horizon: 7 days
the short ones are the large ones: True

The horizon is inversely related to the size, which is what makes the policy affordable. A year of record.json files is a few megabytes and is what every trend report in Chapter 29 will read. A week of waveforms is most of the disk.

5. Age, Measured

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import os
import time
from pathlib import Path

DAY = 86400
now = time.time()

base = Path("runs/retention")
base.mkdir(parents=True, exist_ok=True)

for name, age_days in (("sim.log", 1), ("waves_axi_smoke_1839274.fsdb", 3),
                       ("waves_axi_reset_771002.fsdb", 30), ("old_sim.log", 120)):
    path = base / name
    path.write_text("x" * 10, encoding="utf-8")
    stamp = now - age_days * DAY
    os.utime(path, (stamp, stamp))

RETENTION_DAYS = {"*.log": 90, "*.fsdb": 7}

import fnmatch


def age_days(path, reference):
    return (reference - path.stat().st_mtime) / DAY


def horizon_for(name):
    for pattern, days in RETENTION_DAYS.items():
        if fnmatch.fnmatch(name, pattern):
            return days
    return 0


print(f"{'file':<32} {'age':>6} {'horizon':>8}  expired")
for path in sorted(base.iterdir()):
    age = age_days(path, now)
    horizon = horizon_for(path.name)
    print(f"{path.name:<32} {age:>5.0f}d {horizon:>7}d  {age > horizon}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file                                age  horizon  expired
old_sim.log                        120d      90d  True
sim.log                              1d      90d  False
waves_axi_reset_771002.fsdb         30d       7d  True
waves_axi_smoke_1839274.fsdb         3d       7d  False

Two expired, two not, and the two that expired are a four-month-old log and a one-month-old waveform.

The ages here are constructed with os.utime rather than waited for, which is the only way to demonstrate a retention policy in a reproducible example. The same technique is how you test one: a sweeper whose behaviour you can only observe by waiting ninety days is a sweeper nobody tests.

Every number printed is a difference from the script's own clock reading. No timestamp appears, which keeps the output identical on every machine and every day — Chapter 19.16's rule about publishing figures, applied to time.

6. Report Before You Remove

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import fnmatch
import os
import time
from pathlib import Path

DAY = 86400
now = time.time()
RETENTION_DAYS = {"*.log": 90, "cmd.sh": 90, "record.json": 365, "*.fsdb": 7}

base = Path("runs/sweep")
base.mkdir(parents=True, exist_ok=True)
for name, age, size in (("sim.log", 1, 2_000), ("old.log", 120, 3_000),
                        ("cmd.sh", 120, 100), ("record.json", 120, 500),
                        ("waves_a.fsdb", 30, 1_048_576), ("simv", 1, 40_000)):
    path = base / name
    path.write_bytes(b"x" * size)
    os.utime(path, (now - age * DAY, now - age * DAY))


def horizon_for(name):
    for pattern, days in RETENTION_DAYS.items():
        if fnmatch.fnmatch(name, pattern):
            return days
    return 0


def plan_sweep(root, reference):
    """Decide what would go. Deletes nothing."""
    doomed, total = [], 0
    for path in sorted(Path(root).rglob("*")):
        if not path.is_file():
            continue
        age = (reference - path.stat().st_mtime) / DAY
        horizon = horizon_for(path.name)
        if age > horizon:
            size = path.stat().st_size
            doomed.append((path.name, round(age), horizon, size))
            total += size
    return doomed, total


doomed, total = plan_sweep(base, now)

print(f"{'file':<16} {'age':>5} {'keep':>5} {'bytes':>9}")
for name, age, horizon, size in doomed:
    print(f"{name:<16} {age:>4}d {horizon:>4}d {size:>9,}")
print()
print(f"would remove {len(doomed)} files, {total:,} bytes ({total / 1048576:.2f} MiB)")
print("files still on disk:", len([p for p in base.rglob('*') if p.is_file()]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file               age  keep     bytes
cmd.sh            120d   90d       100
old.log           120d   90d     3,000
simv                1d    0d    40,000
waves_a.fsdb       30d    7d 1,048,576

would remove 4 files, 1,091,676 bytes (1.04 MiB)
files still on disk: 6

Nothing was deleted. The function returns a plan, and the last line proves all six files are still there.

That separation is not politeness; it is what makes the sweeper deployable. A program that walks a shared results volume deleting things is about as destructive as unattended automation gets, and three things follow from writing it this way.

It can be run in anger before anyone trusts it. Point it at the real volume, read the list, argue about the three surprising entries, change the patterns.

The byte total answers the question people actually ask. "Should we enable this?" is really "how much would it free?", and a plan that reports 1.04 MiB on a test tree reports something far more persuasive on a real one.

simv shows up with a horizon of 0d — it matches no pattern, so nothing keeps it and it goes on the first sweep regardless of age. That is the default this policy wants, and seeing it in the plan is how you confirm the default is doing what you think.

7. Keep the Newest, Not Just the Newest-Enough

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import os
import time
from pathlib import Path

DAY = 86400
now = time.time()

base = Path("runs/newest")
base.mkdir(parents=True, exist_ok=True)
for i, age in enumerate((0, 1, 2, 3, 8, 20)):
    path = base / f"waves_run{i}.fsdb"
    path.write_bytes(b"x" * 1000)
    os.utime(path, (now - age * DAY, now - age * DAY))

waves = sorted(base.glob("*.fsdb"), key=lambda p: p.stat().st_mtime, reverse=True)

KEEP_NEWEST = 3
print("newest first:", [p.name for p in waves])
print("keep        :", [p.name for p in waves[:KEEP_NEWEST]])
print("delete      :", [p.name for p in waves[KEEP_NEWEST:]])
print()
print("under a 7-day horizon alone, this many survive:",
      sum(1 for p in waves if (now - p.stat().st_mtime) / DAY <= 7))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
newest first: ['waves_run0.fsdb', 'waves_run1.fsdb', 'waves_run2.fsdb', 'waves_run3.fsdb', 'waves_run4.fsdb', 'waves_run5.fsdb']
keep        : ['waves_run0.fsdb', 'waves_run1.fsdb', 'waves_run2.fsdb']
delete      : ['waves_run3.fsdb', 'waves_run4.fsdb', 'waves_run5.fsdb']

under a 7-day horizon alone, this many survive: 4

Age and count answer different questions, and a good policy uses both.

An age horizon alone has no upper bound on volume. A bad week producing two hundred failing runs puts two hundred waveforms inside the seven-day window, all legitimately young, all retained.

A count alone has no lower bound on usefulness. Keep-the-newest-three on a quiet month discards a waveform from eight days ago that is still the only example of a rare failure.

The combination — keep the newest N, and nothing older than D regardless — bounds both the disk and the surprise. The sort is by st_mtime descending, and reverse=True on the sort rather than reversing afterwards is the readable form.

8. Never Delete What You Cannot Regenerate

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ARTEFACTS = [
    ("sim.log", False, "the only record of what happened"),
    ("record.json", False, "derived from the log, but the log may be gone"),
    ("cmd.sh", False, "the instructions for regenerating everything else"),
    ("waves_*.fsdb", True, "re-run cmd.sh with waves enabled"),
    ("simv", True, "re-run the compile step in cmd.sh"),
    ("csrc/", True, "produced by the compile step"),
    ("obj_dir/", True, "produced by the compile step"),
]

print(f"{'artefact':<16} {'regenerable':<12} how")
for name, regenerable, how in ARTEFACTS:
    print(f"{name:<16} {str(regenerable):<12} {how}")

print()
keepers = [n for n, r, _ in ARTEFACTS if not r]
print("never-delete set:", keepers)
print("and every 'how' above depends on:", "cmd.sh" in keepers)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
artefact         regenerable  how
sim.log          False        the only record of what happened
record.json      False        derived from the log, but the log may be gone
cmd.sh           False        the instructions for regenerating everything else
waves_*.fsdb     True         re-run cmd.sh with waves enabled
simv             True         re-run the compile step in cmd.sh
csrc/            True         produced by the compile step
obj_dir/         True         produced by the compile step

never-delete set: ['sim.log', 'record.json', 'cmd.sh']
and every 'how' above depends on: True

That is the rule that makes aggressive deletion defensible:

Delete freely what you can regenerate. Keep what you cannot, and keep the thing that regenerates the rest.

Every entry in the regenerable column says re-run cmd.sh, which means Chapter 23.10's one-line artefact is carrying the entire policy. Deleting a four-gigabyte build tree is a safe decision because a hundred-byte file can reproduce it; delete that hundred-byte file and the four gigabytes become irreplaceable retrospectively.

Chapter 24.6 closes the remaining gap, and it is worth naming here: re-run cmd.sh assumes the same tools are still installed. The command reproduces the run only to the extent the environment has not moved, which is why the environment goes in the record too.

9. The Sweeper

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import fnmatch
import os
import shutil
import time
from pathlib import Path

DAY = 86400
RETENTION_DAYS = {"*.log": 90, "cmd.sh": 90, "record.json": 365, "*.fsdb": 7}
KEEP_NEWEST = {"*.fsdb": 2}


def horizon_for(name):
    for pattern, days in RETENTION_DAYS.items():
        if fnmatch.fnmatch(name, pattern):
            return days
    return 0


def sweep(root, reference, dry_run=True):
    """Return (plan, bytes). Deletes only when dry_run is False."""
    root = Path(root).resolve()
    protected = set()
    for pattern, count in KEEP_NEWEST.items():
        matches = sorted((p for p in root.rglob("*")
                          if p.is_file() and fnmatch.fnmatch(p.name, pattern)),
                         key=lambda p: p.stat().st_mtime, reverse=True)
        protected.update(matches[:count])

    plan, freed = [], 0
    for path in sorted(root.rglob("*")):
        if not path.is_file() or path in protected:
            continue
        age = (reference - path.stat().st_mtime) / DAY
        horizon = horizon_for(path.name)
        if age <= horizon:
            continue
        size = path.stat().st_size
        plan.append((str(path.relative_to(root)), round(age), horizon, size))
        freed += size
        if not dry_run:
            path.unlink()
    return plan, freed


now = time.time()
base = Path("runs/final")
base.mkdir(parents=True, exist_ok=True)
for name, age, size in (("sim.log", 1, 100), ("old.log", 200, 100),
                        ("cmd.sh", 200, 50), ("waves_a.fsdb", 2, 900),
                        ("waves_b.fsdb", 9, 900), ("waves_c.fsdb", 40, 900),
                        ("simv", 1, 5000)):
    p = base / name
    p.write_bytes(b"x" * size)
    os.utime(p, (now - age * DAY, now - age * DAY))

plan, freed = sweep(base, now, dry_run=True)
print("DRY RUN")
for name, age, horizon, size in plan:
    print(f"   {name:<16} {age:>4}d (keep {horizon}d) {size:>6,} bytes")
print(f"   would free {freed:,} bytes from {len(plan)} files")
print("   still on disk:", len([p for p in base.rglob('*') if p.is_file()]))

plan, freed = sweep(base, now, dry_run=False)
print()
print("FOR REAL")
print("   removed:", len(plan), "files,", f"{freed:,}", "bytes")
print("   left   :", sorted(p.name for p in base.rglob('*') if p.is_file()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
DRY RUN
   cmd.sh            200d (keep 90d)     50 bytes
   old.log           200d (keep 90d)    100 bytes
   simv                1d (keep 0d)  5,000 bytes
   waves_c.fsdb       40d (keep 7d)    900 bytes
   would free 6,050 bytes from 4 files
   still on disk: 7

FOR REAL
   removed: 4 files, 6,050 bytes
   left   : ['sim.log', 'waves_a.fsdb', 'waves_b.fsdb']

Read the two halves together: the dry run named exactly four files and 6,050 bytes, and the live run removed exactly those four and exactly that many bytes. The plan is the contract, and a sweeper whose dry run and live run can disagree is one nobody should enable.

Four details in sweep are worth copying.

cmd.sh is removed, and that is correct. It is 200 days old against a 90-day horizon, so the policy says go — and it is worth noticing because Section 8 called cmd.sh the thing that regenerates everything else. Both are true: the never-delete rule protects it from being deleted as disposable, and the retention horizon eventually expires it as old. If a 200-day-old cmd.sh should outlive a 200-day-old log, say so by giving it a longer horizon — the policy is one dict and the fix is one number.

KEEP_NEWEST protects before the age test runs. waves_a.fsdb and waves_b.fsdb are the two most recent dumps and survive; waves_c.fsdb at 40 days does not. Note that waves_b.fsdb is 9 days old against a 7-day horizon — the count protected a file the age rule would have deleted, which is Section 7's lower bound on usefulness doing its job.

simv goes on the first sweep at one day old, because it matches no pattern and gets the zero horizon. Seeing it in the dry run is how you confirm the default is what you intended.

protected is computed once, over the whole tree, before the deletion loop. Computing it inside the loop would be both slower and wrong — the set of "newest two" must be decided against the full listing, not against whatever is left at the moment each file is considered.

10. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import fnmatch
import os
import time
from pathlib import Path

DAY = 86400
now = time.time()
d = Path("runs/pro")
d.mkdir(parents=True, exist_ok=True)
for name, age in (("a.log", 5), ("b.fsdb", 5), ("c.fsdb", 50)):
    p = d / name
    p.write_bytes(b"x" * 10)
    os.utime(p, (now - age * DAY, now - age * DAY))

# A
print("A:", "waves_axi_1.fsdb" in {"waves.fsdb"})

# B
print("B:", fnmatch.fnmatch("waves_axi_1.fsdb", "*.fsdb"))

# C
print("C:", fnmatch.fnmatch("cmd.sh", "*.log"))

# D
R = {"*.log": 90, "*.fsdb": 7}
print("D:", next((v for k, v in R.items() if fnmatch.fnmatch("x.fsdb", k)), 0))

# E
print("E:", next((v for k, v in R.items() if fnmatch.fnmatch("simv", k)), 0))

# F
print("F:", round((now - (d / "c.fsdb").stat().st_mtime) / DAY))

# G
ordered = sorted(d.glob("*.fsdb"), key=lambda p: p.stat().st_mtime, reverse=True)
print("G:", [p.name for p in ordered])

# H
print("H:", sum(p.stat().st_size for p in d.iterdir() if p.is_file()))

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: False
B: True
C: False
D: 7
E: 0
F: 50
G: ['b.fsdb', 'c.fsdb']
H: 30

A — an exact-name keep-list misses a real wave filename. Section 2.

B — a pattern catches it.

C — and does not over-catch: cmd.sh is not a log.

D — the first matching pattern supplies the horizon.

E — an unmatched file gets 0, which means delete on the first sweep. That default is deliberate.

F — age is a difference in days, so the number is stable on any machine.

G — newest first, by st_mtime descending.

H — sizes are exact when the content is.

11. Exercises

Exercise 1 — list a real one. Take an actual run directory and list every entry. Count them.

Exercise 2 — test the keep-list. Run an exact-name keep-list over that listing. Report what it would delete that it should not.

Exercise 3 — the wave file. Say why a real dump's name cannot be a constant, citing Chapter 24.3.

Exercise 4 — patterns. Convert your keep-list to patterns and re-run it over the same listing.

Exercise 5 — report the reason. Make your matcher return which pattern matched, and say what that buys when a sweep surprises you.

Exercise 6 — too broad. Construct a filename your patterns keep but should not. Say how you would have found it.

Exercise 7 — three questions. For your flow, answer what, when and how long — separately, in writing.

Exercise 8 — the multiplier. Explain why answering only the first two is not a policy.

Exercise 9 — inverse relation. Show that your long horizons are on your small artefacts. If not, justify it.

Exercise 10 — construct an age. Use os.utime to build a directory with four known ages, and say why waiting is not a test strategy.

Exercise 11 — relative only. Explain why the example prints differences in days rather than timestamps.

Exercise 12 — dry run. Write plan_sweep and prove with a file count that it deleted nothing.

Exercise 13 — the persuasive number. Run your plan against a real volume and report the bytes it would free.

Exercise 14 — the zero horizon. Say what a file matching no pattern should do, and why seeing it in the plan is useful.

Exercise 15 — age and count. Give a scenario where an age horizon alone fails, and one where a count alone fails.

Exercise 16 — both. Implement keep-newest-N combined with a hard age limit.

Exercise 17 — regenerable. Classify your artefacts, and confirm every "how" depends on something in your never-delete set.

Exercise 18 — the dependency. Say what deleting cmd.sh does to the regenerability of everything else.

Exercise 19 — the assumption. State what re-run cmd.sh assumes, and which chapter addresses it.

Exercise 20 — possible is not free. Give an artefact that is regenerable and still worth keeping, and price the trade.

Exercise 21 — report the reason. Add the matched pattern to sweep's plan output. Re-run Section 9's scenario and say which line the extra column explains fastest.

12. Summary

  • A keep-list of exact names is a claim about filenames, and it is wrong about real ones: waves.fsdb does not match waves_axi_smoke_1839274.fsdb.
  • The artefact the policy deletes is the waveform — the one thing a failing run exists to produce — and record.json with it.
  • Test the keep-list against a real directory listing. Nothing in a code review surfaces this.
  • Use fnmatch patterns, which read like a .gitignore and which a team can edit.
  • Return which pattern matched, not a boolean, so a surprising sweep can be debugged.
  • A pattern that is too broad fails the other way, and the remedy is the same: run it over real names.
  • A policy answers three questions — what, when, and how long. Chapter 23.7 answered the first two.
  • Without a horizon, a policy is a slower rate of filling, not a limit.
  • Long horizons belong on small artefacts — a year of records is megabytes, a week of waveforms is most of the disk.
  • Construct ages with os.utime rather than waiting; a sweeper you can only observe over ninety days is one nobody tests.
  • Print differences, never timestamps, so the output is the same on any machine on any day.
  • A sweeper must report before it removes, and prove it removed nothing by counting the files afterwards.
  • The byte total is what persuades anyone to enable it.
  • A file matching no pattern gets a horizon of zero and goes on the first sweep — a deliberate default, visible in the plan.
  • Combine a count with an age: an age alone has no bound on volume, a count alone no bound on usefulness.
  • Delete freely what you can regenerate, keep what you cannot, and keep the thing that regenerates the rest — which is Chapter 23.10's cmd.sh.
  • "Regenerable" means the instructions exist, not that re-running is free, and the horizon is where that trade gets priced.
  • The dry run and the live run must agree exactly — same files, same byte total — and a sweeper should print the pattern that decided each file, not just the verdict.
  • KEEP_NEWEST is applied over the whole listing before any deletion, and it legitimately rescues a file the age rule would have taken.

The next chapter stops the failures this module has been describing from reaching a run at all, by checking the environment before anything launches.

Where this fits

Part of the Python curriculum.