@roku-sdk/ai 0.0.0-stage → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -2
  3. package/package.json +47 -3
  4. package/skills/ads-and-raf/SKILL.md +231 -0
  5. package/skills/ads-and-raf/references/integration-flows.md +332 -0
  6. package/skills/app-packaging/SKILL.md +107 -0
  7. package/skills/app-performance/SKILL.md +133 -0
  8. package/skills/cli-dev-loop/SKILL.md +217 -0
  9. package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
  10. package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
  11. package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
  12. package/skills/data-entry-keyboards/SKILL.md +160 -0
  13. package/skills/device-management/SKILL.md +158 -0
  14. package/skills/dialogs-and-modals/SKILL.md +210 -0
  15. package/skills/effects/SKILL.md +183 -0
  16. package/skills/fetch-and-data-loading/SKILL.md +164 -0
  17. package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
  18. package/skills/flex-grid/SKILL.md +236 -0
  19. package/skills/flex-grid/references/focus-integration.md +355 -0
  20. package/skills/flex-grid/references/plugins.md +201 -0
  21. package/skills/focus-management/SKILL.md +210 -0
  22. package/skills/focus-management/references/focus-recipes.md +290 -0
  23. package/skills/fonts-and-glyphs/SKILL.md +231 -0
  24. package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
  25. package/skills/logging-and-errors/SKILL.md +227 -0
  26. package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
  27. package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
  28. package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
  29. package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
  30. package/skills/mcp-setup/SKILL.md +131 -0
  31. package/skills/project-setup/SKILL.md +210 -0
  32. package/skills/project-setup/references/roku-config.md +120 -0
  33. package/skills/screen-navigation/SKILL.md +166 -0
  34. package/skills/sdk-components/SKILL.md +213 -0
  35. package/skills/sdk-components/references/dialogs.md +148 -0
  36. package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
  37. package/skills/sdk-installation/SKILL.md +129 -0
  38. package/skills/slideshow/SKILL.md +157 -0
  39. package/skills/slideshow/references/focus-and-handoff.md +187 -0
  40. package/skills/solid-on-roku/SKILL.md +181 -0
  41. package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
  42. package/skills/testing/SKILL.md +147 -0
  43. package/skills/testing/references/async-and-timing.md +97 -0
  44. package/skills/user-accounts-and-purchases/SKILL.md +164 -0
  45. package/skills-manifest.json +316 -0
  46. package/skills-manifest.schema.json +62 -0
  47. package/src/__tests__/manifest.test.ts +267 -0
  48. package/src/__tests__/snippets.test.ts +221 -0
  49. package/src/__tests__/trigger-routing.test.ts +77 -0
  50. package/src/index.ts +36 -0
  51. package/src/manifest.ts +215 -0
  52. package/src/router.ts +147 -0
  53. package/src/snippets.ts +212 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roku, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,89 @@
1
- # Temporary Holding Version
1
+ # @roku-sdk/ai
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ AI skills library for the Roku RSG TypeScript SDK. Ships SDK-specific "skill"
4
+ documents plus the pure logic that validates and routes them, so AI coding
5
+ assistants give correct, SDK-specific guidance to external Roku SceneGraph
6
+ TypeScript developers.
7
+
8
+ Delivery is one tool-neutral tree: `rk update-ai-context` writes
9
+ `.agents/skills/roku-sdk-<name>/`, the [Agent Skills][spec] location, rather than
10
+ a directory per editor. One artifact, committed to the consuming repo, read by
11
+ every runtime that follows the convention.
12
+
13
+ The package's public API is **content + pure logic only** — no filesystem or
14
+ network access. The effectful delivery pipeline (discovery, generator, writer,
15
+ and the `rk update-ai-context` command) lives in `@roku-sdk/tooling`, which
16
+ depends on this package. The dependency is strictly one-way: `@roku-sdk/ai`
17
+ never imports `@roku-sdk/tooling`.
18
+
19
+ [spec]: https://cursor.com/docs/skills
20
+
21
+ ### Upgrading from 2.14.0
22
+
23
+ 2.14.0 had a `--target` flag that wrote a directory per editor. If you ran the
24
+ command on that version, delete whichever of these it left behind — nothing
25
+ generates or removes them now, and an editor that reads more than one location
26
+ will keep loading the stale copy alongside the current one:
27
+
28
+ ```sh
29
+ rm -f .cursor/rules/roku-sdk-skills.mdc .github/instructions/roku-sdk.instructions.md
30
+ rm -rf .claude/skills/roku-sdk-*
31
+ ```
32
+
33
+ Then run `rk update-ai-context` to generate the tool-neutral tree. Files you
34
+ wrote yourself are untouched: the command only ever owns `.agents/skills/` paths
35
+ under the `roku-sdk-` prefix.
36
+
37
+ ## What ships here
38
+
39
+ | Path | Purpose |
40
+ |------|---------|
41
+ | `skills/<name>/SKILL.md` | Skill documents (directory-per-skill) |
42
+ | `skills-manifest.json` | Standalone manifest mapping triggers to skill paths |
43
+ | `skills-manifest.schema.json` | Published JSON Schema for the manifest |
44
+ | `src/manifest.ts` | TS types, zod schema, and `loadManifest()` |
45
+ | `src/router.ts` | `buildRouter` / `route` / `findCollisions` / `findCollisionsAcross` |
46
+ | `src/snippets.ts` | `extractSnippets()` — the TypeScript code fences in a `SKILL.md` |
47
+
48
+ ## Public API
49
+
50
+ ```ts
51
+ import {
52
+ loadManifest,
53
+ buildRouter,
54
+ route,
55
+ findCollisions,
56
+ findCollisionsAcross,
57
+ extractSnippets,
58
+ type SkillManifest,
59
+ type SkillEntry,
60
+ type SkillSnippet,
61
+ } from "@roku-sdk/ai";
62
+ ```
63
+
64
+ `loadManifest(json)` validates raw JSON against the zod schema and returns a
65
+ typed `SkillManifest` (throwing a descriptive `ManifestValidationError` on
66
+ invalid input). The router functions enforce unique triggers, map a keyword to
67
+ its owning skill, and detect collisions across manifests (also reused by tooling
68
+ discovery).
69
+
70
+ `extractSnippets(markdown)` returns the `ts` / `tsx` code fences of a
71
+ `SKILL.md`, each with the line it starts on in the file and the check mode its
72
+ info string asks for. Line numbers are absolute against the original file so a
73
+ consumer can report a problem at the line an author edits. Only fences starting
74
+ at column 0 are scanned; an indented or blockquoted fence is prose about code,
75
+ not a sample.
76
+
77
+ A fence's info string selects its `mode`. A bare `ts` / `tsx` fence is
78
+ `"default"` — a fragment, which may refer to names its surrounding context
79
+ would provide. `rsg-skill=complete` marks a sample that stands on its own as a
80
+ module, and `rsg-skill=no-check` marks one that is deliberately wrong, such as
81
+ the "don't do this" half of a contrast. An unrecognised `rsg-skill=` value
82
+ throws `SnippetMarkerError` instead of falling back to `"default"`, so a fence
83
+ asking to be exempt is never silently checked.
84
+
85
+ A fence with no closing delimiter throws `UnclosedFenceError`, checked for every
86
+ language rather than only the two extracted here: an open fence consumes the
87
+ fences below it, so tolerating one would return fewer snippets than the file
88
+ appears to hold. Both errors extend `SnippetExtractionError`, which carries the
89
+ `code` and the 1-based `line` of the fence at fault.
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@roku-sdk/ai",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "2.19.0",
4
+ "description": "AI skills library and manifest for the Roku RSG TypeScript SDK",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "files": [
11
+ "src/**",
12
+ "skills/**",
13
+ "skills-manifest.json",
14
+ "skills-manifest.schema.json",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "npm": ">=8.5.0",
19
+ "node": ">=24.0.0"
20
+ },
21
+ "dependencies": {
22
+ "zod": "^4.6.6"
23
+ },
24
+ "devDependencies": {
25
+ "@jest/globals": "^30.4.1",
26
+ "@types/jest": "^30.0.0",
27
+ "jest": "^30.4.1",
28
+ "@roku-sdk/dev-tools": "2.19.0"
29
+ },
30
+ "author": "Roku Inc",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://gitlab.com/rokudev/rsg-sdk.git",
35
+ "directory": "packages/ai"
36
+ },
37
+ "homepage": "https://gitlab.com/rokudev/rsg-sdk/-/tree/main/packages/ai#readme",
38
+ "bugs": {
39
+ "url": "https://gitlab.com/rokudev/rsg-sdk/-/issues"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "test": "jest --passWithNoTests",
46
+ "coverage": "jest --coverage --runInBand --passWithNoTests --testTimeout=60000 --forceExit",
47
+ "clean": "rm -rf dist",
48
+ "lint": "eslint --no-error-on-unmatched-pattern \"src/**/*.{ts,tsx,mjs,cjs,js}\""
49
+ }
6
50
  }
@@ -0,0 +1,231 @@
1
+ # Ads and the Roku Advertising Framework
2
+
3
+ ## Overview
4
+
5
+ `@roku-sdk/ads` exports one component, `<Ads>`, a headless SceneGraph Task that wraps the Roku
6
+ Advertising Framework. You do not call RAF; you write fields on the task and read results off its
7
+ callbacks, in a fixed order. Every trap in the package comes from that being a protocol rather
8
+ than an API.
9
+
10
+ The package's interface may still change between releases: pin the version and re-read the types
11
+ after an upgrade.
12
+
13
+ ## Read the reference first for these tasks
14
+
15
+ Wiring a CSAI, SSAI or CSAS module end to end: read
16
+ [`references/integration-flows.md`](references/integration-flows.md) first — it carries one
17
+ complete wired module per mode. The tables below name the commands and say what each mode needs,
18
+ but no row here is a complete answer to "how do I wire this mode".
19
+
20
+ ## When to Use
21
+
22
+ - Playing ads at all through `@roku-sdk/ads`: pre-roll, mid-roll or post-roll.
23
+ - A command that appears to do nothing, or a result callback firing with an unexpected value.
24
+ - Choosing between CSAI, SSAI and CSAS, or finding a mode is not the one you asked for.
25
+ - RAF never reporting ready in a new app.
26
+ - Audience measurement fields required for certification.
27
+
28
+ ### When not to use
29
+
30
+ | Situation | Use instead | Because |
31
+ |---|---|---|
32
+ | Which node plays the content video, or draws anything on screen | `sdk-components` | `<Ads>` renders nothing; it only needs refs to nodes something else drew |
33
+ | Loading your own catalog or metadata over the network | `fetch-and-data-loading` | RAF makes the ad request itself — the app never fetches the tag |
34
+ | `ChannelStore` sign-in or purchases | `user-accounts-and-purchases` | Same set-data-then-set-command shape, different subsystem and different fields |
35
+ | Where D-pad focus lives generally | `focus-management` | Only the focus an ad break takes is here |
36
+ | Calling RAF directly from BrightScript | — | This skill is the TypeScript wrapper; the task already owns the RAF calls |
37
+
38
+ ## Quick Reference
39
+
40
+ ### The protocol, in order
41
+
42
+ 1. **Wait for `"ready"`.** `onAdsState` reports it once, before the task's command loop starts.
43
+ A command written earlier is never seen — no error, no result.
44
+ 2. **Set the data fields first, `command` last.** The task reacts to the `command` write and reads
45
+ the other fields as they stand at that moment.
46
+ 3. **Read the result off the matching callback**, not off the next thing that happens.
47
+
48
+ Two consequences are invisible in the types and are the most common cause of "it worked once":
49
+
50
+ - **A repeated `command` value writes nothing.** The field is signal-driven and compares by value,
51
+ so setting `"showAds"` twice in a row is one command. Reset through `"none"` between two
52
+ identical commands.
53
+ - **A string result field is cleared to `"none"` before the real value is written**, so
54
+ `onShowAdsResult`, `onStitchedAdsInitResult`, `onConstructStitchedStreamResult` and
55
+ `onRenderStitchedStreamResult` all fire with `"none"` before anything you can act on. Guard it
56
+ and return. The catch is that the *first* command hides it — the field already holds `"none"`,
57
+ so that write changes nothing and notifies nobody. An unguarded handler therefore works
58
+ perfectly until the second command, which is where it starts treating the sentinel as a result.
59
+ `onGetAdsResult` is the exception and needs no guard: it carries an array, and RAF returns a
60
+ fresh one each call.
61
+
62
+ ### Commands
63
+
64
+ | `command` | Needs | Reports on |
65
+ |---|---|---|
66
+ | `getAds` | `adUrl` | `onGetAdsResult` — always an array; an empty or unset `adUrl` yields `[]` without a network call |
67
+ | `showAds` | `adView` | `onShowAdsResult` — `"error"` immediately if `adView` is missing; a viewer leaving the break is `"user_exited"` (2.15.0 and newer), not a failure |
68
+ | `stitchedAdsInit` | non-empty pods | `onStitchedAdsInitResult` — `"error"` on empty pods, which is also the only case that leaves the mode alone |
69
+ | `constructStitchedStream` | `contentMetadata` | `onConstructStitchedStreamResult` |
70
+ | `renderStitchedStream` | a built stream and `adView` | `onRenderStitchedStreamResult` — a viewer back-out is `"user_exited"`, a distinct result from `"error"` |
71
+ | `stop` | — | `onAdsState` reports `"stopped"`; the task's loop exits |
72
+
73
+ ### Choosing a mode
74
+
75
+ | Your situation | Mode | Needs |
76
+ |---|---|---|
77
+ | Pause the content, play ads over it, resume where you left off | CSAI | `adView` and `contentView` |
78
+ | The ads are stitched into the content stream server-side and you react to ad events as it plays | SSAI | `contentView` |
79
+ | Build one stitched stream up front and render it yourself | CSAS | `adView` and `contentMetadata` |
80
+
81
+ Command sequences, after `"ready"`: CSAI is `getAds` → `showAds`, repeated per break. SSAI is
82
+ `getAds` → `stitchedAdsInit`, then content playback drives the ad events. CSAS is
83
+ `getAds` → `constructStitchedStream` → `renderStitchedStream`.
84
+
85
+ ## Instructions
86
+
87
+ 1. **Add `bs_libs_required: "roku_ads_lib"`** to `manifestEntries` in `roku-config.ts`. Without it
88
+ RAF is not linked, `onAdsState` never reports `"ready"`, and the failure names a library path
89
+ rather than the manifest.
90
+ 2. **Give `<Ads>` an `adView`** for any mode that renders ads — a ref to any renderable node whose
91
+ lifetime covers the whole break. A full-screen `Rectangle` behind the ad is enough.
92
+ 3. **Give it `contentView`** for CSAI and SSAI: a ref to the content `Video`. The task watches
93
+ playback position on it, and that is what drives mid-roll — so set the `Video`'s
94
+ `notificationInterval` fine enough that a cue point is reported.
95
+ 4. **Set measurement before the first `getAds`.** `enableAdMeasurements`, `contentGenre` and
96
+ `contentId` are applied as they change, and RAF needs them in place when the pods are fetched.
97
+ Empty strings are skipped. RAF fires the beacons itself — do not intercept or suppress them.
98
+ 5. **Do not write a cue-point loop.** The task calls RAF's `getAds` itself at each cue point it
99
+ sees on `contentView` and hands back only the pods due now, so mid-roll arrives as another
100
+ `onGetAdsResult`. There is nothing to schedule and no pod field to filter on.
101
+ 6. **Re-assert focus when content resumes.** An ad break does not hand focus back; it is left on
102
+ the screen root, and an app that does not take it returns to receiving no keys.
103
+ 7. **Stop with `command = "stop"` while still mounted.** Doing it only from a cleanup is
104
+ best-effort: the callback that would confirm it is torn down by the same unmount.
105
+
106
+ Two more facts a reader cannot derive from the surface. `contentView` binds **once** — the task
107
+ observes position on the first non-invalid value and then stops watching its own field, so
108
+ swapping the `Video` node means unmounting and remounting `<Ads>`. And mid-roll reuses the last
109
+ `adUrl`; there is no per-break tag.
110
+
111
+ ### The mode transitions that do not happen
112
+
113
+ Both leave the task in CSAI while the app believes otherwise, so position events then offer
114
+ mid-roll pods for a stream that was never set up for them. Each reports, but only as an `"error"`
115
+ that is easy to log and walk past:
116
+
117
+ - `stitchedAdsInit` on empty pods changes no mode and reports `"error"`. Check `pods.length > 0`
118
+ before issuing it rather than reacting afterwards.
119
+ - A failed `constructStitchedStream` reverts to CSAI, signalled only by its `"error"` result.
120
+
121
+ Treat either as "this is not the mode I asked for" and stop, rather than starting playback.
122
+
123
+ ## Example
124
+
125
+ Minimal CSAI pre-roll. It demonstrates both consequences above: `onShowAdsResult` guards the
126
+ `"none"` sentinel, and it resets `command` to `"none"` so the next break's `showAds` is a change.
127
+
128
+ It resumes content on the two results that mean the break is over — `"completed"`, and `"error"`
129
+ for a break that never played — and stops on anything else, which is a viewer who walked out.
130
+ That way round on purpose: `"user_exited"` joined `ShowAdsResult` in 2.15.0, so comparing against
131
+ that literal does not compile on 2.14.0, while an allow-list of the results that permit resuming
132
+ reads the same on both and keeps its meaning as the union grows.
133
+
134
+ ```tsx rsg-skill=complete
135
+ import type { AdsCommand, AdsState, EventHandler, EventHandlerAdPods, ShowAdsResult } from "@roku-sdk/ads";
136
+ import { Ads } from "@roku-sdk/ads";
137
+ import { Component, ContentNode, Group, MediaControl, Rectangle, Video } from "@roku-sdk/components";
138
+ import { DomNode } from "@roku-sdk/interfaces";
139
+ import { createSignal } from "@roku-sdk/solid";
140
+
141
+ export const PreRoll: Component = () => {
142
+ const [command, setCommand] = createSignal<AdsCommand>("none");
143
+ const [adUrl, setAdUrl] = createSignal("");
144
+ const [adView, setAdView] = createSignal<DomNode | undefined>();
145
+ const [contentView, setContentView] = createSignal<DomNode | undefined>();
146
+ const [control, setControl] = createSignal<MediaControl>("none");
147
+
148
+ const handleAdsState = (state: AdsState) => {
149
+ if (state !== "ready") return;
150
+ setAdUrl("https://example.com/tag.vmap");
151
+ setCommand("getAds");
152
+ };
153
+
154
+ const handleGetAdsResult: EventHandlerAdPods = pods => {
155
+ if (pods.length === 0) {
156
+ setControl("play");
157
+ return;
158
+ }
159
+ setControl("stop");
160
+ setCommand("showAds");
161
+ };
162
+
163
+ const handleShowAdsResult: EventHandler<ShowAdsResult> = result => {
164
+ if (result === "none") return;
165
+ if (result === "completed" || result === "error") {
166
+ // The break played out, or never played at all: resume content either way.
167
+ setControl("play");
168
+ setCommand("none");
169
+ return;
170
+ }
171
+ // Anything else is the viewer leaving the break, so do not resume.
172
+ setCommand("stop");
173
+ };
174
+
175
+ return (
176
+ <group>
177
+ <Video
178
+ width={1920} height={1080} control={control()} notificationInterval={1.0}
179
+ content={<contentNode url="https://example.com/content.m3u8" />}
180
+ gainFocus={control() === "play"}
181
+ ref={element => setContentView(element)}
182
+ />
183
+ <rectangle
184
+ width={1920} height={1080} color="0x000000FF"
185
+ visible={control() !== "play"}
186
+ ref={element => setAdView(element)}
187
+ />
188
+ <Ads
189
+ adUrl={adUrl()} adView={adView()} contentView={contentView()} command={command()}
190
+ onAdsState={handleAdsState}
191
+ onGetAdsResult={handleGetAdsResult}
192
+ onShowAdsResult={handleShowAdsResult}
193
+ />
194
+ </group>
195
+ );
196
+ };
197
+ ```
198
+
199
+ ## Common Mistakes
200
+
201
+ | Symptom | Cause | Fix |
202
+ |---|---|---|
203
+ | `onAdsState` never reports `"ready"`; log shows a `roku_ads` library path | RAF was not linked into the app | `bs_libs_required: "roku_ads_lib"` in `manifestEntries` |
204
+ | First command does nothing, no error | It was written before `"ready"` | Issue the first command from `onAdsState` |
205
+ | Second ad break never plays; the first did | `command` was already that value, so nothing was written | Reset to `"none"` between two identical commands |
206
+ | A handler that worked in testing mishandles the *second* command | It received the `"none"` sentinel, which the first command hides because the field already held it | `if (result === "none") return;` in every string result handler |
207
+ | `showAds` reports `"error"` at once | No `adView` | Pass a ref to a renderable node that outlives the break |
208
+ | Content restarts when the viewer backs out of an ad | `"user_exited"` was handled as if it were `"completed"` | Resume only on the results that mean the break is over; leaving one early usually means leaving the content. `showAds` reports it from 2.15.0, `renderStitchedStream` from earlier |
209
+ | Mid-roll never fires | No `contentView`, so nothing watches playback position | Pass a ref to the content `Video` |
210
+ | The app stops responding to the remote after an ad | The break took focus and did not return it | Re-assert focus when content resumes |
211
+ | A pod field read fine in testing and is `undefined` in production | Pod fields are optional: which ones arrive depends on the VAST tag that answered | Treat pods as opaque; `pods.length > 0` is the whole decision |
212
+
213
+ ### Ad pods carry ad-server data, so almost every field is optional
214
+
215
+ The names are RAF's own and lowercase — `rendersequence`, `mimetype`, `adtitle` — not camelCase.
216
+ Only `AdPod.ads` is required; everything else is optional, because RAF passes the ad server's
217
+ response through and a different tag carries different fields. There is no ad count — it is
218
+ `ads.length` — and the break is described by two fields rather than one: `rendersequence`, a string
219
+ (`"preroll"`), and `rendertime`, the offset in seconds RAF has already scheduled it at.
220
+ `tracking` is a flat array of event objects, not a record of URLs, so finding one means searching
221
+ on `event`.
222
+
223
+ None of which a correct integration needs: the task fetches cue-point pods itself, so check
224
+ `pods.length` and read nothing else.
225
+
226
+ ## Source of truth
227
+
228
+ `@roku-sdk/ads` ships its `src/`, so the typed surface is in `node_modules/@roku-sdk/ads`: one
229
+ component and 16 types in `src/index.ts`, with TSDoc on every prop of `IAdsProps`. The command loop
230
+ those props drive is `src/xml-components/ads/ads.brs`, and where the two disagree the BrightScript
231
+ is what runs.