From b855adb82211ef203d49aa23a659fb65827a467c Mon Sep 17 00:00:00 2001 From: Worph Date: Wed, 2 Sep 2026 00:05:19 +0200 Subject: [PATCH] rework domain field --- .../src/lib/config/DomainMapping.ts | 85 +++++++++++++++---- 1 file changed, 68 insertions(+), 17 deletions(-) diff --git a/packages/shared/filename-tool/src/lib/config/DomainMapping.ts b/packages/shared/filename-tool/src/lib/config/DomainMapping.ts index 0baff06..3a6ae8c 100644 --- a/packages/shared/filename-tool/src/lib/config/DomainMapping.ts +++ b/packages/shared/filename-tool/src/lib/config/DomainMapping.ts @@ -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 - * key every client filters its catalogue on. It is **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. + * key every client filters its catalogue on — **one value per app**, no + * alternation. `workForm` answers the other question the old `film`/`tv` split + * was secretly encoding: is this work complete in itself, or one instalment of + * 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 * 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 * 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. + * 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 = - 'film' - | 'tv' + 'screen' | 'music' - | 'spoken' | 'literature' | '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 record is **not independently routable** — correct for sidecars @@ -28,15 +39,19 @@ export type Domain = * else. * * ⚠ `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 - * writer stamps `pack`'s domain from its own context — see + * same structural thing, so the kind alone cannot tell `screen` from `music`. + * The writer stamps `pack`'s domain from its own context — see * {@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 } = { - // film / tv - movie: 'film', - series: 'tv', - episode: 'tv', + // screen — films and serials alike; the split lives in workFormByContentKind + movie: 'screen', + series: 'screen', + episode: 'screen', // music track: 'music', album: 'music', @@ -44,9 +59,6 @@ export const domainByContentKind: { [contentKind: string]: Domain } = { musicVideo: 'music', djMix: 'music', liveSet: 'music', - // spoken - podcast: 'spoken', - podcastEpisode: 'spoken', // literature book: 'literature', // An audiobook's identity is the book — same author, same ISBN family, @@ -60,6 +72,35 @@ export const domainByContentKind: { [contentKind: string]: Domain } = { 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. * @@ -72,3 +113,13 @@ export function domainForContentKind(contentKind: string | undefined | null): Do if (!contentKind) return undefined; 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()]; +}