You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
BS#1383 introduced the synonym/relational partition for artist_search_alias.source in jobs/concerts-artist-resolver/query.ts (the resolver's FK-write site). To avoid scope creep, that PR exported SYNONYM_ALIAS_SOURCES + RELATIONAL_ALIAS_SOURCES from the resolver file itself and updated only the immediate consumer (the unit test).
This ticket tracks the broader consolidation surfaced by the BS#1383 round-2 review: the ArtistSearchAliasSource type union is declared (or referenced) in three other places, none of which import from each other or from the resolver's partition constants.
Drift sites
jobs/artist-search-alias-consumer/lml-types.ts:17-21 — closed union of the four sources. The composer/writer side.
apps/backend/services/requestLine/types.ts:17-22 — open union (| (string & {})), used by the catalog-search wire-shape projection. The open-vs-closed call is load-bearing — see "Open-vs-closed wire shape" below.
tests/unit/jobs/artist-search-alias-consumer/orchestrate.test.ts:39 — inline literal union duplicating the same four values.
jobs/concerts-artist-resolver/query.ts:68-71 — the SYNONYM_ALIAS_SOURCES + RELATIONAL_ALIAS_SOURCES const tuples and derived types (added by fix(concerts-artist-resolver): exclude discogs_member from alias arm (BS#1383) #1384 / BS#1383). Resolver-side tests in tests/unit/jobs/concerts-artist-resolver/query.test.ts already import these constants — that file is not a drift site; the 'discogs_alias'-style literals in its mock-row fixtures are intentional data, not type drift.
Failure mode
LML adds a fifth source (e.g., discogs_collaborator, intended as relational-class).
Consumer's ArtistSearchAliasSource union is widened in lml-types.ts. The writer accepts the new source string (already iterated generically; DB column is text with no CHECK).
The resolver's SYNONYM_ALIAS_SOURCES is unchanged — so the new source stays out of the FK-write path (safe-by-default; that's the point of the positive allowlist).
BUT: nothing fails to remind the maintainer that the new source needs a deliberate classification choice. If it should have been synonym-class, it silently drops every concert mention to manual review with no operator-facing signal.
And: nothing checks that lml-types.ts's union ⊇ SYNONYM_ALIAS_SOURCES ∪ RELATIONAL_ALIAS_SOURCES. The two could drift in either direction.
Proposed shape
A single canonical module — shared/database/src/artist-search-alias-sources.ts — keyed on a const tuple of all known sources, with the type derived from it and the partition tuples derived against it:
exportconstSYNONYM_ALIAS_SOURCES=['discogs_name_variation','discogs_alias','wxyc_library_alt']asconst;exportconstRELATIONAL_ALIAS_SOURCES=['discogs_member']asconst;exportconstALL_ALIAS_SOURCES=[...SYNONYM_ALIAS_SOURCES, ...RELATIONAL_ALIAS_SOURCES]asconst;exporttypeArtistSearchAliasSource=(typeofALL_ALIAS_SOURCES)[number];// Type-level exhaustiveness: every value in the union is in exactly one partition.// The brand name embeds the ticket so a future tsc failure grep-points back here.type_AssertAllSourcesClassified_BS1390=Exclude<ArtistSearchAliasSource,(typeofSYNONYM_ALIAS_SOURCES)[number]|(typeofRELATIONAL_ALIAS_SOURCES)[number]>extendsnever
? true
: 'BS#1390: a source in ALL_ALIAS_SOURCES is not classified into SYNONYM_ALIAS_SOURCES or RELATIONAL_ALIAS_SOURCES';const_check: _AssertAllSourcesClassified_BS1390=true;
This is the compile-error-on-omission property the ticket actually wants: adding a 5th value to ALL_ALIAS_SOURCES without also placing it in one partition tuple fails tsc with a message naming this ticket.
shared/database is the right home — semantically these strings are values of the artist_search_alias.source text column, and every consumer (apps/backend, jobs/concerts-artist-resolver, jobs/artist-search-alias-consumer) already depends on @wxyc/database. New file, not schema.ts. The lml-types.ts "decoupled from @wxyc/shared" rationale does not apply because @wxyc/database is a local workspace, not a GitHub Packages registry.
The resolver's SYNONYM_ALIAS_SOURCES + RELATIONAL_ALIAS_SOURCES exports become re-exports from the canonical module so existing callers keep working without churn.
Classifier function — deferred follow-up
A typed classifyAliasSource(s): 'synonym' | 'relational' is the natural next step, but its value is runtime branching (e.g., iOS / dj-site rendering "related artist" vs "alternate name" off the wire). The resolver's compile-time IN-list does not need it — SYNONYM_ALIAS_SOURCES already gives the same compile-time guarantee. Defer until the first runtime consumer lands; track separately.
Open-vs-closed wire shape
requestLine/types.ts currently leaves ArtistSearchAliasSource open with | (string & {}) so the projection cast stays tolerant of unknown DB values. This was load-bearing pre-consolidation; it stops being so once the canonical module exists. Close it. Rationale:
The DB only contains values the writer wrote.
The writer only writes values that came from LML's ArtistSearchAliasVariant.source, typed against the canonical union.
The "wire-format break" risk was hypothetical — closing the union forces iOS / dj-site to update when a 5th source is added, which is the point of the consolidation. Without that, the open union absorbs unknown values into the projection cast and the silent-mislabel hazard from the failure-mode section recurs at the wire boundary.
If a future use case genuinely needs an opaque variant (e.g., a search projection that wants to round-trip an unrecognised DB value through to the client), introduce OpaqueArtistSearchAliasSource = ArtistSearchAliasSource | (string & {}) then. Today none of the consumers want that — the projection cast at library.service.ts:111 and library-search.service.ts:319 only consumes the value to pass it back to iOS, which is itself going to be typed against the closed union.
Acceptance
New module shared/database/src/artist-search-alias-sources.ts exports:
ArtistSearchAliasSource (type derived from (typeof ALL_ALIAS_SOURCES)[number])
Type-level exhaustiveness assertion whose identifier embeds the ticket number (e.g., _AssertAllSourcesClassified_BS1390) so a missing classification fails tsc with a grep-able pointer to this ticket.
jobs/artist-search-alias-consumer/lml-types.ts imports ArtistSearchAliasSource from the canonical module (no inline union).
apps/backend/services/requestLine/types.ts imports ArtistSearchAliasSource from the canonical module and is closed (drop | (string & {})). Update the as ArtistSearchAliasSource casts in library.service.ts:111 and library-search.service.ts:319 to stay typecheck-clean — they should remain casts (the values come from raw SQL), but against the closed union.
tests/unit/jobs/artist-search-alias-consumer/orchestrate.test.ts:39 imports ArtistSearchAliasSource from the canonical module (drop the inline literal union in makeLmlResult).
jobs/concerts-artist-resolver/query.ts:68-71 re-exports SYNONYM_ALIAS_SOURCES and RELATIONAL_ALIAS_SOURCES from the canonical module. Resolver SQL IN-list still builds from SYNONYM_ALIAS_SOURCES, unchanged. The drift-hazard JSDoc paragraph (currently referencing BS#1390) gets replaced by a one-liner pointing at the canonical module.
Adding a 5th value to ALL_ALIAS_SOURCES without classifying it fails tsc — verified by a deliberate local edit during PR review (no need to land a permanent failing test).
classifyAliasSource() function — out of scope; deferred to a follow-up when the first runtime consumer of the partition lands.
Background
BS#1383 introduced the synonym/relational partition for
artist_search_alias.sourceinjobs/concerts-artist-resolver/query.ts(the resolver's FK-write site). To avoid scope creep, that PR exportedSYNONYM_ALIAS_SOURCES+RELATIONAL_ALIAS_SOURCESfrom the resolver file itself and updated only the immediate consumer (the unit test).This ticket tracks the broader consolidation surfaced by the BS#1383 round-2 review: the
ArtistSearchAliasSourcetype union is declared (or referenced) in three other places, none of which import from each other or from the resolver's partition constants.Drift sites
jobs/artist-search-alias-consumer/lml-types.ts:17-21— closed union of the four sources. The composer/writer side.apps/backend/services/requestLine/types.ts:17-22— open union (| (string & {})), used by the catalog-search wire-shape projection. The open-vs-closed call is load-bearing — see "Open-vs-closed wire shape" below.tests/unit/jobs/artist-search-alias-consumer/orchestrate.test.ts:39— inline literal union duplicating the same four values.jobs/concerts-artist-resolver/query.ts:68-71— theSYNONYM_ALIAS_SOURCES+RELATIONAL_ALIAS_SOURCESconst tuples and derived types (added by fix(concerts-artist-resolver): exclude discogs_member from alias arm (BS#1383) #1384 / BS#1383). Resolver-side tests intests/unit/jobs/concerts-artist-resolver/query.test.tsalready import these constants — that file is not a drift site; the'discogs_alias'-style literals in its mock-row fixtures are intentional data, not type drift.Failure mode
LML adds a fifth source (e.g.,
discogs_collaborator, intended as relational-class).ArtistSearchAliasSourceunion is widened inlml-types.ts. The writer accepts the new source string (already iterated generically; DB column istextwith no CHECK).SYNONYM_ALIAS_SOURCESis unchanged — so the new source stays out of the FK-write path (safe-by-default; that's the point of the positive allowlist).lml-types.ts's union ⊇SYNONYM_ALIAS_SOURCES ∪ RELATIONAL_ALIAS_SOURCES. The two could drift in either direction.Proposed shape
A single canonical module —
shared/database/src/artist-search-alias-sources.ts— keyed on a const tuple of all known sources, with the type derived from it and the partition tuples derived against it:This is the compile-error-on-omission property the ticket actually wants: adding a 5th value to
ALL_ALIAS_SOURCESwithout also placing it in one partition tuple failstscwith a message naming this ticket.shared/databaseis the right home — semantically these strings are values of theartist_search_alias.sourcetext column, and every consumer (apps/backend,jobs/concerts-artist-resolver,jobs/artist-search-alias-consumer) already depends on@wxyc/database. New file, notschema.ts. Thelml-types.ts"decoupled from@wxyc/shared" rationale does not apply because@wxyc/databaseis a local workspace, not a GitHub Packages registry.The resolver's
SYNONYM_ALIAS_SOURCES+RELATIONAL_ALIAS_SOURCESexports become re-exports from the canonical module so existing callers keep working without churn.Classifier function — deferred follow-up
A typed
classifyAliasSource(s): 'synonym' | 'relational'is the natural next step, but its value is runtime branching (e.g., iOS / dj-site rendering "related artist" vs "alternate name" off the wire). The resolver's compile-time IN-list does not need it —SYNONYM_ALIAS_SOURCESalready gives the same compile-time guarantee. Defer until the first runtime consumer lands; track separately.Open-vs-closed wire shape
requestLine/types.tscurrently leavesArtistSearchAliasSourceopen with| (string & {})so the projection cast stays tolerant of unknown DB values. This was load-bearing pre-consolidation; it stops being so once the canonical module exists. Close it. Rationale:ArtistSearchAliasVariant.source, typed against the canonical union.If a future use case genuinely needs an opaque variant (e.g., a search projection that wants to round-trip an unrecognised DB value through to the client), introduce
OpaqueArtistSearchAliasSource = ArtistSearchAliasSource | (string & {})then. Today none of the consumers want that — the projection cast atlibrary.service.ts:111andlibrary-search.service.ts:319only consumes the value to pass it back to iOS, which is itself going to be typed against the closed union.Acceptance
shared/database/src/artist-search-alias-sources.tsexports:SYNONYM_ALIAS_SOURCES(const tuple)RELATIONAL_ALIAS_SOURCES(const tuple)ALL_ALIAS_SOURCES(const tuple =[...SYNONYM, ...RELATIONAL])ArtistSearchAliasSource(type derived from(typeof ALL_ALIAS_SOURCES)[number])_AssertAllSourcesClassified_BS1390) so a missing classification failstscwith a grep-able pointer to this ticket.jobs/artist-search-alias-consumer/lml-types.tsimportsArtistSearchAliasSourcefrom the canonical module (no inline union).apps/backend/services/requestLine/types.tsimportsArtistSearchAliasSourcefrom the canonical module and is closed (drop| (string & {})). Update theas ArtistSearchAliasSourcecasts inlibrary.service.ts:111andlibrary-search.service.ts:319to stay typecheck-clean — they should remain casts (the values come from raw SQL), but against the closed union.tests/unit/jobs/artist-search-alias-consumer/orchestrate.test.ts:39importsArtistSearchAliasSourcefrom the canonical module (drop the inline literal union inmakeLmlResult).jobs/concerts-artist-resolver/query.ts:68-71re-exportsSYNONYM_ALIAS_SOURCESandRELATIONAL_ALIAS_SOURCESfrom the canonical module. Resolver SQL IN-list still builds fromSYNONYM_ALIAS_SOURCES, unchanged. The drift-hazard JSDoc paragraph (currently referencing BS#1390) gets replaced by a one-liner pointing at the canonical module.ALL_ALIAS_SOURCESwithout classifying it failstsc— verified by a deliberate local edit during PR review (no need to land a permanent failing test).classifyAliasSource()function — out of scope; deferred to a follow-up when the first runtime consumer of the partition lands.Related