rework domain field

This commit is contained in:
Worph
2026-09-02 00:05:19 +02:00
parent 9bf2e623f9
commit 91601f963f
@@ -1,26 +1,37 @@
/** /**
* `contentKind` → `domain` — the routing table from METADATA_KEYS.md §1. * `contentKind` → `domain` / `workForm` — the routing tables from
* METADATA_KEYS.md §1.
* *
* `domain` answers "which application is this record destined for?" and is the * `domain` answers "which application is this record destined for?" and is the
* key every client filters its catalogue on. It is **stored, not derived on * key every client filters its catalogue on — **one value per app**, no
* read**: that is what lets the `contentKind` vocabulary stay open, because a * alternation. `workForm` answers the other question the old `film`/`tv` split
* peer that has never heard of a kind still routes the record correctly * was secretly encoding: is this work complete in itself, or one instalment of
* without having to interpret the token. * a run? It is cross-domain, so a comic issue is `serial` in `literature`
* exactly as an episode is `serial` in `screen`.
*
* Both are **stored, not derived on read**: that is what lets the `contentKind`
* vocabulary stay open, because a peer that has never heard of a kind still
* routes the record correctly without having to interpret the token.
* *
* ⚠ This binding is **permanent and one-way**. Add kinds freely; never re-map * ⚠ This binding is **permanent and one-way**. Add kinds freely; never re-map
* an existing kind to a different domain. Because the value is stored, two * an existing kind to a different domain. Because the value is stored, two
* peers on different versions that disagree about where a kind belongs produce * peers on different versions that disagree about where a kind belongs produce
* a split-brain that redeploying cannot heal — the wrong domain is already * a split-brain that redeploying cannot heal — the wrong domain is already
* written into records across the mesh. If a kind has to move, mint a new kind. * written into records across the mesh. If a kind has to move, mint a new kind.
* The `film`+`tv` → `screen` merge (METADATA_KEYS.md §14.17) is the one
* deliberate exception, taken once and paid for with a corpus sweep.
*
* ⚠ Mirror of the Rust table in `meta-feeder-sdk::domain`. Edit both together.
*/ */
export type Domain = export type Domain =
'film' 'screen'
| 'tv'
| 'music' | 'music'
| 'spoken'
| 'literature' | 'literature'
| 'science'; | 'science';
/** Is the work complete in itself, or one instalment of a run? */
export type WorkForm = 'standalone' | 'serial';
/** /**
* The one-way binding. A kind that is absent here has no domain, which means * The one-way binding. A kind that is absent here has no domain, which means
* the record is **not independently routable** — correct for sidecars * the record is **not independently routable** — correct for sidecars
@@ -28,15 +39,19 @@ export type Domain =
* else. * else.
* *
* ⚠ `pack` is deliberately absent: a season pack and an album release are the * ⚠ `pack` is deliberately absent: a season pack and an album release are the
* same structural thing, so the kind alone cannot tell `tv` from `music`. The * same structural thing, so the kind alone cannot tell `screen` from `music`.
* writer stamps `pack`'s domain from its own context — see * The writer stamps `pack`'s domain from its own context — see
* {@link domainForContentKind}'s contract. * {@link domainForContentKind}'s contract.
*
* ⚠ `podcast` / `podcastEpisode` are absent too: the `spoken` domain was
* retired unused, and the change that first *writes* those kinds picks their
* domain.
*/ */
export const domainByContentKind: { [contentKind: string]: Domain } = { export const domainByContentKind: { [contentKind: string]: Domain } = {
// film / tv // screen — films and serials alike; the split lives in workFormByContentKind
movie: 'film', movie: 'screen',
series: 'tv', series: 'screen',
episode: 'tv', episode: 'screen',
// music // music
track: 'music', track: 'music',
album: 'music', album: 'music',
@@ -44,9 +59,6 @@ export const domainByContentKind: { [contentKind: string]: Domain } = {
musicVideo: 'music', musicVideo: 'music',
djMix: 'music', djMix: 'music',
liveSet: 'music', liveSet: 'music',
// spoken
podcast: 'spoken',
podcastEpisode: 'spoken',
// literature // literature
book: 'literature', book: 'literature',
// An audiobook's identity is the book — same author, same ISBN family, // An audiobook's identity is the book — same author, same ISBN family,
@@ -60,6 +72,35 @@ export const domainByContentKind: { [contentKind: string]: Domain } = {
paper: 'science', paper: 'science',
}; };
/**
* The `workForm` twin of {@link domainByContentKind}.
*
* ⚠ `pack` is absent for the same reason: a season pack is `serial`, an album
* release is `standalone`, and only the writer knows which.
*/
export const workFormByContentKind: { [contentKind: string]: WorkForm } = {
// screen
movie: 'standalone',
series: 'serial',
episode: 'serial',
// music — an album is a closed work, not an ongoing run, so every rung of
// the music ladder is standalone.
track: 'standalone',
album: 'standalone',
artist: 'standalone',
musicVideo: 'standalone',
djMix: 'standalone',
liveSet: 'standalone',
// literature — the split that proves workForm is not a screen-only axis.
book: 'standalone',
audiobook: 'standalone',
comic: 'serial',
manga: 'serial',
magazine: 'serial',
// science
paper: 'standalone',
};
/** /**
* Resolve the domain a `contentKind` belongs to. * Resolve the domain a `contentKind` belongs to.
* *
@@ -72,3 +113,13 @@ export function domainForContentKind(contentKind: string | undefined | null): Do
if (!contentKind) return undefined; if (!contentKind) return undefined;
return domainByContentKind[contentKind.trim()]; return domainByContentKind[contentKind.trim()];
} }
/**
* Resolve the work form a `contentKind` sits on. Same contract as
* {@link domainForContentKind}: `undefined` means "not derivable", never a
* guess. Whoever writes `contentKind` writes both of these in the same breath.
*/
export function workFormForContentKind(contentKind: string | undefined | null): WorkForm | undefined {
if (!contentKind) return undefined;
return workFormByContentKind[contentKind.trim()];
}