@happyvertical/smrt-education 0.60.2 → 0.60.4

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.
package/AGENTS.md CHANGED
@@ -61,7 +61,8 @@ Course bundles (zip with a closed manifest v1 or v2, or a file set; Word/PowerPo
61
61
  [CourseBundleImportService](agents/course-bundle-import.md). Parse only with the
62
62
  bounded reader; refuse, never trim, hostile input; unsupported members stay
63
63
  visible; nothing auto-published. `attachCourse` makes one family draft course per
64
- unit key (parts merge in any order, mismatches fail closed); review choices never
64
+ unit key (parts merge in any order, mismatches fail closed); `supersede: true`
65
+ is the only way to take a newer build of an attached part (retires, never deletes); review choices never
65
66
  open keys, guidance or an editable original to a learner.
66
67
 
67
68
  Class CrunchLabs (a download read into a manifest v2 bundle; an operator-only
package/README.md CHANGED
@@ -133,7 +133,8 @@ duplicate members as append-only evidence, hands accepted members to ingestion a
133
133
  proposes sections and lessons through `CourseGenerationReviewService`.
134
134
  `attachCourse` turns an import (or each part of a unit split across bundles, in
135
135
  any order) into one family course with lessons, materials, pages and videos;
136
- `chooseMaterialAudience` records review choices before attach; see
136
+ `attachCourse({ supersede: true })` brings an attached unit up to a newer build
137
+ keeping learner progress; `chooseMaterialAudience` records review choices before attach; see
137
138
  [bundle import](agents/course-bundle-import.md).
138
139
  `requestGeneration` also accepts a structured `brief` (subject, category, area,
139
140
  grade, objective, a `course` or one-`lesson` target and a per-request interest
@@ -90,6 +90,27 @@ skipped) or `memoryClassCrunchLabsSource(files)`. It never reads video files.
90
90
  Matches become `videos` references with a title and a step order (materials
91
91
  prep videos are guardian audience); unit-level videos (a teaser) and unmatched
92
92
  ones are only reported.
93
+ - Playlists without step numbers (#4647): the live course playlists title items
94
+ `<Unit>: <name>` in teaching order. Videos that no numbered item names fall back to
95
+ title and kind evidence, never over a numbered match. Playlist items titled
96
+ `Lesson N Recap` are anchors: each pairs with lesson N+1's single `Recap` video
97
+ and they cut the playlist into one segment per lesson (lesson 1 runs from the
98
+ start; the last download lesson runs to the end). Inside a segment a video and
99
+ an item pair when each is the other's single best match (ties pair nothing).
100
+ Evidence is normalized tokens (case, `&` is `and`, simple plurals, the unit
101
+ prefix and `Part N` dropped, `N` kept) for the title, and kind words:
102
+ `Prep`/`MaterialsPrep`, `Exploration`/`StudentHandsOnExploration`,
103
+ `Anchor`/`AnchorCheckIn`, `Explained`/`ForcesExplainer`, `Wrap Up`/`WrapUp`,
104
+ `Recap`, `Check-in`, `Link up`, `Look closer`. Differing numbers (`Part 1` against
105
+ `Sumo Spheres 2`) or an item naming another kind contradict. What is left in a
106
+ segment pairs by order only when the leftover videos and items are equally many,
107
+ the videos sit on distinct steps and nothing contradicts; otherwise they stay
108
+ unmatched. No pair crosses a segment. A playlist with no anchor is not segmented:
109
+ only pairs with title evidence count and nothing is filled by order. Reasons:
110
+ `no_item` (no unnumbered item is left for it, also a whole unit without anchors),
111
+ `ambiguous` (items remain but evidence ties or the leftovers do not pair one to
112
+ one), `unanchored` (its lesson is not bounded by recap anchors in the playlist),
113
+ plus the existing `no_playlist` and `item_taken`.
93
114
  - De-duplication: repeated exports of one logical path collapse (CRC and size
94
115
  when known; the newer export wins, a changed file is `superseded`); the same
95
116
  bytes under another name or place keep one copy (the lesson-named file in its
@@ -251,7 +251,16 @@ plus `skipped` for declared members that were unsupported, refused or failed
251
251
  rendering. A duplicate member resolves to the earlier identical member's bytes
252
252
  (`assetPath`) and one material per (place, role, content).
253
253
 
254
- `receiveMember` also gets `audience` and `courseLevel`. **Slides** keep the original
254
+ `receiveMember` also gets `audience` and `courseLevel`, and an optional `title`
255
+ (SMRT #4653): the manifest title of the one material the member holds ("Teacher
256
+ Mission Manual"), so a host can name the assets it stores. It is absent when the
257
+ manifest gave none or the member backs several materials with different titles.
258
+ **Material titles**: `attachCourse` records that title on the `lesson_assets` /
259
+ `course_assets` row (`title`; null on a lesson row without one, `''` on a course
260
+ row), the same for the original and rendered halves of a pair. Lesson and course
261
+ material views show the row title and fall back to the stored asset's name when
262
+ there is none. A re-attach fills a missing title; a different one is a
263
+ `conflict` unless `supersede` restates it. **Slides** keep the original
255
264
  deck (hand `bytes` to the host as a private asset: "Open in PowerPoint") and its
256
265
  rendered pages (`rendered`: "Present").
257
266
 
@@ -297,6 +306,50 @@ thumbnail? }` asset ids the host stored from `receiveMember`.
297
306
  later, then attach again: the replay adds the newly accepted materials.
298
307
  - `readImport` reports `attachment` (course, unit key, part) once attached.
299
308
 
309
+ ### Bringing a unit up to a newer build (SMRT #4645)
310
+
311
+ `attachCourse({ ..., supersede: true })` is the explicit, opt-in way to replace
312
+ the build attached for a part. Without it nothing changes: another import for an
313
+ attached part is `part-conflict`, and a placement whose title differs is
314
+ `part-conflict`. It uses the same authorization (`education.course.author`),
315
+ tenant rules and lock order (import row, then unit row) as any attach.
316
+
317
+ - **Which build is newer**: manifests carry no build stamp, so "newer" is the
318
+ import record: the importing bundle's `recordedAt` must be strictly later
319
+ than the attached import's. Otherwise `stale-build` (an older or
320
+ same-instant build never overwrites a newer one; re-attaching a superseded
321
+ import is the same). `supersede` with no other build attached for the part is
322
+ an ordinary attach.
323
+ - **Identity**: sections and lessons keep their ids (derived from course id +
324
+ key), so enrolments, progress and submissions stay attached. Titles and order
325
+ follow the newer build (`renamed` counts changed titles); a key that a lower
326
+ part decided still must agree (`part-conflict`). Lesson `body` is not
327
+ rewritten. Sections and lessons the newer build no longer lists stay.
328
+ - **Part count change** stays `unit-mismatch`, supersede or not.
329
+ - **Materials**: the newer build's materials are attached; an existing row
330
+ (same lesson, asset, role) is restated in place (order, title, current again; a
331
+ renamed material shows its new title though it keeps its earlier asset),
332
+ not a `conflict`; its audience stays the family's. Pages are restated and
333
+ never retired (learner work points at page keys).
334
+ - **Retirement**: file materials and external videos the replaced build wrote
335
+ that the newer build does not carry get `retiredAt`. Rows are never deleted,
336
+ so work that references them still resolves; learner and catalog material
337
+ lists skip retired rows. `retired` counts them. Which rows belong to the
338
+ replaced build: `sourceImportId` (new on `lesson_assets`, `course_assets`,
339
+ `lesson_external_videos`, written by every attach/apply); rows from before it
340
+ existed (null) are adopted when their lesson is one of the replaced import's
341
+ lessons, and course materials when that import had course members. Rows
342
+ from another part or source are never touched. A material that returns in a
343
+ later build is current again.
344
+ - **Audit**: the part's `CourseBundleAttachment` moves to the newer import (the
345
+ one sanctioned attachment change, `supersedeWith`) and an append-only
346
+ `CourseBundleSupersession` row (`course_bundle_supersessions`: course, unit
347
+ key, part, new import, replaced import, actor, time) records which replaced
348
+ which. The replaced import no longer reads as attached.
349
+ - **Replay** of the same supersede is `replayed: true` and changes nothing.
350
+ - Schema (additive): `retired_at` and `source_import_id` on `lesson_assets`,
351
+ `course_assets`, `lesson_external_videos`; table `course_bundle_supersessions`.
352
+
300
353
  ### Audience before and after attach
301
354
 
302
355
  `chooseMaterialAudience({ ...actor, importId, path | video: { lessonKey, videoId },
@@ -375,6 +428,8 @@ reason, duplicate-of, role, audience, section and lesson key) and
375
428
  bytes stay with the host's ingestion receipts. Rewrites and deletes throw.
376
429
  `CourseBundleAttachment` (`course_bundle_attachments`) and the mutable
377
430
  `CourseBundleAudienceChoice` (`course_bundle_audience_choices`) and
431
+ `CourseBundleSupersession` (`course_bundle_supersessions`, append-only audit of a
432
+ newer build taking a part) and
378
433
  `CourseBundlePlacement` (`course_bundle_placements`) and `CourseBundleUnit`
379
434
  (`course_bundle_units`) belong to the attach path. Normal manifest migrations create the tables and add `audience` (NULL
380
435
  on older rows) and, on `lesson_assets`/`course_assets`, `rendition`,
@@ -35,7 +35,7 @@ must be private.
35
35
 
36
36
  `listLessonMaterials({enrollmentId, lessonId})` and `listCourseMaterials({enrollmentId})`
37
37
  return `LessonMaterialView`s (`id`, `kind` file or external-video, `scope`, role,
38
- audience, title, order, `assetId` or provider/videoId/`embedUrl`, and for files
38
+ audience, title (the bundle's material title, else the stored asset's name), order, `assetId` or provider/videoId/`embedUrl`, and for files
39
39
  `mimeType` from the stored asset, `rendition`, `pairedMaterialId` (the other half
40
40
  of an original/rendered pair, only when this actor sees both) and
41
41
  `thumbnailAssetId`). Hosts read these instead of guessing by file extension. The learner
@@ -1 +1 @@
1
- {"version":3,"file":"catalog-service.d.ts","sourceRoot":"","sources":["../src/catalog-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,WAAW,EAAoB,MAAM,oBAAoB,CAAC;AAExE,iFAAiF;AACjF,MAAM,MAAM,sBAAsB,GAChC,gBAAgB,CAAC,4BAA4B,CAAC,CAAC;AACjD,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAC1C,UAAU,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CACnD,CAAC,CAAC,CAAC,CAAC;AAEL,MAAM,MAAM,iBAAiB,GAAG,CAC9B,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,CAAC,GAAG;IAC3C,+EAA+E;IAC/E,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,MAAM,EACF,wBAAwB,GACxB,gCAAgC,GAChC,mCAAmC,GACnC,0BAA0B,CAAC;CAChC,KACE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,KAAK,CAAC;QACd,OAAO,EAAE,aAAa,CAAC;QACvB,OAAO,EAAE,KAAK,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,WAAW,EAAE,CAAA;SAAE,CAAC,CAAC;KAC3D,CAAC,CAAC;CACJ;AAED,qBAAa,yBAA0B,SAAQ,KAAK;;CAKnD;AAUD;;;;GAIG;AACH,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAIxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBART,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,uBAAuB,EACjC,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,IAAI,CAC5B,gBAAgB,EAChB,4BAA4B,CAC7B,EACgB,SAAS,EAAE,iBAAiB;IAE/C,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,gBAAgB;IAKlB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAuDjE,wFAAwF;IAClF,aAAa,CACjB,OAAO,EAAE,kBAAkB,GAAG;QAC5B,QAAQ,EAAE,MAAM,CAAC;QACjB,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,GACA,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;CAqErC"}
1
+ {"version":3,"file":"catalog-service.d.ts","sourceRoot":"","sources":["../src/catalog-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,WAAW,EAAoB,MAAM,oBAAoB,CAAC;AAExE,iFAAiF;AACjF,MAAM,MAAM,sBAAsB,GAChC,gBAAgB,CAAC,4BAA4B,CAAC,CAAC;AACjD,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAC1C,UAAU,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CACnD,CAAC,CAAC,CAAC,CAAC;AAEL,MAAM,MAAM,iBAAiB,GAAG,CAC9B,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,CAAC,GAAG;IAC3C,+EAA+E;IAC/E,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,MAAM,EACF,wBAAwB,GACxB,gCAAgC,GAChC,mCAAmC,GACnC,0BAA0B,CAAC;CAChC,KACE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,KAAK,CAAC;QACd,OAAO,EAAE,aAAa,CAAC;QACvB,OAAO,EAAE,KAAK,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,WAAW,EAAE,CAAA;SAAE,CAAC,CAAC;KAC3D,CAAC,CAAC;CACJ;AAED,qBAAa,yBAA0B,SAAQ,KAAK;;CAKnD;AAUD;;;;GAIG;AACH,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAIxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBART,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,uBAAuB,EACjC,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,IAAI,CAC5B,gBAAgB,EAChB,4BAA4B,CAC7B,EACgB,SAAS,EAAE,iBAAiB;IAE/C,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,gBAAgB;IAKlB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAuDjE,wFAAwF;IAClF,aAAa,CACjB,OAAO,EAAE,kBAAkB,GAAG;QAC5B,QAAQ,EAAE,MAAM,CAAC;QACjB,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,GACA,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;CAsErC"}
@@ -678,12 +678,16 @@ var LessonAsset = class extends SmrtObject {
678
678
  rendition = "";
679
679
  renditionOfAssetId = null;
680
680
  thumbnailAssetId = null;
681
+ title = null;
682
+ retiredAt = null;
683
+ sourceImportId = null;
681
684
  constructor(options = {}) {
682
685
  super(options);
683
686
  for (const [key, value] of Object.entries(options)) if (Object.hasOwn(this, key)) this[key] = value;
684
687
  }
685
688
  async save(options) {
686
689
  materialAudience(this.relationship, this.audience);
690
+ if (this.title !== null && (typeof this.title !== "string" || this.title.length > 200)) throw new Error("Invalid lesson material title");
687
691
  const lesson = await this.db.get("lessons", { id: this.lessonId });
688
692
  if (!lesson) throw new Error(`education lesson ${this.lessonId} does not exist`);
689
693
  if (!await this.db.get("lessons", {
@@ -709,6 +713,18 @@ __decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { requir
709
713
  __decorateClass$10([field({ required: true })], LessonAsset.prototype, "relationship", 2);
710
714
  __decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { nullable: true })], LessonAsset.prototype, "renditionOfAssetId", 2);
711
715
  __decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { nullable: true })], LessonAsset.prototype, "thumbnailAssetId", 2);
716
+ __decorateClass$10([field({
717
+ type: "text",
718
+ nullable: true
719
+ })], LessonAsset.prototype, "title", 2);
720
+ __decorateClass$10([field({
721
+ type: "datetime",
722
+ nullable: true
723
+ })], LessonAsset.prototype, "retiredAt", 2);
724
+ __decorateClass$10([field({
725
+ type: "text",
726
+ nullable: true
727
+ })], LessonAsset.prototype, "sourceImportId", 2);
712
728
  LessonAsset = __decorateClass$10([TenantScoped({ mode: "optional" }), smrt({
713
729
  tableName: "lesson_assets",
714
730
  conflictColumns: [
@@ -733,6 +749,8 @@ var CourseAsset = class extends SmrtObject {
733
749
  rendition = "";
734
750
  renditionOfAssetId = null;
735
751
  thumbnailAssetId = null;
752
+ retiredAt = null;
753
+ sourceImportId = null;
736
754
  constructor(options = {}) {
737
755
  super(options);
738
756
  for (const [key, value] of Object.entries(options)) if (Object.hasOwn(this, key)) this[key] = value;
@@ -754,6 +772,14 @@ __decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { requir
754
772
  __decorateClass$10([field({ required: true })], CourseAsset.prototype, "relationship", 2);
755
773
  __decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { nullable: true })], CourseAsset.prototype, "renditionOfAssetId", 2);
756
774
  __decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { nullable: true })], CourseAsset.prototype, "thumbnailAssetId", 2);
775
+ __decorateClass$10([field({
776
+ type: "datetime",
777
+ nullable: true
778
+ })], CourseAsset.prototype, "retiredAt", 2);
779
+ __decorateClass$10([field({
780
+ type: "text",
781
+ nullable: true
782
+ })], CourseAsset.prototype, "sourceImportId", 2);
757
783
  CourseAsset = __decorateClass$10([TenantScoped({ mode: "optional" }), smrt({
758
784
  tableName: "course_assets",
759
785
  conflictColumns: [
@@ -812,6 +838,8 @@ var LessonExternalVideo = class extends SmrtObject {
812
838
  title = "";
813
839
  stepOrder = 0;
814
840
  audience = "";
841
+ retiredAt = null;
842
+ sourceImportId = null;
815
843
  constructor(options = {}) {
816
844
  super(options);
817
845
  for (const [key, value] of Object.entries(options)) if (Object.hasOwn(this, key)) this[key] = value;
@@ -829,6 +857,14 @@ var LessonExternalVideo = class extends SmrtObject {
829
857
  __publicField$10(LessonExternalVideo, "__smrtPackage__", "@happyvertical/smrt-education");
830
858
  __decorateClass$10([tenantId({ nullable: true })], LessonExternalVideo.prototype, "tenantId", 2);
831
859
  __decorateClass$10([foreignKey("Lesson", { required: true })], LessonExternalVideo.prototype, "lessonId", 2);
860
+ __decorateClass$10([field({
861
+ type: "datetime",
862
+ nullable: true
863
+ })], LessonExternalVideo.prototype, "retiredAt", 2);
864
+ __decorateClass$10([field({
865
+ type: "text",
866
+ nullable: true
867
+ })], LessonExternalVideo.prototype, "sourceImportId", 2);
832
868
  LessonExternalVideo = __decorateClass$10([TenantScoped({ mode: "optional" }), smrt({
833
869
  tableName: "lesson_external_videos",
834
870
  conflictColumns: [
@@ -1437,7 +1473,7 @@ var CatalogService = class {
1437
1473
  section,
1438
1474
  lessons: await Promise.all(lessons.filter((lesson) => lesson.tenantId === course.tenantId).map(async (lesson) => ({
1439
1475
  lesson: projectLesson(lesson, includeAnswerKeys),
1440
- assets: (await this.assets.byLeft(String(lesson.id))).filter((asset) => asset.tenantId === course.tenantId && (includeAnswerKeys || asset.relationship !== "answer-key" && materialAudience(asset.relationship, asset.audience) === "learner"))
1476
+ assets: (await this.assets.byLeft(String(lesson.id))).filter((asset) => asset.tenantId === course.tenantId && !asset.retiredAt && (includeAnswerKeys || asset.relationship !== "answer-key" && materialAudience(asset.relationship, asset.audience) === "learner"))
1441
1477
  })))
1442
1478
  });
1443
1479
  }
@@ -33279,7 +33315,7 @@ var SubmissionService = class SubmissionService extends LearnerService {
33279
33315
  scope,
33280
33316
  relationship: row.relationship,
33281
33317
  audience: materialAudience(String(row.relationship), String(row.audience ?? "")),
33282
- title: scope === "course" && String(row.title ?? "") || asset.name,
33318
+ title: String(row.title ?? "") || asset.name,
33283
33319
  order: Number(row.sort_order) || 0,
33284
33320
  assetId: String(row.asset_id),
33285
33321
  provider: null,
@@ -33303,8 +33339,8 @@ var SubmissionService = class SubmissionService extends LearnerService {
33303
33339
  if (!lesson) throw new LearnerAuthorizationError();
33304
33340
  const tenant = lesson.tenant_id ?? null;
33305
33341
  const learnerOnly = await this.learnerAudienceOnly(request);
33306
- const files = (await this.db.list("lesson_assets", { lesson_id: request.lessonId })).filter((row) => (row.tenant_id ?? null) === tenant && this.visibleTo(learnerOnly, String(row.relationship), String(row.audience ?? ""))).sort((a, b) => Number(a.sort_order) - Number(b.sort_order) || String(a.id).localeCompare(String(b.id)));
33307
- const videos = await this.db.tableExists("lesson_external_videos") ? (await this.db.list("lesson_external_videos", { lesson_id: request.lessonId })).filter((row) => (row.tenant_id ?? null) === tenant && this.visibleTo(learnerOnly, "video", String(row.audience ?? ""))).sort((a, b) => Number(a.step_order) - Number(b.step_order) || String(a.id).localeCompare(String(b.id))) : [];
33342
+ const files = (await this.db.list("lesson_assets", { lesson_id: request.lessonId })).filter((row) => (row.tenant_id ?? null) === tenant && !row.retired_at && this.visibleTo(learnerOnly, String(row.relationship), String(row.audience ?? ""))).sort((a, b) => Number(a.sort_order) - Number(b.sort_order) || String(a.id).localeCompare(String(b.id)));
33343
+ const videos = await this.db.tableExists("lesson_external_videos") ? (await this.db.list("lesson_external_videos", { lesson_id: request.lessonId })).filter((row) => (row.tenant_id ?? null) === tenant && !row.retired_at && this.visibleTo(learnerOnly, "video", String(row.audience ?? ""))).sort((a, b) => Number(a.step_order) - Number(b.step_order) || String(a.id).localeCompare(String(b.id))) : [];
33308
33344
  const fileViews = [];
33309
33345
  for (const row of files) fileViews.push(await this.fileView(row, "lesson"));
33310
33346
  const result = pairRenditions(files, fileViews);
@@ -33334,7 +33370,7 @@ var SubmissionService = class SubmissionService extends LearnerService {
33334
33370
  const course = await this.db.get("courses", { id: enrollment.courseId });
33335
33371
  if (!course) throw new LearnerAuthorizationError();
33336
33372
  const learnerOnly = await this.learnerAudienceOnly(request);
33337
- const rows = (await this.db.list("course_assets", { course_id: enrollment.courseId })).filter((row) => (row.tenant_id ?? null) === (course.tenant_id ?? null) && this.visibleTo(learnerOnly, String(row.relationship), String(row.audience ?? ""))).sort((a, b) => Number(a.sort_order) - Number(b.sort_order) || String(a.id).localeCompare(String(b.id)));
33373
+ const rows = (await this.db.list("course_assets", { course_id: enrollment.courseId })).filter((row) => (row.tenant_id ?? null) === (course.tenant_id ?? null) && !row.retired_at && this.visibleTo(learnerOnly, String(row.relationship), String(row.audience ?? ""))).sort((a, b) => Number(a.sort_order) - Number(b.sort_order) || String(a.id).localeCompare(String(b.id)));
33338
33374
  const views = [];
33339
33375
  for (const row of rows) views.push(await this.fileView(row, "course"));
33340
33376
  return pairRenditions(rows, views);
@@ -36940,4 +36976,4 @@ var WorkbookPhotoService = class WorkbookPhotoService extends LessonWorkService
36940
36976
  //#endregion
36941
36977
  export { LessonAlreadyCompleteError as $, Curriculum as $i, QUESTION_COMPLETION_MODES as $n, ReleasedLesson as $r, WorkFeedbackRelease as $t, QUESTION_EXTRACTION_VERSION as A, Attestation as Ai, QuestionRevision as An, CourseCoverChangeCollection as Ar, WorkbookPhotoDecision as At, SELF_ATTESTABLE_LESSON_KINDS as B, CourseAsset as Bi, normalizeQuestionContent as Bn, CourseUpgradeChoiceCollection as Br, WorkCompletionDecision as Bt, MAX_EXTRACTED_OMISSIONS as C, ReadingCollection as Ci, LearningObjectiveCollection as Cn, MAX_REFUSAL_EXPLANATION_LENGTH as Cr, canonicalGroupSharingPolicy as Ct, MAX_EXTRACTED_STUDENT_WORK as D, SubmissionIntakeBindingCollection as Di, QuestionCollection as Dn, CourseArchiveDecision as Dr, BookContextRevisionCollection as Dt, MAX_EXTRACTED_SECTIONS as E, SubmissionCollection as Ei, Question as En, readDelegation as Er, BookContextRevision as Et, normalizeWorkObservedLayout as F, Submission as Fi, resolveEnrollmentLessons as Fn, CourseRelease as Fr, FamilyIntakeAsset as Ft, EnrollmentNotActiveError as G, LESSON_ASSET_RELATIONSHIPS as Gi, MAX_PRINTABLE_ITEMS as Gn, EnrollmentAssignmentRecord as Gr, LessonWorkFiling as Gt, SubmissionStorageCleanupError as H, EXTERNAL_VIDEO_PROVIDERS as Hi, GENERATED_PRINTABLE_RELATIONSHIPS as Hn, CourseVisibilityChangeCollection as Hr, readWorkCompletion as Ht, normalizeWorkAnswerRegions as I, SubmissionAsset as Ii, applyProposalFields as In, CourseReleaseCollection as Ir, FamilyIntakeAssetCollection as It, InterestThemeError as J, LessonExternalVideoCollection as Ji, MAX_PRINTABLE_TOTAL_TEXT as Jn, EnrollmentAutoUpgradeSkipCollection as Jr, WorkAssessmentRevisionCollection as Jt, EnrollmentPaceError as K, LessonAsset as Ki, MAX_PRINTABLE_PAGES as Kn, EnrollmentAssignmentRecordCollection as Kr, LessonWorkFilingCollection as Kt, LessonWorkWorkflowRequiredError as L, SubmissionIntakeBinding as Li, draftFromRevision as Ln, CourseRevision as Lr, FamilyIntakeCollection as Lt, QuestionExtractionPendingError as M, Grade as Mi, CourseAuthoringService as Mn, CourseDiscardTombstoneCollection as Mr, WorkbookPhotoIntake as Mt, QuestionExtractionValidationError as N, LessonSelfAttestation as Ni, authoredVisibilityPermitsGroups as Nn, CourseDraftHead as Nr, WorkbookPhotoIntakeCollection as Nt, MAX_EXTRACTED_TEXT as O, VideoCollection as Oi, QuestionResponse as On, CourseArchiveDecisionCollection as Or, WorkbookPhotoAsset as Ot, LessonWorkService as P, ScannedSubmission as Pi, effectiveCourseVisibility as Pn, CourseDraftHeadCollection as Pr, FamilyIntake as Pt, LearnerValidationError as Q, parseYouTubeReference as Qi, PrintableStorageNotConfiguredError as Qn, LessonRevisionCollection as Qr, WorkFeedbackDeliveryCollection as Qt, OwnAttestationConflictError as R, parseQuestionRubric as Ri, planProposal as Rn, CourseRevisionCollection as Rr, FamilyIntakeDecision as Rt, MAX_EXTRACTED_ANCHORS as S, LessonSelfAttestationCollection as Si, LearningObjective as Sn, MAX_REFUSAL_CATEGORY_LENGTH as Sr, GroupSharingPolicyValidationError as St, MAX_EXTRACTED_QUESTIONS as T, SubmissionAssetCollection as Ti, LearningObjectiveRevisionCollection as Tn, delegationRecordFields as Tr, familyIntakeScopeId as Tt, submissionPageKey as U, GUARDIAN_ONLY_RELATIONSHIPS as Ui, LearningObjectiveKeyConflictError as Un, CurriculumCreationRecord as Ur, LessonActivity as Ut, SubmissionService as V, CourseAssetCollection as Vi, GENERATED_PRINTABLE_AUDIENCES as Vn, CourseVisibilityChange as Vr, WorkCompletionDecisionCollection as Vt, ENROLLMENT_STATUS_TRANSITIONS as W, LESSON_ASSET_DEFAULT_AUDIENCE as Wi, LearningObjectiveNotFoundError as Wn, CurriculumCreationRecordCollection as Wr, LessonActivityCollection as Wt, LearnerHistoryUnavailableError as X, externalVideoEmbedUrl as Xi, PrintableOwnerError as Xn, EnrollmentReleasePinCollection as Xr, WorkAttemptCollection as Xt, LearnerAuthorizationError as Y, MATERIAL_RENDITIONS as Yi, MAX_RESOLVE_QUESTION_REVISIONS as Yn, EnrollmentReleasePin as Yr, WorkAttempt as Yt, LearnerService as Z, materialAudience as Zi, PrintableStorageCleanupError as Zn, LessonRevision as Zr, WorkFeedbackDelivery as Zt, EXTRACTED_OMISSION_REASONS as _, GradeCollection as _i, renderGuardianWorksheetPdf as _n, CourseProposalFieldError as _r, LessonProgressTransitionCollection as _t, questionExtractionProposalDigest as a, interestThemeFromColumns as aa, mostRestrictiveLicense as ai, aggregateWorkQuestions as an, QuestionReferenceError as ar, MIN_LESSONS_PER_WEEK as at, EXTRACTED_QUESTION_STATUSES as b, LessonCollection as bi, GeneratedPrintable as bn, MAX_GENERATION_GRADE as br, GroupSharingAuthorizationError as bt, QuestionExtractionRecord as c, COURSE_VISIBILITIES as ca, AttestationCollection as ci, LessonPage as cn, COURSE_GENERATION_STATUSES as cr, EnrollmentPaceChange as ct, extractedQuestionContent as d, Exercise as da, CourseSectionCollection as di, validLessonPageKey as dn, CourseGenerationBriefError as dr, EnrollmentStatusChangeCollection as dt, Enrollment as ea, ReleasedLessonCollection as ei, WorkFeedbackReleaseCollection as en, QUESTION_FIELDS as er, LessonPutBackError as et, matchExtractedQuestions as f, ExternalCourse as fa, CurriculumCollection as fi, QuestionAuthoringService as fn, CourseGenerationInProgressError as fr, LearnerInterestThemeChange as ft, EXTRACTED_MEDIA_KINDS as g, Worksheet as ga, ExternalCourseCollection as gi, worksheetVersionId as gn, CourseGeneratorNotConfiguredError as gr, LessonProgressTransition as gt, EXTRACTED_LEGIBILITY as h, Video as ha, ExternalCompletionCollection as hi, canonical$2 as hn, CourseGenerationRefusal as hr, LessonCompletionRecordCollection as ht, QuestionExtractionService as i, MAX_INTEREST_THEME_LABEL_CHARS as ia, isGroupShareableLicense as ii, WorkCompletionConflictError as in, QuestionNotFoundError as ir, MAX_NEXT_LESSONS_COUNT as it, QuestionExtractionNotFoundError as j, ExternalCompletion as ji, QuestionRevisionCollection as jn, CourseDiscardTombstone as jr, WorkbookPhotoDecisionCollection as jt, QUESTION_EXTRACTION_SOURCE_KINDS as k, WorksheetCollection as ki, QuestionResponseCollection as kn, CourseCoverChange as kr, WorkbookPhotoAssetCollection as kt, QuestionExtractionRecordCollection as l, Course as la, AuthoredCourseCollection as li, LessonPageCollection as ln, COURSE_GENERATION_TARGETS as lr, EnrollmentPaceChangeCollection as lt, questionExtractionSourceIdentity as m, Reading as ma, ExerciseCollection as mi, buildLearnerWorksheet as mn, CourseGenerationOutputError as mr, LessonCompletionRecord as mt, preservePhotoProposal as n, LessonProgress as na, AUTHORED_RELEASE_SOURCE_KEY as ni, WORK_MATH_VOCABULARY as nn, QUESTION_PROVENANCES as nr, LessonWorkOwnedError as nt, ExtractedQuestionLink as o, parseInterestTheme as oa, CatalogAuthorizationError as oi, validateWorkAssessment as on, WorksheetVersionMismatchError as or, enrollmentView as ot, normalizeQuestionExtraction as p, Lesson as pa, EnrollmentCollection as pi, buildGuardianWorksheet as pn, CourseGenerationNotFoundError as pr, LearnerInterestThemeChangeCollection as pt, EnrollmentStatusTransitionError as q, LessonExternalVideo as qi, MAX_PRINTABLE_TEXT_LENGTH as qn, EnrollmentAutoUpgradeSkip as qr, WorkAssessmentRevision as qt, validateWorkbookPhotoIdentification as r, MAX_INTEREST_THEME_KEY_CHARS as ra, isAuthoredReleaseGroupShareable as ri, WorkAssessmentValidationError as rn, QUESTION_RESPONSE_TYPES as rr, MAX_LESSONS_PER_WEEK as rt, ExtractedQuestionLinkCollection as s, AuthoredCourse as sa, CatalogService as si, workAggregateFeedback as sn, groupSharingDigest as sr, requirePolicyBasis as st, WorkbookPhotoService as t, Learner as ta, AUTHORED_IDENTITY_SOURCE_KEY as ti, LessonWorkPlacementError as tn, QUESTION_PROMPT_SUMMARY_LENGTH as tr, LessonSkippedError as tt, QUESTION_EXTRACTION_ARGS_SCHEMA as u, CourseSection as ua, CourseCollection as ui, lessonPageView as un, COURSE_GENERATION_TERMINAL as ur, EnrollmentStatusChange as ut, EXTRACTED_ORIGINS as v, LearnerCollection as vi, renderLearnerWorksheetPdf as vn, CourseProposalNotFoundError as vr, evaluateGroupSharingPolicy as vt, MAX_EXTRACTED_PAGES as w, ScannedSubmissionCollection as wi, LearningObjectiveRevision as wn, delegationActorFields as wr, validateGroupSharingPolicy as wt, EXTRACTED_READABILITY as x, LessonProgressCollection as xi, GeneratedPrintableCollection as xn, MAX_GENERATION_OBJECTIVE_CHARS as xr, GroupSharingConflictError as xt, EXTRACTED_PAGE_ROLES as y, LessonAssetCollection as yi, es_exports as yn, MAX_GENERATION_BRIEF_FIELD_CHARS as yr, summarizeWorkEvidence as yt, OwnAttestationLessonKindError as z, validateRubricResults as zi, validateCourseGenerationOutput as zn, CourseUpgradeChoice as zr, FamilyIntakeDecisionCollection as zt };
36942
36978
 
36943
- //# sourceMappingURL=workbook-photo-service-DHP5ja82.js.map
36979
+ //# sourceMappingURL=workbook-photo-service-wqZZWGLb.js.map