@happyvertical/smrt-education 0.57.40 → 0.57.41

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
@@ -45,3 +45,7 @@ assignment alone never creates learning history or grants learner inbox access.
45
45
  Publisher catalog previews use [the preview contract](agents/catalog-previews.md).
46
46
  Keep independent image rights and source revisions; serve via `CatalogPreviewService`
47
47
  with live actor/tenant authorization, never direct provider image URLs.
48
+
49
+ Enrollment Plan projections and guardian-only parent notes use
50
+ [the Plan contract](agents/learner-plan.md). Keep awaiting review separate from
51
+ Continue; note authority/provenance never grants learner access or learning evidence.
@@ -0,0 +1,42 @@
1
+ # Enrollment parent notes
2
+
3
+ `LearnerPlanService` provides `listEnrollmentNotes` and `addEnrollmentNote` on
4
+ its existing actor-bound learner transaction boundary. Both require an exact
5
+ owning tenant, learner and enrollment plus a verified directed guardian
6
+ relationship. Own learners are always denied, even with a permissive host grant.
7
+ The host must authorize `education.learner.notes.read` and
8
+ `education.learner.notes.add`, verifying the current relationship type, terms,
9
+ session and permission using the supplied exact `executor`. Membership alone
10
+ never grants note access. Catalog access does not grant note authority.
11
+
12
+ Add accepts `{ ...LearnerRequest, enrollmentId, text, requestKey }`. Text is
13
+ nonblank, at most `ENROLLMENT_NOTE_MAX_LENGTH` (4000 UTF-16 code units), and
14
+ retained verbatim as plain text; render with text escaping, never as HTML or
15
+ markdown. Control characters other than tab/newline/carriage return are rejected.
16
+ The request key is nonblank, at most 200 code units, without control characters.
17
+ Author, author label and creation time are server-derived; extra request properties are ignored.
18
+ The data-only result is `{ id, enrollmentId, learnerId, text, authorUserId, authorLabel,
19
+ createdAt: Date }`. List order is creation time then id.
20
+
21
+ Notes are append-only. Retrying the same tenant/enrollment/requestKey and text
22
+ returns the original row, author, label and time; changing text rejects. PostgreSQL
23
+ serializes writes on the enrollment row and uses a natural-key unique index;
24
+ embedded databases use the owning service transaction queue. Notes share
25
+ `withTransaction` / `withExecutor`, including rollback and poisoned mutations.
26
+ No progress, submission, assessment, feedback or learning history changes occur.
27
+
28
+ `EnrollmentParentNote` and its collection are trusted persistence APIs, never
29
+ request handlers. Generated REST, CLI, MCP and AI surfaces are closed. Persisted
30
+ model saves and deletes reject; creation uses strict insert so natural-key
31
+ collisions cannot rewrite provenance. The internal persisted `recordedAt` maps
32
+ to DTO `createdAt`. Hosts must use the service to derive provenance and authority.
33
+ Normal additive manifest migration creates `enrollment_parent_notes`.
34
+
35
+ `src/__tests__/enrollment-note-workflows.ts` contains shared SQLite/PostgreSQL
36
+ assertions for authority, provenance, validation, concurrency and transactions.
37
+ No full DuckDB workflow support is claimed.
38
+
39
+ `authorLabel` snapshots only the validated actor User's linked Profile name on
40
+ creation, using the same executor. Trim it and cap it at 200 code units; absent
41
+ or blank names use `Parent`. List and retry return the persisted label without
42
+ looking up previous authors, so later profile renames never rewrite history.
@@ -0,0 +1,66 @@
1
+ # Enrollment Plan reads
2
+
3
+ `LearnerPlanService(db, authorizeLearner, authorizeCatalog)` extends the existing
4
+ LearnerService transaction/active actor boundary and adds private parent notes
5
+ ([notes contract](enrollment-notes.md)). Its `withTransaction` and `withExecutor`
6
+ services retain the Plan methods. Root exports `EnrollmentPlanView` and
7
+ `PlanLessonView`; all results are data only.
8
+
9
+ `listPlan(request)` returns exact learner enrollments ordered by sortOrder/id.
10
+ `getEnrollmentPlan({...request,enrollmentId})` verifies that exact tenant,
11
+ learner, enrollment and curriculum before reading its course. Missing or denied
12
+ course visibility returns `availability: 'unavailable'`, `course: null`, and
13
+ empty lessons/evidence; it does not interrupt other authorized enrollments.
14
+ Actor/enrollment denial still rejects the request. Operational database errors
15
+ are not disguised as missing material.
16
+
17
+ The course DTO contains only id, title, subject, summary, modality and sourceKey.
18
+ Technical unfiled capture scaffolding has sourceKey exactly `unfiled-work`;
19
+ consumers exclude it by identity, never by title. The DTO does not invent a cover
20
+ or claim any source/asset preview binding. It returns no teacher or answer-key
21
+ assets. Lessons preserve catalog section/lesson order and existing role-filtered
22
+ progress, including private workbook feedback rules. Optional workbookActivityId
23
+ is resolved for the exact tenant/learner/enrollment/lesson; consumers route that
24
+ work to their workbook interface, not generic scan capture.
25
+
26
+ Workbook progress uses the same explicit parent completion projection as Work.
27
+ Old grade-derived complete/skipped rows without a current decision project in
28
+ progress with completedAt null; they cannot contribute a completion timestamp to
29
+ progressEvidence. Later analysis of the same attempt retains a parent's decision;
30
+ a new correction or practice attempt reopens it. Ordinary lesson progress is
31
+ unchanged. See [the workbook contract](workbook-service.md).
32
+
33
+ For required-online lessons, optional sourceUrl comes only from the same
34
+ catalog-authorized Lesson.externalUrl and must be HTTPS without credentials.
35
+ Answer URLs and asset locations are never projected. A missing or unsafe online
36
+ URL is unavailable to Continue and cannot block a later usable lesson. Consumers
37
+ revalidate links at their own browser boundary and display honest unavailable
38
+ material states.
39
+
40
+ `nextActionableLessonId` considers planned/active enrollments and only unstarted
41
+ or in-progress lessons with usable material. Planned is usable because normal
42
+ `enroll` creates planned enrollments and the base workflow has no activation API.
43
+ Paused/completed/dropped enrollments have no next action. Complete/skipped lessons
44
+ and submitted/needs_review work never instruct rework. `awaitingReviewLessonIds`
45
+ is separate from Continue. Reads never change progress or create learning history.
46
+
47
+ `progressEvidence` is nullable `{kind: 'started'|'completed',at,lessonId}` from
48
+ visible durable progress start/completion timestamps. It is the newest such
49
+ recorded timestamp, **not** latest actual work. Repeated starts preserve the
50
+ original startedAt. Display it as “Last recorded start/completion”, never infer
51
+ activity from updatedAt, enrollment ordering, notes, or missing timestamps.
52
+
53
+ ```ts
54
+ import { LearnerPlanService, ENROLLMENT_NOTE_MAX_LENGTH, validateEnrollmentNote }
55
+ from '@happyvertical/smrt-education';
56
+ const service = new LearnerPlanService(db, authorizeLearner, authorizeCatalog);
57
+ const plan = await service.listPlan(serverLearnerRequest);
58
+ validateEnrollmentNote(text, requestKey); // input validation, not authorization
59
+ const note = await service.addEnrollmentNote({
60
+ ...serverGuardianRequest, enrollmentId, text, requestKey,
61
+ });
62
+ ```
63
+
64
+ Build before consuming the public package; deploy its ordinary additive manifest
65
+ migration before using notes. No runtime handler creates application schema.
66
+ Shared Plan and note workflows run against SQLite and PostgreSQL.
@@ -45,7 +45,12 @@ non-authoritative Grade until guardian review.
45
45
  `reviewSubmission` accepts an existing authorized submission and optional score
46
46
  and feedback. `attestLesson` creates/reuses an Attestation for a matching progress
47
47
  item without requiring uploads. Both require a verified guardian and create a
48
- new guardian Grade, set submission to `graded` and progress to `complete`.
48
+ new guardian Grade and set submission to `graded`. Ordinary non-workbook progress
49
+ becomes `complete`. Workbook-backed progress instead uses its explicit parent
50
+ completion decision; grading alone leaves it in progress, including through a
51
+ plain SubmissionService instance. Existing explicit completion on the current
52
+ attempt is retained. The owning workbook join determines this policy, never a
53
+ caller flag.
49
54
  Omitted/null score is unscored; zero is a real score. The latest guardian grade
50
55
  wins over auto/AI/instructor proposals, and its score replaces progress's score.
51
56
  Review history is retained. ExternalCompletion is available for trusted server
@@ -187,12 +187,48 @@ the authorized education timeline independently of optional scratch processing.
187
187
  Workbook feedback is projected only from the revision/release workflow. The base
188
188
  `SubmissionService` recognizes authoritative `WorkbookActivity` lesson ownership,
189
189
  including calls through a plain service instance. Own-learner compatibility views
190
- withhold Grade and grade-derived status; learner progress projections withhold
191
- bestScore/completedAt and do not let completion alter next-lesson selection.
192
- Guardian and ordinary non-workbook projections retain their existing behavior.
190
+ withhold Grade and bestScore. Every workbook progress projection uses the explicit
191
+ parent decision for status and completedAt; historical grade-derived completion
192
+ alone is Doing with no completion date. Ordinary non-workbook projections retain
193
+ their existing behavior.
193
194
  This applies in both feedback policy modes; direct feedback remains provisional
194
195
  and does not authorize compatibility Grade disclosure.
195
196
 
197
+ ## Explicit parent completion
198
+
199
+ `completeWorkbookActivity` requires the current verified guardian, exact activity,
200
+ submissionId, expectedAttemptId, explicit expectedHeadId (including null), and a
201
+ bounded requestKey. The SDK independently denies own learners for
202
+ `education.learner.complete`, even when a host callback grants it. The head token
203
+ is the latest guardian revision, otherwise latest AI revision, otherwise null;
204
+ null permits a parent's judgment without any assessment. The decision and progress
205
+ update share the authorized transaction and learner serialization lock. No grade,
206
+ assessment, feedback release, or learner visibility is created by completion.
207
+ Generic guardian grading of workbook submissions also cannot complete work.
208
+
209
+ `WorkbookCompletionDecision` retains immutable server actor/time, attempt,
210
+ submission, head provenance, and retry key. Exact retries return that historical
211
+ decision even after a correction; changed retries and stale selectors reject with
212
+ `WorkbookCompletionConflictError` (changed_retry, stale_attempt, stale_head, or
213
+ already_complete). Hosts should acknowledge recording and reread current state,
214
+ never interpret a historical retry as completing a newer attempt.
215
+
216
+ Completion persists across later assessment or guardian review on the same
217
+ attempt. A newly captured correction or independent practice attempt makes the
218
+ previous decision historical and returns the current work to Doing. Corrections
219
+ retain their episode and must name the current predecessor; fresh practice keeps
220
+ its existing separate episode path. No legacy Grade is converted into a decision.
221
+
222
+ Activity lists and timelines expose own-safe `completion` (status and nullable
223
+ completedAt), currentAttemptId, and latestSubmissionId. These current pointers
224
+ are independent of history pagination. Timeline completionContext is guardian-only
225
+ and carries the selectors to display and later submit verbatim; own learners
226
+ receive null. Per-attempt completionDecision provenance is also guardian-only.
227
+ The existing attempt headId continues to mean guardian review head.
228
+
229
+ Deploy the ordinary additive manifest migration before recording decisions. Reads
230
+ of an unmigrated decision table report Doing; runtime handlers never create it.
231
+
196
232
  Generic capture/resubmission rejects workbook enrollment or lesson scaffolding,
197
233
  including idempotent retries. Only the protected exact-activity owning workflow
198
234
  can capture workbook work. Hosts must call public