Skip to content

v3.0.0: provenance you can check instead of trust - #1

Merged
whystrohm merged 6 commits into
mainfrom
feat/v3.0.0-provenance
Jul 30, 2026
Merged

v3.0.0: provenance you can check instead of trust#1
whystrohm merged 6 commits into
mainfrom
feat/v3.0.0-provenance

Conversation

@whystrohm

@whystrohm whystrohm commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Closes the defects in AUDIT-v2.md, an audit of v2.0.0 that found 50 issues: 7 blocking, 28 major, 15 minor.

Most of them shared one root cause. Every artifact in the QA loop had a single fixed path, and every reference between artifacts was a filename. Both hold until someone regenerates a frame, edits a brand-lock mid-project, or runs the loop at the same time as a colleague. Then a filename still resolves while the thing behind it has changed, and nothing notices.

Breaking

The output directory layout moved, and the layout is this project's public interface.

Before Now
output/critique.json output/critiques/round-N/{shot_id}.critique.json
output/prompts/{generator}.txt output/prompts/round-N/{generator}.txt
output/prompts/revised-{generator}.txt output/prompts/round-N/revised-{generator}.txt
output/generated/{shot_id}.png output/frames/round-N/{shot_id}.png

Existing trees still read: the tools fall back to generated/ and to a root critique.json, and critique schema 1.0 still validates with a warning. Nothing auto-migrates. That needs to be in the release note, not only the changelog.

A 12-shot project over 3 rounds used to produce 36 critiques and keep one, and which one depended on review order.

What to review closely

Ranked by how much damage a mistake would do, not by diff size.

  1. tools/_template.py. A hand-rolled template engine, which is a classic place for bugs. Check the escaping and _find_block_end's nesting.
  2. tools/validate_provenance.py. The hash-comparison logic is what the audit trail's credibility rests on. If it can be made to pass on a tampered tree, the rest is decoration.
  3. critique.schema.json at 1.1. Every provenance field is required and nullable, deliberately: null records that an input was genuinely unavailable, a missing key records nothing. Worth arguing about.
  4. tools/validate_prompts.py Rule 3. Exact substring matching on series_lock anchors. Strict by design. If it is too strict in practice, it will be found here first.

Added

  • run.json, written once per run, pinning every input by SHA-256. Schema in skills/storyboard-architect/templates/run.schema.json.
  • tools/validate_provenance.py, recomputes every recorded hash. Catches a frame regenerated after its critique, a brand-lock edited mid-project, a frame never reviewed, two operators colliding on one shot, skipped rounds. --require-accept is the pipeline stop condition as an exit code.
  • tools/validate_shots.py, the first instance validator in the repo. shots.json and text-overlays.json had schemas and no way to check a file against them, so every rule lived in a SKILL.md as a checkbox.
  • tools/validate_prompts.py, enforces the forge's hard rules, including verbatim series_lock anchors.
  • tools/check.sh, one entry point for all 18 checks, called by CI.
  • A --selftest on every validator, each constructing failing fixtures and failing if the check does not catch them.
  • A worked run example, two shots across two rounds with real hashes and a rendered preview.

The finding worth reading

The forge documents four hard rules. Rule 3, series_lock anchors appear verbatim in every prompt, is the rule that makes shots in a series look like the same room. Nothing checked it.

Driving the pipeline through a real seven-shot brief drifted on it in all seven shots, with every other validator green. The worked-run fixture committed earlier in this same branch had drifted on it too.

environment was "minimalist home office, white walls, oak desk, single houseplant" and it got written as "a minimal home office with white walls and an oak desk". That reads better, which is the problem: paraphrasing an anchor is what writing good prose feels like, so it happens every time and nobody notices until six shots show six different rooms.

That is now validate_prompts.py, and it is the reason this branch has three commits after the first one.

Verification

  • 18 checks pass in CI on Python 3.11, having been developed on 3.14
  • the four bundled previews re-render byte-identically on Linux, so the determinism claim survives a change of OS and interpreter
  • full pipeline driven end to end: 7 shots, 2 rounds, all four project validators pass, --require-accept returns 1 then 0, the rendered preview passes 15 deliverable checks
  • the committed tree alone is sufficient, verified by exporting HEAD clean and running the suite there
  • install, uninstall, and a no-dependencies first run all behave
  • all 12 tools compile under Python 3.9 and defer annotations

Not done

Named here rather than left to be discovered:

  • No approval log. run.json records what was built and reviewed. Who approved it, and when, is recorded nowhere.
  • No migration tool for a v2.0.0 output tree.
  • The v2.0.0 tag is still published one commit behind main, and its LICENSE is the short-form notice rather than the full text.
  • The explainer video and demo GIF are v0.1.0 and still show Runway/Sora. remotion/src/ShotkitExplainer.tsx is deliberately left matching the artifact it produced; both are labelled.
  • storyboard.md.tpl uses dotted paths ({{enter.at}}) that _template.py does not support, and a single {{on_screen_text_resolved}} that cannot represent a multi-overlay shot. Nothing is broken today because a model renders it, not the Python. It needs a decision about whether that template should be machine-renderable at all.
  • Nothing enforces any of this at runtime. The validators are CLI tools a human chooses to run. A team can skip the critic and ship, and no mechanism objects. Turning conventions into linters is progress, not a control.

Review note

This branch was written by Claude and its selftests were written by the same author, so they share the same blind spots. The Rule 3 finding is the argument for why a second reader matters: it was invisible to inspection, including its own, and only surfaced by executing the pipeline rather than reading it.

Yuri Strohm and others added 5 commits July 30, 2026 08:06
Addresses the AUDIT-v2 findings. The root cause behind most of them was
one pattern: every loop artifact had a single fixed path, and every
reference between artifacts was a filename. Both held until someone
regenerated a frame, edited a brand-lock mid-project, or ran the loop at
the same time as a colleague.

BREAKING: the output directory layout changed, and the layout is the
public interface.

  output/critique.json              -> output/critiques/round-N/SHOT.critique.json
  output/prompts/GEN.txt            -> output/prompts/round-N/GEN.txt
  output/prompts/revised-GEN.txt    -> output/prompts/round-N/revised-GEN.txt
  output/generated/SHOT.png         -> output/frames/round-N/SHOT.png

Old trees still read. Nothing auto-migrates.

Added:
- run.json, written once per run, hashing every input. Schema at
  skills/storyboard-architect/templates/run.schema.json
- tools/validate_provenance.py, recomputes every recorded hash. Catches a
  frame regenerated after its critique, a brand-lock edited mid-project, a
  frame never reviewed, two operators colliding on one shot, skipped
  rounds. --require-accept is the pipeline stop condition
- tools/validate_shots.py, the first instance validator in the repo.
  shots.json and text-overlays.json had schemas and no way to check a file
  against them, so every rule lived in a SKILL.md as a checkbox
- critique schema 1.1: run_id, round, created_at, image_sha256,
  prompt_ref, prompt_sha256, brand_lock_sha256, generator, model_version,
  seed, meta. Required and nullable, so an absent input is recorded
  rather than omitted
- tools/check.sh, one entry point for all 16 checks, called by CI
- a --selftest on every validator; validate_critique went from 2 cases to 14
- worked-run example: 2 shots, 2 rounds, real hashes, rendered preview

Changed:
- the gate has a threshold instead of discretion: 3+ major forces REJECT
- revision mode stops on REJECT instead of re-emitting a prompt for a
  verdict that means no fix path exists
- post-level-only shots are recorded in run.json, not just mentioned in chat
- shots-to-html.py renders preview.html.tpl, which it previously only
  claimed to share with the skill, and escapes every substitution
- the preview shows a run date and a render date separately; one
  "Generated" date meant re-rendering restamped the run as today
- shots schema 1.2: on_screen_text accepts an array, assets.generated
  carries sha256/round/prompt_ref/critique_ref
- validate_capabilities.py compares adapter prose to the matrix; the
  "JSON wins" rule was stated in all ten adapters and enforced nowhere
- nano-banana aspect_param corrected to aspectRatio, midjourney ceiling
  to 100, gpt-image to 300
- copy-prompt.py reads revision files, which it could not parse at all
- install.sh installs tools/, which the skills cite and never shipped
- CI calls check.sh and re-renders the bundled previews with pinned
  timestamps, failing if a byte moves

Fixed:
- text_07 in the shotkit-explainer example was unreachable from any shot
- an off-palette overlay color in that same example
- a broken table row in critique-rubric.md
- an empty examples/ directory the preview SKILL.md described as populated
- saas-clean.md typography that silently would not parse

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built a project from scratch the way a first-time user would, ran a
12-shot 3-round project, a legacy v2 tree, hostile content, and a
no-dependencies install. Four real defects, all in code added earlier
today and none caught by the fixtures.

- validate_provenance.py ignored a legacy root critique.json while
  validate_critique.py and shots-to-html.py both read it. The preview
  rendered an ACCEPT badge for a shot this tool called unreviewed. All
  three now share critique_paths() as the single definition of where
  critiques live. Selftest added.
- hash mismatch messages truncated to 12 characters, so a one-character
  typo in a hand-authored run.json printed two identical-looking hashes
  and asserted they differed. Both are now printed in full, and the
  message distinguishes a likely typo from a changed file.
- one edited file produced one error per reference to it: editing a
  brand-lock in a 12-shot project reported the same problem 20 times.
  Mismatches are now deduped per (path, recorded, actual) and name the
  places that referenced them.
- check.sh failed all 14 checks with the same missing-package error
  instead of preflighting the two dependencies once.

Also softened nothing: a tree with no run.json is still an error rather
than a warning, so a pipeline gate cannot pass an unauditable tree. The
message now says what to do about it.

Verified end to end: 12 shots across 3 rounds with 19 critiques resolves
every shot to its latest round with nothing lost, --require-accept flips
between 0 and 1 correctly, and a silent re-roll, a mid-project brand-lock
edit, and a shot left at REVISE are each caught.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…un exposed

Drove all five skills through a real 30-second brief end to end: authored the
storyboard from the installed instructions, forged prompts, generated
placeholder frames, critiqued all seven shots, ran a revision round, and
rendered the client-facing preview. Three findings, one of them the most
useful thing this release has surfaced.

The big one. The forge documents four hard rules, and Rule 3 (series_lock
anchors appear verbatim in every prompt) is the rule that produces visual
consistency across a series. Nothing checked it. A careful authoring pass
drifted on it in all seven shots, and the worked-run fixture committed
earlier today had drifted on it too. Every other validator was green.

Paraphrasing an anchor is what writing good prose feels like, which is
exactly why a human or a model will do it every time and never notice.

- tools/validate_prompts.py: header completeness, generator id, aspect
  agreement, the max_prompt_words ceiling, shot coverage, duplicate blocks,
  Rule 1 (no on-screen text copy in a prompt) and Rule 3 (verbatim anchors).
  The character anchor is a warning, since a shot with no person can omit it,
  and the message says so. Ten selftest cases. Wired into check.sh and CI,
  which now run 18 checks.
- worked-run fixture prompts rewritten with verbatim anchors, capitalised at
  sentence starts since the check is case-insensitive, and the run.json and
  three critique hashes re-derived.
- forge SKILL.md Rule 3 now says what verbatim means, shows the exact drift
  that happens, and says capitalising a sentence start is fine. Its quality
  bar defers to the validator instead of listing checkboxes.

install.sh shipped skills and tools but not brand-packs, while
storyboard-architect's documented fallback is to copy brand-packs/_template.md
when no brand-lock is given. That path did not exist after an install. Packs
now install to ~/.claude/shotkit-brand-packs/ and --uninstall removes all
three trees.

Verified end to end after the fixes: 7 shots, 2 rounds, all four project
validators pass, --require-accept flips 1 then 0, and the rendered preview
passes fifteen deliverable checks including picking the round-2 frame for the
revised shot and showing both overlays on the two-overlay shot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A feature-branch push matched neither trigger, so CI produced no signal at all
until a pull request existed. The first run on this branch confirmed it: the
most recent workflow run was still the June push to main.

Adds a concurrency group so a PR branch does not run twice per push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitHub is forcing actions/checkout@v4 and actions/setup-python@v5 onto Node 24
already and warning about it on every run. Bumping to v5 and v6 respectively
so this is a chosen version rather than a fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@whystrohm whystrohm changed the title feat/v3.0.0 provenance v3.0.0: provenance you can check instead of trust Jul 30, 2026
Tagging a heading that still says Unreleased is the same class of drift
this release exists to remove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@whystrohm
whystrohm merged commit 43c1c79 into main Jul 30, 2026
2 checks passed
@whystrohm
whystrohm deleted the feat/v3.0.0-provenance branch July 30, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant