@happyvertical/smrt-education 0.60.3 → 0.60.5

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.
@@ -3,14 +3,14 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-education",
6
- "packageVersion": "0.60.3",
6
+ "packageVersion": "0.60.5",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "ba14754ecf7784f0da6d8e6ad58c216442ff2c9eaa74e33c38eec57adddd29eb",
11
- "packageJson": "8f9d1230deb3477808cb8f5c0b066712c63e8fa4a737285fa25ff8589f012e1a",
10
+ "manifest": "fe462568374ef4ba509893c8c3f5b6c9d4abcd3bc14872b47f78c5ed6b584b3c",
11
+ "packageJson": "030b416125352caf14142da4884dc177bcf5fd2b354e3feb025ee1c7006116b2",
12
12
  "agents": "44f253815661ccb7ef613ae3a517a31c022c583940ba2afce2974017c32a972d",
13
- "moduleDoc:agents/lesson-materials.md": "4647e654fe3f5db0dc786e5ef7126ced5af9a7372805ddff8ebf8ded41bb5c1a",
13
+ "moduleDoc:agents/lesson-materials.md": "f58c0fc813a366c1d69bb636a67c99996996ad90d5b6b3ccc724ea1034e4d0d0",
14
14
  "moduleDoc:agents/learner-service.md": "d7edf68000aec96b52920fb5e8dc4ff4e20b1d81b34413809e8212a885e1c3f9",
15
15
  "moduleDoc:agents/submission-service.md": "0bb827a62848a3e0708fe2545eac7b64f829a6c9be96eee849c8f16c1fa631d0",
16
16
  "moduleDoc:agents/lesson-work-service.md": "286dd0197b9bc8471636d58b67f9e11591a175eb5bafb1f7af03d5fee988b86d",
@@ -20,8 +20,8 @@
20
20
  "moduleDoc:agents/course-generation-review.md": "7f6d5e67ed56ec9923bd00df05650b363fb0db30c1b1f004c8b203f9c62cd117",
21
21
  "moduleDoc:agents/question-records.md": "c1648cccc27ed46b1b60cded93b20c7f3812cbcd2c37f183fd727ac030364703",
22
22
  "moduleDoc:agents/question-extraction.md": "75972dda9b2f837471fb42f963ef143a098005c919a1a49db7ce395a5e5e50a7",
23
- "moduleDoc:agents/course-bundle-import.md": "a40a48b753adecb444dc53b7489d93beb76b9441911a041739eeb708e93a7569",
24
- "moduleDoc:agents/class-crunchlabs.md": "b48067f693f64eac3a0ef812c6c5c09fd77c2aab9120b3427940b08d87eaba41",
23
+ "moduleDoc:agents/course-bundle-import.md": "e567503d69c630f3ab2b6c6b7a7eb7ce3bb2b15019fd49c01a9ca75a5ce82d2b",
24
+ "moduleDoc:agents/class-crunchlabs.md": "c8490ede1897d7c74805d2769f9531dfb37e25c65e409ac8465935b0c8e7af2f",
25
25
  "moduleDoc:agents/source-import.md": "30ee823589d655442bcf1abcd86a2132172065b4bc5e3c4d89b1e8b7afcc66bc",
26
26
  "moduleDoc:agents/family-intake.md": "540b6d37f438496acb0208b6ac7258264e2142bce3d2f986967b7741606fe4f7",
27
27
  "moduleDoc:agents/catalog-previews.md": "5f4e0352274009b3ce00408017a16666036d24df695a7078c27317ba4e66f0a7",
@@ -10884,6 +10884,7 @@
10884
10884
  {
10885
10885
  "name": "LessonAsset",
10886
10886
  "qualifiedName": "@happyvertical/smrt-education:LessonAsset",
10887
+ "displayLabelField": "title",
10887
10888
  "collection": "lessonassets",
10888
10889
  "tableName": "lesson_assets",
10889
10890
  "packageName": "@happyvertical/smrt-education",
@@ -10951,6 +10952,12 @@
10951
10952
  "related": "@happyvertical/smrt-assets:Asset",
10952
10953
  "columnType": "UUID"
10953
10954
  },
10955
+ {
10956
+ "name": "title",
10957
+ "type": "text",
10958
+ "required": false,
10959
+ "columnType": "TEXT"
10960
+ },
10954
10961
  {
10955
10962
  "name": "retiredAt",
10956
10963
  "type": "datetime",
@@ -10962,6 +10969,24 @@
10962
10969
  "type": "text",
10963
10970
  "required": false,
10964
10971
  "columnType": "TEXT"
10972
+ },
10973
+ {
10974
+ "name": "stepKey",
10975
+ "type": "text",
10976
+ "required": false,
10977
+ "columnType": "TEXT"
10978
+ },
10979
+ {
10980
+ "name": "stepOrder",
10981
+ "type": "decimal",
10982
+ "required": false,
10983
+ "columnType": "REAL"
10984
+ },
10985
+ {
10986
+ "name": "stepLabel",
10987
+ "type": "text",
10988
+ "required": false,
10989
+ "columnType": "TEXT"
10965
10990
  }
10966
10991
  ],
10967
10992
  "relationships": [
@@ -11283,6 +11308,24 @@
11283
11308
  "type": "text",
11284
11309
  "required": false,
11285
11310
  "columnType": "TEXT"
11311
+ },
11312
+ {
11313
+ "name": "stepKey",
11314
+ "type": "text",
11315
+ "required": false,
11316
+ "columnType": "TEXT"
11317
+ },
11318
+ {
11319
+ "name": "stepPosition",
11320
+ "type": "decimal",
11321
+ "required": false,
11322
+ "columnType": "REAL"
11323
+ },
11324
+ {
11325
+ "name": "stepLabel",
11326
+ "type": "text",
11327
+ "required": false,
11328
+ "columnType": "TEXT"
11286
11329
  }
11287
11330
  ],
11288
11331
  "relationships": [
@@ -16406,7 +16449,7 @@
16406
16449
  {
16407
16450
  "path": "agents/lesson-materials.md",
16408
16451
  "module": "lesson-materials",
16409
- "content": "# Lesson materials and audience\n\nDesign record: [course work](course-work-design.md). Models are closed (no REST,\nCLI, MCP or AI surface); reads and writes go through `SubmissionService` (and its\n`LessonWorkService` subclass) with the usual actor, learner and enrollment checks.\n\n## Roles and audience\n\n`LessonAssetRelationship` (`LESSON_ASSET_RELATIONSHIPS`): `teacher-guide`, `slides`,\n`learner-pages`, `assessment`, `assessment-guidance`, `handout`, `printable`,\n`answer-key`, `video`, `attachment`. Each `LessonAsset`, `CourseAsset` and\n`LessonExternalVideo` row has `audience: 'learner' | 'guardian'`; `''` means the role\ndefault (`LESSON_ASSET_DEFAULT_AUDIENCE`): guardian for teacher-guide, slides,\nassessment-guidance and answer-key, learner otherwise. `materialAudience(role,\naudience?)` resolves and validates it. `answer-key` and `assessment-guidance`\n(`GUARDIAN_ONLY_RELATIONSHIPS`) can never be learner-audience and, like any join in\na personal-use-copy course, require a private asset under the host's exact\nactor/owner/asset capability.\n\n`CourseAsset` holds course-level material (supply list, posters) with the same\nroles, audience and private-asset rule, plus an optional title.\n\n## Rendition and thumbnail (SMRT #4632)\n\n`LessonAsset` and `CourseAsset` carry `rendition` (`''` a file as supplied,\n`original` an editable Word/PowerPoint file, `rendered` the PDF rendered from\none), `renditionOfAssetId` (a rendering's original) and `thumbnailAssetId` (an\noptional first-page image, null when no converter drew one). The model refuses\nan `original` with a learner audience: the learner gets the rendering, never the\neditable file. A new thumbnail must be an asset of the same family, and private\nunder the host's capability (same owner as the material) whenever the material\nmust be private.\n\n## Learner never sees guardian material\n\n`listLessonMaterials({enrollmentId, lessonId})` and `listCourseMaterials({enrollmentId})`\nreturn `LessonMaterialView`s (`id`, `kind` file or external-video, `scope`, role,\naudience, title, order, `assetId` or provider/videoId/`embedUrl`, and for files\n`mimeType` from the stored asset, `rendition`, `pairedMaterialId` (the other half\nof an original/rendered pair, only when this actor sees both) and\n`thumbnailAssetId`). Hosts read these instead of guessing by file extension. The learner\nthemself sees only learner-audience rows; an authorized guardian sees all. The\ncatalog projection without answer keys (every learner-facing catalog read) applies\nthe same rule. `signLessonMaterial`/`signCourseMaterial` sign only a file in the\nactor's own list (claim scope `lesson:<id>` / `course:<id>`); a guessed id of\nguardian material is denied; `thumbnail: true` signs the thumbnail of a visible\nmaterial instead. `signLessonPrintable` applies the same audience rule to the learner. Storage URIs never appear.\n\n`setMaterialAudience({enrollmentId, scope, kind, materialId, audience})` lets a\nguardian with plan authority change a family-owned row within the role's bounds\n(an `original` stays guardian). Bundle courses (`sourceKey` `course-bundle`) are\nfamily-owned, so this is the after-attach path; before attach use\n`CourseBundleImportService.chooseMaterialAudience`.\nAuthored courses change audience through course authoring (`CourseMaterialInput.audience`,\ncarried to the released join); shared or other families' rows are denied.\n\n## External videos\n\n`LessonExternalVideo` references a video at an allowlisted provider (YouTube only):\n`videoId` (11 characters), title, `stepOrder` and audience. It is never downloaded\nand has no Asset row. `parseYouTubeReference` accepts a bare id or an https\nyoutube.com / m. / music. / youtu.be / youtube-nocookie.com URL (watch, embed,\nshorts, live) and nothing else; `externalVideoEmbedUrl` returns the\nyoutube-nocookie embed URL. `addLessonVideo({enrollmentId, lessonId, reference,\ntitle, stepOrder, audience?})` adds one to a family-owned lesson (plan authority);\nthe same video with the same details replays, other details conflict, and a\nnon-allowlisted reference throws `ExternalVideoReferenceError`.\n\nTests: `src/__tests__/lesson-materials.test.ts`.\n"
16452
+ "content": "# Lesson materials and audience\n\nDesign record: [course work](course-work-design.md). Models are closed (no REST,\nCLI, MCP or AI surface); reads and writes go through `SubmissionService` (and its\n`LessonWorkService` subclass) with the usual actor, learner and enrollment checks.\n\n## Roles and audience\n\n`LessonAssetRelationship` (`LESSON_ASSET_RELATIONSHIPS`): `teacher-guide`, `slides`,\n`learner-pages`, `assessment`, `assessment-guidance`, `handout`, `printable`,\n`answer-key`, `video`, `attachment`. Each `LessonAsset`, `CourseAsset` and\n`LessonExternalVideo` row has `audience: 'learner' | 'guardian'`; `''` means the role\ndefault (`LESSON_ASSET_DEFAULT_AUDIENCE`): guardian for teacher-guide, slides,\nassessment-guidance and answer-key, learner otherwise. `materialAudience(role,\naudience?)` resolves and validates it. `answer-key` and `assessment-guidance`\n(`GUARDIAN_ONLY_RELATIONSHIPS`) can never be learner-audience and, like any join in\na personal-use-copy course, require a private asset under the host's exact\nactor/owner/asset capability.\n\n`CourseAsset` holds course-level material (supply list, posters) with the same\nroles, audience and private-asset rule, plus an optional title.\n\n## Rendition and thumbnail (SMRT #4632)\n\n`LessonAsset` and `CourseAsset` carry `rendition` (`''` a file as supplied,\n`original` an editable Word/PowerPoint file, `rendered` the PDF rendered from\none), `renditionOfAssetId` (a rendering's original) and `thumbnailAssetId` (an\noptional first-page image, null when no converter drew one). The model refuses\nan `original` with a learner audience: the learner gets the rendering, never the\neditable file. A new thumbnail must be an asset of the same family, and private\nunder the host's capability (same owner as the material) whenever the material\nmust be private.\n\n## Learner never sees guardian material\n\n`listLessonMaterials({enrollmentId, lessonId})` and `listCourseMaterials({enrollmentId})`\nreturn `LessonMaterialView`s (`id`, `kind` file or external-video, `scope`, role,\naudience, title (the bundle's material title, else the stored asset's name), order, `assetId` or provider/videoId/`embedUrl`, and for files\n`mimeType` from the stored asset, `rendition`, `pairedMaterialId` (the other half\nof an original/rendered pair, only when this actor sees both) and\n`thumbnailAssetId`). Hosts read these instead of guessing by file extension. The learner\nthemself sees only learner-audience rows; an authorized guardian sees all. The\ncatalog projection without answer keys (every learner-facing catalog read) applies\nthe same rule. `signLessonMaterial`/`signCourseMaterial` sign only a file in the\nactor's own list (claim scope `lesson:<id>` / `course:<id>`); a guessed id of\nguardian material is denied; `thumbnail: true` signs the thumbnail of a visible\nmaterial instead. `signLessonPrintable` signs a lesson file for printing: a `printable`, `handout` or `learner-pages` row (handout and learner pages only when the stored asset is `application/pdf`), learner-audience for a learner and any audience for a guardian. A retired row, an `original` rendition (the editable file) and any other role, answer keys included, are refused (SMRT #4657). Storage URIs never appear.\n\n`setMaterialAudience({enrollmentId, scope, kind, materialId, audience})` lets a\nguardian with plan authority change a family-owned row within the role's bounds\n(an `original` stays guardian). Bundle courses (`sourceKey` `course-bundle`) are\nfamily-owned, so this is the after-attach path; before attach use\n`CourseBundleImportService.chooseMaterialAudience`.\nAuthored courses change audience through course authoring (`CourseMaterialInput.audience`,\ncarried to the released join); shared or other families' rows are denied.\n\n## Lesson steps (SMRT #4656)\n\n`listLessonSteps({enrollmentId, lessonId})` walks a multi-step lesson in order,\nfiltered like `listLessonMaterials` (same actor and enrollment checks, learner\naudience only for a learner, retired rows left out, same `LessonMaterialView`s):\n\n```ts\n{ setup: LessonMaterialView[]; // stepless printable / handout / learner-pages files\n steps: Array<{ key: string; label: string; // label falls back to the key\n kind: 'present' | 'watch' | 'do' | 'check';\n order: number; // 1b sorts after 1\n materials: LessonMaterialView[] }> }\n```\n\nSteps come from the `step_key`/`step_order`/`step_label` recorded on lesson files and\n(`step_position`) external videos by a course bundle ([manifest steps](course-bundle-import.md)).\nMaterials sharing a key are one step (the original and rendered halves of a deck stay\ntogether); steps sort by order then key; `kind` is `present` if any material is\n`slides`, else `watch` if any is an external video, else `check` if any is an\n`assessment`, else `do`. A lesson with no declared steps returns `steps: []`.\nCourse-level materials (supply lists) are not part of a lesson listing and stay with\n`listCourseMaterials`.\n\n## External videos\n\n`LessonExternalVideo` references a video at an allowlisted provider (YouTube only):\n`videoId` (11 characters), title, `stepOrder` and audience. It is never downloaded\nand has no Asset row. `parseYouTubeReference` accepts a bare id or an https\nyoutube.com / m. / music. / youtu.be / youtube-nocookie.com URL (watch, embed,\nshorts, live) and nothing else; `externalVideoEmbedUrl` returns the\nyoutube-nocookie embed URL. `addLessonVideo({enrollmentId, lessonId, reference,\ntitle, stepOrder, audience?})` adds one to a family-owned lesson (plan authority);\nthe same video with the same details replays, other details conflict, and a\nnon-allowlisted reference throws `ExternalVideoReferenceError`.\n\nTests: `src/__tests__/lesson-materials.test.ts`.\n"
16410
16453
  },
16411
16454
  {
16412
16455
  "path": "agents/learner-service.md",
@@ -16456,12 +16499,12 @@
16456
16499
  {
16457
16500
  "path": "agents/course-bundle-import.md",
16458
16501
  "module": "course-bundle-import",
16459
- "content": "# Course bundle import\n\n`CourseBundleImportService` (SMRT #4578, extends `CourseAuthoringService`,\nconsumed by Hearth #94) imports a course bundle, a zip with an optional manifest\nor a plain file set, as bounded evidence, hands its supported members to the\nhost for ingestion extraction and proposes sections and lessons for review.\nRisk is high: untrusted archive parsing and new schema. It reuses the course\nauthorizer: `importBundle` needs `education.course.author`, `readImport`\n`education.course.read-draft`, `proposeStructure` `education.course.generate`\n(via `CourseGenerationReviewService`). Operations: `import-course-bundle`,\n`read-course-bundle`. Server only (Node `zlib`/`crypto`); no new dependency.\n\n## Flow\n\n1. `importBundle({ ...actor, bundle, limits?, onProgress?, receiveMember? })`\n authorizes **before parsing**, unpacks (stage `unpack`), classifies and lays\n out the structure (`classify`), writes the evidence in one transaction and\n then calls `receiveMember` for each accepted member (`extract`), outside any\n transaction. `bundle` is `{ archive: Uint8Array }` or `{ files: [{ path,\n bytes }] }`. Hostile or damaged input throws `CourseBundleRefusedError`\n (`reason`, optional `entry`) and writes nothing.\n2. `receiveMember(member, { replayed })` is the host's ingestion seam: call\n `IngestionService.receive` with `sourceKind: 'course_import'` and the member\n bytes, then queue extraction and `preserveQuestionExtractionAnalysis` as in\n [question extraction](question-extraction.md). Members carry `role`\n (manifest page role), `lessonKey`, `sectionKey`. It runs on a replay too\n (ingestion dedups by content hash), so an interrupted first run resumes. Its\n return values are in `result.receipts`.\n3. `proposeStructure({ ...actor, importId, bundle, courseId,\n expectedHeadRevisionId })` (stage `propose`) re-supplies the bundle (bytes\n are never retained), checks it is the imported one, and runs a deterministic\n `CourseMaterialGenerator` (`smrt-bundle-import`) through\n `requestGeneration`/`runGeneration`. The result is an ordinary proposal: a\n new draft revision of additions and fills; conflicts wait for\n `decideProposal`. Nothing is released, assigned or published. The request key\n is `bundle:<courseId>:<bundleIdentity>`, so a retry returns the same job; the\n same key with another head is `CourseRequestKeyConflictError`.\n4. `readImport({ ...actor, importId })` returns the recorded evidence.\n\nProgress is an observer callback (`onProgress({ stage, completed, total })`,\nstages `unpack`, `classify`, `render`, `extract`, `propose`; each starts at `completed: 0`;\n`render` is emitted only when the bundle has Word/PowerPoint members).\nIngestion has no public job-progress seam, so none is invented. A throwing\nlistener never affects the import.\n\n## Hard bounds (`COURSE_BUNDLE_LIMITS`; a host may only lower them)\n\n| limit | value |\n|---|---|\n| `maxArchiveBytes` (input) | 100 MiB |\n| `maxEntries` | 200 |\n| `maxEntryBytes` (inflated) | 50 MiB |\n| `maxTotalBytes` (inflated) | 200 MiB |\n| `maxCompressionRatio` (members over `ratioFloorBytes` = 64 KiB) | 100 |\n| `maxPathBytes` / `maxSegmentBytes` / `maxPathDepth` | 512 / 255 / 8 |\n| `maxManifestBytes` | 256 KiB |\n| `maxLessons` (the proposal schema's cap) | 50 |\n| `maxLessonBodyChars` | 100 000 |\n| `maxOfficeMembers` (Word/PowerPoint members rendered per bundle) | 50 |\n| `maxRenderedBytes` (PDF accepted back from the converter) | 50 MiB |\n| `conversionTimeoutMs` (one Office member) | 60 000 |\n| `maxThumbnailBytes` (first-page image accepted back from the converter) | 5 MiB |\n| archive nesting | 1: a zip inside a zip is never expanded |\n\nA lesson whose combined text members exceed `maxLessonBodyChars` refuses the\nbundle (`lesson_too_large`, `entry` = lesson key) at import; text is never\ntrimmed or dropped. A plain file set enforces `maxTotalBytes` on its member\nbytes exactly as an archive does on inflated bytes.\n\nThe reader (`course-bundle-zip.ts`) parses the end record and central directory,\nvalidates every offset and size against the real buffer, inflates with\n`maxOutputLength` equal to the declared size (declared sizes are never trusted:\na mismatch either way is `size_mismatch`), checks CRC32 and re-checks the total\non actual output. Stored and deflate only. Refusals (`CourseBundleRefusalReason`)\ncover: not a zip, truncated, ambiguous end record (two end records that both fit\nthe file, for example one forged in a comment), zip64, multi-disk, central\ndirectory not ending at the end record, entries out of range or overlapping,\nlocal header name/method mismatch, encrypted entries (flags 0/6/13, AES method),\nsymlink and special files (unix mode; DOS reparse flag), unsupported method,\nbad CRC, ratio, entry count, totals, duplicate names (case/NFC-insensitive,\nbackslashes normalised first) and bad paths (`..`, `.`, empty segments, absolute,\ndrive letter or `:` anywhere, NUL/control/bidi characters, lone surrogates,\ntoo long or deep). An **honestly** oversize member (declared over\n`maxEntryBytes`) is not inflated and is recorded `refused / entry_too_large`.\n\n## Members and statuses\n\nBy content (never by extension alone): `docx`/`pptx`/`xlsx` (see below), `pdf` (`%PDF-` in the first 1 KiB),\n`image` (PNG, JPEG, WebP), `text` (`.txt`/`.md`, strict UTF-8, no control\ncharacters, body-sized), `manifest` (root `manifest.json`), else `unsupported`.\nStatus: `accepted` (handed to extraction), `duplicate` (same sha256 as an earlier\nmember in canonical order; recorded once as evidence, `duplicateOfPath`, never\nextracted again), `unsupported` (`unsupported_type`, `nested_archive`,\n`unreadable_text`), `refused` (`encrypted_pdf`, `entry_too_large`), `metadata`\n(the manifest). Nothing is dropped. An encrypted PDF is any PDF with an\n`/Encrypt` token anywhere in its bytes (trailer, xref stream): a false positive\non literal text inside an uncompressed stream is refused, never read.\n\n## Word and PowerPoint members (SMRT #4615)\n\nA `.docx`/`.pptx`/`.xlsx` member is an OOXML package only when the name and the\ncontent agree: a zip whose entries include `[Content_Types].xml` and\n`word/document.xml` (docx), `ppt/presentation.xml` (pptx) or `xl/workbook.xml`\n(xlsx). A workbook (a supply list) is rendered and kept like the others but has\nno text reader. Before the bytes go anywhere, the\npackage itself passes the same bounded zip reader (ratio, overlap, paths, CRC,\n100 MiB inflated); a failure is `refused / unsafe_office`, never a refusal of the\nwhole bundle. Other zips stay `unsupported / nested_archive`.\n\nTwo separate things happen to an accepted Office member, both after\nauthorization and before the evidence is written:\n\n- **Pages**: a `DocumentConverter` (`document-converter.ts`) renders the original\n to PDF. The importer enforces the byte and time limits itself (input\n `maxEntryBytes`, output `maxRenderedBytes`, `conversionTimeoutMs` with an abort\n signal and a hard race, so an adapter that ignores the signal still times out),\n and rejects output that is not a PDF or is encrypted. The converter is told a\n neutral file name (`document.docx`), never the bundle path. Identical documents\n are rendered once.\n- **Text**: an injectable `OfficeTextReader` (best effort; a failure or a `null`\n costs the member nothing) returns whole-document text and, for decks, text and\n speaker notes per slide. Office text is handed to the host on the accepted\n member (`officeText`) and is **never** inlined into a proposed lesson body:\n teacher guides and notes carry answers, and audience belongs to manifest v2.\n\n`receiveMember` gets the original `bytes` (store them as the private original\nasset: \"Open in PowerPoint\" for decks) and `rendered` (`{ bytes, sha256, mediaType:\n'application/pdf', converterId }`, store as a private asset beside the original:\nthe pages for \"Present\"/reading). The member row records `renderedSha256` and\n`renderedSizeBytes`; bytes are still not retained here. A converter may also\nreturn a first-page image (`DocumentConversionResult.thumbnail`); the importer\nkeeps it only when it is a PNG/JPEG/WebP within `maxThumbnailBytes` and hands it\nover as `rendered.thumbnail` (`{ bytes, sha256, mediaType }`), else null. An\nunusable thumbnail never fails the member. Gotenberg draws none, so it is null.\n\n**Fail closed.** Any converter problem records that member `unsupported` with a\nreason and writes the rest of the bundle normally: `converter_unavailable` (none\nconfigured), `converter_failed` (unreachable or 5xx), `converter_timeout`,\n`converter_too_large`, `converter_rejected` (HTTP 4xx, not a PDF, encrypted),\n`office_limit` (beyond `maxOfficeMembers`, in natural path order). A member that\nfailed is not a lesson in the proposal, and identical copies of a failed document\nfail the same way rather than becoming duplicates of nothing. Outcomes are part\nof the append-only evidence, so a replay does not retry a recorded failure (to\nretry, change the bundle). A replay does re-render recorded-accepted members so the\nhost receives the bytes again; if it can no longer render one it throws\n`DocumentConversionError` rather than silently dropping an accepted member.\n\n### Host configuration\n\n```ts\nconst service = new CourseBundleImportService(db, authorizer, {\n converter: new GotenbergDocumentConverter({\n baseUrl: process.env.GOTENBERG_URL, // for example http://gotenberg:3000\n timeoutMs: 60_000,\n maxBytes: 50 * 1024 * 1024,\n }),\n officeTextReader, // optional, see below\n});\n```\n\n`GotenbergDocumentConverter` posts to `<baseUrl>/forms/libreoffice/convert`, talks\nto nothing else, refuses redirects and URLs with credentials, and caps the response\nas it streams. Run Gotenberg as an internal-only service with **no outbound\nnetwork**, its own size/time limits and no shared volume with the app; this\npackage assumes untrusted files never run next to the application. Tests use\n`FakeDocumentConverter` (records calls; `failWith`, `failWhen`, `delayMs`, `output`).\n\n`OfficeTextReader` reads Office text through the SDK without this package\ndepending on `@happyvertical/documents`: the host imports `extractOffice`\n(released in `@happyvertical/documents` 0.111.4, sdk#1486, Kreuzberg) and passes\nit to `createOfficeTextReader(extractOffice)`, which returns whole-document\ntext for Word and text plus speaker notes per slide for PowerPoint (images are\nnever requested). An absent engine reads as `null`; a corrupt file rejects and\nthe importer treats that as \"no text\". The repository's current `catalog:`\npin is 0.111.3, which predates the Office processors, so the host bumps the SDK\nset (and its minimum release age) before it can import `extractOffice`.\n\n## Manifest (`manifest.json`, version 1, closed)\n\n```json\n{ \"version\": 1, \"course\": { \"subject\": \"Math\" },\n \"sections\": [ { \"key\": \"unit-1\", \"title\": \"Unit One\",\n \"lessons\": [ { \"key\": \"sums\", \"title\": \"Sums\",\n \"members\": [ { \"path\": \"sheets/sums.pdf\", \"role\": \"exercise\" } ] } ] } ] }\n```\nEvery object is closed: an unknown key anywhere is `manifest_unknown_key` (with\nits path in `entry`); another version is `manifest_version`. Keys match\n`[A-Za-z0-9._:-]{1,120}` and are unique; roles are the extraction page roles\n(`exercise`, `example`, `instructions`, `cover`, `answer_key`, `mixed`,\n`unknown`); a listed path must exist and be listed once. The manifest is a source\nfact: roles are `source`, anything the extractor infers stays `inferred`, and a\nmanifest/extraction disagreement belongs in extraction review, not here. Without\na manifest each accepted member is one lesson, sections come from the top-level\ndirectory and order is natural path order (digit runs compare numerically, no\nlocale). Manifest order: the manifest, declared members, then the rest by path.\nLesson bodies are the readable text members of a lesson, joined; PDFs and images\nare never inlined. Members whose declared role is `answer_key` or `mixed` never\ncontribute text to a lesson body (they remain recorded evidence members); the\nproposal rationale only counts how many were withheld.\n\n## Manifest version 2 (SMRT #4615)\n\nA root `manifest.json` with `\"version\": 2` is laid out by the `manifest-v2`\nreader; version 1 keeps its own reader and page roles. The schema is closed in\nthe same way: an unknown key anywhere is `manifest_unknown_key` (with its path in\n`entry`), another version is `manifest_version`.\n\n```json\n{ \"version\": 2, \"course\": { \"subject\": \"Science\" },\n \"courseMembers\": [ { \"path\": \"unit/supplies.pdf\", \"role\": \"attachment\", \"title\": \"Supply list\" } ],\n \"sections\": [ { \"key\": \"unit\", \"title\": \"Unit\", \"lessons\": [ {\n \"key\": \"l1\", \"title\": \"Lesson 1\",\n \"members\": [ { \"path\": \"l1/log.docx\", \"role\": \"learner-pages\" },\n { \"path\": \"l1/deck.pptx\", \"role\": \"slides\" },\n { \"path\": \"l1/guidance.pdf\", \"role\": \"assessment-guidance\" } ],\n \"pages\": [ { \"member\": \"l1/log.docx\", \"from\": 1, \"to\": 4, \"label\": \"Mission log\" } ],\n \"videos\": [ { \"reference\": \"dQw4w9WgXcQ\", \"title\": \"Recap\", \"step\": 1 } ] } ] } ] }\n```\n\n- **Members** (lesson `members` and root `courseMembers`): `path` (must exist, listed\n once), `role` (required) one of `teacher-guide`, `slides`, `learner-pages`,\n `assessment`, `assessment-guidance`, `handout`, `printable`, `answer-key`,\n `attachment` (`manifest_role` otherwise; a video is a reference, never a file),\n optional `audience` and `title`. The audience is the role default\n (`LESSON_ASSET_DEFAULT_AUDIENCE`) unless stated; `answer-key` and\n `assessment-guidance` are always guardian (a learner audience is\n `manifest_audience`). Members not listed stay recorded evidence with no role.\n- **Pages**: `{ member, from, to, label? }` ranges over a member of the same lesson\n (PDF, Word or PowerPoint; the rendered PDF for Office). Each page becomes a\n `LessonPage` keyed `p<n>`, labelled `<label> <n>` (default `Page <n>`), ordered by\n declaration. Bad, overlapping or unpageable ranges are `manifest_pages`; at most\n 500 pages per lesson.\n- **Videos**: `{ reference, title, step, audience? }` where `reference` is a bare id\n or an allowlisted https YouTube URL (`parseYouTubeReference`; anything else is\n `manifest_video`), `step` an integer 0-9999. They become `LessonExternalVideo`\n rows; nothing is downloaded.\n- **Course members** become `CourseAsset` rows, not tied to any lesson.\n- **Unit and parts** (SMRT #4632): `course.key` (a manifest key) names the unit\n and `course.title` its course title; `part: { index, count }` (1-50) marks one\n bundle of a unit split across several, and requires `course.key`\n (`manifest_invalid` otherwise). Sections and lessons may carry a unit-wide\n `order` (0-9999); without one the position in this manifest is used, and\n the order counts as implicit when parts are merged.\n\nGuardian-audience text (teacher guides, guidance, keys) never enters a proposed\nlesson body; only learner-audience text members do. Under v2 `role` on the member\nevidence row is the material role and `audience` the effective audience (NULL on\nrows recorded before v2). The plan carries `materials` (also on\n`CourseBundleImportResult.materials`): the resolved materials, pages and videos,\nplus `skipped` for declared members that were unsupported, refused or failed\nrendering. A duplicate member resolves to the earlier identical member's bytes\n(`assetPath`) and one material per (place, role, content).\n\n`receiveMember` also gets `audience` and `courseLevel`. **Slides** keep the original\ndeck (hand `bytes` to the host as a private asset: \"Open in PowerPoint\") and its\nrendered pages (`rendered`: \"Present\").\n\n### Attaching a family course (SMRT #4632)\n\n`attachCourse({ ...actor, importId, bundle, assets, courseId?, title?,\nlicenseKind? })` (`education.course.author`, operation `attach-course-bundle`) is\nthe published path from an import to a course: in one transaction it creates (or\nfinds) the family course, its sections and lessons, and writes the materials as\n`applyMaterials` does. `assets` maps member path to `{ original, rendered?,\nthumbnail? }` asset ids the host stored from `receiveMember`.\n\n- **Course**: a non-authored family draft (`sourceKey` `course-bundle`,\n `sourceRef` the unit key, `licenseKind` default `personal-use-copy`, title from\n the request, `course.title`, subject or first section). Ids derive from (family,\n unit key), sections and lessons from (course, key), so every part of a unit and\n every retry find the same rows. Lessons keep the manifest key in `externalRef`,\n `order` as `sortOrder` and, on creation, learner-audience text as `body` (after\n review choices). Nothing is released or published.\n- **Unit key**: `course.key`, else `bundle:<bundleIdentity>` (a one-part unit).\n- **Parts**: one `CourseBundleAttachment` per import (unique per import and per\n (course, part)). Attach parts in any order; the result says `part.attached` and\n `part.complete`. Fails closed and writes nothing: a `courseId` of another unit or\n another part count is `unit-mismatch`; a second import for an attached part is\n `part-conflict`. A replay is `replayed: true` and writes nothing new.\n- **Shared sections and lessons**: parts that share a key must agree, and the\n result never depends on arrival order. `CourseBundlePlacement`\n (`course_bundle_placements`) records each key's title and whether its order was\n declared. Another title, two different declared orders, or a declared order\n against an implicit one is `part-conflict` (nothing written). Between implicit\n positions the part with the lowest index decides; a lower part arriving later\n re-sorts the row. An existing row with no placement is `part-conflict`.\n- **Concurrency**: every attach transaction takes, in order, the import row lock\n (shared with `chooseMaterialAudience` and `applyMaterials`) and the unit lock: a\n `CourseBundleUnit` row (`course_bundle_units`, one per family and unit key)\n created if absent and then locked before anything that decides the outcome\n (attachments, the course, placements, choices) is read. On PostgreSQL a\n concurrent first create of the same unit waits and adopts the winner's row,\n so two first attaches can never both pass the checks.\n- **Zero renderings**: with no converter, Word/PowerPoint members are recorded\n `converter_unavailable` and listed in `skipped`; no rendered or original join,\n page or thumbnail is written for them, and nothing is faked. `retryRendering`\n later, then attach again: the replay adds the newly accepted materials.\n- `readImport` reports `attachment` (course, unit key, part) once attached.\n\n### Bringing a unit up to a newer build (SMRT #4645)\n\n`attachCourse({ ..., supersede: true })` is the explicit, opt-in way to replace\nthe build attached for a part. Without it nothing changes: another import for an\nattached part is `part-conflict`, and a placement whose title differs is\n`part-conflict`. It uses the same authorization (`education.course.author`),\ntenant rules and lock order (import row, then unit row) as any attach.\n\n- **Which build is newer**: manifests carry no build stamp, so \"newer\" is the\n import record: the importing bundle's `recordedAt` must be strictly later\n than the attached import's. Otherwise `stale-build` (an older or\n same-instant build never overwrites a newer one; re-attaching a superseded\n import is the same). `supersede` with no other build attached for the part is\n an ordinary attach.\n- **Identity**: sections and lessons keep their ids (derived from course id +\n key), so enrolments, progress and submissions stay attached. Titles and order\n follow the newer build (`renamed` counts changed titles); a key that a lower\n part decided still must agree (`part-conflict`). Lesson `body` is not\n rewritten. Sections and lessons the newer build no longer lists stay.\n- **Part count change** stays `unit-mismatch`, supersede or not.\n- **Materials**: the newer build's materials are attached; an existing row\n (same lesson, asset, role) is restated in place (order, title, current again),\n not a `conflict`; its audience stays the family's. Pages are restated and\n never retired (learner work points at page keys).\n- **Retirement**: file materials and external videos the replaced build wrote\n that the newer build does not carry get `retiredAt`. Rows are never deleted,\n so work that references them still resolves; learner and catalog material\n lists skip retired rows. `retired` counts them. Which rows belong to the\n replaced build: `sourceImportId` (new on `lesson_assets`, `course_assets`,\n `lesson_external_videos`, written by every attach/apply); rows from before it\n existed (null) are adopted when their lesson is one of the replaced import's\n lessons, and course materials when that import had course members. Rows\n from another part or source are never touched. A material that returns in a\n later build is current again.\n- **Audit**: the part's `CourseBundleAttachment` moves to the newer import (the\n one sanctioned attachment change, `supersedeWith`) and an append-only\n `CourseBundleSupersession` row (`course_bundle_supersessions`: course, unit\n key, part, new import, replaced import, actor, time) records which replaced\n which. The replaced import no longer reads as attached.\n- **Replay** of the same supersede is `replayed: true` and changes nothing.\n- Schema (additive): `retired_at` and `source_import_id` on `lesson_assets`,\n `course_assets`, `lesson_external_videos`; table `course_bundle_supersessions`.\n\n### Audience before and after attach\n\n`chooseMaterialAudience({ ...actor, importId, path | video: { lessonKey, videoId },\naudience })` (`education.course.author`, operation `choose-course-bundle-audience`)\nrecords a review choice (`course_bundle_audience_choices`, one per import and\ntarget; a later choice replaces it) within the role's bounds: keys and assessment\nguidance never become learner material (`invalid-audience`), and a path must be a\ndeclared v2 material of the import. `readImport` lists `audienceChoices`.\n`attachCourse` and `applyMaterials` apply them (a slides choice applies to the\nrendered pages; the original deck stays guardian). After attach a choice is\n`already-attached`: change the row with `SubmissionService.setMaterialAudience`.\nOn a re-attach an existing row keeps its audience (the family's choice), except an\noriginal, which is repaired to guardian.\n\n### Applying materials\n\n`applyMaterials({ ...actor, importId, bundle, courseId, lessons, assets })`\n(`education.course.author`, operation `apply-course-bundle-materials`) writes the\nrows in one transaction. The host has stored each accepted member as a private\nasset and created the course's lessons: `lessons` maps manifest lesson key to the\n`Lesson` id, `assets` maps member path to `{ original, rendered }` asset ids.\nSlides join both the original deck (`rendition: 'original'`) and the rendered PDF\n(`rendition: 'rendered'`, `renditionOfAssetId` the original); the original join is always\nguardian-only whatever the deck's declared audience (the audience applies to the\nrendered PDF; the model refuses a learner original), and a join written earlier with a\nwider audience is repaired on re-apply (`repaired`); any other Word/PowerPoint\nmember joins only its rendered PDF, so a learner is never handed an editable\noriginal. `assets[path].thumbnail` is recorded on the rendered join (or a plain\nfile's own join); rows written before this release get their rendition and\nthumbnail filled in on re-apply (`updated`). `LessonAsset`/`CourseAsset` saves keep their private-asset rule (guardian-\nonly roles and personal-use-copy courses need private assets). It targets\nfamily-owned courses; an authored course is `authored-course` because authored\nmaterial changes through course authoring. Idempotent: an identical row is\n`unchanged`, a row with other details (order, title, rendition, a video's title or\nstep) is `CourseBundleMaterialsError('conflict')`; audience is not compared (see\nabove). A missing asset or lesson is `asset-missing`/`lesson-missing`, a thumbnail\nunder another owner than its material `asset-owner`; any error writes nothing. A\nmember named twice (material and page range) is listed once in `skipped`.\n\n### Retrying a failed rendering\n\n`retryRendering({ ...actor, importId, bundle, paths?, receiveMember? })`\n(`education.course.author`, operation `retry-course-bundle-rendering`, needs a\nconverter) renders the Word/PowerPoint members whose first import recorded a\n*transient* failure (`converter_unavailable`, `converter_failed`,\n`converter_timeout`; `COURSE_BUNDLE_RETRYABLE_REASONS`). The first import's member\nrows are never rewritten. A success appends one `course_bundle_renderings` row\nper (import, content hash); every reader (`readImport`, replayed imports,\nproposals) overlays it, so the member reads `accepted` (identical copies as\nduplicates) with its rendered hash. A failure records nothing, so asking again is\nalways allowed; a second retry of a repaired member is `unchanged` and renders\nnothing. Converter rejections, size limits and the Office member cap are not\nretryable: change the bundle. The bundle must be the imported one.\n\n## Readers\n\n`BundleReader { formatId, detect, prepare }` in a `BundleReaderRegistry`;\nbuilt-ins are `manifest-v2`, `manifest-v1` and `plain-files`. SCORM and Common Cartridge are\n**not implemented** (their manifests describe launch resources and activity\ntrees that do not map to sections/lessons without design); a host can register\na reader ahead of the built-ins (`register(reader, { first: true })`).\n\n## Identity and replay\n\n`bundleIdentity` (`cbi1-...`) digests the sorted (path, sha256) pairs, so\nre-zipping, reordering and timestamps replay. One `CourseBundleImport` per\n(tenant, identity); a concurrent first import replays once on the unique index.\n\n## Models (closed, required tenant, no REST/CLI/MCP/AI, append-only)\n\n`CourseBundleImport` (`course_bundle_imports`: identity, format, reader,\nmanifest version, counts, actor), `CourseBundleMember` (`course_bundle_members`:\nunique tenant/import/path; ordinal, sha256, size, media type, kind, status,\nreason, duplicate-of, role, audience, section and lesson key) and\n`CourseBundleRendering` (`course_bundle_renderings`: a deliberate retry's success). Hashes and placements only:\nbytes stay with the host's ingestion receipts. Rewrites and deletes throw.\n`CourseBundleAttachment` (`course_bundle_attachments`) and the mutable\n`CourseBundleAudienceChoice` (`course_bundle_audience_choices`) and\n`CourseBundleSupersession` (`course_bundle_supersessions`, append-only audit of a\nnewer build taking a part) and\n`CourseBundlePlacement` (`course_bundle_placements`) and `CourseBundleUnit`\n(`course_bundle_units`) belong to the attach path. Normal manifest migrations create the tables and add `audience` (NULL\non older rows) and, on `lesson_assets`/`course_assets`, `rendition`,\n`rendition_of_asset_id` and `thumbnail_asset_id` (additive; older joins read as a\nfile as supplied with no thumbnail).\n\n## Not in this release\n\n- Material rows for authored courses: a proposal still carries lesson text only;\n authored course material goes through `CourseMaterialInput` and release. The\n family path is `attachCourse`; `proposeStructure` stays the authored-draft path.\n- Ingestion's receipt contract is unchanged: there is no `application/zip` part\n type or `parentEvidenceId` in ingestion. The education side unpacks and the\n host receives members one by one, so archive-to-member lineage lives in\n `course_bundle_members` (and the host's receipts), not in ingestion evidence.\n Changing ingestion's published receipt is a separate design decision.\n- A manifest/extraction disagreement is not compared automatically.\n- SCORM / Common Cartridge readers; zip64; nested archives; encrypted zips.\n"
16502
+ "content": "# Course bundle import\n\n`CourseBundleImportService` (SMRT #4578, extends `CourseAuthoringService`,\nconsumed by Hearth #94) imports a course bundle, a zip with an optional manifest\nor a plain file set, as bounded evidence, hands its supported members to the\nhost for ingestion extraction and proposes sections and lessons for review.\nRisk is high: untrusted archive parsing and new schema. It reuses the course\nauthorizer: `importBundle` needs `education.course.author`, `readImport`\n`education.course.read-draft`, `proposeStructure` `education.course.generate`\n(via `CourseGenerationReviewService`). Operations: `import-course-bundle`,\n`read-course-bundle`. Server only (Node `zlib`/`crypto`); no new dependency.\n\n## Flow\n\n1. `importBundle({ ...actor, bundle, limits?, onProgress?, receiveMember? })`\n authorizes **before parsing**, unpacks (stage `unpack`), classifies and lays\n out the structure (`classify`), writes the evidence in one transaction and\n then calls `receiveMember` for each accepted member (`extract`), outside any\n transaction. `bundle` is `{ archive: Uint8Array }` or `{ files: [{ path,\n bytes }] }`. Hostile or damaged input throws `CourseBundleRefusedError`\n (`reason`, optional `entry`) and writes nothing.\n2. `receiveMember(member, { replayed })` is the host's ingestion seam: call\n `IngestionService.receive` with `sourceKind: 'course_import'` and the member\n bytes, then queue extraction and `preserveQuestionExtractionAnalysis` as in\n [question extraction](question-extraction.md). Members carry `role`\n (manifest page role), `lessonKey`, `sectionKey`. It runs on a replay too\n (ingestion dedups by content hash), so an interrupted first run resumes. Its\n return values are in `result.receipts`.\n3. `proposeStructure({ ...actor, importId, bundle, courseId,\n expectedHeadRevisionId })` (stage `propose`) re-supplies the bundle (bytes\n are never retained), checks it is the imported one, and runs a deterministic\n `CourseMaterialGenerator` (`smrt-bundle-import`) through\n `requestGeneration`/`runGeneration`. The result is an ordinary proposal: a\n new draft revision of additions and fills; conflicts wait for\n `decideProposal`. Nothing is released, assigned or published. The request key\n is `bundle:<courseId>:<bundleIdentity>`, so a retry returns the same job; the\n same key with another head is `CourseRequestKeyConflictError`.\n4. `readImport({ ...actor, importId })` returns the recorded evidence.\n\nProgress is an observer callback (`onProgress({ stage, completed, total })`,\nstages `unpack`, `classify`, `render`, `extract`, `propose`; each starts at `completed: 0`;\n`render` is emitted only when the bundle has Word/PowerPoint members).\nIngestion has no public job-progress seam, so none is invented. A throwing\nlistener never affects the import.\n\n## Hard bounds (`COURSE_BUNDLE_LIMITS`; a host may only lower them)\n\n| limit | value |\n|---|---|\n| `maxArchiveBytes` (input) | 100 MiB |\n| `maxEntries` | 200 |\n| `maxEntryBytes` (inflated) | 50 MiB |\n| `maxTotalBytes` (inflated) | 200 MiB |\n| `maxCompressionRatio` (members over `ratioFloorBytes` = 64 KiB) | 100 |\n| `maxPathBytes` / `maxSegmentBytes` / `maxPathDepth` | 512 / 255 / 8 |\n| `maxManifestBytes` | 256 KiB |\n| `maxLessons` (the proposal schema's cap) | 50 |\n| `maxLessonBodyChars` | 100 000 |\n| `maxOfficeMembers` (Word/PowerPoint members rendered per bundle) | 50 |\n| `maxRenderedBytes` (PDF accepted back from the converter) | 50 MiB |\n| `conversionTimeoutMs` (one Office member) | 60 000 |\n| `maxThumbnailBytes` (first-page image accepted back from the converter) | 5 MiB |\n| archive nesting | 1: a zip inside a zip is never expanded |\n\nA lesson whose combined text members exceed `maxLessonBodyChars` refuses the\nbundle (`lesson_too_large`, `entry` = lesson key) at import; text is never\ntrimmed or dropped. A plain file set enforces `maxTotalBytes` on its member\nbytes exactly as an archive does on inflated bytes.\n\nThe reader (`course-bundle-zip.ts`) parses the end record and central directory,\nvalidates every offset and size against the real buffer, inflates with\n`maxOutputLength` equal to the declared size (declared sizes are never trusted:\na mismatch either way is `size_mismatch`), checks CRC32 and re-checks the total\non actual output. Stored and deflate only. Refusals (`CourseBundleRefusalReason`)\ncover: not a zip, truncated, ambiguous end record (two end records that both fit\nthe file, for example one forged in a comment), zip64, multi-disk, central\ndirectory not ending at the end record, entries out of range or overlapping,\nlocal header name/method mismatch, encrypted entries (flags 0/6/13, AES method),\nsymlink and special files (unix mode; DOS reparse flag), unsupported method,\nbad CRC, ratio, entry count, totals, duplicate names (case/NFC-insensitive,\nbackslashes normalised first) and bad paths (`..`, `.`, empty segments, absolute,\ndrive letter or `:` anywhere, NUL/control/bidi characters, lone surrogates,\ntoo long or deep). An **honestly** oversize member (declared over\n`maxEntryBytes`) is not inflated and is recorded `refused / entry_too_large`.\n\n## Members and statuses\n\nBy content (never by extension alone): `docx`/`pptx`/`xlsx` (see below), `pdf` (`%PDF-` in the first 1 KiB),\n`image` (PNG, JPEG, WebP), `text` (`.txt`/`.md`, strict UTF-8, no control\ncharacters, body-sized), `manifest` (root `manifest.json`), else `unsupported`.\nStatus: `accepted` (handed to extraction), `duplicate` (same sha256 as an earlier\nmember in canonical order; recorded once as evidence, `duplicateOfPath`, never\nextracted again), `unsupported` (`unsupported_type`, `nested_archive`,\n`unreadable_text`), `refused` (`encrypted_pdf`, `entry_too_large`), `metadata`\n(the manifest). Nothing is dropped. An encrypted PDF is any PDF with an\n`/Encrypt` token anywhere in its bytes (trailer, xref stream): a false positive\non literal text inside an uncompressed stream is refused, never read.\n\n## Word and PowerPoint members (SMRT #4615)\n\nA `.docx`/`.pptx`/`.xlsx` member is an OOXML package only when the name and the\ncontent agree: a zip whose entries include `[Content_Types].xml` and\n`word/document.xml` (docx), `ppt/presentation.xml` (pptx) or `xl/workbook.xml`\n(xlsx). A workbook (a supply list) is rendered and kept like the others but has\nno text reader. Before the bytes go anywhere, the\npackage itself passes the same bounded zip reader (ratio, overlap, paths, CRC,\n100 MiB inflated); a failure is `refused / unsafe_office`, never a refusal of the\nwhole bundle. Other zips stay `unsupported / nested_archive`.\n\nTwo separate things happen to an accepted Office member, both after\nauthorization and before the evidence is written:\n\n- **Pages**: a `DocumentConverter` (`document-converter.ts`) renders the original\n to PDF. The importer enforces the byte and time limits itself (input\n `maxEntryBytes`, output `maxRenderedBytes`, `conversionTimeoutMs` with an abort\n signal and a hard race, so an adapter that ignores the signal still times out),\n and rejects output that is not a PDF or is encrypted. The converter is told a\n neutral file name (`document.docx`), never the bundle path. Identical documents\n are rendered once.\n- **Text**: an injectable `OfficeTextReader` (best effort; a failure or a `null`\n costs the member nothing) returns whole-document text and, for decks, text and\n speaker notes per slide. Office text is handed to the host on the accepted\n member (`officeText`) and is **never** inlined into a proposed lesson body:\n teacher guides and notes carry answers, and audience belongs to manifest v2.\n\n`receiveMember` gets the original `bytes` (store them as the private original\nasset: \"Open in PowerPoint\" for decks) and `rendered` (`{ bytes, sha256, mediaType:\n'application/pdf', converterId }`, store as a private asset beside the original:\nthe pages for \"Present\"/reading). The member row records `renderedSha256` and\n`renderedSizeBytes`; bytes are still not retained here. A converter may also\nreturn a first-page image (`DocumentConversionResult.thumbnail`); the importer\nkeeps it only when it is a PNG/JPEG/WebP within `maxThumbnailBytes` and hands it\nover as `rendered.thumbnail` (`{ bytes, sha256, mediaType }`), else null. An\nunusable thumbnail never fails the member. Gotenberg draws none, so it is null.\n\n**Fail closed.** Any converter problem records that member `unsupported` with a\nreason and writes the rest of the bundle normally: `converter_unavailable` (none\nconfigured), `converter_failed` (unreachable or 5xx), `converter_timeout`,\n`converter_too_large`, `converter_rejected` (HTTP 4xx, not a PDF, encrypted),\n`office_limit` (beyond `maxOfficeMembers`, in natural path order). A member that\nfailed is not a lesson in the proposal, and identical copies of a failed document\nfail the same way rather than becoming duplicates of nothing. Outcomes are part\nof the append-only evidence, so a replay does not retry a recorded failure (to\nretry, change the bundle). A replay does re-render recorded-accepted members so the\nhost receives the bytes again; if it can no longer render one it throws\n`DocumentConversionError` rather than silently dropping an accepted member.\n\n### Host configuration\n\n```ts\nconst service = new CourseBundleImportService(db, authorizer, {\n converter: new GotenbergDocumentConverter({\n baseUrl: process.env.GOTENBERG_URL, // for example http://gotenberg:3000\n timeoutMs: 60_000,\n maxBytes: 50 * 1024 * 1024,\n }),\n officeTextReader, // optional, see below\n});\n```\n\n`GotenbergDocumentConverter` posts to `<baseUrl>/forms/libreoffice/convert`, talks\nto nothing else, refuses redirects and URLs with credentials, and caps the response\nas it streams. Run Gotenberg as an internal-only service with **no outbound\nnetwork**, its own size/time limits and no shared volume with the app; this\npackage assumes untrusted files never run next to the application. Tests use\n`FakeDocumentConverter` (records calls; `failWith`, `failWhen`, `delayMs`, `output`).\n\n`OfficeTextReader` reads Office text through the SDK without this package\ndepending on `@happyvertical/documents`: the host imports `extractOffice`\n(released in `@happyvertical/documents` 0.111.4, sdk#1486, Kreuzberg) and passes\nit to `createOfficeTextReader(extractOffice)`, which returns whole-document\ntext for Word and text plus speaker notes per slide for PowerPoint (images are\nnever requested). An absent engine reads as `null`; a corrupt file rejects and\nthe importer treats that as \"no text\". The repository's current `catalog:`\npin is 0.111.3, which predates the Office processors, so the host bumps the SDK\nset (and its minimum release age) before it can import `extractOffice`.\n\n## Manifest (`manifest.json`, version 1, closed)\n\n```json\n{ \"version\": 1, \"course\": { \"subject\": \"Math\" },\n \"sections\": [ { \"key\": \"unit-1\", \"title\": \"Unit One\",\n \"lessons\": [ { \"key\": \"sums\", \"title\": \"Sums\",\n \"members\": [ { \"path\": \"sheets/sums.pdf\", \"role\": \"exercise\" } ] } ] } ] }\n```\nEvery object is closed: an unknown key anywhere is `manifest_unknown_key` (with\nits path in `entry`); another version is `manifest_version`. Keys match\n`[A-Za-z0-9._:-]{1,120}` and are unique; roles are the extraction page roles\n(`exercise`, `example`, `instructions`, `cover`, `answer_key`, `mixed`,\n`unknown`); a listed path must exist and be listed once. The manifest is a source\nfact: roles are `source`, anything the extractor infers stays `inferred`, and a\nmanifest/extraction disagreement belongs in extraction review, not here. Without\na manifest each accepted member is one lesson, sections come from the top-level\ndirectory and order is natural path order (digit runs compare numerically, no\nlocale). Manifest order: the manifest, declared members, then the rest by path.\nLesson bodies are the readable text members of a lesson, joined; PDFs and images\nare never inlined. Members whose declared role is `answer_key` or `mixed` never\ncontribute text to a lesson body (they remain recorded evidence members); the\nproposal rationale only counts how many were withheld.\n\n## Manifest version 2 (SMRT #4615)\n\nA root `manifest.json` with `\"version\": 2` is laid out by the `manifest-v2`\nreader; version 1 keeps its own reader and page roles. The schema is closed in\nthe same way: an unknown key anywhere is `manifest_unknown_key` (with its path in\n`entry`), another version is `manifest_version`.\n\n```json\n{ \"version\": 2, \"course\": { \"subject\": \"Science\" },\n \"courseMembers\": [ { \"path\": \"unit/supplies.pdf\", \"role\": \"attachment\", \"title\": \"Supply list\" } ],\n \"sections\": [ { \"key\": \"unit\", \"title\": \"Unit\", \"lessons\": [ {\n \"key\": \"l1\", \"title\": \"Lesson 1\",\n \"members\": [ { \"path\": \"l1/log.docx\", \"role\": \"learner-pages\" },\n { \"path\": \"l1/deck.pptx\", \"role\": \"slides\" },\n { \"path\": \"l1/guidance.pdf\", \"role\": \"assessment-guidance\" } ],\n \"pages\": [ { \"member\": \"l1/log.docx\", \"from\": 1, \"to\": 4, \"label\": \"Mission log\" } ],\n \"videos\": [ { \"reference\": \"dQw4w9WgXcQ\", \"title\": \"Recap\", \"step\": 1 } ] } ] } ] }\n```\n\n- **Members** (lesson `members` and root `courseMembers`): `path` (must exist, listed\n once), `role` (required) one of `teacher-guide`, `slides`, `learner-pages`,\n `assessment`, `assessment-guidance`, `handout`, `printable`, `answer-key`,\n `attachment` (`manifest_role` otherwise; a video is a reference, never a file),\n optional `audience`, `title` and (lesson members only) `step`. The audience is the role default\n (`LESSON_ASSET_DEFAULT_AUDIENCE`) unless stated; `answer-key` and\n `assessment-guidance` are always guardian (a learner audience is\n `manifest_audience`). Members not listed stay recorded evidence with no role.\n- **Lesson steps** (SMRT #4656): a lesson member or video may declare\n `step: { key, order, label? }` (a closed object; `manifest_invalid` for a key over\n 32 characters or outside `A-Za-z0-9._:-`, a non-finite or non-number `order`\n (|order| up to 1,000,000), or an empty label; `manifest_unknown_key` for anything\n else, and a course member cannot carry one). Materials that share a `key` are\n one step; fractions order `1b` after `1`. A video's `step` is still accepted as a\n whole number (its rank); the object form also ranks it by `floor(order)`.\n- **Pages**: `{ member, from, to, label? }` ranges over a member of the same lesson\n (PDF, Word or PowerPoint; the rendered PDF for Office). Each page becomes a\n `LessonPage` keyed `p<n>`, labelled `<label> <n>` (default `Page <n>`), ordered by\n declaration. Bad, overlapping or unpageable ranges are `manifest_pages`; at most\n 500 pages per lesson.\n- **Videos**: `{ reference, title, step, audience? }` where `reference` is a bare id\n or an allowlisted https YouTube URL (`parseYouTubeReference`; anything else is\n `manifest_video`), `step` an integer 0-9999 or a lesson step object (see above). They become `LessonExternalVideo`\n rows; nothing is downloaded.\n- **Course members** become `CourseAsset` rows, not tied to any lesson.\n- **Unit and parts** (SMRT #4632): `course.key` (a manifest key) names the unit\n and `course.title` its course title; `part: { index, count }` (1-50) marks one\n bundle of a unit split across several, and requires `course.key`\n (`manifest_invalid` otherwise). Sections and lessons may carry a unit-wide\n `order` (0-9999); without one the position in this manifest is used, and\n the order counts as implicit when parts are merged.\n\nGuardian-audience text (teacher guides, guidance, keys) never enters a proposed\nlesson body; only learner-audience text members do. Under v2 `role` on the member\nevidence row is the material role and `audience` the effective audience (NULL on\nrows recorded before v2). The plan carries `materials` (also on\n`CourseBundleImportResult.materials`): the resolved materials, pages and videos,\nplus `skipped` for declared members that were unsupported, refused or failed\nrendering. A duplicate member resolves to the earlier identical member's bytes\n(`assetPath`) and one material per (place, role, content).\n\n`receiveMember` also gets `audience` and `courseLevel`, and an optional `title`\n(SMRT #4653): the manifest title of the one material the member holds (\"Teacher\nMission Manual\"), so a host can name the assets it stores. It is absent when the\nmanifest gave none or the member backs several materials with different titles.\n**Material titles**: `attachCourse` records that title on the `lesson_assets` /\n`course_assets` row (`title`; null on a lesson row without one, `''` on a course\nrow), the same for the original and rendered halves of a pair. Lesson and course\nmaterial views show the row title and fall back to the stored asset's name when\nthere is none. A re-attach fills a missing title; a different one is a\n`conflict` unless `supersede` restates it. **Lesson steps** (#4656): the declared\nstep is recorded on the `lesson_assets` row (`step_key`, `step_order`, `step_label`;\nboth halves of an original/rendered pair share it) and on `lesson_external_videos`\n(`step_key`, `step_position`, `step_label`; its existing integer `step_order` stays\nthe rank, `floor(order)` for an object step). All are additive nullable columns.\nA re-attach fills a row's missing step without a conflict; a different step is a\n`conflict` unless `supersede` restates it (replays are unchanged). Hosts read it\nwith `SubmissionService.listLessonSteps` ([lesson materials](lesson-materials.md)).\n**Slides** keep the original\ndeck (hand `bytes` to the host as a private asset: \"Open in PowerPoint\") and its\nrendered pages (`rendered`: \"Present\").\n\n### Attaching a family course (SMRT #4632)\n\n`attachCourse({ ...actor, importId, bundle, assets, courseId?, title?,\nlicenseKind? })` (`education.course.author`, operation `attach-course-bundle`) is\nthe published path from an import to a course: in one transaction it creates (or\nfinds) the family course, its sections and lessons, and writes the materials as\n`applyMaterials` does. `assets` maps member path to `{ original, rendered?,\nthumbnail? }` asset ids the host stored from `receiveMember`.\n\n- **Course**: a non-authored family draft (`sourceKey` `course-bundle`,\n `sourceRef` the unit key, `licenseKind` default `personal-use-copy`, title from\n the request, `course.title`, subject or first section). Ids derive from (family,\n unit key), sections and lessons from (course, key), so every part of a unit and\n every retry find the same rows. Lessons keep the manifest key in `externalRef`,\n `order` as `sortOrder` and, on creation, learner-audience text as `body` (after\n review choices). Nothing is released or published.\n- **Unit key**: `course.key`, else `bundle:<bundleIdentity>` (a one-part unit).\n- **Parts**: one `CourseBundleAttachment` per import (unique per import and per\n (course, part)). Attach parts in any order; the result says `part.attached` and\n `part.complete`. Fails closed and writes nothing: a `courseId` of another unit or\n another part count is `unit-mismatch`; a second import for an attached part is\n `part-conflict`. A replay is `replayed: true` and writes nothing new.\n- **Shared sections and lessons**: parts that share a key must agree, and the\n result never depends on arrival order. `CourseBundlePlacement`\n (`course_bundle_placements`) records each key's title and whether its order was\n declared. Another title, two different declared orders, or a declared order\n against an implicit one is `part-conflict` (nothing written). Between implicit\n positions the part with the lowest index decides; a lower part arriving later\n re-sorts the row. An existing row with no placement is `part-conflict`.\n- **Concurrency**: every attach transaction takes, in order, the import row lock\n (shared with `chooseMaterialAudience` and `applyMaterials`) and the unit lock: a\n `CourseBundleUnit` row (`course_bundle_units`, one per family and unit key)\n created if absent and then locked before anything that decides the outcome\n (attachments, the course, placements, choices) is read. On PostgreSQL a\n concurrent first create of the same unit waits and adopts the winner's row,\n so two first attaches can never both pass the checks.\n- **Zero renderings**: with no converter, Word/PowerPoint members are recorded\n `converter_unavailable` and listed in `skipped`; no rendered or original join,\n page or thumbnail is written for them, and nothing is faked. `retryRendering`\n later, then attach again: the replay adds the newly accepted materials.\n- `readImport` reports `attachment` (course, unit key, part) once attached.\n\n### Bringing a unit up to a newer build (SMRT #4645)\n\n`attachCourse({ ..., supersede: true })` is the explicit, opt-in way to replace\nthe build attached for a part. Without it nothing changes: another import for an\nattached part is `part-conflict`, and a placement whose title differs is\n`part-conflict`. It uses the same authorization (`education.course.author`),\ntenant rules and lock order (import row, then unit row) as any attach.\n\n- **Which build is newer**: manifests carry no build stamp, so \"newer\" is the\n import record: the importing bundle's `recordedAt` must be strictly later\n than the attached import's. Otherwise `stale-build` (an older or\n same-instant build never overwrites a newer one; re-attaching a superseded\n import is the same). `supersede` with no other build attached for the part is\n an ordinary attach.\n- **Identity**: sections and lessons keep their ids (derived from course id +\n key), so enrolments, progress and submissions stay attached. Titles and order\n follow the newer build (`renamed` counts changed titles); a key that a lower\n part decided still must agree (`part-conflict`). Lesson `body` is not\n rewritten. Sections and lessons the newer build no longer lists stay.\n- **Part count change** stays `unit-mismatch`, supersede or not.\n- **Materials**: the newer build's materials are attached; an existing row\n (same lesson, asset, role) is restated in place (order, title, current again; a\n renamed material shows its new title though it keeps its earlier asset),\n not a `conflict`; its audience stays the family's. Pages are restated and\n never retired (learner work points at page keys).\n- **Retirement**: file materials and external videos the replaced build wrote\n that the newer build does not carry get `retiredAt`. Rows are never deleted,\n so work that references them still resolves; learner and catalog material\n lists skip retired rows. `retired` counts them. Which rows belong to the\n replaced build: `sourceImportId` (new on `lesson_assets`, `course_assets`,\n `lesson_external_videos`, written by every attach/apply); rows from before it\n existed (null) are adopted when their lesson is one of the replaced import's\n lessons, and course materials when that import had course members. Rows\n from another part or source are never touched. A material that returns in a\n later build is current again.\n- **Audit**: the part's `CourseBundleAttachment` moves to the newer import (the\n one sanctioned attachment change, `supersedeWith`) and an append-only\n `CourseBundleSupersession` row (`course_bundle_supersessions`: course, unit\n key, part, new import, replaced import, actor, time) records which replaced\n which. The replaced import no longer reads as attached.\n- **Replay** of the same supersede is `replayed: true` and changes nothing.\n- Schema (additive): `retired_at` and `source_import_id` on `lesson_assets`,\n `course_assets`, `lesson_external_videos`; table `course_bundle_supersessions`.\n\n### Audience before and after attach\n\n`chooseMaterialAudience({ ...actor, importId, path | video: { lessonKey, videoId },\naudience })` (`education.course.author`, operation `choose-course-bundle-audience`)\nrecords a review choice (`course_bundle_audience_choices`, one per import and\ntarget; a later choice replaces it) within the role's bounds: keys and assessment\nguidance never become learner material (`invalid-audience`), and a path must be a\ndeclared v2 material of the import. `readImport` lists `audienceChoices`.\n`attachCourse` and `applyMaterials` apply them (a slides choice applies to the\nrendered pages; the original deck stays guardian). After attach a choice is\n`already-attached`: change the row with `SubmissionService.setMaterialAudience`.\nOn a re-attach an existing row keeps its audience (the family's choice), except an\noriginal, which is repaired to guardian.\n\n### Applying materials\n\n`applyMaterials({ ...actor, importId, bundle, courseId, lessons, assets })`\n(`education.course.author`, operation `apply-course-bundle-materials`) writes the\nrows in one transaction. The host has stored each accepted member as a private\nasset and created the course's lessons: `lessons` maps manifest lesson key to the\n`Lesson` id, `assets` maps member path to `{ original, rendered }` asset ids.\nSlides join both the original deck (`rendition: 'original'`) and the rendered PDF\n(`rendition: 'rendered'`, `renditionOfAssetId` the original); the original join is always\nguardian-only whatever the deck's declared audience (the audience applies to the\nrendered PDF; the model refuses a learner original), and a join written earlier with a\nwider audience is repaired on re-apply (`repaired`); any other Word/PowerPoint\nmember joins only its rendered PDF, so a learner is never handed an editable\noriginal. `assets[path].thumbnail` is recorded on the rendered join (or a plain\nfile's own join); rows written before this release get their rendition and\nthumbnail filled in on re-apply (`updated`). `LessonAsset`/`CourseAsset` saves keep their private-asset rule (guardian-\nonly roles and personal-use-copy courses need private assets). It targets\nfamily-owned courses; an authored course is `authored-course` because authored\nmaterial changes through course authoring. Idempotent: an identical row is\n`unchanged`, a row with other details (order, title, rendition, a video's title or\nstep) is `CourseBundleMaterialsError('conflict')`; audience is not compared (see\nabove). A missing asset or lesson is `asset-missing`/`lesson-missing`, a thumbnail\nunder another owner than its material `asset-owner`; any error writes nothing. A\nmember named twice (material and page range) is listed once in `skipped`.\n\n### Retrying a failed rendering\n\n`retryRendering({ ...actor, importId, bundle, paths?, receiveMember? })`\n(`education.course.author`, operation `retry-course-bundle-rendering`, needs a\nconverter) renders the Word/PowerPoint members whose first import recorded a\n*transient* failure (`converter_unavailable`, `converter_failed`,\n`converter_timeout`; `COURSE_BUNDLE_RETRYABLE_REASONS`). The first import's member\nrows are never rewritten. A success appends one `course_bundle_renderings` row\nper (import, content hash); every reader (`readImport`, replayed imports,\nproposals) overlays it, so the member reads `accepted` (identical copies as\nduplicates) with its rendered hash. A failure records nothing, so asking again is\nalways allowed; a second retry of a repaired member is `unchanged` and renders\nnothing. Converter rejections, size limits and the Office member cap are not\nretryable: change the bundle. The bundle must be the imported one.\n\n## Readers\n\n`BundleReader { formatId, detect, prepare }` in a `BundleReaderRegistry`;\nbuilt-ins are `manifest-v2`, `manifest-v1` and `plain-files`. SCORM and Common Cartridge are\n**not implemented** (their manifests describe launch resources and activity\ntrees that do not map to sections/lessons without design); a host can register\na reader ahead of the built-ins (`register(reader, { first: true })`).\n\n## Identity and replay\n\n`bundleIdentity` (`cbi1-...`) digests the sorted (path, sha256) pairs, so\nre-zipping, reordering and timestamps replay. One `CourseBundleImport` per\n(tenant, identity); a concurrent first import replays once on the unique index.\n\n## Models (closed, required tenant, no REST/CLI/MCP/AI, append-only)\n\n`CourseBundleImport` (`course_bundle_imports`: identity, format, reader,\nmanifest version, counts, actor), `CourseBundleMember` (`course_bundle_members`:\nunique tenant/import/path; ordinal, sha256, size, media type, kind, status,\nreason, duplicate-of, role, audience, section and lesson key) and\n`CourseBundleRendering` (`course_bundle_renderings`: a deliberate retry's success). Hashes and placements only:\nbytes stay with the host's ingestion receipts. Rewrites and deletes throw.\n`CourseBundleAttachment` (`course_bundle_attachments`) and the mutable\n`CourseBundleAudienceChoice` (`course_bundle_audience_choices`) and\n`CourseBundleSupersession` (`course_bundle_supersessions`, append-only audit of a\nnewer build taking a part) and\n`CourseBundlePlacement` (`course_bundle_placements`) and `CourseBundleUnit`\n(`course_bundle_units`) belong to the attach path. Normal manifest migrations create the tables and add `audience` (NULL\non older rows) and, on `lesson_assets`/`course_assets`, `rendition`,\n`rendition_of_asset_id` and `thumbnail_asset_id` (additive; older joins read as a\nfile as supplied with no thumbnail).\n\n## Not in this release\n\n- Material rows for authored courses: a proposal still carries lesson text only;\n authored course material goes through `CourseMaterialInput` and release. The\n family path is `attachCourse`; `proposeStructure` stays the authored-draft path.\n- Ingestion's receipt contract is unchanged: there is no `application/zip` part\n type or `parentEvidenceId` in ingestion. The education side unpacks and the\n host receives members one by one, so archive-to-member lineage lives in\n `course_bundle_members` (and the host's receipts), not in ingestion evidence.\n Changing ingestion's published receipt is a separate design decision.\n- A manifest/extraction disagreement is not compared automatically.\n- SCORM / Common Cartridge readers; zip64; nested archives; encrypted zips.\n"
16460
16503
  },
16461
16504
  {
16462
16505
  "path": "agents/class-crunchlabs.md",
16463
16506
  "module": "class-crunchlabs",
16464
- "content": "# Class CrunchLabs source (SMRT #4616)\n\nClass CrunchLabs is a global catalog provider (`class-crunchlabs`) whose units are\nlisted under Science, with the @ClassCrunchLabs YouTube course playlists as\npreviews. \"Add to a course\" reads the family's own download of the unit and runs\nthe [bundle import](course-bundle-import.md) with a manifest version 2 built from\nit. Files come from CrunchLabs' own downloads and YouTube; **nothing is hosted\nhere** and the listing has no acquirable formats.\n\n## Operator-only until permission, one public switch\n\n`CatalogVisibility` gains `operator`. A `CatalogPolicy` built with\n`operatorTenantIds` lets only those families (host configuration, never a\nrequest) see `operator` records; the default policy shows them to nobody, and\n`authorize` answers `operator-only`. Materialization refuses them.\n\n`createClassCrunchLabsCatalogAdapter({ playlists, publicListing? })` is a snapshot\n`CatalogSyncAdapter` (one terminal page, no cursor). By default\n(`CLASS_CRUNCHLABS_PUBLIC_LISTING_DEFAULT = false`) every record is `operator`\nvisibility with licence state `unknown`, no permitted operations and the text\n\"free curriculum, permission pending\". Only the literal `publicListing: true`,\nset by the operator in host configuration, makes records `catalog` visibility\nwith `verified` evidence permitting `link` only. The switch is part of each\nrecord's revision, so the next sync rewrites the rows when it is flipped.\n`classCrunchLabsPreviewEmbedUrl(record)` returns the youtube-nocookie playlist\nembed for a listed unit (null for anything else). The playlist data is an\ninjected `ClassCrunchLabsPlaylistSource`; the adapter never touches the network.\n\n`addClassCrunchLabsUnit({ service, actor, catalogActor, record, listing, policy,\nfiles, playlists, pageCounter?, receiveMember? })` re-checks\n`canAddClassCrunchLabsUnit(actor, policy, listing)`: visibility is derived from the\nhost's own `listing.publicListing` (operator unless the literal `true`), never from\nthe record or any caller-supplied visibility or licence; an operator-only listing\nis refused to every other family, and the course actor must be the catalog actor, builds the bundle and imports each part. Then store each\npart's accepted members and `attachCourse` every part (any order): they merge\ninto one family course. Courses are `personal-use-copy` (`build.licenseKind`).\n\n## Reading a download\n\n`buildClassCrunchLabsBundle({ files, playlists?, unit?, pageCounter?,\nincludeUnitWideAggregates?, maxPartBytes? })` takes a `ClassCrunchLabsFileSource`:\n`folderClassCrunchLabsSource(dir)` (Node only, from `@happyvertical/smrt-education/node`, never the root: a folder, the Drive zip parts, or both;\nzips are read lazily from their central directory, zip64/encrypted/symlinks are\nskipped) or `memoryClassCrunchLabsSource(files)`. It never reads video files.\n\n- Layout: `<Unit>_All Unit Wide Materials` and `<Unit>_Lesson <n>`; letters A\n teacher manual, B slides, C student mission log, D status check assessment\n (plus guidance and the editable master), E handouts, F extras, G videos. A\n lettered directory wins over a file's own letter. Several units need `unit`.\n- Mapping to manifest v2: A `teacher-guide`, B `slides`, C `learner-pages`, D\n `assessment` / `assessment-guidance`, E `handout`, F `printable`, the unit-wide\n Supply List `attachment`. Audiences are the role defaults (teacher manual and\n guidance guardian, slides guardian, mission log learner); the editable\n assessment master is guardian. Unit-wide files become course members.\n- The unit-wide \"(all lessons)\" combined files repeat the per-lesson files and\n are large, so they are left out unless `includeUnitWideAggregates` (reported as\n `aggregate-of-lessons`); the supply list, posters and other extras are kept.\n- Titles (#4642): a material's title is its file name tidied: the letter prefix,\n lesson code (`L02`), unit name, version (`v1.0`, `v.1.1`), `copy` and stray dots\n and underscores are dropped (`A. L01_Teacher Mission Manual_<Unit>_v1.0.pdf` is\n `Teacher Mission Manual`; `... Assessment (Editable)` keeps its qualifier), and a\n trailing ` - detail` becomes `(detail)` (`SQIREL Poster - 8.5x11` is `SQIREL\n Poster (8.5×11)`). Two different files that tidy to one title in the same\n lesson or the course are told apart as `Title (2)`; byte-identical copies are\n already collapsed by de-duplication. A lesson is titled `Lesson N: Name`. The name is, in order: the cover headline of the lesson's\n teacher manual PDF, when the host passes `readFirstPageHeadline({ bytes, kind:\n 'pdf' })` (the most prominent, largest-type text on page 1, lines joined by a\n space; this package has no PDF text reader, so the host supplies one, for\n example from pdfjs; all capitals fold to title case, `WEIGHT & MOTION` is\n `Weight & Motion`; a throw or null is ignored); else the activity of the lesson's\n first (lowest step) `StudentHandsOnExploration` video name, in title case with\n small words lower (`L03.6_Video_StudentHandsOnExploration_PlayWithPlungers` is\n `Lesson 3: Play with Plungers`); else `Lesson N`. Video names are read whether or\n not a playlist matched them, both `L01.4_Video_` and `L01.4 Video_` forms, `-NNN`\n download suffixes dropped, and loose lesson videos at the download's root count\n for the unit their name ends with. Titles are manifest data, so a section or lesson title that differs from\n an already attached part of the same unit is `part-conflict` (see the bundle\n import's shared-placement rules, which are deliberately not relaxed): a unit\n imported before this change keeps its `Lesson N` titles, and getting the new\n titles means importing the unit again into a fresh course (a different course\n id, not `courseId` of the old one).\n- Lesson pages: the first mission log of a lesson gets a `Mission log` range\n from `pageCounter` (`createClassCrunchLabsPageCounter({ converter? })` counts\n PDFs with pdf-lib and Word files by rendering them through the converter).\n Without a count the lesson gets no pages and the report says so.\n- Videos: names `Lnn.step_Video_Kind_Title` are matched, by unit (playlist\n title), lesson and step (`L03.6` or `Lesson 3 ... Step 6` in the item title),\n to the unit playlist; two videos sharing a step are told apart by the item\n title naming their title or kind, else both stay unmatched (`ambiguous`).\n Matches become `videos` references with a title and a step order (materials\n prep videos are guardian audience); unit-level videos (a teaser) and unmatched\n ones are only reported.\n- Playlists without step numbers (#4647): the live course playlists title items\n `<Unit>: <name>` in teaching order. Videos that no numbered item names fall back to\n title and kind evidence, never over a numbered match. Playlist items titled\n `Lesson N Recap` are anchors: each pairs with lesson N+1's single `Recap` video\n and they cut the playlist into one segment per lesson (lesson 1 runs from the\n start; the last download lesson runs to the end). Inside a segment a video and\n an item pair when each is the other's single best match (ties pair nothing).\n Evidence is normalized tokens (case, `&` is `and`, simple plurals, the unit\n prefix and `Part N` dropped, `N` kept) for the title, and kind words:\n `Prep`/`MaterialsPrep`, `Exploration`/`StudentHandsOnExploration`,\n `Anchor`/`AnchorCheckIn`, `Explained`/`ForcesExplainer`, `Wrap Up`/`WrapUp`,\n `Recap`, `Check-in`, `Link up`, `Look closer`. Differing numbers (`Part 1` against\n `Sumo Spheres 2`) or an item naming another kind contradict. What is left in a\n segment pairs by order only when the leftover videos and items are equally many,\n the videos sit on distinct steps and nothing contradicts; otherwise they stay\n unmatched. No pair crosses a segment. A playlist with no anchor is not segmented:\n only pairs with title evidence count and nothing is filled by order. Reasons:\n `no_item` (no unnumbered item is left for it, also a whole unit without anchors),\n `ambiguous` (items remain but evidence ties or the leftovers do not pair one to\n one), `unanchored` (its lesson is not bounded by recap anchors in the playlist),\n plus the existing `no_playlist` and `item_taken`.\n- De-duplication: repeated exports of one logical path collapse (CRC and size\n when known; the newer export wins, a changed file is `superseded`); the same\n bytes under another name or place keep one copy (the lesson-named file in its\n lesson, posters unit wide, never a `copy`).\n- A unit larger than one archive (bundle cap 100 MiB) is split by lesson into\n `build.parts` (default 90 MiB each; the first part carries the course members and\n any lesson that fits beside them, others go to the first part with room; if no\n lesson fits beside the course members the build refuses with `too_large`); `build.bundle`\n and `build.manifest` are the first part. Every part's manifest names the unit\n (`course.key` = `build.unitKey`, `classCrunchLabsUnitKey(unit)`, and `course.title`),\n its `part` (`index`/`count`) and each lesson's unit-wide `order` (the lesson\n number), so `attachCourse` merges the parts in lesson order whatever order they\n arrive in.\n\nThe `report` lists lessons, members, parts, pages, matched and unmatched videos,\nduplicates, ignored files and warnings. Fixtures in tests are synthetic files\nwith the same naming; no real download is ever copied into the repository.\n\nTests: `src/__tests__/class-crunchlabs.test.ts`.\n"
16507
+ "content": "# Class CrunchLabs source (SMRT #4616)\n\nClass CrunchLabs is a global catalog provider (`class-crunchlabs`) whose units are\nlisted under Science, with the @ClassCrunchLabs YouTube course playlists as\npreviews. \"Add to a course\" reads the family's own download of the unit and runs\nthe [bundle import](course-bundle-import.md) with a manifest version 2 built from\nit. Files come from CrunchLabs' own downloads and YouTube; **nothing is hosted\nhere** and the listing has no acquirable formats.\n\n## Operator-only until permission, one public switch\n\n`CatalogVisibility` gains `operator`. A `CatalogPolicy` built with\n`operatorTenantIds` lets only those families (host configuration, never a\nrequest) see `operator` records; the default policy shows them to nobody, and\n`authorize` answers `operator-only`. Materialization refuses them.\n\n`createClassCrunchLabsCatalogAdapter({ playlists, publicListing? })` is a snapshot\n`CatalogSyncAdapter` (one terminal page, no cursor). By default\n(`CLASS_CRUNCHLABS_PUBLIC_LISTING_DEFAULT = false`) every record is `operator`\nvisibility with licence state `unknown`, no permitted operations and the text\n\"free curriculum, permission pending\". Only the literal `publicListing: true`,\nset by the operator in host configuration, makes records `catalog` visibility\nwith `verified` evidence permitting `link` only. The switch is part of each\nrecord's revision, so the next sync rewrites the rows when it is flipped.\n`classCrunchLabsPreviewEmbedUrl(record)` returns the youtube-nocookie playlist\nembed for a listed unit (null for anything else). The playlist data is an\ninjected `ClassCrunchLabsPlaylistSource`; the adapter never touches the network.\n\n`addClassCrunchLabsUnit({ service, actor, catalogActor, record, listing, policy,\nfiles, playlists, pageCounter?, receiveMember? })` re-checks\n`canAddClassCrunchLabsUnit(actor, policy, listing)`: visibility is derived from the\nhost's own `listing.publicListing` (operator unless the literal `true`), never from\nthe record or any caller-supplied visibility or licence; an operator-only listing\nis refused to every other family, and the course actor must be the catalog actor, builds the bundle and imports each part. Then store each\npart's accepted members and `attachCourse` every part (any order): they merge\ninto one family course. Courses are `personal-use-copy` (`build.licenseKind`).\n\n## Reading a download\n\n`buildClassCrunchLabsBundle({ files, playlists?, unit?, pageCounter?,\nincludeUnitWideAggregates?, maxPartBytes? })` takes a `ClassCrunchLabsFileSource`:\n`folderClassCrunchLabsSource(dir)` (Node only, from `@happyvertical/smrt-education/node`, never the root: a folder, the Drive zip parts, or both;\nzips are read lazily from their central directory, zip64/encrypted/symlinks are\nskipped) or `memoryClassCrunchLabsSource(files)`. It never reads video files.\n\n- Layout: `<Unit>_All Unit Wide Materials` and `<Unit>_Lesson <n>`; letters A\n teacher manual, B slides, C student mission log, D status check assessment\n (plus guidance and the editable master), E handouts, F extras, G videos. A\n lettered directory wins over a file's own letter. Several units need `unit`.\n- Mapping to manifest v2: A `teacher-guide`, B `slides`, C `learner-pages`, D\n `assessment` / `assessment-guidance`, E `handout`, F `printable`, the unit-wide\n Supply List `attachment`. Audiences are the role defaults (teacher manual and\n guidance guardian, slides guardian, mission log learner); the editable\n assessment master is guardian. Unit-wide files become course members.\n- The unit-wide \"(all lessons)\" combined files repeat the per-lesson files and\n are large, so they are left out unless `includeUnitWideAggregates` (reported as\n `aggregate-of-lessons`); the supply list, posters and other extras are kept.\n- Titles (#4642): a material's title is its file name tidied: the letter prefix,\n lesson code (`L02`), unit name, version (`v1.0`, `v.1.1`), `copy` and stray dots\n and underscores are dropped (`A. L01_Teacher Mission Manual_<Unit>_v1.0.pdf` is\n `Teacher Mission Manual`; `... Assessment (Editable)` keeps its qualifier), and a\n trailing ` - detail` becomes `(detail)` (`SQIREL Poster - 8.5x11` is `SQIREL\n Poster (8.5×11)`). Two different files that tidy to one title in the same\n lesson or the course are told apart as `Title (2)`; byte-identical copies are\n already collapsed by de-duplication. A lesson is titled `Lesson N: Name`. The name is, in order: the cover headline of the lesson's\n teacher manual PDF, when the host passes `readFirstPageHeadline({ bytes, kind:\n 'pdf' })` (the most prominent, largest-type text on page 1, lines joined by a\n space; this package has no PDF text reader, so the host supplies one, for\n example from pdfjs; all capitals fold to title case, `WEIGHT & MOTION` is\n `Weight & Motion`; a throw or null is ignored); else the activity of the lesson's\n first (lowest step) `StudentHandsOnExploration` video name, in title case with\n small words lower (`L03.6_Video_StudentHandsOnExploration_PlayWithPlungers` is\n `Lesson 3: Play with Plungers`); else `Lesson N`. Video names are read whether or\n not a playlist matched them, both `L01.4_Video_` and `L01.4 Video_` forms, `-NNN`\n download suffixes dropped, and loose lesson videos at the download's root count\n for the unit their name ends with. Titles are manifest data, so a section or lesson title that differs from\n an already attached part of the same unit is `part-conflict` (see the bundle\n import's shared-placement rules, which are deliberately not relaxed): a unit\n imported before this change keeps its `Lesson N` titles, and getting the new\n titles means importing the unit again into a fresh course (a different course\n id, not `courseId` of the old one).\n- Lesson pages: the first mission log of a lesson gets a `Mission log` range\n from `pageCounter` (`createClassCrunchLabsPageCounter({ converter? })` counts\n PDFs with pdf-lib and Word files by rendering them through the converter).\n Without a count the lesson gets no pages and the report says so.\n- Videos: names `Lnn.step_Video_Kind_Title` are matched, by unit (playlist\n title), lesson and step (`L03.6` or `Lesson 3 ... Step 6` in the item title),\n to the unit playlist; two videos sharing a step are told apart by the item\n title naming their title or kind, else both stay unmatched (`ambiguous`).\n Matches become `videos` references with a title and a step order (materials\n prep videos are guardian audience); unit-level videos (a teaser) and unmatched\n ones are only reported.\n- Playlists without step numbers (#4647): the live course playlists title items\n `<Unit>: <name>` in teaching order. Videos that no numbered item names fall back to\n title and kind evidence, never over a numbered match. Playlist items titled\n `Lesson N Recap` are anchors: each pairs with lesson N+1's single `Recap` video\n and they cut the playlist into one segment per lesson (lesson 1 runs from the\n start; the last download lesson runs to the end). Inside a segment a video and\n an item pair when each is the other's single best match (ties pair nothing).\n Evidence is normalized tokens (case, `&` is `and`, simple plurals, the unit\n prefix and `Part N` dropped, `N` kept) for the title, and kind words:\n `Prep`/`MaterialsPrep`, `Exploration`/`StudentHandsOnExploration`,\n `Anchor`/`AnchorCheckIn`, `Explained`/`ForcesExplainer`, `Wrap Up`/`WrapUp`,\n `Recap`, `Check-in`, `Link up`, `Look closer`. Differing numbers (`Part 1` against\n `Sumo Spheres 2`) or an item naming another kind contradict. What is left in a\n segment pairs by order only when the leftover videos and items are equally many,\n the videos sit on distinct steps and nothing contradicts; otherwise they stay\n unmatched. No pair crosses a segment. A playlist with no anchor is not segmented:\n only pairs with title evidence count and nothing is filled by order. Reasons:\n `no_item` (no unnumbered item is left for it, also a whole unit without anchors),\n `ambiguous` (items remain but evidence ties or the leftovers do not pair one to\n one), `unanchored` (its lesson is not bounded by recap anchors in the playlist),\n plus the existing `no_playlist` and `item_taken`.\n- De-duplication: repeated exports of one logical path collapse (CRC and size\n when known; the newer export wins, a changed file is `superseded`); the same\n bytes under another name or place keep one copy (the lesson-named file in its\n lesson, posters unit wide, never a `copy`).\n- A unit larger than one archive (bundle cap 100 MiB) is split by lesson into\n `build.parts` (default 90 MiB each; the first part carries the course members and\n any lesson that fits beside them, others go to the first part with room; if no\n lesson fits beside the course members the build refuses with `too_large`); `build.bundle`\n and `build.manifest` are the first part. Every part's manifest names the unit\n (`course.key` = `build.unitKey`, `classCrunchLabsUnitKey(unit)`, and `course.title`),\n its `part` (`index`/`count`) and each lesson's unit-wide `order` (the lesson\n number), so `attachCourse` merges the parts in lesson order whatever order they\n arrive in.\n\n- Lesson steps (#4656): the slides are step `launch` (order 0, \"Launch\"); each\n matched video is a step keyed by its real step code (`6`, `1b`) with the\n video's title as label, ordered numerically (`classCrunchLabsStepOrder`: `1` <\n `1a` < `1b` < `2`; videos sharing a code share the step, offset by 0.001 each);\n the status check assessment (printable PDF and the editable master) is step\n `check` (order 9999, \"Status check\"). The mission log, handouts, printables, the\n teacher manual and the assessment guidance carry no step: the first three are the\n learner's setup, the others stay guardian material.\n\nThe `report` lists lessons, members, parts, pages, matched and unmatched videos,\nduplicates, ignored files and warnings. Fixtures in tests are synthetic files\nwith the same naming; no real download is ever copied into the repository.\n\nTests: `src/__tests__/class-crunchlabs.test.ts`.\n"
16465
16508
  },
16466
16509
  {
16467
16510
  "path": "agents/source-import.md",
@@ -219,6 +219,11 @@ export interface ClassCrunchLabsBuild {
219
219
  licenseKind: typeof CLASS_CRUNCHLABS_LICENSE_KIND;
220
220
  report: ClassCrunchLabsReport;
221
221
  }
222
+ /**
223
+ * The numeric order of a step code (SMRT #4656): its number, plus a small
224
+ * fraction for a letter suffix so `1` < `1b` < `2` (`1a` is 1.01, `1b` 1.02).
225
+ */
226
+ export declare function classCrunchLabsStepOrder(code: string): number;
222
227
  /** Read a Class CrunchLabs unit download and build a manifest version 2 bundle. */
223
228
  export declare function buildClassCrunchLabsBundle(options: ClassCrunchLabsBuildOptions): Promise<ClassCrunchLabsBuild>;
224
229
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"class-crunchlabs.d.ts","sourceRoot":"","sources":["../../src/sources/class-crunchlabs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"class-crunchlabs.d.ts","sourceRoot":"","sources":["../../src/sources/class-crunchlabs.ts"],"names":[],"mappings":"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"}
@@ -141,6 +141,28 @@ export type LessonMaterialView = Readonly<{
141
141
  */
142
142
  thumbnailAssetId: string | null;
143
143
  }>;
144
+ /** What a lesson step asks of the learner (SMRT #4656). */
145
+ export type LessonStepKind = 'present' | 'watch' | 'do' | 'check';
146
+ /** One step of a lesson, in order, with the materials it carries. */
147
+ export type LessonStepView = Readonly<{
148
+ /** The bundle's step key (a step code such as `6` or `1b`, or `launch`). */
149
+ key: string;
150
+ /** The step's readable label; the key when the bundle gave none. */
151
+ label: string;
152
+ /** `present` slides, `watch` an external video, `check` an assessment, else `do`. */
153
+ kind: LessonStepKind;
154
+ /** Position in the lesson; fractions order `1b` after `1`. */
155
+ order: number;
156
+ /** Every visible material of the step, files then videos, original and rendered halves together. */
157
+ materials: LessonMaterialView[];
158
+ }>;
159
+ /** A lesson walked in order: what to prepare, then the steps. */
160
+ export type LessonStepsView = Readonly<{
161
+ /** Printed or prepared before the lesson: stepless handouts, learner pages and printables. */
162
+ setup: LessonMaterialView[];
163
+ /** Ordered by step order then key; empty when the lesson declares no steps. */
164
+ steps: LessonStepView[];
165
+ }>;
144
166
  /** The page key a file carries: its own, or `p<position>` for older rows. */
145
167
  export declare function submissionPageKey(pageKey: string, sortOrder: number): string;
146
168
  /** Failure includes fresh asset identities only, for explicit storage reconciliation. */
@@ -295,6 +317,16 @@ export declare class SubmissionService extends LearnerService {
295
317
  * learner-audience material; keys and guidance never reach them.
296
318
  */
297
319
  listLessonMaterials(request: LessonRequest): Promise<LessonMaterialView[]>;
320
+ private collectLessonMaterials;
321
+ /**
322
+ * A lesson walked in order (SMRT #4656), audience-filtered exactly like
323
+ * `listLessonMaterials`: stepless handouts, learner pages and printables as
324
+ * `setup`, then the declared steps. A step groups every material sharing its
325
+ * key (the original and rendered halves of a deck stay together). A lesson
326
+ * with no declared steps returns `steps: []`. Course-level supply lists are
327
+ * not part of a lesson's listing and stay with `listCourseMaterials`.
328
+ */
329
+ listLessonSteps(request: LessonRequest): Promise<LessonStepsView>;
298
330
  /** Course-level materials (supply lists, posters) under the same audience rule. */
299
331
  listCourseMaterials(request: LearnerRequest & {
300
332
  enrollmentId: string;
@@ -346,6 +378,11 @@ export declare class SubmissionService extends LearnerService {
346
378
  stepOrder: number;
347
379
  audience?: MaterialAudience;
348
380
  }): Promise<LessonMaterialView>;
381
+ /**
382
+ * Sign a lesson file for printing: a `printable`, `handout` or `learner-pages`
383
+ * PDF the actor may see (learner-audience, or any for a guardian). Never an
384
+ * editable `original` rendition and never an answer key.
385
+ */
349
386
  signLessonPrintable(request: LessonRequest & {
350
387
  assetId: string;
351
388
  }): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"submission-service.d.ts","sourceRoot":"","sources":["../src/submission-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"submission-service.d.ts","sourceRoot":"","sources":["../src/submission-service.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-education",
3
- "version": "0.60.3",
3
+ "version": "0.60.5",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Tenant-aware learning catalog models and authorized catalog access for SMRT",
6
6
  "type": "module",
@@ -29,18 +29,18 @@
29
29
  }
30
30
  },
31
31
  "dependencies": {
32
- "@happyvertical/smrt-assets": "0.60.3",
33
- "@happyvertical/smrt-core": "0.60.3",
34
- "@happyvertical/smrt-profiles": "0.60.3",
35
- "@happyvertical/smrt-tenancy": "0.60.3",
36
- "@happyvertical/smrt-users": "0.60.3",
32
+ "@happyvertical/smrt-assets": "0.60.5",
33
+ "@happyvertical/smrt-core": "0.60.5",
34
+ "@happyvertical/smrt-profiles": "0.60.5",
35
+ "@happyvertical/smrt-tenancy": "0.60.5",
36
+ "@happyvertical/smrt-users": "0.60.5",
37
37
  "@happyvertical/files": "^0.111.3",
38
38
  "@happyvertical/sql": "^0.111.3",
39
39
  "pdf-lib": "1.17.1",
40
- "@happyvertical/smrt-ingestion": "0.60.3"
40
+ "@happyvertical/smrt-ingestion": "0.60.5"
41
41
  },
42
42
  "devDependencies": {
43
- "@happyvertical/smrt-vitest": "0.60.3",
43
+ "@happyvertical/smrt-vitest": "0.60.5",
44
44
  "@types/node": "24.13.6",
45
45
  "typescript": "6.0.3",
46
46
  "vite": "8.1.6",