Featured image of post You can't reshoot a wedding

You can't reshoot a wedding

A wedding happens once. The first kiss, the confetti, the father of the bride going to pieces halfway through his speech, all of it unrepeatable. If the photographer loses the RAW files from the day, there is no second take. You can apologise, you can offer the money back, but you cannot give anyone the morning again.

So for a tool that sits between a photographer and four thousand irreplaceable files, “don’t damage the originals” is not a feature on a list. It is the entire job. Everything clever krites does, the culling, the sorting, the edits, is worthless the instant it harms a single frame it was handed.

Most software handles this with care. krites handles it by making harm impossible.

Careful isn’t the same as locked out

There is a real difference between a tool that tries not to touch your originals and one that can’t. The first is an intention: a line in the contributing guide, a “we never modify originals” promise, a reviewer remembering to check. Intentions hold right up until the afternoon someone adds a feature in a hurry and forgets. The second is a property of the architecture, and it holds whether or not anyone is paying attention.

krites is built to be the second kind. The guarantee isn’t that I’m careful. It’s that there is no code path that writes to an original, by construction.

Originals go in, and only ever come out as copies

A shoot in krites is just a folder. The imported frames live in originals/, and they are treated as read-only inputs, never written after import. Every decision krites makes lands in a hidden .krites/ sidecar tree beside them. The only place new pixels are ever produced is a separate export/ directory, written when you explicitly export. (The layout of all this is the shoot workspace, spec 0001 §3.)

The code says the same thing the design does. The package that owns the workspace opens with it:

// Package shoot is the non-destructive shoot workspace: a directory of original
// frames plus a .krites sidecar tree holding the manifest and the culling
// verdicts. Originals are inputs, never written after ingest.

And the only function that opens an original opens it one way:

// OpenFrame opens an original frame read-only by its manifest-relative path.

There is no open-an-original-for-writing function anywhere, because nothing legitimately needs one (shoot.go, OpenFrame).

A verdict is a record, not a scalpel

This works because of one decision that runs through everything: a verdict, a crop, a straighten, a colour grade, is a record, not a change to a pixel. Keep, maybe, reject is a line in verdicts.yaml. An edit is an instruction in edits/. Nothing has been done to the photo; krites has just written down what it would do.

The only command in the whole tool that produces output pixels is export, and it renders those instructions into the export/ directory, never back over an original. Even the function that copies a chosen frame out says so in as many words:

// CopyToExport copies an original frame verbatim into the shoot's export/
// directory. It is the only writer outside .krites and never touches the
// originals.

That’s the load-bearing contract the spec calls out by name (R-ND-1 and R-ND-2): originals are never modified, and the only thing that writes new pixels is export (the non-destructive guarantee, CopyToExport).

Two things fall out of it for free. Because every decision is a record, undo is total: a “reset” returns a frame to exactly its just-imported state, because nothing was ever actually changed (R-ND-3). And because the analysis scores and the browsing previews are only caches, you can delete the whole .krites/analysis/ and .krites/previews/ and rebuild them with no loss of a single decision (R-ND-4). The durable things are the verdicts and the edits; everything else is disposable.

Even “delete” doesn’t delete

The most frightening verb in any library tool is delete. You hover over the button… you think about what’s behind it.

In krites, removing a shoot forgets it. It drops the shoot from the studio’s list of folders it knows about, and it does not touch a single file on disk:

// Remove forgets a shoot from the library; it never touches the shoot's files.

The HTTP endpoint behind the studio’s delete button is held to the same rule: it forgets the shoot, it never deletes files (Remove). The scariest control in the interface has been defanged. The worst it can do is make krites stop paying attention to a folder that is still, untouched, exactly where you left it.

Where the rule actually lives

The studio’s contract states it outright: non-destructive by construction, no endpoint mutates an original. That phrase is the whole idea in three words. The promise isn’t kept by me being disciplined every time I add a feature. It’s kept by how the code is built: originals open read-only, exactly one writer lives outside the sidecar tree and it writes to export/, and there is no other door.

When the cost of a mistake is total and unrecoverable, you don’t build a tool that avoids the mistake. You build one where the mistake can’t be made.

The motto on this blog is to leave the codebase better than you found it. Here the rule is narrower and stricter, and a wedding depends on it: leave the originals exactly as you found them. Every byte, every frame, every time.

The folder it can’t write to

Hailey is never going to read a requirement labelled R-API-2. She doesn’t need to. She needs to know that the tool sorting four thousand photos of someone’s most important day cannot, under any circumstances, be the thing that loses them. So I made sure it can’t. There’s one folder krites is simply never allowed to write to. The rest of the tool I’ll argue about all day; not that one.

Built with Hugo
Theme Stack designed by Jimmy