@happyvertical/smrt-education 0.57.43 → 0.57.44
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 +12 -0
- package/README.md +5 -0
- package/agents/course-authoring-service.md +179 -0
- package/agents/course-generation-review.md +79 -0
- package/agents/learner-service.md +33 -0
- package/agents/question-records.md +100 -0
- package/dist/chunks/{workbook-photo-service-C6mbUUp6.js → workbook-photo-service-BxvZDqjM.js} +1440 -309
- package/dist/chunks/workbook-photo-service-BxvZDqjM.js.map +1 -0
- package/dist/course-append-only.d.ts +15 -0
- package/dist/course-append-only.d.ts.map +1 -0
- package/dist/course-authoring-contracts.d.ts +464 -0
- package/dist/course-authoring-contracts.d.ts.map +1 -0
- package/dist/course-authoring-licence.d.ts +15 -0
- package/dist/course-authoring-licence.d.ts.map +1 -0
- package/dist/course-authoring-models.d.ts +291 -0
- package/dist/course-authoring-models.d.ts.map +1 -0
- package/dist/course-authoring-service.d.ts +244 -0
- package/dist/course-authoring-service.d.ts.map +1 -0
- package/dist/course-delegation.d.ts +26 -0
- package/dist/course-delegation.d.ts.map +1 -0
- package/dist/course-generation-contracts.d.ts +150 -0
- package/dist/course-generation-contracts.d.ts.map +1 -0
- package/dist/course-generation-models.d.ts +94 -0
- package/dist/course-generation-models.d.ts.map +1 -0
- package/dist/course-generation-output.d.ts +31 -0
- package/dist/course-generation-output.d.ts.map +1 -0
- package/dist/course-generation-proposal-generator.d.ts +94 -0
- package/dist/course-generation-proposal-generator.d.ts.map +1 -0
- package/dist/course-generation-review-service.d.ts +149 -0
- package/dist/course-generation-review-service.d.ts.map +1 -0
- package/dist/course-release-scope.d.ts +37 -0
- package/dist/course-release-scope.d.ts.map +1 -0
- package/dist/course-review-contracts.d.ts +127 -0
- package/dist/course-review-contracts.d.ts.map +1 -0
- package/dist/course-review-digest.d.ts +4 -0
- package/dist/course-review-digest.d.ts.map +1 -0
- package/dist/course-review-evaluation.d.ts +63 -0
- package/dist/course-review-evaluation.d.ts.map +1 -0
- package/dist/course-review-gate.d.ts +15 -0
- package/dist/course-review-gate.d.ts.map +1 -0
- package/dist/course-review-models.d.ts +96 -0
- package/dist/course-review-models.d.ts.map +1 -0
- package/dist/group-sharing-contracts.d.ts +5 -0
- package/dist/group-sharing-contracts.d.ts.map +1 -1
- package/dist/group-sharing-policy.d.ts.map +1 -1
- package/dist/group-sharing-reader.d.ts.map +1 -1
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5658 -116
- package/dist/index.js.map +1 -1
- package/dist/ingestion.js +1 -1
- package/dist/learner-plan-service.d.ts.map +1 -1
- package/dist/learner-service.d.ts +81 -7
- package/dist/learner-service.d.ts.map +1 -1
- package/dist/learners.d.ts +6 -0
- package/dist/learners.d.ts.map +1 -1
- package/dist/manifest.json +14304 -2177
- package/dist/question-authoring-service.d.ts +125 -0
- package/dist/question-authoring-service.d.ts.map +1 -0
- package/dist/question-content.d.ts +25 -0
- package/dist/question-content.d.ts.map +1 -0
- package/dist/question-contracts.d.ts +240 -0
- package/dist/question-contracts.d.ts.map +1 -0
- package/dist/question-models.d.ts +124 -0
- package/dist/question-models.d.ts.map +1 -0
- package/dist/question-render.d.ts +20 -0
- package/dist/question-render.d.ts.map +1 -0
- package/dist/question-response-contracts.d.ts +54 -0
- package/dist/question-response-contracts.d.ts.map +1 -0
- package/dist/question-response-service.d.ts +63 -0
- package/dist/question-response-service.d.ts.map +1 -0
- package/dist/question-rubric.d.ts +23 -0
- package/dist/question-rubric.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +5410 -910
- package/dist/submission-service.d.ts.map +1 -1
- package/dist/submissions.d.ts +5 -0
- package/dist/submissions.d.ts.map +1 -1
- package/dist/workbook-contracts.d.ts +6 -0
- package/dist/workbook-contracts.d.ts.map +1 -1
- package/dist/workbook-models.d.ts.map +1 -1
- package/package.json +8 -8
- package/dist/chunks/workbook-photo-service-C6mbUUp6.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -34,6 +34,18 @@ sharing separate from guardian Grade authority. Photo-first receipt/context
|
|
|
34
34
|
capture uses [WorkbookPhotoService](agents/photo-intake.md); covers/references
|
|
35
35
|
never create learning evidence, and blank exercises need explicit work intent.
|
|
36
36
|
|
|
37
|
+
Revisioned authored courses, release, archive, enrollment pins and licence use
|
|
38
|
+
[CourseAuthoringService](agents/course-authoring-service.md). Keep history
|
|
39
|
+
append-only, require request keys, and never weaken personal-use-copy rules.
|
|
40
|
+
|
|
41
|
+
Generation proposals into new draft revisions and revision-bound educational
|
|
42
|
+
review use [CourseGenerationReviewService](agents/course-generation-review.md).
|
|
43
|
+
Keep supplied content unless a decision is recorded; reviews carry no score.
|
|
44
|
+
|
|
45
|
+
Immutable questions, answer keys/rubrics, stable learning objectives, worksheet
|
|
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.
|
|
48
|
+
|
|
37
49
|
Source import: [host boundary and transaction contract](agents/source-import.md).
|
|
38
50
|
`SourceImportService` requires host opt-in, matching active actor/tenant and
|
|
39
51
|
explicit licensing authorization; never add a system/global write bypass.
|
package/README.md
CHANGED
|
@@ -15,6 +15,11 @@ work and revision-bound comments remain retained after leaving a group.
|
|
|
15
15
|
See the [group sharing contract](agents/group-sharing.md) for the required host
|
|
16
16
|
authorizer, private media delivery, facet semantics and migration/test matrix.
|
|
17
17
|
|
|
18
|
+
Guardians author courses with `CourseAuthoringService`: append-only course and
|
|
19
|
+
lesson revisions, a reviewed release that materializes immutable lessons,
|
|
20
|
+
reversible archive, and recorded enrollment pins and upgrades. See the
|
|
21
|
+
[course authoring contract](agents/course-authoring-service.md).
|
|
22
|
+
|
|
18
23
|
## Install
|
|
19
24
|
|
|
20
25
|
```sh
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Course authoring contract
|
|
2
|
+
|
|
3
|
+
`CourseAuthoringService` (SMRT #4514, consumed by Hearth #68) writes revisioned,
|
|
4
|
+
family-owned courses and releases them into the existing Course/Section/Lesson
|
|
5
|
+
shape. Risk is high: tenancy, authorization, new schema and a public contract.
|
|
6
|
+
It is a standalone service (not a `LearnerService` subclass); every write is one
|
|
7
|
+
transaction and every authorizer call receives that transaction's executor.
|
|
8
|
+
|
|
9
|
+
## Models (closed, required tenant, no REST/CLI/MCP/AI)
|
|
10
|
+
|
|
11
|
+
`AuthoredCourse` (source key `authored`) stays the stable identity row; it never
|
|
12
|
+
holds content, is never assigned and its licence is never edited. Append-only:
|
|
13
|
+
`CourseRevision`, `LessonRevision` (stable `lessonKey` across versions; an
|
|
14
|
+
unchanged lesson reuses its row), `CourseRelease`, `ReleasedLesson`,
|
|
15
|
+
`CourseArchiveDecision`, `CourseDiscardTombstone`, `EnrollmentReleasePin`,
|
|
16
|
+
`CourseUpgradeChoice`. `CourseDraftHead` is the only mutable row and moves only
|
|
17
|
+
through the service's expected-head compare-and-swap. `save()` on an existing
|
|
18
|
+
append-only row and `delete()` throw; erasure belongs to a retention operation.
|
|
19
|
+
Schema ships via normal manifest migrations (additive: new tables only, no
|
|
20
|
+
change to existing tables, test-pinned); the runtime never creates tables.
|
|
21
|
+
|
|
22
|
+
## Actions and host mapping
|
|
23
|
+
|
|
24
|
+
`education.course.read-draft|author|generate|review|release|assign|archive`. The host
|
|
25
|
+
maps roles; the authorizer also receives `operation`, `courseId`,
|
|
26
|
+
`enrollmentId`/`learnerId` and `delegatingGuardianUserId` (agent calls) and must
|
|
27
|
+
re-validate that delegation on the supplied executor. The service checks the
|
|
28
|
+
ambient tenant context, active actor and delegating-guardian membership first.
|
|
29
|
+
`generated` provenance additionally needs `generate`. Releasing and assigning
|
|
30
|
+
are independent grants: `pinEnrollment`, `upgradeEnrollment` and `setAutoUpgrade`
|
|
31
|
+
(operations `pin-enrollment`, `upgrade-enrollment`, `set-auto-upgrade`) use only
|
|
32
|
+
`assign`, never `release`. A host maps `assign` per learner from its assignment
|
|
33
|
+
setting (guardians only, guardians plus agents, learner can ask) using
|
|
34
|
+
`operation`, `enrollmentId`/`learnerId` and `delegatingGuardianUserId`; a
|
|
35
|
+
learner-initiated request is host policy, the service adds no learner authority.
|
|
36
|
+
Archive and discard use `archive`. `generate` and `review` are used by the [generation and review service](course-generation-review.md).
|
|
37
|
+
Foreign or missing courses/enrollments are not-found/denied alike.
|
|
38
|
+
|
|
39
|
+
Delegated actor: the authenticated actor is the agent; `delegatingGuardianUserId`
|
|
40
|
+
and a host `delegationId` (opaque, 1-200 printable characters) are validated as a
|
|
41
|
+
unit (one without the other is `invalid-request`; the guardian may not be the agent,
|
|
42
|
+
which is denied). Validation grants nothing: the guardian keeps no authority via the
|
|
43
|
+
agent, the host authorizer decides, and both users need active membership. One helper
|
|
44
|
+
(`course-delegation.ts`) builds actor, authorizer and record fields. `delegationId` is
|
|
45
|
+
passed to every authorizer call (all `education.course.*` actions, including the
|
|
46
|
+
per-enrollment `auto-upgrade` call, which gets the opt-in's recorded id) and recorded
|
|
47
|
+
(nullable on views) on revisions, releases, archive/discard, pins, opt-ins, skips
|
|
48
|
+
(`optInDelegationId`; automatic pins also record the opt-in's) and generation, review
|
|
49
|
+
and question records. Request-key replays compare it: another delegation id conflicts.
|
|
50
|
+
Delegation is opt-in and fails closed: `verifyDelegation(executor, { tenantId,
|
|
51
|
+
actorUserId, delegatingGuardianUserId, delegationId, action, operation, courseId?,
|
|
52
|
+
enrollmentId?, learnerId? }) => boolean | { allowed, reason?, code? }` must be set in the
|
|
53
|
+
service options. A delegated request without it, or when it denies or throws, is a
|
|
54
|
+
`CourseAuthoringAuthorizationError` before the host authorizer runs. The per-enrollment
|
|
55
|
+
`auto-upgrade` check re-verifies the opt-in's recorded delegation through the same port
|
|
56
|
+
(on the savepoint executor); a failure skips the move. Plain guardians never need it.
|
|
57
|
+
Assignment (`LearnerService.enroll`) is described in the [learner contract](learner-service.md).
|
|
58
|
+
|
|
59
|
+
## Writes
|
|
60
|
+
|
|
61
|
+
Every write requires a `requestKey`. A retried key replays its original result;
|
|
62
|
+
the same key with a different payload throws `CourseRequestKeyConflictError`.
|
|
63
|
+
`saveDraft` takes `expectedHeadRevisionId` and throws `CourseStaleHeadError`
|
|
64
|
+
(with the current head) when stale. Errors extend `CourseAuthoringError` with a
|
|
65
|
+
stable `code`; never match on messages.
|
|
66
|
+
|
|
67
|
+
## Release
|
|
68
|
+
|
|
69
|
+
`release` requires an injected `releaseReviewGate(executor, {tenantId, courseId,
|
|
70
|
+
revisionId}) => {current, reviewId?, reason?}`. No gate configured throws
|
|
71
|
+
`ReviewGateRequiredError`; `current !== true` throws `ReviewNotCurrentError`.
|
|
72
|
+
`createReviewBackedReleaseGate()` is the review-backed implementation (the default of
|
|
73
|
+
`CourseGenerationReviewService`; see [generation and review](course-generation-review.md)). The decision records actor,
|
|
74
|
+
delegating guardian and `reviewId`, one release per revision. It materializes a
|
|
75
|
+
release-only `AuthoredCourse` row (source key `authored-release`), sections and
|
|
76
|
+
`Lesson` subclasses whose ids derive deterministically from the revision id and
|
|
77
|
+
lesson identity; `Lesson.externalRef` and `ReleasedLesson` carry the stable
|
|
78
|
+
lesson identity for progress/reporting. Learners on different releases never
|
|
79
|
+
share mutable rows. Personal-use releases stay `draft` status (the existing rule).
|
|
80
|
+
|
|
81
|
+
## Enrollment pin and upgrades
|
|
82
|
+
|
|
83
|
+
`Enrollment` is untouched (its `courseId` stays immutable): enroll through
|
|
84
|
+
`LearnerService` on a release's `releaseCourseId`, then `pinEnrollment` records
|
|
85
|
+
the pin. `upgradeEnrollment` appends an explicit move to a newer release.
|
|
86
|
+
`setAutoUpgrade` records an opt-in/out at course or enrollment level (enrollment
|
|
87
|
+
wins) for future releases; each automatic move is a recorded pin without actor. Pin, upgrade and
|
|
88
|
+
`setAutoUpgrade` need a `policyBasis` (the assignment-setting value relied on, e.g.
|
|
89
|
+
`guardians-only`; 1-200 printable chars, else `invalid-request`). The caller's is optional: the
|
|
90
|
+
`assign` authorizer decision may return `policyBasis`, which then wins; both given must be equal
|
|
91
|
+
(`CoursePolicyBasisMismatchError`, `policy-basis-mismatch`) and neither is `invalid-request`; an invalid
|
|
92
|
+
authorizer basis is a validation error, nothing written. The authorizer runs first, also on replays, and
|
|
93
|
+
replays compare the effective basis. Recorded with actor and delegating guardian on the pin or
|
|
94
|
+
choice row and exposed on the views. An automatic move records the per-enrollment `auto-upgrade`
|
|
95
|
+
decision's `policyBasis` (an invalid one denies the move), else the basis of the
|
|
96
|
+
opt-in that governs it (the same for a skip), plus its `optInScope` (`course`|`enrollment`). Each
|
|
97
|
+
automatic move is authorized per enrollment during `release` with action
|
|
98
|
+
`assign`, operation `auto-upgrade`, that `enrollmentId`/`learnerId`, the opt-in's
|
|
99
|
+
recorded actor and delegating guardian (current membership re-checked, the host
|
|
100
|
+
re-evaluates the delegation or relationship) and the transaction executor; hosts
|
|
101
|
+
must map `auto-upgrade` per learner from the assignment setting. A denial or
|
|
102
|
+
authorizer failure (each call runs in a SAVEPOINT, rolled back on a throw, so a failing host statement cannot abort the release transaction) never blocks the release or other enrollments: that
|
|
103
|
+
enrollment stays on its pin and an `EnrollmentAutoUpgradeSkip` row records
|
|
104
|
+
`not-authorized` (or the authorizer's `reason`), the optional `code`, the opt-in scope, actor,
|
|
105
|
+
guardian and `policyBasis`. A replayed
|
|
106
|
+
request key with a different basis is a request-key conflict.
|
|
107
|
+
`EnrollmentPinView` (from `pinEnrollment`, `upgradeEnrollment`, `getEnrollmentRelease`) carries
|
|
108
|
+
`learnerId`, `courseId` (authored identity) and `releaseCourseId` (history rows carry `releaseCourseId`);
|
|
109
|
+
`setAutoUpgrade` returns `learnerId` (null for a course choice). All derive from the enrollment row on the
|
|
110
|
+
server, with no new input: hosts must not read the `enrollments` table: `learnerId` and `courseId` (plus `releaseId`/`releaseCourseId` for pin and upgrade) arrive in the assign authorizer and `verifyDelegation` requests. `getEnrollmentRelease` returns
|
|
111
|
+
`EnrollmentReleaseView` (`learnerId`, `courseId`, `releaseCourseId`, `pin`, `lessons`): after any move
|
|
112
|
+
(automatic or explicit), started lessons stay on the immutable row they were
|
|
113
|
+
started on, unstarted lessons follow the current release. `LearnerService`
|
|
114
|
+
consumes the same rule (see `learner-service.md`).
|
|
115
|
+
|
|
116
|
+
## Authorizer answers
|
|
117
|
+
|
|
118
|
+
An authorizer returns `boolean` or `{ allowed, reason?, code? }` (only `allowed: true`
|
|
119
|
+
allows). On an auto-upgrade denial `reason` (at most 200 characters, control characters
|
|
120
|
+
stripped) and `code` (`[a-z0-9._-]`, at most 64; e.g. `assignment-setting`,
|
|
121
|
+
`agent-permission`, `guardian-link`) are stored on the skip; invalid values fall back to
|
|
122
|
+
`not-authorized` and an empty code. A thrown error's message is never recorded. The per-enrollment `verifyDelegation` refusal is recorded the
|
|
123
|
+
same way: an object refusal's `reason`/`code` (same sanitizing); a boolean refusal, a throw or no port
|
|
124
|
+
records the generic `not-authorized` and an empty code.
|
|
125
|
+
|
|
126
|
+
## Reads
|
|
127
|
+
|
|
128
|
+
All use `read-draft`, the executor and delegation fields, and are tenant-exact (a foreign
|
|
129
|
+
course is not-found, a foreign enrollment a denial). Lists take `limit` (default 50, capped at
|
|
130
|
+
100) and an opaque `cursor`, and return `{ items, nextCursor }` in a total order.
|
|
131
|
+
- `listCourses({ includeDiscarded? })` (operation `list-courses`): `CourseSummaryView`
|
|
132
|
+
per course: state, `head` (`revisionId` is the expected-head token), `latestRelease`,
|
|
133
|
+
`archive`, `licenseKind` (most restrictive of head and every release), `groupShareable`
|
|
134
|
+
(false for personal-use and until a release exists) and `releaseCount`; hosts need no
|
|
135
|
+
`getCourse` per row. Oldest first; discarded courses only when requested.
|
|
136
|
+
- `listUpgradeChoices({ courseId, enrollmentId? })` (`list-upgrade-choices`): opt-in/out
|
|
137
|
+
history in sequence order; with an enrollment, its own rows plus `governing`.
|
|
138
|
+
- `listAutoUpgradeSkips({ courseId?, enrollmentId? })` (`list-auto-upgrade-skips`, one
|
|
139
|
+
required): `AutoUpgradeSkipView` with `reason`, `code`, opt-in actor/guardian/delegation.
|
|
140
|
+
- `QuestionAuthoringService.readReleasedLesson({ releaseId, lessonKey })`
|
|
141
|
+
(`read-released-lesson`): lesson revision, layout and the keyed worksheet bundle.
|
|
142
|
+
- `QuestionResponseService.readReleasedLesson({ enrollmentId, lessonId })` (learner
|
|
143
|
+
authorizer, `education.learner.read`; the learner or an authorized guardian): only the
|
|
144
|
+
enrollment's effective lessons (else `QuestionResponseScopeError`), keyless bundle,
|
|
145
|
+
no materials or provenance, personal-use body withheld (`bodyWithheld`).
|
|
146
|
+
|
|
147
|
+
## Archive and discard
|
|
148
|
+
|
|
149
|
+
`archive`/`unarchive` append reversible decisions, mirror release `Course.status`
|
|
150
|
+
for catalog listings only and never touch enrollments, progress, submissions,
|
|
151
|
+
grades, workbooks, notes or evidence. Archived courses refuse release, new pins
|
|
152
|
+
and upgrades. `discardDraft` ends an unreferenced draft with a tombstone (history
|
|
153
|
+
stays); it is refused (`CourseDiscardRefusedError.references`) for releases and
|
|
154
|
+
enrollments, and for review/generation/variant rows via the injected `referenceGuard`
|
|
155
|
+
(`createCourseReferenceGuard`).
|
|
156
|
+
|
|
157
|
+
## Licence and sharing
|
|
158
|
+
|
|
159
|
+
Most restrictive wins: own-work < open-license < licensed-for-sharing <
|
|
160
|
+
personal-use-copy (`mostRestrictiveLicense`). A lesson's own licence folds every restriction the course ever recorded
|
|
161
|
+
for its `lessonKey` or its normalized content, so rename, omitted keys and
|
|
162
|
+
remove-then-re-add keep it; only dropping that content lifts it, and a
|
|
163
|
+
`lessonKey` once restricted stays restricted. A reused asset keeps the most
|
|
164
|
+
restrictive licence ever recorded for it. Uploaded materials default to
|
|
165
|
+
`personal-use-copy` unless the author states otherwise. Any personal-use part
|
|
166
|
+
makes the course tenant-private: `GroupSharingWorkFacts.courseShareable=false`
|
|
167
|
+
(reason `course-private`) for authored releases, and the policy option list hides
|
|
168
|
+
them. Work is attributed to the course of its own lesson, and any personal-use
|
|
169
|
+
release anywhere in the enrollment's pin lineage also makes it `course-private`
|
|
170
|
+
and hides the course from the options; unpinned enrollments and databases without the pin tables are unchanged. Existing workbook personal-use scaffolds are unchanged.
|
|
171
|
+
|
|
172
|
+
## Materials
|
|
173
|
+
|
|
174
|
+
Uploads are copied by the injected `materialCopy` port into education-owned
|
|
175
|
+
assets (tenant-checked afterwards) with source/actor/time provenance, so
|
|
176
|
+
ingestion expiry never removes them; replays never copy twice, and a port must
|
|
177
|
+
not leave side effects outside the transaction it cannot compensate. Releases
|
|
178
|
+
link them with `LessonAsset` under private asset access for personal-use and
|
|
179
|
+
answer-key material.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Material generation proposals and educational review
|
|
2
|
+
|
|
3
|
+
`CourseGenerationReviewService` (SMRT #4516, extends `QuestionAuthoringService`,
|
|
4
|
+
consumed by Hearth #68) adds generation proposals and revision-bound review to the
|
|
5
|
+
course contract. Risk is high: schema and public contract. It uses the same
|
|
6
|
+
authorizer, tenancy checks and compare-and-swap save path. Every call receives
|
|
7
|
+
the transaction executor and `operation` (`request-generation`, `run-generation`,
|
|
8
|
+
`cancel-generation`, `accept-proposal`, `read-generation`, `record-review`,
|
|
9
|
+
`resolve-finding`, `note-context-change`, `read-review`). Foreign ids are not found.
|
|
10
|
+
|
|
11
|
+
## Models (closed, required tenant, no REST/CLI/MCP/AI, append-only)
|
|
12
|
+
|
|
13
|
+
`CourseGenerationJob` (request plus retained input snapshot),
|
|
14
|
+
`CourseGenerationEvent` (status log: queued, running, succeeded, failed,
|
|
15
|
+
cancelled; provider, model, response model, nullable token/cost usage, error
|
|
16
|
+
code, outcome), `CourseProposal`, `CourseProposalDecision`, `CourseReview`,
|
|
17
|
+
`CourseReviewFinding`, `CourseReviewOutdatedMarker`, `CourseFindingResolution`.
|
|
18
|
+
`save()` on an existing row and `delete()` throw. Eight additive tables, test-pinned;
|
|
19
|
+
the runtime never creates tables. `CourseReview` has no score, rating or grade.
|
|
20
|
+
|
|
21
|
+
## Generation
|
|
22
|
+
|
|
23
|
+
The host injects a `CourseMaterialGenerator` (`createProposalCourseGenerator`
|
|
24
|
+
adapts an ingestion `ProposalGenerator`); the service never selects a provider.
|
|
25
|
+
`requestGeneration` (action `education.course.generate`) records a job against the
|
|
26
|
+
head the caller saw; the request key is idempotent, a retry or duplicate returns
|
|
27
|
+
the existing job and proposal, and the same key with other input throws
|
|
28
|
+
`CourseRequestKeyConflictError`. `runGeneration` calls the generator outside any
|
|
29
|
+
transaction (deadline and output cap), then validates the untrusted output against
|
|
30
|
+
a closed schema. Invalid output, provider error, timeout, cancellation or a moved
|
|
31
|
+
head record `failed`/`cancelled` and leave the head unchanged; the input stays.
|
|
32
|
+
Personal-use lesson bodies and assets are never sent to the provider.
|
|
33
|
+
|
|
34
|
+
Success appends one new draft revision (`generate` plus `author`, provenance
|
|
35
|
+
`generated`) holding only additions and fills of empty fields. A proposal that
|
|
36
|
+
differs from supplied content is a `conflict` and is held in `CourseProposal`.
|
|
37
|
+
`getProposal` returns the per-field comparison (supplied, proposed, current,
|
|
38
|
+
decision). `decideProposal` accepts or rejects conflict fields: an accept appends
|
|
39
|
+
a revision on `expectedHeadRevisionId` and records the decision; supplied content
|
|
40
|
+
is replaced only that way. Never write generated content to a released revision or
|
|
41
|
+
assign it to a learner.
|
|
42
|
+
|
|
43
|
+
## Review
|
|
44
|
+
|
|
45
|
+
`recordReview` (action `education.course.review`) binds to an exact revision and a
|
|
46
|
+
context digest (level target and theme from the revision; learner plus evidence
|
|
47
|
+
snapshot reference when given). Findings point at a course, lesson or question
|
|
48
|
+
revision id plus field/region, with concern, explanation, suggested change,
|
|
49
|
+
`uncertain`, `humanVerificationRequired` and `blocking`. Sections are separate:
|
|
50
|
+
`general-level` and `individual-learner` (needs a learner context). Every review
|
|
51
|
+
stores `COURSE_REVIEW_DISCLAIMER`; it is an aid, never certification.
|
|
52
|
+
|
|
53
|
+
A review is outdated when a new draft revision is saved (a persisted marker is
|
|
54
|
+
appended inside that save), when `noteContextChange` reports a new evidence
|
|
55
|
+
snapshot, or when the head differs or the digest no longer verifies (derived).
|
|
56
|
+
History, authorship (actor plus delegating guardian) and findings are kept.
|
|
57
|
+
`resolveFinding` records one resolution per finding; a delegated agent cannot
|
|
58
|
+
resolve a `humanVerificationRequired` finding.
|
|
59
|
+
|
|
60
|
+
## Release gate and discard guard
|
|
61
|
+
|
|
62
|
+
`createReviewBackedReleaseGate()` (the service default) makes a revision releasable
|
|
63
|
+
only when its latest review is for that revision, is the draft head, has no marker,
|
|
64
|
+
a matching digest, and no unresolved blocking or human-verification finding on
|
|
65
|
+
any review of it or of any ancestor revision (a newer or empty review never hides
|
|
66
|
+
one). Nothing drops a finding automatically: not an edit, rename, copied question
|
|
67
|
+
or removal of its target. Only `resolveFinding` clears one (a guardian can, even
|
|
68
|
+
when the target is gone; a delegated agent cannot resolve human-verification
|
|
69
|
+
findings); otherwise `ReviewNotCurrentError.reason` is `no-review`, `outdated`,
|
|
70
|
+
`context-mismatch` or `unresolved-findings`. A plain `CourseAuthoringService`
|
|
71
|
+
without a gate still fails closed. `createCourseReferenceGuard(extra)` blocks
|
|
72
|
+
discard for review and generation rows; pass variant guards through `extra`.
|
|
73
|
+
|
|
74
|
+
## Evaluation
|
|
75
|
+
|
|
76
|
+
`evaluateReviewer(cases, reviewer)` scores a supplied reviewer function against
|
|
77
|
+
seeded flaws and suitable material (precision, recall, false-alarm rate, per
|
|
78
|
+
category). Fixtures live in `src/__tests__/course-review-fixtures.ts`; no live model
|
|
79
|
+
is called by this package.
|
|
@@ -47,6 +47,27 @@ Same-tenant membership alone never grants sibling access.
|
|
|
47
47
|
A learner cannot plan their own curriculum. Title is required.
|
|
48
48
|
- `enroll` requires that guardian authority and a matching tenant curriculum.
|
|
49
49
|
It returns the existing enrollment for the same curriculum/course unchanged.
|
|
50
|
+
A delegated agent may enroll, only when the host opts in: construct
|
|
51
|
+
`LearnerService(db, authorize, authorizeCatalog, { verifyDelegation })` where
|
|
52
|
+
`verifyDelegation(executor, { tenantId, actorUserId, delegatingGuardianUserId,
|
|
53
|
+
delegationId, learnerId, courseId? }) => boolean | { allowed, reason?, code? }` runs on the
|
|
54
|
+
current executor. Without the port, or if it denies or throws, `enroll` refuses with
|
|
55
|
+
`LearnerAuthorizationError` before the learner authorizer and before any write. Supply
|
|
56
|
+
`delegatingGuardianUserId` and `delegationId` (both or neither, 1-200 printable characters,
|
|
57
|
+
guardian not the agent). After the port, the service re-checks the DELEGATING guardian's
|
|
58
|
+
active membership and `guardianRelationshipId` (that guardian's profile to the learner, same
|
|
59
|
+
tenant) and the agent's active membership. The authorizer gets `education.learner.plan`,
|
|
60
|
+
`own: false`, `delegated: true`, `relationshipTypeId` undefined (so a legacy
|
|
61
|
+
`relationshipTypeId === guardian` rule denies), the guardian's type as
|
|
62
|
+
`delegatingRelationshipTypeId`, `delegatingGuardianUserId` and `delegationId`, and decides.
|
|
63
|
+
A new delegated enrollment appends one `EnrollmentAssignmentRecord` (`Enrollment` is
|
|
64
|
+
unchanged). Only `enroll` and `createCurriculum` accept a delegation (on `LearnerService` itself; subclasses without
|
|
65
|
+
the port refuse); every other operation ignores it, so the agent has no
|
|
66
|
+
other learner authority. Catalog reads still use the agent as actor.
|
|
67
|
+
`createCurriculum` accepts the same delegated actor (`operation: 'create-curriculum'`
|
|
68
|
+
on the verifier request, `enroll` sends `'enroll'`), with identical verification, guardian
|
|
69
|
+
relationship recheck, authorizer shape and stripping of caller authority keys; each new
|
|
70
|
+
delegated curriculum appends one `CurriculumCreationRecord` (own table, `Curriculum` unchanged).
|
|
50
71
|
- `startLesson` allows the learner or authorized guardian to create their own
|
|
51
72
|
enrollment's lesson progress in `in_progress`. It derives learner identity
|
|
52
73
|
from the validated enrollment. Repeated starts return existing state without
|
|
@@ -54,6 +75,18 @@ Same-tenant membership alone never grants sibling access.
|
|
|
54
75
|
- `listProgress` reads only the validated learner/enrollment and lessons still
|
|
55
76
|
visible in that enrollment's course. Inconsistent persisted links fail
|
|
56
77
|
closed or are omitted from the list.
|
|
78
|
+
- An enrollment with an `EnrollmentReleasePin` uses its effective lesson set
|
|
79
|
+
instead of `enrollment.courseId` alone (`resolveEnrollmentLessons`, shared
|
|
80
|
+
with `CourseAuthoringService.getEnrollmentRelease`, on the current executor):
|
|
81
|
+
lessons started (non-`not_started` status or attempts) stay on their original
|
|
82
|
+
immutable rows in any release the enrollment followed; the rest follow the
|
|
83
|
+
current pin, in that release's order. `nextLessons`, `assignmentLessons`,
|
|
84
|
+
`listProgress`, plan reads, `startLesson` and submission/workbook lesson
|
|
85
|
+
assertions all use it, so a lesson outside the set is rejected and progress
|
|
86
|
+
rows never move. Unpinned enrollments (or hosts without the authoring
|
|
87
|
+
tables) read their own course as before. Pins recorded for another learner
|
|
88
|
+
fail closed. `LessonProgress` accepts a lesson on any release course the
|
|
89
|
+
enrollment is pinned to; no data migration.
|
|
57
90
|
|
|
58
91
|
Service results are data-only views, with no persistence or database methods.
|
|
59
92
|
Extra request fields are never model patches: a learner cannot set score,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Question and learning-objective records
|
|
2
|
+
|
|
3
|
+
`QuestionAuthoringService` (SMRT #4515, extends `CourseAuthoringService`, consumed
|
|
4
|
+
by Hearth #68) keeps authored questions and learning objectives. Risk is high:
|
|
5
|
+
schema and public contract. It reuses the course authorizer: writes need
|
|
6
|
+
`education.course.author` (plus `generate` for generated provenance), reads that
|
|
7
|
+
return answer keys need `education.course.read-draft`; every call receives the
|
|
8
|
+
transaction executor and the operation (`create-question`, `save-question`,
|
|
9
|
+
`link-objectives`, `create-objective`, `save-objective`, `read-question`,
|
|
10
|
+
`read-worksheet`, `resolve-question-revisions`). Tenant is required; foreign ids are not-found.
|
|
11
|
+
|
|
12
|
+
## Models (closed, required tenant, no REST/CLI/MCP/AI, append-only)
|
|
13
|
+
|
|
14
|
+
`Question` (identity, optional authored `courseId`), `QuestionRevision`,
|
|
15
|
+
`LearningObjective` (identity with tenant-unique stable `externalKey`, for
|
|
16
|
+
coordination with hearthed.org#35), `LearningObjectiveRevision`,
|
|
17
|
+
`QuestionResponse`. `save()` on an existing row and `delete()` throw. There is no
|
|
18
|
+
head row: the head is the highest `sequence`, and writes take an expected-head
|
|
19
|
+
token under a row lock and a unique `(identity, sequence)` index.
|
|
20
|
+
|
|
21
|
+
A revision holds prompt, response type, choices, subparts, media (tenant assets),
|
|
22
|
+
objective identity ids, concept ids, answer key, rubric and points. A missing key
|
|
23
|
+
or rubric is stored NULL and read back `null`; an empty key is invalid and is never
|
|
24
|
+
inferred. Provenance (`supplied|inferred|generated|edited|extracted`) is recorded
|
|
25
|
+
for each present field, defaulting to the revision's; provenance for an absent
|
|
26
|
+
field is refused. Objectives are linked by identity id, so wording changes, variants
|
|
27
|
+
and themes keep the same objective.
|
|
28
|
+
|
|
29
|
+
## Writes
|
|
30
|
+
|
|
31
|
+
`createQuestion`, `saveQuestionRevision`, `linkObjectives`,
|
|
32
|
+
`createLearningObjective`, `saveLearningObjectiveRevision`. Every write needs a
|
|
33
|
+
request key; a retry replays its original result and the same key with another
|
|
34
|
+
payload throws `CourseRequestKeyConflictError`. A stale head throws
|
|
35
|
+
`CourseStaleHeadError`; a reused objective key throws
|
|
36
|
+
`LearningObjectiveKeyConflictError`. Errors keep stable `code`s.
|
|
37
|
+
|
|
38
|
+
`resolveQuestionRevisions({ revisionIds })` (`read-draft`, no course; the executor and delegation fields
|
|
39
|
+
are passed) returns `{ items, missing }`: per found id `revisionId`, `questionId`, `sequence`,
|
|
40
|
+
`responseType`, `promptSummary` (plain text, whitespace collapsed, at most 200 characters) and, when
|
|
41
|
+
linked, `learningObjectiveKeys`. Never keys, rubrics, choices, media or provenance. 1-200 ids (else
|
|
42
|
+
validation error), de-duplicated, request order. An id with no revision in this tenant, foreign or
|
|
43
|
+
unknown alike, appears only in `missing`.
|
|
44
|
+
|
|
45
|
+
## Lessons, attempts, grades
|
|
46
|
+
|
|
47
|
+
A lesson input may carry ordered `questions` (revision id, section, column, source
|
|
48
|
+
page/rect anchor) and flat `layoutParams`; they are part of the lesson revision
|
|
49
|
+
digest, and saving requires each id to be an existing question revision of the
|
|
50
|
+
tenant (`QuestionReferenceError`). Newer question revisions never move a pin.
|
|
51
|
+
|
|
52
|
+
`WorkbookQuestion.questionRevisionId`/`lessonRevisionId` and the nullable
|
|
53
|
+
`Grade.questionRevisionId` are additive. A referenced revision must exist in the
|
|
54
|
+
tenant; `Grade.rubricResults` must then fit its rubric (criterion id, number or
|
|
55
|
+
`{points}` up to `maxPoints`; empty stays unscored; results need a rubric).
|
|
56
|
+
Unreferenced legacy records are unchanged. `QuestionResponse` is an append-only
|
|
57
|
+
learner response to one exact revision with completion mode
|
|
58
|
+
(`printable|online|interactive`), assistance, a correction chain, `episodeId` and
|
|
59
|
+
the `worksheetVersionId` seen, a numbered `attemptNumber` and the writing request's
|
|
60
|
+
`payloadDigest`. Writes go through `QuestionResponseService` below.
|
|
61
|
+
|
|
62
|
+
## Responses (`QuestionResponseService`)
|
|
63
|
+
|
|
64
|
+
Extends `LearnerService`, so it uses the learner authorizer, active-actor/tenant
|
|
65
|
+
checks and executor-bound policy reads. New authorizer actions:
|
|
66
|
+
`education.learner.respond` (the learner's own enrollment; a guardian never starts
|
|
67
|
+
an attempt) and `education.learner.review-response` (guardian only; a learner
|
|
68
|
+
claiming it is refused before the host is asked).
|
|
69
|
+
|
|
70
|
+
- `recordResponse`: online attempt (new episode, next `attemptNumber`) or, with
|
|
71
|
+
`correctsResponseId`, the learner's correction in the same episode.
|
|
72
|
+
- `recordGuardianCorrection`: names the response it corrects; `response` omitted
|
|
73
|
+
records assistance only and keeps the answer. Both append; nothing is rewritten.
|
|
74
|
+
- `readLessonResponses`: the audience follows the actor. The learner gets the
|
|
75
|
+
keyless bundle (keys never leave the database call), the guardian the keyed one.
|
|
76
|
+
|
|
77
|
+
The question revision must be in the enrollment's effective lesson set
|
|
78
|
+
(`resolveEnrollmentLessons`, then the lesson revision's `questions`); otherwise
|
|
79
|
+
`QuestionResponseScopeError`. Unpinned enrollments have no authored questions.
|
|
80
|
+
The response is a closed shape (`text`, `numeric`, `choiceIds`, `parts`) checked
|
|
81
|
+
against the revision's choices and subparts (`QuestionResponseValidationError`).
|
|
82
|
+
Every write needs a request key (unique per learner); a replay returns the stored
|
|
83
|
+
row, another payload throws `CourseRequestKeyConflictError`. Other errors:
|
|
84
|
+
`QuestionResponseNotFoundError`, `LearnerAuthorizationError`.
|
|
85
|
+
|
|
86
|
+
## Rendering
|
|
87
|
+
|
|
88
|
+
`getWorksheet` (or the pure `buildLearnerWorksheet`/`buildGuardianWorksheet`)
|
|
89
|
+
builds one bundle from a release lesson's pinned revisions. `worksheetVersionId` is
|
|
90
|
+
a digest of ordered revision ids, placement and layout params, so printable and
|
|
91
|
+
online renders of the same revision match and a scan can be matched later. The
|
|
92
|
+
learner bundle is built from an allowlist and has no key, rubric or provenance
|
|
93
|
+
type members; only the guardian bundle carries them. Both service audiences need
|
|
94
|
+
`read-draft`; learner delivery belongs to the host.
|
|
95
|
+
|
|
96
|
+
## Migration
|
|
97
|
+
|
|
98
|
+
Schema ships through normal manifest migrations: five new tables (`question_responses` included) plus nullable
|
|
99
|
+
columns on `grades` (`question_revision_id`) and `lesson_revisions` (`questions_json`,
|
|
100
|
+
`layout_params_json`); test-pinned as additive. The runtime never creates tables.
|