dcramer / dcramer/peated

Flatten bottlings into concrete Bottles with automatic BottleGroups

Open
#483 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
TypeScript
Stars
103
Forks
18
Avg merge
1h 19m
Merged PRs (30d)
555

Description

Summary

Make every concrete marketed release a Bottle, and group related releases through an automatically managed BottleGroup.

This supersedes #481. That proposal made Bottle+Bottling a first-class paired target, but it preserves the underlying ambiguity: users and developers still have to decide whether a product is a Bottle or a Bottling, and an existing Bottle may need to be split when another release is discovered.

Problem

Today the same physical product can be represented as either:

Bottle containing edition/year/ABV

or

Bottle
└── Bottling containing edition/year/ABV

This creates recurring failure modes:

  • “Oops, I created a Bottle when this should have been a Bottling.”
  • “This Bottle was valid until another release appeared; now it must be split.”
  • Every downstream workflow carries and validates a potentially inconsistent (bottleId, releaseId) pair.
  • Add Bottle and Add Bottling compete as separate creation paths.
  • Search, aliases, prices, reviews, tastings, collections, and flights can lose exact identity.

The model is asking creators to make a catalog-structure decision that often cannot be known until later.

Target model

BottleGroup (same marketed expression)
  ├── Bottle (one concrete batch/year/edition)
  └── Bottle (another concrete batch/year/edition)
Bottle

A Bottle is the only concrete catalog entity. It owns all exact release facts:

  • edition or batch
  • release and vintage year
  • release-specific age
  • ABV
  • single-cask and cask-strength flags
  • cask traits
  • exact images, aliases, prices, reviews, and statistics

There is one Add Bottle workflow. “Add another release” creates another Bottle in the same group; it does not create a different entity type.

BottleGroup

A BottleGroup is automatic grouping infrastructure for releases of the same marketed expression. It owns stable group display identity, aliases, representative Bottle, and aggregate statistics. It never impersonates an exact Bottle or owns exact release fields.

  • Every Bottle belongs to exactly one group.
  • Independent Bottle creation atomically creates a singleton group.
  • “Add another release” reuses the source Bottle's group.
  • Existing parent+Bottling relationships migrate into one group.
  • Independently created groups are not silently fuzzy-merged.
  • Name similarity and shared Series membership may produce suggestions, not automatic semantic merges.

BottleGroup means same expression across releases. BottleSeries remains a broader merchandising relationship across potentially distinct expressions.

Catalog target

Downstream activity should reference one internal targetId, not (bottleId, releaseId):

  • exact release known → exact Bottle target
  • expression known but exact release unknown → BottleGroup target

This preserves casual “I drank this expression but do not know the batch” behavior without creating a fake generic Bottle. Database constraints must enforce one group target per group, one exact target per Bottle, and consistent group membership.

Grouping behavior

Automatic group creation is reliable; automatically inferring that two independent Bottles belong together is not.

Trusted automatic grouping contexts:

  • creating “another release” from an existing Bottle
  • migrating an existing Bottle and its Bottlings
  • an exact curated group/alias relationship
  • a moderator-approved group merge

Group merge and split operations must preserve exact Bottle ids. Generic group activity remains generic unless a moderator explicitly reassigns it.

Migration rules

The migration must be additive, resumable, and audited:

  1. Add BottleGroup, Bottle membership, CatalogTarget, release-promotion mappings, and nullable target references.
  2. Create one group from every legacy parent Bottle.
  3. A parent with no releases remains the concrete Bottle and preserves its id.
  4. Promote every bottle_release into a new Bottle in its parent's group.
  5. Map non-null legacy releaseId references to the promoted Bottle target.
  6. Map parent-only references under a parent with releases to the group target.
  7. Map parent-only references under a parent without releases to the retained Bottle target.
  8. Preserve old nested Bottling URLs and API references through permanent mappings and redirects.
  9. Dual-read and verify parity before removing any legacy columns or routes.
  10. Remove bottle_release only after mappings are complete and legacy write traffic reaches zero.

Ambiguous parent release fields, incompatible ages, name/alias collisions, or invalid references must block destructive cleanup rather than be guessed away.

Implementation phases

  • Inventory every releaseId/bottle_release reader and writer; add a production dry-run audit.
  • Add BottleGroup, CatalogTarget, mappings, Bottle membership, and nullable consumer targets through generated Drizzle migrations.
  • Implement atomic singleton creation, “another release,” target loading, group merge/split, and exact/group aggregation.
  • Cut all new writes—web, API, classifier, proposals, importers, workers, and CLI—over to concrete Bottles.
  • Promote legacy releases and backfill consumers in resumable batches.
  • Dual-read and prove target/aggregate/search parity.
  • Replace Add Bottle/Add Bottling with one Bottle form and add related-release/group UI.
  • Redirect legacy routes, enforce non-null constraints, and disable legacy writes.
  • Remove BottleRelease storage and compatibility code in a separate cleanup release.
  • Rewrite the whisky identity and schema documentation around Bottle, BottleGroup, BottleSeries, CatalogTarget, and Observation.

Acceptance criteria

  • Every concrete catalog product has one bottleId; no new BottleRelease records are created.
  • Add Bottle accepts all exact release fields and never asks Bottle-versus-Bottling.
  • Adding another release creates a Bottle in the existing automatic group.
  • Independent creation safely creates a singleton group without fuzzy merging.
  • Search, prices, reviews, collections, flights, and exact tastings reference the concrete Bottle directly.
  • Unknown-release activity references the BottleGroup without selecting a representative Bottle as its target.
  • Group aggregation includes member Bottle activity plus generic group activity exactly once.
  • Moderator group merge/split preserves exact Bottle ids and has an audit trail.
  • Every legacy release has a promoted-Bottle mapping and old URLs remain reachable.
  • Cleanup cannot run while mappings, parity checks, or legacy traffic gates fail.

Non-goals

  • Treating BottleSeries as the expression group.
  • Automatically grouping from fuzzy names, prefix matching, or Series membership alone.
  • Modeling an individual physical bottle owned by a user.
  • Removing legacy storage in the same deployment that introduces the new model.

Related

  • Supersedes #481
  • #210
  • #246
  • docs/architecture/whisky-identity-model.md
  • docs/architecture/bottle-normalization-contract.md

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with docs/architecture/whisky-identity-model.md and docs/architecture/bottle-normalization-contract.md, then inventory every releaseId and bottle_release reader and writer. Review the generated Drizzle migration approach before changing consumers across web, API, workers, importers, and CLI. Done requires audited resumable migration, parity verification, preserved mappings and routes, and no legacy writes.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design, databases, full-stack
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.