Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/architecture/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions docs/developer/DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions docs/user/CURRENT_LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
49 changes: 47 additions & 2 deletions gui/src/app/oneGuideProjection.generated.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions gui/src/app/oneGuideProjectionValidator.generated.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -237,7 +237,7 @@ const len0 = data9.length;
for(let i0=0; i0<len0; i0++){
let data10 = data9[i0];
const _errs20 = errors;
if(!((((((((data10 === "live-now") || (data10 === "starting-soon")) || (data10 === "wrestling")) || (data10 === "football")) || (data10 === "baseball")) || (data10 === "soccer")) || (data10 === "movies")) || (data10 === "news"))){
if(!(((((((((((((((((((((((data10 === "live-now") || (data10 === "starting-soon")) || (data10 === "football")) || (data10 === "baseball")) || (data10 === "basketball")) || (data10 === "hockey")) || (data10 === "soccer")) || (data10 === "wrestling")) || (data10 === "motorsports")) || (data10 === "boxing")) || (data10 === "mma")) || (data10 === "tennis")) || (data10 === "golf")) || (data10 === "rugby")) || (data10 === "cricket")) || (data10 === "lacrosse")) || (data10 === "other-sports")) || (data10 === "movies")) || (data10 === "news")) || (data10 === "kids")) || (data10 === "entertainment")) || (data10 === "documentary")) || (data10 === "comedy"))){
validate21.errors = [{instancePath:instancePath+"/CategoryKeys/" + i0,schemaPath:"#/$defs/categoryKey/enum",keyword:"enum",params:{allowedValues: schema33.enum},message:"must be equal to one of the allowed values"}];
return false;
}
Expand Down Expand Up @@ -983,7 +983,7 @@ if(valid0){
if(data.CategoryKey !== undefined){
let data3 = data.CategoryKey;
const _errs6 = errors;
if(!((((((((data3 === "live-now") || (data3 === "starting-soon")) || (data3 === "wrestling")) || (data3 === "football")) || (data3 === "baseball")) || (data3 === "soccer")) || (data3 === "movies")) || (data3 === "news"))){
if(!(((((((((((((((((((((((data3 === "live-now") || (data3 === "starting-soon")) || (data3 === "football")) || (data3 === "baseball")) || (data3 === "basketball")) || (data3 === "hockey")) || (data3 === "soccer")) || (data3 === "wrestling")) || (data3 === "motorsports")) || (data3 === "boxing")) || (data3 === "mma")) || (data3 === "tennis")) || (data3 === "golf")) || (data3 === "rugby")) || (data3 === "cricket")) || (data3 === "lacrosse")) || (data3 === "other-sports")) || (data3 === "movies")) || (data3 === "news")) || (data3 === "kids")) || (data3 === "entertainment")) || (data3 === "documentary")) || (data3 === "comedy"))){
validate20.errors = [{instancePath:instancePath+"/CategoryKey",schemaPath:"#/properties/CategoryKey/enum",keyword:"enum",params:{allowedValues: schema31.properties.CategoryKey.enum},message:"must be equal to one of the allowed values"}];
return false;
}
Expand Down
38 changes: 34 additions & 4 deletions schemas/one-guide-projection.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,27 @@
"enum": [
"live-now",
"starting-soon",
"wrestling",
"football",
"baseball",
"basketball",
"hockey",
"soccer",
"wrestling",
"motorsports",
"boxing",
"mma",
"tennis",
"golf",
"rugby",
"cricket",
"lacrosse",
"other-sports",
"movies",
"news"
"news",
"kids",
"entertainment",
"documentary",
"comedy"
]
},
"Offset": { "type": "integer", "minimum": 0 },
Expand All @@ -46,12 +61,27 @@
"enum": [
"live-now",
"starting-soon",
"wrestling",
"football",
"baseball",
"basketball",
"hockey",
"soccer",
"wrestling",
"motorsports",
"boxing",
"mma",
"tennis",
"golf",
"rugby",
"cricket",
"lacrosse",
"other-sports",
"movies",
"news"
"news",
"kids",
"entertainment",
"documentary",
"comedy"
]
},
"item": {
Expand Down
Loading
Loading