Keep the recipe beside the generated image

Ghostwriter · No. 149

Shipped

I added a Codex-generated card path to Ghostwriter with a seed-image manifest and a prompt receipt saved beside each candidate. The receipt records the reference image, brand source, palette, exact text, prompt, and checks. The release also requires copying selected output into a durable project image path instead of leaving it only in the generation tool’s temporary location.

The transferable part is the receipt. A generated image can look finished while the information needed to revise it still lives only in a conversation.

Record the input before there is an output

A useful receipt answers two different questions: what did you ask for, and which returned file did you select? Keep those separate. A prompt is not proof that the image contains the requested wording, and an image filename is not enough to reconstruct its inputs.

The W3C provenance model distinguishes entities from the activities that produce or use them. You do not need to implement that standard to borrow the distinction. In this example, the reference and candidate are artifacts; the generation request connects them.

Use Python 3.11 or newer in an empty folder. The example is an offline receipt builder, not an image generator. It uses tiny text fixtures so you can verify the file behavior without a paid service or someone else’s image. In your application, pass the real PNG paths to the same functions.

Save this as receipt.py. Python’s hashlib supplies SHA-256; recording the digest identifies the bytes you inspected, even if someone later reuses the filename.

import hashlib
import json
from pathlib import Path

def artifact(path: Path) -> dict:
    data = path.read_bytes()
    return {"path": str(path.resolve()), "sha256": hashlib.sha256(data).hexdigest()}

def write_new(path: Path, value: dict) -> None:
    with path.open("x", encoding="utf-8") as stream:
        json.dump(value, stream, indent=2, ensure_ascii=False)
        stream.write("\n")

def request_receipt(seed: Path, exact_text: list[str], prompt: str) -> dict:
    if not exact_text or any(not text.strip() for text in exact_text):
        raise ValueError("exact_text must contain nonempty strings")
    if not prompt.strip():
        raise ValueError("prompt is required")
    return {
        "schema_version": 1,
        "seed": artifact(seed),
        "seed_role": "style only; do not copy subject or layout",
        "exact_text": exact_text,
        "prompt": prompt,
    }

def candidate_receipt(request: Path, candidate: Path, checks: list[str]) -> dict:
    return {
        "request": artifact(request),
        "candidate": artifact(candidate),
        "checks": checks,
        "approved": False,
    }

This is a teaching extension of the tagged receipt shape, not a claim that the release added hashes. The tag stores paths and request details; this example adds content identity so the reader can detect a changed artifact mechanically.

The exclusive file mode prevents accidentally replacing a receipt with another request under the same name. It does not make the directory tamper-proof. Keep receipts under normal source control or durable artifact storage if you need an audit history across machines.

JSON is convenient because it preserves exact strings in a format other tools can read. The Python JSON documentation describes the encoder and decoder used here. Keep human observations as data rather than hiding them in a prompt paragraph that a future tool must parse.

Exercise a candidate without generating one

Save the next block as check_receipt.py beside the first file. It makes a temporary workspace, writes a request, associates a candidate, and proves that changing the candidate breaks its recorded identity.

import hashlib
import json
import tempfile
from pathlib import Path
from receipt import request_receipt, candidate_receipt, write_new

with tempfile.TemporaryDirectory(prefix="receipt-demo-") as directory:
    root = Path(directory)
    seed = root / "reference.txt"
    seed.write_text("offline reference fixture", encoding="utf-8")
    request = root / "request-v1.json"
    write_new(request, request_receipt(
        seed, ["Shared source", "Two clients"],
        "Use the reference for style only. Draw one source serving two clients.",
    ))
    candidate = root / "candidate-v1.txt"
    candidate.write_text("offline candidate fixture", encoding="utf-8")
    selection = root / "candidate-v1.json"
    write_new(selection, candidate_receipt(request, candidate, ["fixture only"]))
    saved = json.loads(selection.read_text(encoding="utf-8"))
    assert saved["approved"] is False
    original = saved["candidate"]["sha256"]
    assert original == hashlib.sha256(candidate.read_bytes()).hexdigest()
    print("candidate identity: matched")
    candidate.write_text("changed candidate", encoding="utf-8")
    assert original != hashlib.sha256(candidate.read_bytes()).hexdigest()
    print("changed candidate: detected")
    try:
        write_new(request, {})
    except FileExistsError:
        print("receipt overwrite: refused")
    else:
        raise AssertionError("existing receipt was overwritten")

Run:

python3 check_receipt.py

You should see:

candidate identity: matched
changed candidate: detected
receipt overwrite: refused

The test deliberately leaves approval false. Passing a file-identity check is not visual approval. A production review still needs to inspect the actual selected image, read its text, and record the user’s decision against that candidate.

Make an edit a new candidate

For a real generation, save the request receipt first. Pass its seed and exact text to the tool, then copy the returned image into a stable candidate path. Record that path and its digest before asking anyone to approve it.

If the headline needs a correction, create another candidate with its own receipt. Include the previous request identity and the edit instruction in your application’s request record. Do not silently reuse candidate-v1.png for a different image while keeping its old approval.

The release uses a seed manifest to select by information shape. That is a useful distinction: a branching system and a left-to-right flow may share a palette without sharing a layout. The receipt should preserve what the reference was allowed to influence, not merely which file was attached.

A receipt makes the request inspectable and reusable. It does not guarantee that repeating a model call produces identical pixels. Preserve the selected output itself whenever exact reproduction matters, and make model or tool version information explicit when it is available.

Gotchas

A tool-local output path is not durable storage. The tagged instructions explicitly require copying the selected image out of the generation location. Otherwise a later edit or publish step can point at an unavailable file. Save the candidate under the project’s artifact directory and verify it exists before recording selection.

A style reference can become a copied layout. The release limits seeds to brand and craft and requires variation across recent cards. If every result repeats the reference’s composition, the reference has taken over the content. Record its permitted role and inspect the result rather than assuming the prompt restriction held.

Matching bytes do not prove a correct diagram. The later v0.20.1 correction adds source-backed nodes and edges plus reconciliation of the rendered graph. The receipt here detects changed files, not invented relationships. Use a separate semantic check before approving architecture imagery.

A digest beside a mutable file is not an authenticity signature. The offline check detects a mismatch against the saved digest. Someone able to replace both can replace the evidence too. Use access controls and trusted storage for adversarial settings; do not advertise this small receipt as signed provenance.

Sources

Changelog

  • feat(ghostwriter): generate branded post cards (#258) (445f653)