@happyvertical/smrt-education 0.57.38 → 0.57.40

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.
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-education",
5
- "packageVersion": "0.57.38",
5
+ "packageVersion": "0.57.40",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-education:CatalogProviderCollection": {
8
8
  "name": "catalogprovidercollection",
@@ -108,7 +108,7 @@
108
108
  "collectionExportName": "CatalogResourceCollectionCollection",
109
109
  "schema": {
110
110
  "tableName": "catalog_resources",
111
- "ddl": "CREATE TABLE IF NOT EXISTS \"catalog_resources\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"provider_id\" TEXT NOT NULL,\n \"stable_id\" TEXT NOT NULL,\n \"revision\" TEXT NOT NULL,\n \"canonical_url\" TEXT,\n \"title\" TEXT NOT NULL,\n \"supplied_description\" TEXT,\n \"author\" TEXT,\n \"subjects\" TEXT,\n \"grades\" TEXT,\n \"language\" TEXT,\n \"resource_type\" TEXT,\n \"parent_stable_id\" TEXT,\n \"available_formats\" TEXT,\n \"representation\" TEXT,\n \"license_evidence\" TEXT,\n \"license_state\" TEXT DEFAULT 'unknown',\n \"visibility\" TEXT DEFAULT 'catalog',\n \"last_seen_run_id\" TEXT,\n \"withdrawn_at\" TEXT\n);",
111
+ "ddl": "CREATE TABLE IF NOT EXISTS \"catalog_resources\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"provider_id\" TEXT NOT NULL,\n \"stable_id\" TEXT NOT NULL,\n \"revision\" TEXT NOT NULL,\n \"canonical_url\" TEXT,\n \"title\" TEXT NOT NULL,\n \"supplied_description\" TEXT,\n \"author\" TEXT,\n \"subjects\" TEXT,\n \"grades\" TEXT,\n \"language\" TEXT,\n \"resource_type\" TEXT,\n \"parent_stable_id\" TEXT,\n \"available_formats\" TEXT,\n \"representation\" TEXT,\n \"preview\" TEXT,\n \"license_evidence\" TEXT,\n \"license_state\" TEXT DEFAULT 'unknown',\n \"visibility\" TEXT DEFAULT 'catalog',\n \"last_seen_run_id\" TEXT,\n \"withdrawn_at\" TEXT\n);",
112
112
  "columns": {
113
113
  "id": {
114
114
  "type": "UUID",
@@ -205,6 +205,11 @@
205
205
  "notNull": false,
206
206
  "unique": false
207
207
  },
208
+ "preview": {
209
+ "type": "TEXT",
210
+ "notNull": false,
211
+ "unique": false
212
+ },
208
213
  "license_evidence": {
209
214
  "type": "TEXT",
210
215
  "notNull": false,
@@ -249,7 +254,7 @@
249
254
  ]
250
255
  }
251
256
  ],
252
- "version": "e75f6bd0"
257
+ "version": "6ea66f0d"
253
258
  },
254
259
  "importPath": "@happyvertical/smrt-education"
255
260
  },
@@ -809,6 +814,10 @@
809
814
  "type": "text",
810
815
  "required": false
811
816
  },
817
+ "preview": {
818
+ "type": "text",
819
+ "required": false
820
+ },
812
821
  "licenseEvidence": {
813
822
  "type": "text",
814
823
  "required": false
@@ -843,6 +852,36 @@
843
852
  }
844
853
  },
845
854
  "methods": {
855
+ "getPreview": {
856
+ "name": "getPreview",
857
+ "async": false,
858
+ "parameters": [],
859
+ "returnType": "CatalogPreview | undefined",
860
+ "isStatic": false,
861
+ "isPublic": true
862
+ },
863
+ "setPreview": {
864
+ "name": "setPreview",
865
+ "async": false,
866
+ "parameters": [
867
+ {
868
+ "name": "value",
869
+ "type": "CatalogPreview | undefined",
870
+ "optional": false,
871
+ "unionBranches": [
872
+ {
873
+ "type": "CatalogPreview"
874
+ },
875
+ {
876
+ "type": "undefined"
877
+ }
878
+ ]
879
+ }
880
+ ],
881
+ "returnType": "void",
882
+ "isStatic": false,
883
+ "isPublic": true
884
+ },
846
885
  "getRepresentation": {
847
886
  "name": "getRepresentation",
848
887
  "async": false,
@@ -907,7 +946,7 @@
907
946
  ],
908
947
  "schema": {
909
948
  "tableName": "catalog_resources",
910
- "ddl": "CREATE TABLE IF NOT EXISTS \"catalog_resources\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"provider_id\" TEXT NOT NULL,\n \"stable_id\" TEXT NOT NULL,\n \"revision\" TEXT NOT NULL,\n \"canonical_url\" TEXT,\n \"title\" TEXT NOT NULL,\n \"supplied_description\" TEXT,\n \"author\" TEXT,\n \"subjects\" TEXT,\n \"grades\" TEXT,\n \"language\" TEXT,\n \"resource_type\" TEXT,\n \"parent_stable_id\" TEXT,\n \"available_formats\" TEXT,\n \"representation\" TEXT,\n \"license_evidence\" TEXT,\n \"license_state\" TEXT DEFAULT 'unknown',\n \"visibility\" TEXT DEFAULT 'catalog',\n \"last_seen_run_id\" TEXT,\n \"withdrawn_at\" TEXT\n);",
949
+ "ddl": "CREATE TABLE IF NOT EXISTS \"catalog_resources\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"provider_id\" TEXT NOT NULL,\n \"stable_id\" TEXT NOT NULL,\n \"revision\" TEXT NOT NULL,\n \"canonical_url\" TEXT,\n \"title\" TEXT NOT NULL,\n \"supplied_description\" TEXT,\n \"author\" TEXT,\n \"subjects\" TEXT,\n \"grades\" TEXT,\n \"language\" TEXT,\n \"resource_type\" TEXT,\n \"parent_stable_id\" TEXT,\n \"available_formats\" TEXT,\n \"representation\" TEXT,\n \"preview\" TEXT,\n \"license_evidence\" TEXT,\n \"license_state\" TEXT DEFAULT 'unknown',\n \"visibility\" TEXT DEFAULT 'catalog',\n \"last_seen_run_id\" TEXT,\n \"withdrawn_at\" TEXT\n);",
911
950
  "columns": {
912
951
  "id": {
913
952
  "type": "UUID",
@@ -1004,6 +1043,11 @@
1004
1043
  "notNull": false,
1005
1044
  "unique": false
1006
1045
  },
1046
+ "preview": {
1047
+ "type": "TEXT",
1048
+ "notNull": false,
1049
+ "unique": false
1050
+ },
1007
1051
  "license_evidence": {
1008
1052
  "type": "TEXT",
1009
1053
  "notNull": false,
@@ -1048,7 +1092,7 @@
1048
1092
  ]
1049
1093
  }
1050
1094
  ],
1051
- "version": "e75f6bd0"
1095
+ "version": "6ea66f0d"
1052
1096
  },
1053
1097
  "importPath": "@happyvertical/smrt-education"
1054
1098
  },
@@ -3,19 +3,20 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-education",
6
- "packageVersion": "0.57.38",
6
+ "packageVersion": "0.57.40",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "f63db1cd3e8bf488aa731aed6f5b4cb1ff6791a221b9bac788a2a6ce223abe1c",
11
- "packageJson": "5075accbaa27c88f5eff6b23f3462dcb8ddd4c50b862775a1720809f73cfe2c1",
12
- "agents": "dd188ff6cfca738bf1dcc08ef4e86c8870c8add515092d971c9a2befd3568942",
10
+ "manifest": "dfc6a322afc28d7f92eadddb2a755c106a6c2723d60de92fab371dac8687f545",
11
+ "packageJson": "1a169485fd12e1a3ac3105dbf10e918fa3035f639e2e11d752772b4d577c4a85",
12
+ "agents": "f06312dca0737d6487b4f310b22b6c24b3276644e9ccaf9b7a27b71ebc1ca2ff",
13
13
  "moduleDoc:agents/learner-service.md": "7da70d214547eb78fbf058e997599cc80ab86f5bb55748bd158a9a4b30070d3b",
14
14
  "moduleDoc:agents/submission-service.md": "8e63b83ac84628d5e5ddacbb6a47077c0e65eee3da87518d9bf6e30fb8348336",
15
15
  "moduleDoc:agents/workbook-service.md": "3ca3fb60d39daf76b94ec668fc19dc67aa7e090b7f44412bed5c9d92e8efaf5a",
16
16
  "moduleDoc:agents/photo-intake.md": "3b7057201ee30b2361aad23ea52e8a55c8ed5354e7a7e4a8193b7de17ca2f0db",
17
17
  "moduleDoc:agents/source-import.md": "30ee823589d655442bcf1abcd86a2132172065b4bc5e3c4d89b1e8b7afcc66bc",
18
- "moduleDoc:agents/family-intake.md": "540b6d37f438496acb0208b6ac7258264e2142bce3d2f986967b7741606fe4f7"
18
+ "moduleDoc:agents/family-intake.md": "540b6d37f438496acb0208b6ac7258264e2142bce3d2f986967b7741606fe4f7",
19
+ "moduleDoc:agents/catalog-previews.md": "5f4e0352274009b3ce00408017a16666036d24df695a7078c27317ba4e66f0a7"
19
20
  },
20
21
  "exports": [
21
22
  ".",
@@ -283,6 +284,12 @@
283
284
  "required": false,
284
285
  "columnType": "TEXT"
285
286
  },
287
+ {
288
+ "name": "preview",
289
+ "type": "text",
290
+ "required": false,
291
+ "columnType": "TEXT"
292
+ },
286
293
  {
287
294
  "name": "licenseEvidence",
288
295
  "type": "text",
@@ -318,14 +325,27 @@
318
325
  ],
319
326
  "relationships": [],
320
327
  "methods": [
328
+ "getPreview",
321
329
  "getRepresentation",
330
+ "setPreview",
322
331
  "setRepresentation"
323
332
  ],
324
333
  "methodSignatures": [
334
+ {
335
+ "name": "getPreview",
336
+ "returns": "CatalogPreview | undefined"
337
+ },
325
338
  {
326
339
  "name": "getRepresentation",
327
340
  "returns": "CatalogResourceRepresentation | undefined"
328
341
  },
342
+ {
343
+ "name": "setPreview",
344
+ "params": [
345
+ "value: CatalogPreview | undefined"
346
+ ],
347
+ "returns": "void"
348
+ },
329
349
  {
330
350
  "name": "setRepresentation",
331
351
  "params": [
@@ -336,6 +356,13 @@
336
356
  ],
337
357
  "surfaces": [],
338
358
  "withheldSurfaces": [
359
+ {
360
+ "kind": "api",
361
+ "operation": "getPreview",
362
+ "code": "api-disabled",
363
+ "reason": "api is disabled",
364
+ "objectName": "@happyvertical/smrt-education:CatalogResource"
365
+ },
339
366
  {
340
367
  "kind": "api",
341
368
  "operation": "getRepresentation",
@@ -343,6 +370,13 @@
343
370
  "reason": "api is disabled",
344
371
  "objectName": "@happyvertical/smrt-education:CatalogResource"
345
372
  },
373
+ {
374
+ "kind": "api",
375
+ "operation": "setPreview",
376
+ "code": "api-disabled",
377
+ "reason": "api is disabled",
378
+ "objectName": "@happyvertical/smrt-education:CatalogResource"
379
+ },
346
380
  {
347
381
  "kind": "api",
348
382
  "operation": "setRepresentation",
@@ -6401,7 +6435,7 @@
6401
6435
  "polymorphicAssociations": 0,
6402
6436
  "uuidColumns": 320
6403
6437
  },
6404
- "agentDoc": "# smrt-education\n\nCatalog objects are optional-tenant scoped and have no generated REST, CLI, MCP,\nor AI surface. Applications use `CatalogService`, supplying their actor-bound\nauthorization decision and the trusted `TenantCollection.withTenantCatalogReadScope`\nadapter from smrt-users. Catalog authorization receives the exact shared collection\nexecutor; live session/permission checks must use that executor. That read scope may widen only an allowlisted catalog\ncollection's `list` to self, ancestors, and globals; it never authorizes a get,\nwrite, raw query, sibling, or descendant read.\n\n`personal-use-copy` courses and all `answer-key` lesson assets are private\nfamily material. Do not publish them, promote them, or expose them through a\nlearner-facing result. `LessonAsset` is the sole owner join for lesson files;\ndo not add asset-id fields to lesson subclasses.\n\nLearner objects require exact tenant ownership and close generated surfaces.\n`LearnerService` owns actor/resource authorization and transactional planning and\nstart-progress; see [learner contract](agents/learner-service.md). Guardian\nrelationships require a host decision about their type and effective terms.\nNever expose direct collection writes as learner handlers, reset progress on a\nretry, or treat an unscored nullable decimal as zero.\n\nSubmission capture, guardian grading and private PDF packet splitting use\n`SubmissionService`; see [submission contract](agents/submission-service.md).\nKeep asset metadata on the same transaction and compensate only newly planned\nfiles after rollback; never expose source URIs or accept learner grade patches.\n\nValidation: `pnpm --filter @happyvertical/smrt-education test`,\n`test:postgres`, `typecheck`, and `build`.\n\nPhysical workbook capture, immutable assessment/feedback history and descriptive\nevidence use [WorkbookService](agents/workbook-service.md). Keep provisional\nsharing separate from guardian Grade authority. Photo-first receipt/context\ncapture uses [WorkbookPhotoService](agents/photo-intake.md); covers/references\nnever create learning evidence, and blank exercises need explicit work intent.\n\nSource import: [host boundary and transaction contract](agents/source-import.md).\n`SourceImportService` requires host opt-in, matching active actor/tenant and\nexplicit licensing authorization; never add a system/global write bypass.\n\nFamily-owned unassigned originals and audited learner adoption use\n[FamilyIntakeService](agents/family-intake.md). Keep family source scope immutable;\nassignment alone never creates learning history or grants learner inbox access.\n",
6438
+ "agentDoc": "# smrt-education\n\nCatalog objects are optional-tenant scoped and have no generated REST, CLI, MCP,\nor AI surface. Applications use `CatalogService`, supplying their actor-bound\nauthorization decision and the trusted `TenantCollection.withTenantCatalogReadScope`\nadapter from smrt-users. Catalog authorization receives the exact shared collection\nexecutor; live session/permission checks must use that executor. That read scope may widen only an allowlisted catalog\ncollection's `list` to self, ancestors, and globals; it never authorizes a get,\nwrite, raw query, sibling, or descendant read.\n\n`personal-use-copy` courses and all `answer-key` lesson assets are private\nfamily material. Do not publish them, promote them, or expose them through a\nlearner-facing result. `LessonAsset` is the sole owner join for lesson files;\ndo not add asset-id fields to lesson subclasses.\n\nLearner objects require exact tenant ownership and close generated surfaces.\n`LearnerService` owns actor/resource authorization and transactional planning and\nstart-progress; see [learner contract](agents/learner-service.md). Guardian\nrelationships require a host decision about their type and effective terms.\nNever expose direct collection writes as learner handlers, reset progress on a\nretry, or treat an unscored nullable decimal as zero.\n\nSubmission capture, guardian grading and private PDF packet splitting use\n`SubmissionService`; see [submission contract](agents/submission-service.md).\nKeep asset metadata on the same transaction and compensate only newly planned\nfiles after rollback; never expose source URIs or accept learner grade patches.\n\nValidation: `pnpm --filter @happyvertical/smrt-education test`,\n`test:postgres`, `typecheck`, and `build`.\n\nPhysical workbook capture, immutable assessment/feedback history and descriptive\nevidence use [WorkbookService](agents/workbook-service.md). Keep provisional\nsharing separate from guardian Grade authority. Photo-first receipt/context\ncapture uses [WorkbookPhotoService](agents/photo-intake.md); covers/references\nnever create learning evidence, and blank exercises need explicit work intent.\n\nSource import: [host boundary and transaction contract](agents/source-import.md).\n`SourceImportService` requires host opt-in, matching active actor/tenant and\nexplicit licensing authorization; never add a system/global write bypass.\n\nFamily-owned unassigned originals and audited learner adoption use\n[FamilyIntakeService](agents/family-intake.md). Keep family source scope immutable;\nassignment alone never creates learning history or grants learner inbox access.\n\nPublisher catalog previews use [the preview contract](agents/catalog-previews.md).\nKeep independent image rights and source revisions; serve via `CatalogPreviewService`\nwith live actor/tenant authorization, never direct provider image URLs.\n",
6405
6439
  "moduleDocs": [
6406
6440
  {
6407
6441
  "path": "agents/learner-service.md",
@@ -6432,6 +6466,11 @@
6432
6466
  "path": "agents/family-intake.md",
6433
6467
  "module": "family-intake",
6434
6468
  "content": "# Family intake\n\n`FamilyIntakeService` retains guardian-only originals before a learner is known.\nThe host supplies a trusted actor/tenant and a current family authorizer; each\nread, original download, assignment and adoption revalidates this authority on\nthe service's exact executor. Learners cannot browse the family inbox. Generated\nCRUD, CLI, MCP and AI surfaces remain closed.\n\nRetain the whole document with its source identity, delivery key, immutable hash\nand original private assets. Source delivery retries must preserve identical\ncontent and provenance. Email and scanner adapters are separate host configuration;\nthis service does not configure or select them. Trusted candidate DTOs and their\nquoted evidence are advisory. A guardian's active learner preference, visible\nmarks, uncertain names or a first-page guess never establish ownership. Mixed\nlearner PDFs stay unresolved until an explicit whole-document assignment is safe.\n\nAssignment records an immutable guardian decision with a revision and request\nkey. It does not create learner work. Adoption freezes that assignment and borrows\nthe exact original assets through learner photo ownership joins; it never rewrites\nsource confidentiality or creates grades. The existing photo resolution and\ncompleted-work capture preconditions still apply. Existing derived capture copies\nare permitted. Once adopted, a receipt cannot silently change learners; recovery\nrequires a separate explicit policy. An unadopted attribution can be corrected by\nanother audited decision with the current revision.\n\nFamily ownership retains originals beyond generic receipt expiry. Adoption does\nnot transfer or delete that ownership. Cleanup and rollback must preserve shared\nretained bytes and compensate only newly created files. Every access checks the\noriginal hash before adoption or download.\n\nValidation uses shared family workflows on real SQLite and isolated PostgreSQL;\nno AI providers, scanner/email services or containers are required.\n\nImport `FamilyIntakeService`, `receiveFamilyIntake` and\n`retainFamilyIntakeReceipt` from `@happyvertical/smrt-education/ingestion`.\nThe bridge uses the fixed `familyIntakeScopeId(tenantId)` namespace and public\n`getReceiptSource` to retain authoritative delivery identity. This namespace is\nnever an authorization grant. The host's `IngestionService.authorize` callback\nmust call the same current family authorizer on every operation, including direct\n`getEvidence`, `readEvidence` and `getReceiptSource` calls. For example, await\n`family.authorizeFamilyIntake({ tenantId: scope.tenantId, actorUserId: scope.actorId })`\nthen return true; propagate denial. Actor equality or tenant membership alone is\ninsufficient. This protects raw envelopes and rejected/unretained originals as\nwell as adopted receipts. Adoption permits only normal authorized learner work\nreads; it never grants the learner generic family ingestion access. A receive that\nsucceeds before education retention fails is repaired by retrying the same\nreceipt, never by inventing another delivery identity.\n\nDeploy manifest migrations for all three FamilyIntake models and the nullable\n`WorkbookPhotoIntake.familyIntakeId` FK before use. There is no runtime DDL.\nAdoption commits its immutable audit and exact photo/original joins atomically;\nthe family asset remains the sole original owner. Existing derived submission\ncopies use normal capture compensation. The exact adoption audit is required\nwhen validating borrowing joins, reads and immutable family-scope analysis.\n`countFamilyIntakes` counts unadopted receipts across the full inbox, while\n`listFamilyIntakes` pages the complete receipt history.\n\nAfter adoption, use the returned `photoIntakeId` with ordinary photo\nresolution/capture methods. Family-scope photo analysis checks the exact adopted\nitem/learner binding; learner-scoped uploads retain their existing strict scope.\nThe existing submission/assessment ingestion handlers still revalidate the\ncaptured learner/attempt. Temporary ingestion expiry does not prevent manual\nresolution/capture from retained education originals; re-analysis of an expired\nreceipt needs a separately authorized retained-input orchestration.\n\nThis initial contract assigns a whole receipt/document. It does not split a\nmixed-child PDF, automatically infer a child or configure a source adapter.\nFuture range adoption must preserve original hashes and page provenance; the\nretained immutable originals remain available for such an explicit workflow.\n\nSource-tree retention includes image/PDF attachments beneath metadata envelopes.\nA supported-media descendant of another supported original (for example a PDF's\npage thumbnail) stays in the evidence graph but is not duplicated as learner\nwork. Photo identification and captured-work analysis preservation use this same\noriginal-selection convention, so retained attachment hashes remain authoritative\nafter adoption and retries. The immutable graph retains only IDs, parent IDs, media types and hashes;\nit does not copy email bodies, arbitrary headers or credentials into public\nsource metadata. A source image such as an email logo is not automatically\nclassified as completed work. Whole-document attribution/classification remains\nan explicit guardian decision; ambiguous mixed contents need resolution.\n\nCustom source adapters must follow this supported convention: metadata parents\ncontain independent original attachments; descendants of supported media are\nrenditions of that media. Extraction outputs already live separately from source\nevidence. An adapter that uses media-child relationships for independently\nauthored attachments is unsupported by this bridge until its role is explicitly\nestablished; do not claim complete attachment fidelity for that shape or infer\nroles from first-page content. Full immutable evidence lineage is retained even\nwhen a rendition is excluded from capture pages.\n\nThe bridge rejects an entire receipt containing an unsupported independent\noriginal, including a mixed PDF/TIFF bundle, before family retention. Generic\nsource originals remain available for recovery; no partial family receipt or\nadoption is created. Hosts must acknowledge successful family import only after\nfamily retention succeeds and surface failed source imports. Known JSON\nmetadata wrappers and media descendants under the supported rendition convention\nare not independent originals.\n"
6469
+ },
6470
+ {
6471
+ "path": "agents/catalog-previews.md",
6472
+ "module": "catalog-previews",
6473
+ "content": "# Publisher catalog previews\n\n`CatalogSourceRecord.preview` and `CatalogResourceView.preview` are optional\n`CatalogPreview` metadata. They describe a publisher image, not an acquired\noriginal, cover guarantee, download permission, or learner access grant. The\npublic root exports `CatalogPreviewService`, `CatalogPreviewStore`,\n`CatalogPreviewLoader`, `catalogPreviewFor`, and `normalizeCatalogPreview`.\n`CatalogResource.getPreview()` reads the guarded persisted JSON. Deploy the normal\nadditive manifest migration for its nullable `preview` column before sync; legacy\nrows have no preview. Sync retains the first retrieval timestamp on identical\nrevision retries and rejects changing an existing preview within that revision.\nNew revisions replace the current reference; old revision requests become 404.\nPNG verification resolves its Node host capability only when invoked; importing\ncatalog metadata/models remains browser-safe. Serving/verification requires a\nNode host and fails closed without it.\nThe image hash participates in the IM page record's revision, so changed bytes\ncannot silently reuse a preview revision.\n\nThe public IM first-edition adapter automatically selects at most one embedded\ninstructional diagram on a K–5 family-materials page. It requires that exact\npage's CC BY 4.0 footer and attribution, an official ImageFile figure with an\nempty caption, no separate credits or image-license notice in the instructional\ncontent, a descriptive chart/diagram alt, and an exact official asset identifier.\nBranding, photographs, credited or ambiguous images, other providers, unsupported\nformats and missing metadata remain absent. This narrow page-scoped policy does\nnot grant a provider-wide image license. The source footer explicitly excludes\nthe name/logo and preserves third-party image licenses; never remove those\nexclusions or apply this rule to arbitrary images. Existing teacher visibility\nis retained, including family hierarchy records currently classified teacher.\n\nConcrete publisher evidence, retrieved 2026-10-28:\n[Grade 1 Unit 1 Family Materials](https://im.kendallhunt.com/k5/families/grade-1/unit-1/family-materials.html)\nembeds ImageFile 38531, “Tally Chart. Turtle, tallies, 6. Dog, tallies, 2.\nRabbit, tallies, 12.” Its\n[official PNG](https://cms-assets.illustrativemathematics.org/fd1cykl2wl4zyfc6vzfkl3x09pvr)\nis 93,072 bytes, 1350 × 674. The licensed instructional fragment and PNG are\nfixtures in `src/__tests__/fixtures/im-family-*`, attributed to © 2021\nIllustrative Mathematics, CC BY 4.0. The image is unmodified. Captions or separate\nattribution appearing in a future sync make the preview unavailable.\n\nEnumeration performs metadata reads and at most one **preview image** GET per\npage; it never GETs original PDFs/ZIPs. It accepts only official HTTPS origin and\nopaque path, refuses redirects, and bounds the complete network/body operation\nby the adapter deadline (20 seconds default, 60 maximum). Required HTML/HEAD\nwork completes before optional sync previews, which get an independent deadline\nof one quarter of that budget, capped at two seconds and the remaining total\nenumeration budget. A hung preview cannot pause\nmetadata enumeration or its cursor. PNG previews have a\n2 MiB byte cap and 4096-pixel width/height cap. SVG, HTML, JPEG and other formats\nare currently unavailable. Only non-interlaced, static 8-bit RGB/RGBA PNGs are\nverified: complete chunks/CRCs/IEND and bounded inflated scanlines must be valid;\ntruncated or corrupt data never becomes a verified preview. This uses existing provider request bounds rather\nthan a general URL proxy. A preview failure leaves catalog metadata readable.\n`CatalogReadService.search()` performs no image or content fetches.\n\nWire a host endpoint using only the published root API:\n\n```ts\nimport {\n CatalogPreviewService,\n createPublicIllustrativeMathematicsFirstEditionAdapter,\n} from '@happyvertical/smrt-education';\n\nconst previews = new CatalogPreviewService({\n // Both functions use your trusted active-session/tenant executor. Adapt your\n // persisted resource to CatalogSourceRecord, including row.getPreview().\n getResource: (id, actor) => catalogStore.getReadableResource(id, actor),\n authorize: (actor, resource) => catalogPolicy.canReadNow(actor, resource.id),\n}, createPublicIllustrativeMathematicsFirstEditionAdapter());\n\n// Inside GET /catalog/preview; actor is derived from the server session.\nreturn previews.serve({ actor, resourceId, revision });\n```\n\nRender your own endpoint URL only when `view.preview` is present, using `alt`,\nwidth/height and rights attribution. Never place `reference` or `sourceUrl` into\nan image URL. The endpoint re-reads the current resource and checks active\nhost authorization, tenant/actor identity, teacher visibility, withdrawal,\nrevision and optional rights expiry on **every request**, including cache hits.\nThe host must check actor membership/active tenant, not merely trust strings in a\nrequest. Do not expose an unscoped collection read as this adapter.\n\nRights expiry is checked again after awaited loading/verification and before\nserving or inserting cached bytes. The per-service cache holds at most 16 MiB of verified public image bytes for at\nmost 60 seconds; reuse a service instance to benefit from it. Cache lookup occurs\nafter authorization. Changed length, dimensions or SHA-256, expiry, missing or\ninvalid metadata, upstream failures and access denial yield uniform 404 with\n`Cache-Control: no-store`; successful responses are PNG, `nosniff`, sandboxed,\nand `private, no-store`, so browsers cannot serve a revoked preview from cache.\nNo credential, upstream image URL, redirect or raw provider error reaches the\nbrowser. Synchronize regularly to observe publisher withdrawal/license changes;\nserver byte caching never bypasses the durable current-resource decision.\n\nPrivate asset previews and derivatives of acquired originals are unavailable in\nthis publisher-only contract. Use the asset authority and the separate acquired\noriginal derivative capability (#4470), with authorization on every private\nasset read. Possessing an asset, granting full-resource download/print, or being\nable to view a publisher page never supplies preview permission here.\n"
6435
6474
  }
6436
6475
  ],
6437
6476
  "recipes": [
@@ -1,4 +1,5 @@
1
1
  import type { CatalogAcquireAdapter, CatalogSyncAdapter } from '../catalog-contracts.js';
2
+ import { type CatalogPreviewLoader } from '../catalog-preview.js';
2
3
  export interface PublicIllustrativeMathematicsFirstEditionConfig {
3
4
  /** Standard fetch, including streaming Response bodies; defaults to global fetch. */
4
5
  fetch?: typeof globalThis.fetch;
@@ -9,9 +10,10 @@ export interface PublicIllustrativeMathematicsFirstEditionConfig {
9
10
  }
10
11
  /**
11
12
  * Public first-edition English/linked Spanish K–12 teacher/family hierarchy.
12
- * Each enumeration reads one HTML page and at most four asset HEADs. Only an
13
+ * Each enumeration reads one HTML page, at most one bounded preview PNG and
14
+ * at most four original asset HEADs. Only an
13
15
  * explicit, validated selection can GET PDF bytes; generic acquisition retains
14
16
  * policy, tenant isolation, idempotence, attribution and notices.
15
17
  */
16
- export declare function createPublicIllustrativeMathematicsFirstEditionAdapter(config?: PublicIllustrativeMathematicsFirstEditionConfig): CatalogSyncAdapter & CatalogAcquireAdapter;
18
+ export declare function createPublicIllustrativeMathematicsFirstEditionAdapter(config?: PublicIllustrativeMathematicsFirstEditionConfig): CatalogSyncAdapter & CatalogAcquireAdapter & CatalogPreviewLoader;
17
19
  //# sourceMappingURL=public-illustrative-mathematics-adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public-illustrative-mathematics-adapter.d.ts","sourceRoot":"","sources":["../../src/sources/public-illustrative-mathematics-adapter.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,qBAAqB,EAErB,kBAAkB,EAEnB,MAAM,yBAAyB,CAAC;AAYjC,MAAM,WAAW,+CAA+C;IAC9D,qFAAqF;IACrF,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,0FAA0F;IAC1F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAuID;;;;;GAKG;AACH,wBAAgB,sDAAsD,CACpE,MAAM,GAAE,+CAAoD,GAC3D,kBAAkB,GAAG,qBAAqB,CAsY5C"}
1
+ {"version":3,"file":"public-illustrative-mathematics-adapter.d.ts","sourceRoot":"","sources":["../../src/sources/public-illustrative-mathematics-adapter.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,qBAAqB,EAErB,kBAAkB,EAEnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAGL,KAAK,oBAAoB,EAG1B,MAAM,uBAAuB,CAAC;AAY/B,MAAM,WAAW,+CAA+C;IAC9D,qFAAqF;IACrF,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,0FAA0F;IAC1F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAuID;;;;;;GAMG;AACH,wBAAgB,sDAAsD,CACpE,MAAM,GAAE,+CAAoD,GAC3D,kBAAkB,GAAG,qBAAqB,GAAG,oBAAoB,CAyenE"}
@@ -24,6 +24,11 @@ export interface ImArtifact {
24
24
  allowed: boolean;
25
25
  }
26
26
  export interface ImParsedPage {
27
+ /** Page-scoped instructional ImageFiles; never branding or credited third-party images. */
28
+ previewImage?: {
29
+ id: string;
30
+ alt: string;
31
+ };
27
32
  children: string[];
28
33
  artifacts: ImArtifact[];
29
34
  exclusions: {
@@ -1 +1 @@
1
- {"version":3,"file":"public-illustrative-mathematics-parser.d.ts","sourceRoot":"","sources":["../../src/sources/public-illustrative-mathematics-parser.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,eAAO,MAAM,SAAS,+BAA+B,CAAC;AACtD,eAAO,MAAM,eAAe,mDAAmD,CAAC;AAChF,eAAO,MAAM,UAAU,iDAAiD,CAAC;AACzE,eAAO,MAAM,QAAQ,UAIpB,CAAC;AAEF,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS5C;AACD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AACD,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAiCxD;AACD,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAUtD;AACD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjE;AACD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAS7D;AACD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AACD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAiLD,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,YAAY,CA2Jd"}
1
+ {"version":3,"file":"public-illustrative-mathematics-parser.d.ts","sourceRoot":"","sources":["../../src/sources/public-illustrative-mathematics-parser.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,eAAO,MAAM,SAAS,+BAA+B,CAAC;AACtD,eAAO,MAAM,eAAe,mDAAmD,CAAC;AAChF,eAAO,MAAM,UAAU,iDAAiD,CAAC;AACzE,eAAO,MAAM,QAAQ,UAIpB,CAAC;AAEF,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS5C;AACD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AACD,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAiCxD;AACD,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAUtD;AACD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjE;AACD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAS7D;AACD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AACD,MAAM,WAAW,YAAY;IAC3B,2FAA2F;IAC3F,YAAY,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAiLD,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,YAAY,CA2Md"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-education",
3
- "version": "0.57.38",
3
+ "version": "0.57.40",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Tenant-aware learning catalog models and authorized catalog access for SMRT",
6
6
  "type": "module",
@@ -25,18 +25,18 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "@happyvertical/smrt-assets": "0.57.38",
29
- "@happyvertical/smrt-core": "0.57.38",
30
- "@happyvertical/smrt-profiles": "0.57.38",
31
- "@happyvertical/smrt-tenancy": "0.57.38",
32
- "@happyvertical/smrt-users": "0.57.38",
33
- "@happyvertical/files": "^0.110.0",
34
- "@happyvertical/sql": "^0.110.0",
28
+ "@happyvertical/smrt-assets": "0.57.40",
29
+ "@happyvertical/smrt-core": "0.57.40",
30
+ "@happyvertical/smrt-profiles": "0.57.40",
31
+ "@happyvertical/smrt-tenancy": "0.57.40",
32
+ "@happyvertical/smrt-users": "0.57.40",
33
+ "@happyvertical/files": "^0.111.0",
34
+ "@happyvertical/sql": "^0.111.0",
35
35
  "pdf-lib": "1.17.1",
36
- "@happyvertical/smrt-ingestion": "0.57.38"
36
+ "@happyvertical/smrt-ingestion": "0.57.40"
37
37
  },
38
38
  "devDependencies": {
39
- "@happyvertical/smrt-vitest": "0.57.38",
39
+ "@happyvertical/smrt-vitest": "0.57.40",
40
40
  "@types/node": "24.13.6",
41
41
  "typescript": "6.0.3",
42
42
  "vite": "8.1.6",