@happyvertical/smrt-education 0.57.50 → 0.57.52
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 +1 -1
- package/README.md +18 -1
- package/agents/course-authoring-service.md +39 -0
- package/agents/course-bundle-import.md +80 -2
- package/agents/course-generation-review.md +53 -0
- package/agents/learner-service.md +20 -0
- package/agents/tutor-context.md +2 -1
- package/dist/chunks/{workbook-photo-service-WwDDC5GF.js → workbook-photo-service-BtMQsbhq.js} +1036 -402
- package/dist/chunks/workbook-photo-service-BtMQsbhq.js.map +1 -0
- package/dist/course-authoring-contracts.d.ts +23 -1
- package/dist/course-authoring-contracts.d.ts.map +1 -1
- package/dist/course-authoring-models.d.ts +2 -0
- package/dist/course-authoring-models.d.ts.map +1 -1
- package/dist/course-authoring-service.d.ts +56 -2
- package/dist/course-authoring-service.d.ts.map +1 -1
- package/dist/course-bundle-contracts.d.ts +23 -3
- package/dist/course-bundle-contracts.d.ts.map +1 -1
- package/dist/course-bundle-models.d.ts +3 -0
- package/dist/course-bundle-models.d.ts.map +1 -1
- package/dist/course-bundle-service.d.ts +45 -1
- package/dist/course-bundle-service.d.ts.map +1 -1
- package/dist/course-bundle.d.ts +32 -0
- package/dist/course-bundle.d.ts.map +1 -1
- package/dist/course-generation-contracts.d.ts +91 -0
- package/dist/course-generation-contracts.d.ts.map +1 -1
- package/dist/course-generation-models.d.ts +29 -0
- package/dist/course-generation-models.d.ts.map +1 -1
- package/dist/course-generation-output.d.ts +9 -3
- package/dist/course-generation-output.d.ts.map +1 -1
- package/dist/course-generation-proposal-generator.d.ts +171 -1
- package/dist/course-generation-proposal-generator.d.ts.map +1 -1
- package/dist/course-generation-review-service.d.ts +15 -1
- package/dist/course-generation-review-service.d.ts.map +1 -1
- package/dist/document-converter.d.ts +138 -0
- package/dist/document-converter.d.ts.map +1 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +750 -340
- package/dist/index.js.map +1 -1
- package/dist/ingestion.js +1 -1
- package/dist/learner-history.d.ts +23 -0
- package/dist/learner-history.d.ts.map +1 -1
- package/dist/learner-interest-theme.d.ts +25 -0
- package/dist/learner-interest-theme.d.ts.map +1 -0
- package/dist/learner-service.d.ts +31 -2
- package/dist/learner-service.d.ts.map +1 -1
- package/dist/learners.d.ts +4 -0
- package/dist/learners.d.ts.map +1 -1
- package/dist/manifest.json +3716 -2783
- package/dist/question-authoring-service.d.ts +12 -0
- package/dist/question-authoring-service.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +380 -17
- package/dist/tutor-context-service.d.ts.map +1 -1
- package/dist/tutor-contracts.d.ts +8 -0
- package/dist/tutor-contracts.d.ts.map +1 -1
- package/package.json +8 -8
- package/dist/chunks/workbook-photo-service-WwDDC5GF.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -52,7 +52,7 @@ Structured questions from scans and imported course files use
|
|
|
52
52
|
ingestion handler. Anchors come from provider region catalogs only; match by worksheet
|
|
53
53
|
id and layout, never text; ambiguous matches stay unattached; source-keyed replays.
|
|
54
54
|
|
|
55
|
-
Course bundles (zip with a closed manifest, or a file set) use
|
|
55
|
+
Course bundles (zip with a closed manifest, or a file set; Word/PowerPoint members render through an injected `DocumentConverter`, fail closed) use
|
|
56
56
|
[CourseBundleImportService](agents/course-bundle-import.md). Parse only with the
|
|
57
57
|
bounded reader; refuse, never trim, hostile input; unsupported members stay
|
|
58
58
|
visible; proposals only, nothing auto-published.
|
package/README.md
CHANGED
|
@@ -109,7 +109,12 @@ host setting and optional evidence submission; guardian `attestLesson` is
|
|
|
109
109
|
unchanged. `setEnrollmentPace` sets or clears an enrollment's planned lessons
|
|
110
110
|
per week (1 to 14) under the host's `education.enrollment.set-pace` decision with
|
|
111
111
|
an immutable change row, and `nextLessons({ count })` returns a week's set of
|
|
112
|
-
unfinished lessons (default one).
|
|
112
|
+
unfinished lessons (default one). `setInterestTheme` stores a learner's interest
|
|
113
|
+
theme (a short plain label and an optional stable key such as `minecraft`; the
|
|
114
|
+
host owns its starter list) under `education.learner.set-interest-theme`: a
|
|
115
|
+
guardian may set any of their learners' themes, a learner only their own
|
|
116
|
+
(the host decides), nobody another learner's. Each change appends an immutable
|
|
117
|
+
row; `getLearner` and the tutor context read it back.
|
|
113
118
|
`TutorContextService.compose` builds a bounded, learner-safe `TutorContext`
|
|
114
119
|
(lesson text, learner-visible questions, own progress, released feedback; for the
|
|
115
120
|
learner audience only `answerKeyAvailable`, never keys, rubrics, notes or storage
|
|
@@ -123,6 +128,18 @@ or file set under hard size, ratio, path and entry bounds, keeps unsupported and
|
|
|
123
128
|
duplicate members as append-only evidence, hands accepted members to ingestion and
|
|
124
129
|
proposes sections and lessons through `CourseGenerationReviewService`; see
|
|
125
130
|
[bundle import](agents/course-bundle-import.md).
|
|
131
|
+
`requestGeneration` also accepts a structured `brief` (subject, category, area,
|
|
132
|
+
grade, objective, a `course` or one-`lesson` target and a per-request interest
|
|
133
|
+
theme that beats the learner's saved theme for that request only); the theme used
|
|
134
|
+
and whether it was an override show on the guardian's review, and `release`
|
|
135
|
+
accepts a `policyBasis` so a host can honour its own approve-requested-lessons
|
|
136
|
+
setting without bypassing the review gate; see
|
|
137
|
+
[generation and review](agents/course-generation-review.md).
|
|
138
|
+
`CourseAuthoringService.appendLesson` adds one lesson (materials and question
|
|
139
|
+
records included) to a released course as a new, optionally released revision:
|
|
140
|
+
append-only, idempotent by request key, refused on archived, unreleased or
|
|
141
|
+
foreign courses; enrollments stay on their pinned release until the host
|
|
142
|
+
upgrades them; see [course authoring](agents/course-authoring-service.md).
|
|
126
143
|
Repeated starts preserve the existing progress, including attempts and scores.
|
|
127
144
|
A new curriculum/year creates a distinct enrollment and fresh progress history.
|
|
128
145
|
An unscored lesson has `bestScore: null`, distinct from zero. The service does
|
|
@@ -78,6 +78,45 @@ lesson identity; `Lesson.externalRef` and `ReleasedLesson` carry the stable
|
|
|
78
78
|
lesson identity for progress/reporting. Learners on different releases never
|
|
79
79
|
share mutable rows. Personal-use releases stay `draft` status (the existing rule).
|
|
80
80
|
|
|
81
|
+
## Append a lesson to a released course
|
|
82
|
+
|
|
83
|
+
`appendLesson({ courseId, expectedHeadRevisionId, requestKey, sectionKey,
|
|
84
|
+
sectionTitle?, lesson, release?, policyBasis? })` adds exactly one lesson (a
|
|
85
|
+
`CourseLessonInput` with a required `lessonKey`, its `materials` and `questions`
|
|
86
|
+
exactly as `saveDraft` takes them) to a course that already has a release, as one
|
|
87
|
+
new revision, and releases that revision in the same transaction (`release:
|
|
88
|
+
false` only appends). Returns `CourseAppendLessonView` (`revisionId`, `version`,
|
|
89
|
+
`lessonKey`, `release | null`). Host mapping: action `education.course.author`,
|
|
90
|
+
operation `append-lesson` (also `generate` for generated provenance), and, when
|
|
91
|
+
releasing, `education.course.release` with the usual `policyBasis` and delegation
|
|
92
|
+
rules. Hosts that grant `author` but not `release` use `release: false`.
|
|
93
|
+
|
|
94
|
+
- **Append-only.** The new revision is the head plus the lesson at the end of
|
|
95
|
+
`sectionKey` (a new section needs `sectionTitle`). No lesson, revision or release
|
|
96
|
+
is changed; the release is a new Course row with the next release number.
|
|
97
|
+
- **Refused.** Unknown or foreign-tenant course (`CourseNotFoundError`), discarded,
|
|
98
|
+
archived (`CourseArchivedError`), no release yet (`CourseAppendRefusedError`
|
|
99
|
+
`not-released`), head with changes that were never released (`unreleased-changes`:
|
|
100
|
+
nothing unreviewed rides along; release or discard it first), `lessonKey` already
|
|
101
|
+
in the course (`duplicate-lesson`), stale `expectedHeadRevisionId`
|
|
102
|
+
(`CourseStaleHeadError`).
|
|
103
|
+
- **Review gate.** Releasing goes through the injected `releaseReviewGate` unchanged.
|
|
104
|
+
The default review-backed gate needs a current review of the new revision, which
|
|
105
|
+
cannot exist before it does, so with that gate use `release: false`, `recordReview`,
|
|
106
|
+
then `release`. A host whose gate honours its own approval (the guardian already
|
|
107
|
+
approved the requested lesson) can release in the one call. A gate that is not
|
|
108
|
+
current rolls the append back entirely.
|
|
109
|
+
- **Idempotent.** The same `requestKey` and request replays the original result, also
|
|
110
|
+
after the head moved (a no-op: no revision, release or lesson rows); a changed lesson
|
|
111
|
+
or section or release intent (`release` true versus false) under the same key is
|
|
112
|
+
`CourseRequestKeyConflictError`; publish a `release: false` append later with
|
|
113
|
+
`release` and its own key.
|
|
114
|
+
- **Enrollment pins.** Existing enrollments keep their pinned release and so never see
|
|
115
|
+
the new lesson implicitly. The host advances them with `upgradeEnrollment` on the new
|
|
116
|
+
release (explicit, recorded), or an enrollment/course that opted in with
|
|
117
|
+
`setAutoUpgrade` is moved by the release exactly as for any release (unstarted
|
|
118
|
+
lessons follow, started lessons stay on their row). Nothing else moves a pin.
|
|
119
|
+
|
|
81
120
|
## Enrollment pin and upgrades
|
|
82
121
|
|
|
83
122
|
`Enrollment` is untouched (its `courseId` stays immutable): enroll through
|
|
@@ -38,7 +38,8 @@ authorizer: `importBundle` needs `education.course.author`, `readImport`
|
|
|
38
38
|
4. `readImport({ ...actor, importId })` returns the recorded evidence.
|
|
39
39
|
|
|
40
40
|
Progress is an observer callback (`onProgress({ stage, completed, total })`,
|
|
41
|
-
stages `unpack`, `classify`, `extract`, `propose`; each starts at `completed: 0
|
|
41
|
+
stages `unpack`, `classify`, `render`, `extract`, `propose`; each starts at `completed: 0`;
|
|
42
|
+
`render` is emitted only when the bundle has Word/PowerPoint members).
|
|
42
43
|
Ingestion has no public job-progress seam, so none is invented. A throwing
|
|
43
44
|
listener never affects the import.
|
|
44
45
|
|
|
@@ -55,6 +56,9 @@ listener never affects the import.
|
|
|
55
56
|
| `maxManifestBytes` | 256 KiB |
|
|
56
57
|
| `maxLessons` (the proposal schema's cap) | 50 |
|
|
57
58
|
| `maxLessonBodyChars` | 100 000 |
|
|
59
|
+
| `maxOfficeMembers` (Word/PowerPoint members rendered per bundle) | 50 |
|
|
60
|
+
| `maxRenderedBytes` (PDF accepted back from the converter) | 50 MiB |
|
|
61
|
+
| `conversionTimeoutMs` (one Office member) | 60 000 |
|
|
58
62
|
| archive nesting | 1: a zip inside a zip is never expanded |
|
|
59
63
|
|
|
60
64
|
A lesson whose combined text members exceed `maxLessonBodyChars` refuses the
|
|
@@ -80,7 +84,7 @@ too long or deep). An **honestly** oversize member (declared over
|
|
|
80
84
|
|
|
81
85
|
## Members and statuses
|
|
82
86
|
|
|
83
|
-
By content (never by extension alone): `pdf` (`%PDF-` in the first 1 KiB),
|
|
87
|
+
By content (never by extension alone): `docx`/`pptx` (see below), `pdf` (`%PDF-` in the first 1 KiB),
|
|
84
88
|
`image` (PNG, JPEG, WebP), `text` (`.txt`/`.md`, strict UTF-8, no control
|
|
85
89
|
characters, body-sized), `manifest` (root `manifest.json`), else `unsupported`.
|
|
86
90
|
Status: `accepted` (handed to extraction), `duplicate` (same sha256 as an earlier
|
|
@@ -91,6 +95,74 @@ extracted again), `unsupported` (`unsupported_type`, `nested_archive`,
|
|
|
91
95
|
`/Encrypt` token anywhere in its bytes (trailer, xref stream): a false positive
|
|
92
96
|
on literal text inside an uncompressed stream is refused, never read.
|
|
93
97
|
|
|
98
|
+
## Word and PowerPoint members (SMRT #4615)
|
|
99
|
+
|
|
100
|
+
A `.docx`/`.pptx` member is an OOXML package only when the name and the content
|
|
101
|
+
agree: a zip whose entries include `[Content_Types].xml` and `word/document.xml`
|
|
102
|
+
(docx) or `ppt/presentation.xml` (pptx). Before the bytes go anywhere, the
|
|
103
|
+
package itself passes the same bounded zip reader (ratio, overlap, paths, CRC,
|
|
104
|
+
100 MiB inflated); a failure is `refused / unsafe_office`, never a refusal of the
|
|
105
|
+
whole bundle. Other zips stay `unsupported / nested_archive`.
|
|
106
|
+
|
|
107
|
+
Two separate things happen to an accepted Office member, both after
|
|
108
|
+
authorization and before the evidence is written:
|
|
109
|
+
|
|
110
|
+
- **Pages**: a `DocumentConverter` (`document-converter.ts`) renders the original
|
|
111
|
+
to PDF. The importer enforces the byte and time limits itself (input
|
|
112
|
+
`maxEntryBytes`, output `maxRenderedBytes`, `conversionTimeoutMs` with an abort
|
|
113
|
+
signal and a hard race, so an adapter that ignores the signal still times out),
|
|
114
|
+
and rejects output that is not a PDF or is encrypted. The converter is told a
|
|
115
|
+
neutral file name (`document.docx`), never the bundle path. Identical documents
|
|
116
|
+
are rendered once.
|
|
117
|
+
- **Text**: an injectable `OfficeTextReader` (best effort; a failure or a `null`
|
|
118
|
+
costs the member nothing) returns whole-document text and, for decks, text and
|
|
119
|
+
speaker notes per slide. Office text is handed to the host on the accepted
|
|
120
|
+
member (`officeText`) and is **never** inlined into a proposed lesson body:
|
|
121
|
+
teacher guides and notes carry answers, and audience belongs to manifest v2.
|
|
122
|
+
|
|
123
|
+
`receiveMember` gets the original `bytes` (store them as the private original
|
|
124
|
+
asset: "Open in PowerPoint" for decks) and `rendered` (`{ bytes, sha256, mediaType:
|
|
125
|
+
'application/pdf', converterId }`, store as a private asset beside the original:
|
|
126
|
+
the pages for "Present"/reading). The member row records `renderedSha256` and
|
|
127
|
+
`renderedSizeBytes`; bytes are still not retained here.
|
|
128
|
+
|
|
129
|
+
**Fail closed.** Any converter problem records that member `unsupported` with a
|
|
130
|
+
reason and writes the rest of the bundle normally: `converter_unavailable` (none
|
|
131
|
+
configured), `converter_failed` (unreachable or 5xx), `converter_timeout`,
|
|
132
|
+
`converter_too_large`, `converter_rejected` (HTTP 4xx, not a PDF, encrypted),
|
|
133
|
+
`office_limit` (beyond `maxOfficeMembers`, in natural path order). A member that
|
|
134
|
+
failed is not a lesson in the proposal, and identical copies of a failed document
|
|
135
|
+
fail the same way rather than becoming duplicates of nothing. Outcomes are part
|
|
136
|
+
of the append-only evidence, so a replay does not retry a recorded failure (to
|
|
137
|
+
retry, change the bundle). A replay does re-render recorded-accepted members so the
|
|
138
|
+
host receives the bytes again; if it can no longer render one it throws
|
|
139
|
+
`DocumentConversionError` rather than silently dropping an accepted member.
|
|
140
|
+
|
|
141
|
+
### Host configuration
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const service = new CourseBundleImportService(db, authorizer, {
|
|
145
|
+
converter: new GotenbergDocumentConverter({
|
|
146
|
+
baseUrl: process.env.GOTENBERG_URL, // for example http://gotenberg:3000
|
|
147
|
+
timeoutMs: 60_000,
|
|
148
|
+
maxBytes: 50 * 1024 * 1024,
|
|
149
|
+
}),
|
|
150
|
+
officeTextReader, // optional, see below
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`GotenbergDocumentConverter` posts to `<baseUrl>/forms/libreoffice/convert`, talks
|
|
155
|
+
to nothing else, refuses redirects and URLs with credentials, and caps the response
|
|
156
|
+
as it streams. Run Gotenberg as an internal-only service with **no outbound
|
|
157
|
+
network**, its own size/time limits and no shared volume with the app; this
|
|
158
|
+
package assumes untrusted files never run next to the application. Tests use
|
|
159
|
+
`FakeDocumentConverter` (records calls; `failWith`, `failWhen`, `delayMs`, `output`).
|
|
160
|
+
|
|
161
|
+
`OfficeTextReader` is deliberately not wired to the SDK here, so this package
|
|
162
|
+
does not depend on an unreleased `@happyvertical/documents`. Once the Office
|
|
163
|
+
processors ship (sdk#1485, `extractOffice(bytes, 'docx' | 'pptx')`), implement
|
|
164
|
+
`read` with it; the snippet is in the `OfficeTextReader` doc comment.
|
|
165
|
+
|
|
94
166
|
## Manifest (`manifest.json`, version 1, closed)
|
|
95
167
|
|
|
96
168
|
```json
|
|
@@ -139,6 +211,12 @@ Normal manifest migrations create both tables.
|
|
|
139
211
|
|
|
140
212
|
## Not in this release
|
|
141
213
|
|
|
214
|
+
- Manifest version 2 (material roles with audience, lesson pages, course-level
|
|
215
|
+
members, external video references) waits for the phase 1 role vocabulary
|
|
216
|
+
(smrt#4614). The page-role vocabulary and the closed v1 schema are unchanged;
|
|
217
|
+
see the hand-off note on smrt#4615.
|
|
218
|
+
- Re-rendering a member whose first import recorded a converter failure (outcomes
|
|
219
|
+
are append-only per bundle identity).
|
|
142
220
|
- Ingestion's receipt contract is unchanged: there is no `application/zip` part
|
|
143
221
|
type or `parentEvidenceId` in ingestion. The education side unpacks and the
|
|
144
222
|
host receives members one by one, so archive-to-member lineage lives in
|
|
@@ -40,6 +40,59 @@ a revision on `expectedHeadRevisionId` and records the decision; supplied conten
|
|
|
40
40
|
is replaced only that way. Never write generated content to a released revision or
|
|
41
41
|
assign it to a learner.
|
|
42
42
|
|
|
43
|
+
## Structured requests and one-lesson generation
|
|
44
|
+
|
|
45
|
+
`requestGeneration` also takes an optional `brief` (`CourseGenerationBriefInput`)
|
|
46
|
+
next to the free `instruction`: `target` (`'course'`, the default and today's
|
|
47
|
+
behaviour, or `'lesson'`), `subject`, `category`, `area`, `grade` (whole 0-12),
|
|
48
|
+
`objective`, `learningObjectiveId`, `learnerId` and a per-request `theme`
|
|
49
|
+
(`{ label, key? }`). Text is bounded and sent to the provider as data. A request
|
|
50
|
+
without `brief` behaves exactly as before and `brief` reads back `null`.
|
|
51
|
+
|
|
52
|
+
- **Objective.** A `lesson` target needs an objective (`objective`, else `area`);
|
|
53
|
+
without one `CourseGenerationBriefError` is thrown before anything is written.
|
|
54
|
+
The objective is stored on the job's `CourseGenerationBriefRecord`, sent in the
|
|
55
|
+
generation brief as fixed, and copied verbatim onto the proposed lesson. The
|
|
56
|
+
theme changes story and examples only: output that states a different
|
|
57
|
+
objective fails the job (`CourseGenerationOutputError`).
|
|
58
|
+
- **Theme.** The per-request `theme` is an override that wins for that request
|
|
59
|
+
only. Without one the learner's saved `interestTheme` (read-only, from
|
|
60
|
+
`brief.learnerId`) applies; with neither there is no theme. The brief records
|
|
61
|
+
`theme` and `themeSource` (`'override' | 'saved' | 'none'`) at request time, and
|
|
62
|
+
`CourseGenerationView.brief` / `CourseProposalView.brief` show it to the
|
|
63
|
+
guardian reviewing. Nothing in generation writes to the learner, so the saved
|
|
64
|
+
preference is never changed, on success or failure.
|
|
65
|
+
- **Authority.** The host authorizer is asked for `education.course.generate`
|
|
66
|
+
with `learnerId` in the context so it can enforce its own "may this learner ask
|
|
67
|
+
for a lesson" policy; an unknown or foreign-tenant learner is `CourseNotFoundError`.
|
|
68
|
+
The package does not check the requester's relationship to `brief.learnerId` (the
|
|
69
|
+
brief echoes that learner's saved theme): the host authorizer MUST deny `generate`
|
|
70
|
+
when the actor is not that learner or a guardian with an effective relationship, as
|
|
71
|
+
it does for `assign`.
|
|
72
|
+
Replaying a request key with the same brief returns the same job; a changed
|
|
73
|
+
brief is `CourseRequestKeyConflictError`.
|
|
74
|
+
- **Output.** For `lesson` the generator is given `request.brief` and
|
|
75
|
+
`COURSE_LESSON_PROPOSAL_ARGS_SCHEMA` (via `createProposalCourseGenerator`):
|
|
76
|
+
exactly one new lesson with `title`, `body` (the themed explanation) and
|
|
77
|
+
`questions` (worksheet; every question needs an `answerKey`), and no course-level
|
|
78
|
+
fields. The worksheet becomes immutable question records, linked to
|
|
79
|
+
`learningObjectiveId`, in the same transaction as the draft revision; answer keys
|
|
80
|
+
stay out of learner views. Review, `decideProposal` and release are unchanged.
|
|
81
|
+
- **Approving requested lessons.** The package has no per-learner setting. To honour
|
|
82
|
+
the host's own "approve requested lessons" setting without bypassing authority,
|
|
83
|
+
the host calls `recordReview` then `release` under the guardian delegation as for
|
|
84
|
+
any course, passing `policyBasis` (for example `'auto-approve-requested-lessons'`)
|
|
85
|
+
to `release`, or returning `policyBasis` from its authorizer decision (the two
|
|
86
|
+
must agree). The basis is recorded on `CourseRelease.policyBasis` and
|
|
87
|
+
`CourseReleaseView.policyBasis`; it grants nothing and the review gate, the
|
|
88
|
+
`education.course.release` grant and the delegation check still apply. The
|
|
89
|
+
explicit alternative, a guardian approving each lesson, is the existing
|
|
90
|
+
`recordReview` plus `release` by the guardian.
|
|
91
|
+
|
|
92
|
+
Additive schema: table `course_generation_briefs` and column
|
|
93
|
+
`course_releases.policy_basis`. On a host without the table, plain requests still
|
|
94
|
+
work and structured ones fail closed with `schema-unavailable`.
|
|
95
|
+
|
|
43
96
|
## Review
|
|
44
97
|
|
|
45
98
|
`recordReview` (action `education.course.review`) binds to an exact revision and a
|
|
@@ -131,6 +131,26 @@ Same-tenant membership alone never grants sibling access.
|
|
|
131
131
|
without `enrollment_pace_changes` gets `LearnerHistoryUnavailableError`.
|
|
132
132
|
Apply the additive migration (new column plus table) before deploying writes;
|
|
133
133
|
reads of an un-migrated table report no pace.
|
|
134
|
+
- `setInterestTheme({ theme: { label, key? } | null, policyBasis })` stores the
|
|
135
|
+
learner's interest theme under `education.learner.set-interest-theme`. The
|
|
136
|
+
label is a short plain string (1-60 characters after whitespace is collapsed;
|
|
137
|
+
control characters, `@`, `://` and `www.` are refused so it cannot carry
|
|
138
|
+
contact details) and the optional key is lowercase `[a-z0-9-]` up to 40
|
|
139
|
+
characters. The package ships no starter list: the host owns its keys
|
|
140
|
+
(for Hearth `minecraft`, `roman`, `hockey`, `baseball`, `football`, `soccer`,
|
|
141
|
+
`cars`) and family-added themes are free labels without a key. A guardian with
|
|
142
|
+
an effective relationship may set any of their learners' themes; the learner
|
|
143
|
+
may set their own when the host authorizer answers `own: true` as allowed;
|
|
144
|
+
another learner's theme needs the actor's own guardian relationship, so a
|
|
145
|
+
learner, sibling or stranger is refused by the same actor/tenant checks as
|
|
146
|
+
every other operation. It is a skin for stories and examples only; it never
|
|
147
|
+
changes an objective. Columns `learners.interest_theme_label` and
|
|
148
|
+
`interest_theme_key` (additive; empty = none) hold the value and each change
|
|
149
|
+
appends a `LearnerInterestThemeChange` (actor, `policyBasis`, previous and new
|
|
150
|
+
value). An unchanged value is `{ changed: false }`; a bad value is
|
|
151
|
+
`InterestThemeError`; a host without `learner_interest_theme_changes` gets
|
|
152
|
+
`LearnerHistoryUnavailableError`. `getLearner` returns `interestTheme`
|
|
153
|
+
(`{ label, key } | null`). Apply the additive migration before deploying.
|
|
134
154
|
- `nextLessons({ enrollmentId?, count? })`: `count` is an integer from 1 to
|
|
135
155
|
14 (`MAX_NEXT_LESSONS_COUNT`, default 1, else `LearnerValidationError`) and
|
|
136
156
|
returns up to that many unfinished (not complete or skipped) lessons per
|
package/agents/tutor-context.md
CHANGED
|
@@ -15,7 +15,8 @@ enrollment's effective set; a focus question must belong to that lesson
|
|
|
15
15
|
(otherwise `LearnerAuthorizationError`). The guardian audience is refused for
|
|
16
16
|
the learner's own actor whatever the host answers.
|
|
17
17
|
|
|
18
|
-
Result: learner (name, level
|
|
18
|
+
Result: learner (name, level, `interestTheme`: the learner-safe label and key or
|
|
19
|
+
null, read-only; never the free `settings`), enrollment (status, pace; no notes), course,
|
|
19
20
|
section and lesson titles, learner-facing lesson body (withheld for
|
|
20
21
|
personal-use-copy), objectives, own progress status, the learner-visible
|
|
21
22
|
question rendering (prompt, response type, choices, subpart prompts, points; no
|