Skip to content

refactor(style): canonical v2 style config with hash-stable projection - #1174

Open
tombeckenham wants to merge 2 commits into
mainfrom
858-canonical-style-config-v2
Open

refactor(style): canonical v2 style config with hash-stable projection#1174
tombeckenham wants to merge 2 commits into
mainfrom
858-canonical-style-config-v2

Conversation

@tombeckenham

Copy link
Copy Markdown
Contributor

Summary

Re-implements the #858 style-config redesign against current main, replacing closed PR #863. Same design goal — one canonical StyleConfig with grouped look/motion signatures — but with the migration strategy inverted: instead of a null-sweep that erases all staleness data, hashing is shape-stable by construction, so the reshape ships with no migration at all.

v2 shape

{ version: 2, summary?, tone?,
  look:   { mood, artStyle, lighting, colorPalette, colorGrading, medium?, texture?, composition? },
  motion: { camera, shots?, pace?, energy? },     // camera <- v1 cameraWork
  references: [] }                                 // <- v1 referenceFilms

The speculative design/casting/sound/source groups from #863 are cut until their consumers (Phase 3/4) exist.

Why no migration

All hashers (visual/motion prompt hashes + computeStyleConfigHash) now share one projection, styleConfigHashBody, which keeps the legacy flat key names and joins the new optional fields only when authored. A v2 config with no authored refinements therefore hashes byte-identically to its v1 row — pinned by a test asserting deep equality with the raw v1 object. PROMPT_INPUT_HASH_VERSION stays 4; no columns are nulled; genuinely-stale artifacts keep their banners. Authoring shots/pace/energy later flips staleness — correctly, because that IS an input change.

What changed

  • src/lib/style/style-config.ts (new): v2 schema with version: 2 tag, validated migrateStyleConfigV1ToV2 (output is schema-parsed — a constraint-violating legacy blob fails loudly instead of flowing into prompts), parseStyleConfig, StyleProjection + toStyleProjection, styleConfigHashBody, and total (never-throwing) UI accessors.
  • Honest column typing: styles.config is StoredStyleConfig (v1 | v2) until backfill — direct .look/.mood access does not compile; every read boundary goes through parseStyleConfig (prompt-context, launchers, sequence-characters/locations, style catalog, enhancer, scripts/analyze-script.ts).
  • Enhancer: EnhanceStyle.config is whole-or-absent parsed v2 (no more Partial), tags always string[]; toEnhanceInputs up-converts at the row boundary so UI and API feed identical shapes; prompt renders the motion refinements when authored.
  • UI: gradient/tiles/detail dialog use total accessors — a malformed row renders empty instead of a ZodError blanking the grid; shot-staleness catches escalate ZodError (data corruption) to logger.error while transient failures stay warns.
  • Optionality: category required on create (via .extend — drizzle-zod keeps nullable columns optional under refinement callbacks), tags/useCases default [], references defaults [], config whole-or-omitted on update. No existing caller sends creates without category (the create-style UI doesn't exist yet).
  • Templates: all 81 stay v1-authored in one file; the seed mapper converts through the shared validated converter; a test loops every template through it.
  • Backfill: scripts/backfill-style-config-v2.ts validates v2-shaped rows instead of skipping them blind (corrupt "v2-shaped" blobs are exactly what it exists to find), converts+validates everything before the first write, skips updatedAt, and is idempotent.
  • e2e fixtures: recorded match.userMessage strings truncated at the <DIRECTOR_STYLE>/style-block boundary so replays survive the injected-JSON reshape (responses/timings untouched).

Verification

  • bun typecheck 0 errors · bun run test 2347 passed · bun lint 0 errors · bun dead-code clean · bun format:check clean
  • bun db:migrate:local && bun db:seed:local — 81 templates re-sync as valid v2
  • Backfill proven on local D1: migrates a real inserted v1 row, re-run reports 0, dry-run writes nothing
  • bun test:e2e sequence-flow — 4 passed (script-enhance → analyze → visual/motion prompt fixtures replay through the truncated matches)

Deploy notes

No DB migration. Deploy order is free: readers tolerate v1+v2 before and after the backfill. Run bun scripts/backfill-style-config-v2.ts --d1 any time after deploy; the up-converting v1 style config at read time warn (PostHog logs, service openstory-prd) going quiet is the signal the v1 branch + StyleConfigV1 can be deleted.

Follow-ups

  • Eval experiment before authoring motion-rich templates: the "richer motion signatures → better output" claim is untested — A/B a handful of templates with authored shots/pace/energy through score-style-previews / eval-style-sample-videos / compare-eval-scores before investing in a new template generation (see Style schema design: aesthetics-blob vs identity-columns split + inconsistent optionality #858 discussion; the preferred vehicle is a new v2-authored template set with the old ones hidden).
  • Drop the v1 branch + retire StoredStyleConfig to plain StyleConfig once prod backfill is confirmed quiet.

Closes #858

🤖 Generated with Claude Code

Redesign the styles config blob around one canonical type (issue #858),
re-implemented against current main after PR #863 was closed as stale.

- New drizzle-free src/lib/style/style-config.ts: grouped v2 StyleConfigSchema
  (look/motion core + references, version tag, optional summary/tone and
  motion refinements shots/pace/energy, look medium/texture/composition),
  validated migrateStyleConfigV1ToV2, parseStyleConfig at every read
  boundary, StyleProjection with a real constructor, and total UI accessors.
- Hash-stable by construction: styleConfigHashBody projects v2 back to the
  legacy flat key names and adds new fields only when authored, and all
  hashers (visual/motion prompt hashes + computeStyleConfigHash) share it.
  A pure reshape flips NO stored hash: no PROMPT_INPUT_HASH_VERSION bump,
  no null-sweep migration, no staleness data destroyed. Pinned by test.
- Column typed StoredStyleConfig (v1 | v2) so skipping parseStyleConfig
  does not compile; corrupt blobs throw loudly, ZodError escalates to
  logger.error in shot staleness, and the v1 read branch logs a warn as an
  observable retirement signal.
- Optionality fixed: category required on create (.extend — drizzle-zod
  keeps nullable columns optional under refinements), tags/useCases default
  [], references default [], config whole-or-omitted on update.
- 81 templates stay v1-authored, converted+validated at the seed mapper;
  scripts/backfill-style-config-v2.ts validates v2-shaped rows instead of
  skipping them blind and never bumps updatedAt.
- e2e fixture matches truncated at the style-block boundary so recorded
  replays survive the reshape.

Closes #858

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

Cloudflare Preview deployed successfully

URL https://pr-1174.openstory.workers.dev
Worker pr-1174
D1 Database openstory-pr-1174
Commit f58cc83

Renders the same generated brief through the real pipeline with a base
template vs its motion-enriched variant, into per-arm sample-video dirs
that eval-style-sample-videos.ts / compare-eval-scores.ts consume.
Resumable via persisted sequence ids. Results on #858.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

Style schema design: aesthetics-blob vs identity-columns split + inconsistent optionality

1 participant