diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 5aaaac6..84ad490 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -110,9 +110,13 @@ generated, these routes return HTTP 503 with `one-guide-unavailable`. An accepted guide that has no programmes matching a query remains a successful HTTP 200 response with an empty result; unavailable source state is never represented as a valid empty guide. -The Slice 1 built-in category registry contains only Live Now, Starting Soon, -Wrestling, Football, Baseball, Soccer, Movies, and News. It maps only those -recognized XMLTV categories to stable keys; unrecognized categories are omitted. +The built-in category registry is centralized and data-driven. It defines +`live-now`, `starting-soon`, football, baseball, basketball, hockey, soccer, +wrestling, motorsports, boxing, MMA, tennis, golf, rugby, cricket, lacrosse, +`other-sports`, movies, news, kids, entertainment, documentary, and comedy. +Deterministic aliases map recognized XMLTV labels; generic Sports maps to +`other-sports` only when no specific sport category is also present. Unknown +categories remain omitted. Cross-channel grouping requires an explicit `CanonicalEventId` in projection input; the generated item identity also includes the normalized title, subtitle, episode number, and schedule. Without that explicit ID, rows are scoped to a diff --git a/docs/developer/DEVELOPER_GUIDE.md b/docs/developer/DEVELOPER_GUIDE.md index 28b3538..6e0109a 100644 --- a/docs/developer/DEVELOPER_GUIDE.md +++ b/docs/developer/DEVELOPER_GUIDE.md @@ -556,11 +556,16 @@ The read endpoints are: | `GET`, `HEAD` | `/api/one-guide/category/{key}` | Bounded items in a fixed One Guide category | | `GET`, `HEAD` | `/api/one-guide/items/{item-id}` | One deterministic item detail | -Slice 1 built-in category keys: `live-now`, `starting-soon`, `wrestling`, -`football`, `baseball`, `soccer`, `movies`, and `news`. Category aliases map -deterministically; other XMLTV categories are omitted. Responses use UTC timestamps and contain no raw -URLs, credentials, private paths, accepted hashes, or parser diagnostics. The -server remains loopback-only; authentication and LAN access are not implemented. +Built-in category keys: `live-now`, `starting-soon`, `football`, `baseball`, +`basketball`, `hockey`, `soccer`, `wrestling`, `motorsports`, `boxing`, `mma`, +`tennis`, `golf`, `rugby`, `cricket`, `lacrosse`, `other-sports`, `movies`, +`news`, `kids`, `entertainment`, `documentary`, and `comedy`. The `motorsports` +display label is “Auto Racing & Motorsports.” Category aliases map deterministically. +Generic Sports maps to `other-sports` only when no specific recognized sport +category is present; unknown XMLTV categories are omitted. Responses use UTC +timestamps and contain no raw URLs, credentials, private paths, accepted +hashes, or parser diagnostics. The local server remains loopback-only; +authentication and LAN access are not implemented. The browser includes a read-only Live Now page backed only by the versioned One Guide endpoints. It fetches bounded pages until the API's `TotalCount` is diff --git a/docs/user/CURRENT_LIMITATIONS.md b/docs/user/CURRENT_LIMITATIONS.md index 3c5c550..ba0fadd 100644 --- a/docs/user/CURRENT_LIMITATIONS.md +++ b/docs/user/CURRENT_LIMITATIONS.md @@ -50,10 +50,13 @@ request reports HTTP 503 `one-guide-unavailable`. This is different from a valid accepted guide with no programmes matching the requested query, which returns HTTP 200 with an empty result. Starting Soon means a programme starts after now and within two hours; Live Now -includes its start but excludes its stop. The built-in category keys are -`live-now`, `starting-soon`, `wrestling`, `football`, `baseball`, `soccer`, -`movies`, and `news`. Category aliases are deterministic; other XMLTV -categories are omitted. +includes its start but excludes its stop. Built-in category keys are `live-now`, +`starting-soon`, `football`, `baseball`, `basketball`, `hockey`, `soccer`, +`wrestling`, `motorsports`, `boxing`, `mma`, `tennis`, `golf`, `rugby`, +`cricket`, `lacrosse`, `other-sports`, `movies`, `news`, `kids`, +`entertainment`, `documentary`, and `comedy`. Category aliases are +deterministic; Generic Sports maps to `other-sports` only when no specific +sport category is present. Unknown XMLTV categories are omitted. List `GET` and `HEAD` requests support `limit` (1–100) and `offset` (0–2,147,483,647); item Details does not accept an offset. A nonzero offset also requires `at` (the first page's `EvaluationTimeUtc` in Unix milliseconds) diff --git a/gui/src/app/oneGuideProjection.generated.ts b/gui/src/app/oneGuideProjection.generated.ts index 9740668..5886915 100644 --- a/gui/src/app/oneGuideProjection.generated.ts +++ b/gui/src/app/oneGuideProjection.generated.ts @@ -1,7 +1,29 @@ // Generated from schemas/one-guide-projection.schema.json. Do not edit. export type CategoryKey = - 'live-now' | 'starting-soon' | 'wrestling' | 'football' | 'baseball' | 'soccer' | 'movies' | 'news' + | 'live-now' + | 'starting-soon' + | 'football' + | 'baseball' + | 'basketball' + | 'hockey' + | 'soccer' + | 'wrestling' + | 'motorsports' + | 'boxing' + | 'mma' + | 'tennis' + | 'golf' + | 'rugby' + | 'cricket' + | 'lacrosse' + | 'other-sports' + | 'movies' + | 'news' + | 'kids' + | 'entertainment' + | 'documentary' + | 'comedy' /** * Bounded, deterministic, read-only projection of accepted XMLTV. Cross-channel grouping, source provenance, and programme Kind are exposed only when preserved by input evidence. @@ -10,7 +32,30 @@ export interface ChannelForgeOneGuideReadProjection { Version: 'one-guide/v1' EvaluationTimeUtc: string Query: 'LiveNow' | 'StartingSoon' | 'Category' | 'Details' - CategoryKey?: 'live-now' | 'starting-soon' | 'wrestling' | 'football' | 'baseball' | 'soccer' | 'movies' | 'news' + CategoryKey?: + | 'live-now' + | 'starting-soon' + | 'football' + | 'baseball' + | 'basketball' + | 'hockey' + | 'soccer' + | 'wrestling' + | 'motorsports' + | 'boxing' + | 'mma' + | 'tennis' + | 'golf' + | 'rugby' + | 'cricket' + | 'lacrosse' + | 'other-sports' + | 'movies' + | 'news' + | 'kids' + | 'entertainment' + | 'documentary' + | 'comedy' Offset: number MaximumItems: number TotalCount: number diff --git a/gui/src/app/oneGuideProjectionValidator.generated.js b/gui/src/app/oneGuideProjectionValidator.generated.js index f64f40b..a5e24da 100644 --- a/gui/src/app/oneGuideProjectionValidator.generated.js +++ b/gui/src/app/oneGuideProjectionValidator.generated.js @@ -7,11 +7,11 @@ import { default as ajvRuntime1Module } from 'ajv/dist/runtime/equal.js' const ajvRuntime1 = ajvRuntime1Module.default ?? ajvRuntime1Module export const validate = validate20; export default validate20; -const schema31 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://channelforge.invalid/schemas/one-guide-projection.schema.json","title":"ChannelForge One Guide read projection","description":"Bounded, deterministic, read-only projection of accepted XMLTV. Cross-channel grouping, source provenance, and programme Kind are exposed only when preserved by input evidence.","type":"object","additionalProperties":false,"required":["Version","EvaluationTimeUtc","Query","Offset","MaximumItems","TotalCount","ItemsTruncated","Items"],"properties":{"Version":{"const":"one-guide/v1"},"EvaluationTimeUtc":{"type":"string","format":"date-time"},"Query":{"enum":["LiveNow","StartingSoon","Category","Details"]},"CategoryKey":{"enum":["live-now","starting-soon","wrestling","football","baseball","soccer","movies","news"]},"Offset":{"type":"integer","minimum":0},"MaximumItems":{"type":"integer","minimum":1,"maximum":100},"TotalCount":{"type":"integer","minimum":0},"ItemsTruncated":{"type":"boolean"},"Items":{"type":"array","maxItems":100,"items":{"$ref":"#/$defs/item"}}},"$defs":{"categoryKey":{"enum":["live-now","starting-soon","wrestling","football","baseball","soccer","movies","news"]},"item":{"type":"object","additionalProperties":false,"required":["ItemId","Kind","Title","StartUtc","StopUtc","Status","CategoryKeys","Sport","League","HomeParticipant","AwayParticipant","Promotion","Offerings","OfferingCount","OfferingsTruncated","FreshnessState","ConfidenceState","ConfidenceScore"],"properties":{"ItemId":{"type":"string","pattern":"^[a-f0-9]{64}$"},"Kind":{"enum":["Programme","Event","Movie","SeriesEpisode","Other"]},"Title":{"type":"string","minLength":1,"maxLength":256},"Subtitle":{"type":["string","null"],"maxLength":256},"Description":{"type":["string","null"],"maxLength":512},"EpisodeNumber":{"type":["string","null"],"maxLength":128},"StartUtc":{"type":"string","format":"date-time"},"StopUtc":{"type":"string","format":"date-time"},"Status":{"enum":["Live","StartingSoon","Upcoming","Past"]},"CategoryKeys":{"type":"array","uniqueItems":true,"items":{"$ref":"#/$defs/categoryKey"}},"Sport":{"type":["string","null"],"maxLength":128},"League":{"type":["string","null"],"maxLength":128},"HomeParticipant":{"type":["string","null"],"maxLength":128},"AwayParticipant":{"type":["string","null"],"maxLength":128},"Promotion":{"anyOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["Id","Name"],"properties":{"Id":{"type":"string","pattern":"^cf-[a-f0-9]{64}$"},"Name":{"type":"string","minLength":1,"maxLength":128}}}]},"OfferingCount":{"type":"integer","minimum":1},"OfferingsTruncated":{"type":"boolean"},"Offerings":{"type":"array","minItems":1,"maxItems":16,"items":{"type":"object","additionalProperties":false,"required":["OfferingId","SourceId","SourceLabel","Availability","Entitlement","Launch","DvrSupported","TimeshiftSupported"],"properties":{"OfferingId":{"type":"string","pattern":"^[a-f0-9]{64}$"},"SourceId":{"type":"string","pattern":"^cf-[a-f0-9]{64}$"},"SourceLabel":{"type":"string","minLength":1,"maxLength":128},"Availability":{"const":"GuideOnly"},"Entitlement":{"const":"Unknown"},"Launch":{"type":"object","additionalProperties":false,"required":["Kind","ChannelReference"],"properties":{"Kind":{"const":"Channel"},"ChannelReference":{"type":"string","pattern":"^cf-[a-f0-9]{64}$"}}},"DvrSupported":{"type":["boolean","null"]},"TimeshiftSupported":{"type":["boolean","null"]}}}},"FreshnessState":{"enum":["Current","Stale","Unavailable","Unknown"]},"ConfidenceState":{"enum":["Confirmed","NeedsReview","Unknown"]},"ConfidenceScore":{"type":["integer","null"],"minimum":0,"maximum":100}}}}}; +const schema31 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://channelforge.invalid/schemas/one-guide-projection.schema.json","title":"ChannelForge One Guide read projection","description":"Bounded, deterministic, read-only projection of accepted XMLTV. Cross-channel grouping, source provenance, and programme Kind are exposed only when preserved by input evidence.","type":"object","additionalProperties":false,"required":["Version","EvaluationTimeUtc","Query","Offset","MaximumItems","TotalCount","ItemsTruncated","Items"],"properties":{"Version":{"const":"one-guide/v1"},"EvaluationTimeUtc":{"type":"string","format":"date-time"},"Query":{"enum":["LiveNow","StartingSoon","Category","Details"]},"CategoryKey":{"enum":["live-now","starting-soon","football","baseball","basketball","hockey","soccer","wrestling","motorsports","boxing","mma","tennis","golf","rugby","cricket","lacrosse","other-sports","movies","news","kids","entertainment","documentary","comedy"]},"Offset":{"type":"integer","minimum":0},"MaximumItems":{"type":"integer","minimum":1,"maximum":100},"TotalCount":{"type":"integer","minimum":0},"ItemsTruncated":{"type":"boolean"},"Items":{"type":"array","maxItems":100,"items":{"$ref":"#/$defs/item"}}},"$defs":{"categoryKey":{"enum":["live-now","starting-soon","football","baseball","basketball","hockey","soccer","wrestling","motorsports","boxing","mma","tennis","golf","rugby","cricket","lacrosse","other-sports","movies","news","kids","entertainment","documentary","comedy"]},"item":{"type":"object","additionalProperties":false,"required":["ItemId","Kind","Title","StartUtc","StopUtc","Status","CategoryKeys","Sport","League","HomeParticipant","AwayParticipant","Promotion","Offerings","OfferingCount","OfferingsTruncated","FreshnessState","ConfidenceState","ConfidenceScore"],"properties":{"ItemId":{"type":"string","pattern":"^[a-f0-9]{64}$"},"Kind":{"enum":["Programme","Event","Movie","SeriesEpisode","Other"]},"Title":{"type":"string","minLength":1,"maxLength":256},"Subtitle":{"type":["string","null"],"maxLength":256},"Description":{"type":["string","null"],"maxLength":512},"EpisodeNumber":{"type":["string","null"],"maxLength":128},"StartUtc":{"type":"string","format":"date-time"},"StopUtc":{"type":"string","format":"date-time"},"Status":{"enum":["Live","StartingSoon","Upcoming","Past"]},"CategoryKeys":{"type":"array","uniqueItems":true,"items":{"$ref":"#/$defs/categoryKey"}},"Sport":{"type":["string","null"],"maxLength":128},"League":{"type":["string","null"],"maxLength":128},"HomeParticipant":{"type":["string","null"],"maxLength":128},"AwayParticipant":{"type":["string","null"],"maxLength":128},"Promotion":{"anyOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["Id","Name"],"properties":{"Id":{"type":"string","pattern":"^cf-[a-f0-9]{64}$"},"Name":{"type":"string","minLength":1,"maxLength":128}}}]},"OfferingCount":{"type":"integer","minimum":1},"OfferingsTruncated":{"type":"boolean"},"Offerings":{"type":"array","minItems":1,"maxItems":16,"items":{"type":"object","additionalProperties":false,"required":["OfferingId","SourceId","SourceLabel","Availability","Entitlement","Launch","DvrSupported","TimeshiftSupported"],"properties":{"OfferingId":{"type":"string","pattern":"^[a-f0-9]{64}$"},"SourceId":{"type":"string","pattern":"^cf-[a-f0-9]{64}$"},"SourceLabel":{"type":"string","minLength":1,"maxLength":128},"Availability":{"const":"GuideOnly"},"Entitlement":{"const":"Unknown"},"Launch":{"type":"object","additionalProperties":false,"required":["Kind","ChannelReference"],"properties":{"Kind":{"const":"Channel"},"ChannelReference":{"type":"string","pattern":"^cf-[a-f0-9]{64}$"}}},"DvrSupported":{"type":["boolean","null"]},"TimeshiftSupported":{"type":["boolean","null"]}}}},"FreshnessState":{"enum":["Current","Stale","Unavailable","Unknown"]},"ConfidenceState":{"enum":["Confirmed","NeedsReview","Unknown"]},"ConfidenceScore":{"type":["integer","null"],"minimum":0,"maximum":100}}}}}; const func1 = Object.prototype.hasOwnProperty; const formats0 = formats.fullFormats["date-time"]; const schema32 = {"type":"object","additionalProperties":false,"required":["ItemId","Kind","Title","StartUtc","StopUtc","Status","CategoryKeys","Sport","League","HomeParticipant","AwayParticipant","Promotion","Offerings","OfferingCount","OfferingsTruncated","FreshnessState","ConfidenceState","ConfidenceScore"],"properties":{"ItemId":{"type":"string","pattern":"^[a-f0-9]{64}$"},"Kind":{"enum":["Programme","Event","Movie","SeriesEpisode","Other"]},"Title":{"type":"string","minLength":1,"maxLength":256},"Subtitle":{"type":["string","null"],"maxLength":256},"Description":{"type":["string","null"],"maxLength":512},"EpisodeNumber":{"type":["string","null"],"maxLength":128},"StartUtc":{"type":"string","format":"date-time"},"StopUtc":{"type":"string","format":"date-time"},"Status":{"enum":["Live","StartingSoon","Upcoming","Past"]},"CategoryKeys":{"type":"array","uniqueItems":true,"items":{"$ref":"#/$defs/categoryKey"}},"Sport":{"type":["string","null"],"maxLength":128},"League":{"type":["string","null"],"maxLength":128},"HomeParticipant":{"type":["string","null"],"maxLength":128},"AwayParticipant":{"type":["string","null"],"maxLength":128},"Promotion":{"anyOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["Id","Name"],"properties":{"Id":{"type":"string","pattern":"^cf-[a-f0-9]{64}$"},"Name":{"type":"string","minLength":1,"maxLength":128}}}]},"OfferingCount":{"type":"integer","minimum":1},"OfferingsTruncated":{"type":"boolean"},"Offerings":{"type":"array","minItems":1,"maxItems":16,"items":{"type":"object","additionalProperties":false,"required":["OfferingId","SourceId","SourceLabel","Availability","Entitlement","Launch","DvrSupported","TimeshiftSupported"],"properties":{"OfferingId":{"type":"string","pattern":"^[a-f0-9]{64}$"},"SourceId":{"type":"string","pattern":"^cf-[a-f0-9]{64}$"},"SourceLabel":{"type":"string","minLength":1,"maxLength":128},"Availability":{"const":"GuideOnly"},"Entitlement":{"const":"Unknown"},"Launch":{"type":"object","additionalProperties":false,"required":["Kind","ChannelReference"],"properties":{"Kind":{"const":"Channel"},"ChannelReference":{"type":"string","pattern":"^cf-[a-f0-9]{64}$"}}},"DvrSupported":{"type":["boolean","null"]},"TimeshiftSupported":{"type":["boolean","null"]}}}},"FreshnessState":{"enum":["Current","Stale","Unavailable","Unknown"]},"ConfidenceState":{"enum":["Confirmed","NeedsReview","Unknown"]},"ConfidenceScore":{"type":["integer","null"],"minimum":0,"maximum":100}}}; -const schema33 = {"enum":["live-now","starting-soon","wrestling","football","baseball","soccer","movies","news"]}; +const schema33 = {"enum":["live-now","starting-soon","football","baseball","basketball","hockey","soccer","wrestling","motorsports","boxing","mma","tennis","golf","rugby","cricket","lacrosse","other-sports","movies","news","kids","entertainment","documentary","comedy"]}; const func3 = ajvRuntime0; const func0 = ajvRuntime1; const pattern4 = new RegExp("^[a-f0-9]{64}$", "u"); @@ -237,7 +237,7 @@ const len0 = data9.length; for(let i0=0; i0