@happyvertical/smrt-education 0.57.44 → 0.57.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -44,7 +44,8 @@ Keep supplied content unless a decision is recorded; reviews carry no score.
44
44
 
45
45
  Immutable questions, answer keys/rubrics, stable learning objectives, worksheet
46
46
  render ids and exact-revision references use [QuestionAuthoringService](agents/question-records.md).
47
- Keep keys out of learner bundles; a missing key stays null.
47
+ Keep keys out of learner bundles; a missing key stays null. `attachGeneratedPrintable`
48
+ stores the rendered PDFs; the guardian copy is always an `answer-key`, never `printable`.
48
49
 
49
50
  Source import: [host boundary and transaction contract](agents/source-import.md).
50
51
  `SourceImportService` requires host opt-in, matching active actor/tenant and
@@ -7,7 +7,7 @@ schema and public contract. It reuses the course authorizer: writes need
7
7
  return answer keys need `education.course.read-draft`; every call receives the
8
8
  transaction executor and the operation (`create-question`, `save-question`,
9
9
  `link-objectives`, `create-objective`, `save-objective`, `read-question`,
10
- `read-worksheet`, `resolve-question-revisions`). Tenant is required; foreign ids are not-found.
10
+ `read-worksheet`, `resolve-question-revisions`, `attach-printable`). Tenant is required; foreign ids are not-found.
11
11
 
12
12
  ## Models (closed, required tenant, no REST/CLI/MCP/AI, append-only)
13
13
 
@@ -93,8 +93,48 @@ learner bundle is built from an allowlist and has no key, rubric or provenance
93
93
  type members; only the guardian bundle carries them. Both service audiences need
94
94
  `read-draft`; learner delivery belongs to the host.
95
95
 
96
+ ## Generated printables (`attachGeneratedPrintable`)
97
+
98
+ Hosts render worksheets from `QuestionRevision` records but cannot write `LessonAsset`
99
+ rows. `attachGeneratedPrintable({ ...actor, lessonId, worksheetVersionId, audience })`
100
+ (`audience`: `learner` or `guardian`) renders the released lesson's worksheet to a PDF
101
+ upstream with `pdf-lib` (no host PDF dependency), stores it as a private education-owned
102
+ asset and links it to the lesson, so the durable print queue can serve it. It returns the
103
+ closed `{ lessonId, worksheetVersionId, audience, assetId, relationship, created }`; never keys,
104
+ answers or source URIs.
105
+
106
+ - `lessonId` is the materialised `lessons` row id of a released lesson (`released_lessons.lesson_id`),
107
+ resolved in the actor's tenant; unknown and foreign ids alike are `QuestionNotFoundError`.
108
+ `worksheetVersionId` must equal the digest of that lesson revision's refs and layout, else
109
+ `WorksheetVersionMismatchError` (`worksheet-version-mismatch`), so a later scan matches. A lesson with no
110
+ questions is a validation error.
111
+ - Relationship rules: the learner copy is built from the keyless learner bundle only and linked
112
+ `printable`; the guardian copy is built from the keyed bundle (answer keys, explanations, rubric summaries)
113
+ and linked `answer-key`. `SubmissionService.signLessonPrintable` matches only `printable` joins
114
+ (explicit filter, test-pinned), so an answer-key copy is never signed for a learner.
115
+ - Authorization: `education.course.author` for either audience, plus `education.course.read-draft`
116
+ for `guardian` (the audience only picks the artefact and never widens authority). A learner of the
117
+ tenant, or a delegation naming one as actor or delegating guardian, is refused with
118
+ `CourseAuthoringAuthorizationError` before the host authorizer is asked. Delegated agents use
119
+ `verifyDelegation` like every authoring call (refused without it).
120
+ - Idempotent per (tenant, lesson, worksheet version, audience) via the append-only closed
121
+ `GeneratedPrintable` model (unique index). A repeat returns the same asset with `created: false` and
122
+ renders and stores nothing. The lesson row is locked before the check; a lost unique-index race is
123
+ compensated and replayed once, so concurrent first calls converge on one asset with no orphan file.
124
+ - Storage: pass `printableAssets: { storage, storeOptions? }` (same shape as `SubmissionService`) to the
125
+ service. Without it the call throws `PrintableStorageNotConfiguredError`. The PDF is a `private` asset
126
+ (`typeSlug` `education-page`) owned by the acting guardian's profile, or for a delegated agent the delegating
127
+ guardian's; none available is `PrintableOwnerError`. Asset rows, the join and the model row commit in one
128
+ transaction; newly planned files are removed after rollback unless the asset row committed
129
+ (`PrintableStorageCleanupError` carries fresh asset ids if removal fails).
130
+ - Renderer limits: standard Helvetica fonts only, Letter pages. Characters outside WinAnsi are replaced
131
+ with `?` (never an error); media refs print as a short placeholder line, never the media. Bounded:
132
+ 200 items, 10,000 characters per text value, 400,000 in total, 150 pages (`CourseAuthoringValidationError`).
133
+ Output is deterministic: fixed creation/modification dates, footer on every page with the worksheet
134
+ version id and audience, PDF title/subject/keywords carrying the version id.
135
+
96
136
  ## Migration
97
137
 
98
- Schema ships through normal manifest migrations: five new tables (`question_responses` included) plus nullable
138
+ Schema ships through normal manifest migrations: six new tables (`question_responses` and `generated_printables` included) plus nullable
99
139
  columns on `grades` (`question_revision_id`) and `lesson_revisions` (`questions_json`,
100
140
  `layout_params_json`); test-pinned as additive. The runtime never creates tables.