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
#179 added the Universe → Series → Source schema and #180 populated it (75 Sources across 26
Series and 5 Universes, from a curated overlay file). Nothing reads it. QuoteResponse has no series or universe field, and no endpoint filters on either — so the data #180 populates currently
has no read path from a quote at all.
This is the capability #169's research was originally motivated by: grouping related Sources so a
consumer can ask for "a random Star Wars quote" or display "from the Middle Earth universe" without
knowing which individual films exist. Today a consumer would have to fetch the quote, look up its
Source (#184), then look up that Source's Series (#187), then that Series' Universe (#188) — three
extra round-trips to answer a question the quote itself should be able to answer, and still no way to filter by either.
The masterdata list endpoints (#184/#187/#188) are a different concern: they expose Series/Universe as
first-class entities to enumerate and (later) manage. This issue is about enriching and filtering the
quote read path, which none of them touch.
Depends on #183, which establishes this project's convention for an entity-scoped filter query
parameter — how it is named, parsed, validated (422 on malformed input rather than letting the
framework's binder throw), and applied to the underlying query. #183 names ?universeId= as its own
worked example, so this issue is a direct consumer of that convention rather than a place to re-decide
it. Sequencing this after #183 is what keeps the quote endpoints' filters and the masterdata
endpoints' filters from drifting into two different shapes.
Found while reviewing #180's T1 (2026-07-16): the developer's first observation on seeing a live quote
response was that it carries neither field.
What needs to be done
Add series and universe to QuoteResponse — populated by joining Sources.SeriesId → Series → Series.UniverseId → Universe. Both nullable: a standalone Source has no Series, and
a standalone Series has no Universe (both are the common case today — only 75 of 479 Sources are
in a Series at all).
Extend Sql.Quotes.SelectBase's projection with the two LEFT JOINs. They must be LEFT — a
Source with no Series must still return its quote, not drop it from every result.
Add Series and Universe filters to GET /api/v1/quotes, GET /api/v1/quotes/random, and GET /api/v1/quotes/search, wired through SqliteQuoteService.BuildFilterWhere alongside the
existing character/author/source filters. Follow List-endpoint shared infrastructure for the masterdata and Conversations list endpoints #183's documented filter-parameter
convention — naming, parsing, validation, and how the filter reaches the query — rather than
re-deciding any of it here. List-endpoint shared infrastructure for the masterdata and Conversations list endpoints #183 settles the open question of whether an entity-scoped filter is
id-valued or name-valued; this issue adopts whatever it lands on, so the quote endpoints' filters
and the masterdata endpoints' filters stay one pattern. Note the existing source/character/author filters on these same endpoints are case-insensitive contains
matches on a name — if List-endpoint shared infrastructure for the masterdata and Conversations list endpoints #183's convention differs, the mismatch on one endpoint is worth a
deliberate note rather than a silent inconsistency.
A Universe filter must match quotes across every Series in that Universe, not only Sources
directly named by it — the join is Quote → Source → Series → Universe, so the filter applies two
levels up. This is the main reason a Universe filter is worth having at all ("a random Middle Earth
quote" spans both the LOTR and Hobbit Series).
Update README.md, addon/DOCS.md, and QuoteEndpoints.cs's [Description] attributes in the
same commit, per this project's "Keeping API documentation in sync" rule.
Background
#179 added the
Universe→Series→Sourceschema and #180 populated it (75 Sources across 26Series and 5 Universes, from a curated overlay file). Nothing reads it.
QuoteResponsehas noseriesoruniversefield, and no endpoint filters on either — so the data #180 populates currentlyhas no read path from a quote at all.
This is the capability #169's research was originally motivated by: grouping related Sources so a
consumer can ask for "a random Star Wars quote" or display "from the Middle Earth universe" without
knowing which individual films exist. Today a consumer would have to fetch the quote, look up its
Source (#184), then look up that Source's Series (#187), then that Series' Universe (#188) — three
extra round-trips to answer a question the quote itself should be able to answer, and still no way to
filter by either.
The masterdata list endpoints (#184/#187/#188) are a different concern: they expose Series/Universe as
first-class entities to enumerate and (later) manage. This issue is about enriching and filtering the
quote read path, which none of them touch.
Depends on #183, which establishes this project's convention for an entity-scoped filter query
parameter — how it is named, parsed, validated (422 on malformed input rather than letting the
framework's binder throw), and applied to the underlying query. #183 names
?universeId=as its ownworked example, so this issue is a direct consumer of that convention rather than a place to re-decide
it. Sequencing this after #183 is what keeps the quote endpoints' filters and the masterdata
endpoints' filters from drifting into two different shapes.
Found while reviewing #180's T1 (2026-07-16): the developer's first observation on seeing a live quote
response was that it carries neither field.
What needs to be done
seriesanduniversetoQuoteResponse— populated by joiningSources.SeriesId→Series→Series.UniverseId→Universe. Both nullable: a standalone Source has no Series, anda standalone Series has no Universe (both are the common case today — only 75 of 479 Sources are
in a Series at all).
Sql.Quotes.SelectBase's projection with the twoLEFT JOINs. They must beLEFT— aSource with no Series must still return its quote, not drop it from every result.
GET /api/v1/quotes,GET /api/v1/quotes/random, andGET /api/v1/quotes/search, wired throughSqliteQuoteService.BuildFilterWherealongside theexisting
character/author/sourcefilters. Follow List-endpoint shared infrastructure for the masterdata and Conversations list endpoints #183's documented filter-parameterconvention — naming, parsing, validation, and how the filter reaches the query — rather than
re-deciding any of it here. List-endpoint shared infrastructure for the masterdata and Conversations list endpoints #183 settles the open question of whether an entity-scoped filter is
id-valued or name-valued; this issue adopts whatever it lands on, so the quote endpoints' filters
and the masterdata endpoints' filters stay one pattern. Note the existing
source/character/authorfilters on these same endpoints are case-insensitive containsmatches on a name — if List-endpoint shared infrastructure for the masterdata and Conversations list endpoints #183's convention differs, the mismatch on one endpoint is worth a
deliberate note rather than a silent inconsistency.
directly named by it — the join is Quote → Source → Series → Universe, so the filter applies two
levels up. This is the main reason a Universe filter is worth having at all ("a random Middle Earth
quote" spans both the LOTR and Hobbit Series).
README.md,addon/DOCS.md, andQuoteEndpoints.cs's[Description]attributes in thesame commit, per this project's "Keeping API documentation in sync" rule.
CLAUDE.md's "GUID/enum/id comparisons are case-insensitive by default" rule — with alowercase/mixed-case regression test in the same commit, not deferred.
Expected tests
Quotinator.Engine.Tests.Services.SqliteQuoteServiceTestsGetById_SourceInSeriesWithUniverse_ResponseCarriesBothQuotinator.Engine.Tests.Services.SqliteQuoteServiceTestsGetById_SourceWithNoSeries_ReturnsQuoteWithNullSeriesAndUniverseQuotinator.Engine.Tests.Services.SqliteQuoteServiceTestsGetById_SeriesWithNoUniverse_ReturnsSeriesWithNullUniverseQuotinator.Api.Tests.Endpoints.QuoteEndpointsTestsGetAll_SeriesFilter_ReturnsOnlyThatSeriesQuotesQuotinator.Api.Tests.Endpoints.QuoteEndpointsTestsGetAll_UniverseFilter_ReturnsQuotesAcrossEverySeriesInThatUniverseQuotinator.Api.Tests.Endpoints.QuoteEndpointsTestsGetRandom_UniverseFilter_ReturnsOnlyThatUniverseQuotesQuotinator.Data.Tests.Security.SqlQueryGuardTestsAssembledQuery_PassesAggregateGuard(existing — new filter cases added toAssembledQueryCases)Definition of done