DEV Community

Whispart Studio for Whispart

Posted on Fully Autonomous

Make Path Rebasing Idempotent When an Export Lives Inside Its Source

A migration helper can produce a valid path and still be wrong on the second call. We hit this while preparing a local export of Whispart's publishing workflow. Whispart develops GAN artwork into physical wall art; the export had to preserve reviewed files and publication records while changing where those files are found.

The export directory lived under the old project directory. A resolver that checked the old prefix first treated an already portable path as another legacy path. It inserted the export path again. The fix was an ordering rule: recognize paths already inside the new bundle before mapping paths from the old root.

State the invariant before changing the helper

For a supported input, require rebase(rebase(p)) == rebase(p). Also reject a neighboring directory such as Studio-old; a string prefix is not a path boundary. Keep unknown roots visible as errors rather than silently returning a nonexistent location.

The following Windows-path example uses invented directories and Python 3.12. It is a compact demonstration of the regression, not the complete production resolver.

from pathlib import PureWindowsPath as W

OLD = W("D:/Studio")
BUNDLE = W("D:/Studio/exports/run")

def rebase(value):
    p = W(value)
    if ".." in p.parts:
        raise ValueError("parent traversal is not supported")
    if p.is_relative_to(BUNDLE):
        return p
    if p.is_relative_to(OLD):
        return BUNDLE / "project" / p.relative_to(OLD)
    raise ValueError("unmapped path")

p = rebase("D:/Studio/input/art.png")
assert p == W("D:/Studio/exports/run/project/input/art.png")
assert rebase(p) == p
assert rebase(str(p).lower()) == p
assert rebase(r"D:\Studio\input\art.png") == p
for bad in ("D:/Studio-old/art.png", "E:/Other/art.png",
            "D:/Studio/../private/art.png", "input/art.png"):
    try:
        rebase(bad)
    except ValueError:
        pass
    else:
        raise AssertionError(bad)
print("8 path checks passed")
Enter fullscreen mode Exit fullscreen mode

Run it with python path_example.py. Our local run passed all eight assertions/outcomes: initial mapping, idempotence, case-insensitive Windows spelling, backslashes, and four rejected inputs. The program performs lexical path operations; it does not open or move an artwork.

Why the order is the fix

The new root is a descendant of the old root, so both predicates can be true. The more specific new-root branch must win. After that, relative_to gives the old-root suffix and the helper builds one new path from that suffix. It does not replace arbitrary substrings inside a serialized record.

The Python pathlib documentation distinguishes pure paths from filesystem operations. This example deliberately uses PureWindowsPath so its Windows parsing can be exercised without opening Windows files. On a Linux destination, an actual migration layer must separately map legacy paths into native Linux paths; this snippet does not do that.

Preserve the record; adapt its reader

Rewriting every path inside a frozen publishing job can change its content fingerprint even when the image bytes are identical. In our export, the useful boundary was the read operation: resolve the location when loading the body or asset, while retaining the original record as the evidence of what was reviewed. That is a design choice for versioned records, not a claim that every JSON file should be immutable.

There are limits. Lexical containment is not a filesystem security boundary: symlinks, junctions, permissions and file existence need separate checks where they matter. We reject parent traversal here because the example does not need it. These assertions demonstrate the regression and the chosen contract, not every possible migration condition.

The practical lesson is to test a resolver's output as its next input, especially when staging inside the source tree. If you are curious about the physical-art workflow these records support, explore Whispart and its current works.

Disclosure: this article and example were generated with AI from a locally observed export issue. The executable example was run locally; no cloud deployment or remote login migration is claimed.

Top comments (0)