/** * The content-feed app's fixture contract — the single home for the catalog and * for the shape of the ground truth the Rust fixture binary emits. * * Everything else imports this: the harness (to seed and to serve * `/catalog.json`), the page (via that endpoint), and the specs (to pick probe * items and to read the oracle). Without one contract, five consumers each * decide independently what an item is and which ids exist. * * Data and types only. No I/O, no HTTP, and — deliberately — no ranking * arithmetic: `CODING_GUIDELINES.md:88` puts scoring in a named ranking profile * inside the database, and this app exists to demonstrate exactly that. */ /** The embedding width the schema declares (`k8s/cluster/schema-configmap.yaml:51`). */ export const EMBEDDING_DIM = 1536; /** * Items per category. 15 keeps a whole category inside one 100-id embedding * cluster; crossing 100 would spill into the next cluster and silently destroy * the ground-truth separation the oracle depends on. */ export const ITEMS_PER_CATEGORY = 15; /** * Each category owns ONE embedding cluster. * * `embedding_for` assigns cluster = id / 100 (`tidal-stress/src/recall.rs:104`), * so a category's ids must share a 100-id band. These bands were probed against * the live corpus and are unoccupied: the nearest existing vector sits at * distance ~2.04 while intra-cluster neighbours sit at ~0.435. That 4.7x * separation is what makes a brute-force top-k over just these 60 items the * *global* top-k — no 6 GB oracle required. * * The `999_000_0xx` band is deliberately avoided: earlier verification probes * already wrote items there, so its cluster is polluted. */ export const CATEGORIES = [ { name: 'Field recordings', idBase: 900_000_000 }, { name: 'Analog synthesis', idBase: 900_000_100 }, { name: 'Choral & sacred', idBase: 900_000_200 }, { name: 'Free jazz', idBase: 900_000_300 }, ] as const; export type CatalogItem = { entityId: number; title: string; category: string; }; /** * Titles per category, in id order. These are the only prose a viewer reads on * screen, so they are real and human rather than `post-1` — a bare id would make * the recording read as a developer artifact rather than a product. */ const TITLES: Record = { 'Field recordings': [ 'Harbour Ice at Thaw', 'Nightjars, Dungeness Shingle', 'Tram Depot, 04:40', 'Rain on a Zinc Roof', 'Cicadas Before the Storm', 'Understory, Monsoon Week', 'Fog Signal, Outer Channel', 'Beehive Interior, Midsummer', 'Grain Elevator, Idling', 'Salt Flats, Wind Only', 'Cathedral Steps at Dusk', 'Snowmelt Under Rock', 'Ferry Wake, North Passage', 'Sparrows in a Bus Shelter', 'Powerlines, Dry Heat', ], 'Analog synthesis': [ 'Ladder Filter Sketch No. 4', 'Two Oscillators, Slight Detune', 'Ring Modulator Study', 'Tape Delay, Self-Oscillating', 'Sample and Hold Lullaby', 'Patchbay at Low Voltage', 'Sawtooth Descending', 'Envelope Follower Duet', 'Noise Source, Filtered Slowly', 'Sequencer Drift', 'Bucket Brigade Chorus', 'Sync Lead, Held Open', 'Resonance at the Edge', 'Pulse Width Breathing', 'Cold Start, Warm Bias', ], 'Choral & sacred': [ 'Vespers for a Small Room', 'Antiphon in Two Voices', 'Kyrie, Winter Setting', 'Plainchant, Reconstructed', 'Nunc Dimittis at Compline', 'Motet for Eight Parts', 'Requiem Fragment, Anonymous', 'Magnificat in the Old Style', 'Litany with Drone', 'Alleluia, Second Mode', 'Lament for Holy Saturday', 'Te Deum, Village Choir', 'Canticle of the Three', 'Hymn at the Lighting of Lamps', 'Psalm 130, Unaccompanied', ], 'Free jazz': [ 'Ashfall Quartet, Take 3', 'Blindfold Duet', 'Circular Breathing Suite', 'Downtown Loft, Second Set', 'Extended Technique No. 9', 'Fractured Standard', 'Glass Reeds', 'Horns Against a Wall', 'Inside the Piano', 'Junk Percussion Trio', 'Kinetic Sculpture Session', 'Long Tones, No Meter', 'Multiphonic Conversation', 'Nine Bells and a Bass', 'Overblown Ballad', ], }; /** * The 60-item catalog: 4 categories x 15 items, ids inside each category's band. * * Insertion order is category-grouped, and the feed's initial order is * id-ascending, so the first page shows one category. That is not a layout bug: * a freshly seeded corpus has no signals, every item ties on score, and * `for_you`'s tie-break is the entity id. Interleaving was tried and reverted — * the `for_you` candidate scan declares `sort_field: "created_at"` * (`tidal/src/ranking/builtins.rs:49`) but ignores a `created_at` metadata value * entirely (measured: an order matching neither id-ascending nor * created_at-descending came back strictly id-ascending), so insertion order and * timestamps have no observable effect. The page stops looking uniform the * instant a signal is written, which is the point. */ export const CATALOG: readonly CatalogItem[] = CATEGORIES.flatMap(({ name, idBase }) => (TITLES[name] ?? []).map((title, offset) => ({ entityId: idBase + offset, title, category: name, })), ); /** Id -> item, for the id→title join the page performs (`/feed` returns no metadata). */ export const CATALOG_BY_ID: Record = Object.fromEntries( CATALOG.map((item) => [String(item.entityId), item]), ); /** * Items the spec probes for the ANN assertion — one per category, so every * cluster is covered rather than just the first. */ export const PROBE_IDS = [900_000_007, 900_000_107, 900_000_207, 900_000_307] as const; /** * What `tidal-stress`'s `feed-fixture` binary writes. A generated artifact, never * committed: a checked-in oracle drifts from its generator the first time either * one changes, and the drift is silent. */ export type FixtureGroundTruth = { dim: number; /** entityId -> its true cosine-ranked neighbour ids, nearest first. */ neighboursByItem: Record; /** * entityId -> its raw 1536-float vector, for the handful of items the spec * probes. The ANN assertion has to POST a query vector, and the vectors * otherwise exist only inside the Rust binary. Emitting a few probes rather * than all 60 keeps this artifact ~100 KB instead of ~1 MB. */ probeVectors: Record; /** * How far an item's own vector may land from itself and still count as "zero". * Non-zero purely because the vector round-trips through f32 JSON and the * engine re-normalises on write. */ selfDistanceTolerance: number; }; /** The three signal names the schema declares. Nothing else exists. */ export const SIGNALS = ['view', 'like', 'skip'] as const; export type SignalName = (typeof SIGNALS)[number];