@happyvertical/smrt-education 0.57.47 → 0.57.49
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 +9 -0
- package/README.md +23 -2
- package/agents/course-authoring-service.md +44 -1
- package/agents/course-bundle-import.md +148 -0
- package/agents/group-sharing.md +20 -0
- package/agents/learner-service.md +35 -0
- package/agents/question-extraction.md +4 -1
- package/agents/tutor-context.md +62 -0
- package/dist/catalog.d.ts +12 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/chunks/{workbook-photo-service-B7Tujv__.js → workbook-photo-service-WwDDC5GF.js} +692 -18
- package/dist/chunks/workbook-photo-service-WwDDC5GF.js.map +1 -0
- package/dist/course-authoring-contracts.d.ts +26 -3
- package/dist/course-authoring-contracts.d.ts.map +1 -1
- package/dist/course-authoring-models.d.ts +44 -1
- package/dist/course-authoring-models.d.ts.map +1 -1
- package/dist/course-authoring-service.d.ts +39 -0
- package/dist/course-authoring-service.d.ts.map +1 -1
- package/dist/course-bundle-contracts.d.ts +93 -0
- package/dist/course-bundle-contracts.d.ts.map +1 -0
- package/dist/course-bundle-models.d.ts +66 -0
- package/dist/course-bundle-models.d.ts.map +1 -0
- package/dist/course-bundle-service.d.ts +126 -0
- package/dist/course-bundle-service.d.ts.map +1 -0
- package/dist/course-bundle-zip.d.ts +48 -0
- package/dist/course-bundle-zip.d.ts.map +1 -0
- package/dist/course-bundle.d.ts +135 -0
- package/dist/course-bundle.d.ts.map +1 -0
- package/dist/course-visibility.d.ts +22 -0
- package/dist/course-visibility.d.ts.map +1 -0
- package/dist/group-sharing-reader.d.ts.map +1 -1
- package/dist/index.d.ts +17 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2563 -510
- package/dist/index.js.map +1 -1
- package/dist/ingestion.js +1 -1
- package/dist/learner-history.d.ts +50 -1
- package/dist/learner-history.d.ts.map +1 -1
- package/dist/learner-plan-service.d.ts.map +1 -1
- package/dist/learner-service.d.ts +91 -2
- package/dist/learner-service.d.ts.map +1 -1
- package/dist/learners.d.ts +2 -0
- package/dist/learners.d.ts.map +1 -1
- package/dist/manifest.json +11774 -7626
- package/dist/question-response-service.d.ts +21 -3
- package/dist/question-response-service.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +3507 -2008
- package/dist/tutor-context-service.d.ts +34 -0
- package/dist/tutor-context-service.d.ts.map +1 -0
- package/dist/tutor-contracts.d.ts +123 -0
- package/dist/tutor-contracts.d.ts.map +1 -0
- package/dist/tutor-models.d.ts +77 -0
- package/dist/tutor-models.d.ts.map +1 -0
- package/dist/tutor-session-service.d.ts +115 -0
- package/dist/tutor-session-service.d.ts.map +1 -0
- package/package.json +8 -8
- package/dist/chunks/workbook-photo-service-B7Tujv__.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -52,6 +52,11 @@ 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
|
|
56
|
+
[CourseBundleImportService](agents/course-bundle-import.md). Parse only with the
|
|
57
|
+
bounded reader; refuse, never trim, hostile input; unsupported members stay
|
|
58
|
+
visible; proposals only, nothing auto-published.
|
|
59
|
+
|
|
55
60
|
Source import: [host boundary and transaction contract](agents/source-import.md).
|
|
56
61
|
`SourceImportService` requires host opt-in, matching active actor/tenant and
|
|
57
62
|
explicit licensing authorization; never add a system/global write bypass.
|
|
@@ -75,3 +80,7 @@ then revalidate each exact family edge and current policy. Media delivery must c
|
|
|
75
80
|
the reader on every request; signed storage URLs are not a sharing grant. Saved
|
|
76
81
|
parent comment visibility applies on every child read. Progress with feedback
|
|
77
82
|
disabled contains counts only, never correctness, scores or concept labels.
|
|
83
|
+
|
|
84
|
+
Learner-safe tutor context and append-only tutor transcripts use
|
|
85
|
+
[TutorContextService and TutorSessionService](agents/tutor-context.md). Learner
|
|
86
|
+
audience never carries keys, rubrics, notes or storage URIs; no AI call here.
|
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@ authorizer, private media delivery, facet semantics and migration/test matrix.
|
|
|
17
17
|
|
|
18
18
|
Guardians author courses with `CourseAuthoringService`: append-only course and
|
|
19
19
|
lesson revisions, a reviewed release that materializes immutable lessons,
|
|
20
|
-
reversible archive,
|
|
20
|
+
reversible archive, recorded enrollment pins and upgrades, a recorded
|
|
21
|
+
visibility (`family`, `groups`, `public`) and an education-owned cover image. See the
|
|
21
22
|
[course authoring contract](agents/course-authoring-service.md).
|
|
22
23
|
|
|
23
24
|
## Install
|
|
@@ -101,7 +102,27 @@ host's permission decision. Sibling membership does not grant access.
|
|
|
101
102
|
`putBackLesson` returns started work to `not_started` and `setEnrollmentStatus`
|
|
102
103
|
starts, pauses, resumes or drops an enrollment along a fixed transition table;
|
|
103
104
|
both append immutable history (actor, and for status the host policy basis)
|
|
104
|
-
and never touch submissions, grades or other progress.
|
|
105
|
+
and never touch submissions, grades or other progress. `completeOwnLesson`
|
|
106
|
+
lets a learner mark their own lesson of any kind done when the host's
|
|
107
|
+
`education.progress.complete-own` decision allows it, recording the actor, the
|
|
108
|
+
host setting and optional evidence submission; guardian `attestLesson` is
|
|
109
|
+
unchanged. `setEnrollmentPace` sets or clears an enrollment's planned lessons
|
|
110
|
+
per week (1 to 14) under the host's `education.enrollment.set-pace` decision with
|
|
111
|
+
an immutable change row, and `nextLessons({ count })` returns a week's set of
|
|
112
|
+
unfinished lessons (default one).
|
|
113
|
+
`TutorContextService.compose` builds a bounded, learner-safe `TutorContext`
|
|
114
|
+
(lesson text, learner-visible questions, own progress, released feedback; for the
|
|
115
|
+
learner audience only `answerKeyAvailable`, never keys, rubrics, notes or storage
|
|
116
|
+
URIs) under the host's `education.tutor.read` decision, and `TutorSessionService`
|
|
117
|
+
(`open`, `append`, `close`, `listForLearner`; `education.tutor.exchange` and
|
|
118
|
+
`education.tutor.read`) keeps append-only guardian-readable transcripts. No model
|
|
119
|
+
call happens in the package; see [tutor contract](agents/tutor-context.md).
|
|
120
|
+
|
|
121
|
+
`CourseBundleImportService` imports a course zip (optional closed `manifest.json`)
|
|
122
|
+
or file set under hard size, ratio, path and entry bounds, keeps unsupported and
|
|
123
|
+
duplicate members as append-only evidence, hands accepted members to ingestion and
|
|
124
|
+
proposes sections and lessons through `CourseGenerationReviewService`; see
|
|
125
|
+
[bundle import](agents/course-bundle-import.md).
|
|
105
126
|
Repeated starts preserve the existing progress, including attempts and scores.
|
|
106
127
|
A new curriculum/year creates a distinct enrollment and fresh progress history.
|
|
107
128
|
An unscored lesson has `bestScore: null`, distinct from zero. The service does
|
|
@@ -21,7 +21,7 @@ change to existing tables, test-pinned); the runtime never creates tables.
|
|
|
21
21
|
|
|
22
22
|
## Actions and host mapping
|
|
23
23
|
|
|
24
|
-
`education.course.read-draft|author|generate|review|release|assign|archive`. The host
|
|
24
|
+
`education.course.read-draft|author|generate|review|release|assign|archive|set-visibility|set-cover`. The host
|
|
25
25
|
maps roles; the authorizer also receives `operation`, `courseId`,
|
|
26
26
|
`enrollmentId`/`learnerId` and `delegatingGuardianUserId` (agent calls) and must
|
|
27
27
|
re-validate that delegation on the supplied executor. The service checks the
|
|
@@ -144,6 +144,49 @@ course is not-found, a foreign enrollment a denial). Lists take `limit` (default
|
|
|
144
144
|
enrollment's effective lessons (else `QuestionResponseScopeError`), keyless bundle,
|
|
145
145
|
no materials or provenance, personal-use body withheld (`bodyWithheld`).
|
|
146
146
|
|
|
147
|
+
## Visibility and cover
|
|
148
|
+
|
|
149
|
+
Two optional attributes live on the stable `AuthoredCourse` identity row (added
|
|
150
|
+
columns `visibility`, `cover_asset_id` on `courses`; additive, migration-tested on
|
|
151
|
+
a populated database). Each change is an append-only row (`CourseVisibilityChange`,
|
|
152
|
+
`CourseCoverChange`: previous, new, actor, delegation, request key, time) and every
|
|
153
|
+
write needs a `requestKey`; the same key with another payload is
|
|
154
|
+
`CourseRequestKeyConflictError`, an unchanged value writes nothing. Reads degrade
|
|
155
|
+
to `family` and no cover on a database without the columns; a write without the
|
|
156
|
+
change tables throws `schema-unavailable`.
|
|
157
|
+
|
|
158
|
+
- `setVisibility({ courseId, visibility: 'family'|'groups'|'public', requestKey })`
|
|
159
|
+
(action `education.course.set-visibility`, operation `set-visibility`). Default
|
|
160
|
+
is `family`. A stored value of `''` (never set, including every course authored
|
|
161
|
+
before this existed) reads as `family`; an explicit `setVisibility('family')` on
|
|
162
|
+
such a course is recorded (it is how a host pins a course family-only).
|
|
163
|
+
A course whose licence is `personal-use-copy` (head revision or any release,
|
|
164
|
+
which never loosens) refuses `groups` and `public` with
|
|
165
|
+
`CourseVisibilityRefusedError` (`visibility-refused`). A revision that turns a
|
|
166
|
+
`groups`/`public` course personal-use narrows it to `family` in the same
|
|
167
|
+
transaction with a change row (`reason: 'licence'`, request key
|
|
168
|
+
`licence:<revisionId>`). `AuthoredCourse.save()` also refuses the combination.
|
|
169
|
+
- `listCourses({ visibility })` filters by effective visibility; views
|
|
170
|
+
(`CourseSummaryView`, `CourseStateView`) carry `visibility` and `coverAssetId`.
|
|
171
|
+
- `setCover({ courseId, assetId: string | null, requestKey })` (action
|
|
172
|
+
`education.course.set-cover`, operation `set-cover`; `null` clears). The asset
|
|
173
|
+
must be a material this course copied into the tenant (`materialCopy`: the only
|
|
174
|
+
education-owned provenance the service records), with image content type
|
|
175
|
+
(png, jpeg, webp, gif), in the course's tenant, never `answer-key` in any lesson
|
|
176
|
+
revision or released lesson link, and readable by the acting user through the
|
|
177
|
+
asset guard (a private asset only when its owner is the actor's profile).
|
|
178
|
+
Anything else is `CourseCoverInvalidError` (`cover-invalid`, `reason`:
|
|
179
|
+
`missing`, `not-image`, `not-course-material`, `answer-key`, `unreadable`);
|
|
180
|
+
fail closed. A host that wants a standalone cover adds the image as an
|
|
181
|
+
attachment material of a lesson first.
|
|
182
|
+
- `release` snapshots the current cover onto the immutable release Course row
|
|
183
|
+
(re-checked for eligibility, not for the releasing actor's read; dropped if no
|
|
184
|
+
longer eligible). `CourseReleaseView.coverAssetId` and
|
|
185
|
+
`latestRelease.coverAssetId` report it; later cover changes never rewrite it.
|
|
186
|
+
- Group sharing: see [group sharing](group-sharing.md#authored-course-visibility).
|
|
187
|
+
`public` is a read-filter label: no marketplace, no payment, and no cross-tenant
|
|
188
|
+
read exists for it.
|
|
189
|
+
|
|
147
190
|
## Archive and discard
|
|
148
191
|
|
|
149
192
|
`archive`/`unarchive` append reversible decisions, mirror release `Course.status`
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Course bundle import
|
|
2
|
+
|
|
3
|
+
`CourseBundleImportService` (SMRT #4578, extends `CourseAuthoringService`,
|
|
4
|
+
consumed by Hearth #94) imports a course bundle, a zip with an optional manifest
|
|
5
|
+
or a plain file set, as bounded evidence, hands its supported members to the
|
|
6
|
+
host for ingestion extraction and proposes sections and lessons for review.
|
|
7
|
+
Risk is high: untrusted archive parsing and new schema. It reuses the course
|
|
8
|
+
authorizer: `importBundle` needs `education.course.author`, `readImport`
|
|
9
|
+
`education.course.read-draft`, `proposeStructure` `education.course.generate`
|
|
10
|
+
(via `CourseGenerationReviewService`). Operations: `import-course-bundle`,
|
|
11
|
+
`read-course-bundle`. Server only (Node `zlib`/`crypto`); no new dependency.
|
|
12
|
+
|
|
13
|
+
## Flow
|
|
14
|
+
|
|
15
|
+
1. `importBundle({ ...actor, bundle, limits?, onProgress?, receiveMember? })`
|
|
16
|
+
authorizes **before parsing**, unpacks (stage `unpack`), classifies and lays
|
|
17
|
+
out the structure (`classify`), writes the evidence in one transaction and
|
|
18
|
+
then calls `receiveMember` for each accepted member (`extract`), outside any
|
|
19
|
+
transaction. `bundle` is `{ archive: Uint8Array }` or `{ files: [{ path,
|
|
20
|
+
bytes }] }`. Hostile or damaged input throws `CourseBundleRefusedError`
|
|
21
|
+
(`reason`, optional `entry`) and writes nothing.
|
|
22
|
+
2. `receiveMember(member, { replayed })` is the host's ingestion seam: call
|
|
23
|
+
`IngestionService.receive` with `sourceKind: 'course_import'` and the member
|
|
24
|
+
bytes, then queue extraction and `preserveQuestionExtractionAnalysis` as in
|
|
25
|
+
[question extraction](question-extraction.md). Members carry `role`
|
|
26
|
+
(manifest page role), `lessonKey`, `sectionKey`. It runs on a replay too
|
|
27
|
+
(ingestion dedups by content hash), so an interrupted first run resumes. Its
|
|
28
|
+
return values are in `result.receipts`.
|
|
29
|
+
3. `proposeStructure({ ...actor, importId, bundle, courseId,
|
|
30
|
+
expectedHeadRevisionId })` (stage `propose`) re-supplies the bundle (bytes
|
|
31
|
+
are never retained), checks it is the imported one, and runs a deterministic
|
|
32
|
+
`CourseMaterialGenerator` (`smrt-bundle-import`) through
|
|
33
|
+
`requestGeneration`/`runGeneration`. The result is an ordinary proposal: a
|
|
34
|
+
new draft revision of additions and fills; conflicts wait for
|
|
35
|
+
`decideProposal`. Nothing is released, assigned or published. The request key
|
|
36
|
+
is `bundle:<courseId>:<bundleIdentity>`, so a retry returns the same job; the
|
|
37
|
+
same key with another head is `CourseRequestKeyConflictError`.
|
|
38
|
+
4. `readImport({ ...actor, importId })` returns the recorded evidence.
|
|
39
|
+
|
|
40
|
+
Progress is an observer callback (`onProgress({ stage, completed, total })`,
|
|
41
|
+
stages `unpack`, `classify`, `extract`, `propose`; each starts at `completed: 0`).
|
|
42
|
+
Ingestion has no public job-progress seam, so none is invented. A throwing
|
|
43
|
+
listener never affects the import.
|
|
44
|
+
|
|
45
|
+
## Hard bounds (`COURSE_BUNDLE_LIMITS`; a host may only lower them)
|
|
46
|
+
|
|
47
|
+
| limit | value |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `maxArchiveBytes` (input) | 100 MiB |
|
|
50
|
+
| `maxEntries` | 200 |
|
|
51
|
+
| `maxEntryBytes` (inflated) | 50 MiB |
|
|
52
|
+
| `maxTotalBytes` (inflated) | 200 MiB |
|
|
53
|
+
| `maxCompressionRatio` (members over `ratioFloorBytes` = 64 KiB) | 100 |
|
|
54
|
+
| `maxPathBytes` / `maxSegmentBytes` / `maxPathDepth` | 512 / 255 / 8 |
|
|
55
|
+
| `maxManifestBytes` | 256 KiB |
|
|
56
|
+
| `maxLessons` (the proposal schema's cap) | 50 |
|
|
57
|
+
| `maxLessonBodyChars` | 100 000 |
|
|
58
|
+
| archive nesting | 1: a zip inside a zip is never expanded |
|
|
59
|
+
|
|
60
|
+
A lesson whose combined text members exceed `maxLessonBodyChars` refuses the
|
|
61
|
+
bundle (`lesson_too_large`, `entry` = lesson key) at import; text is never
|
|
62
|
+
trimmed or dropped. A plain file set enforces `maxTotalBytes` on its member
|
|
63
|
+
bytes exactly as an archive does on inflated bytes.
|
|
64
|
+
|
|
65
|
+
The reader (`course-bundle-zip.ts`) parses the end record and central directory,
|
|
66
|
+
validates every offset and size against the real buffer, inflates with
|
|
67
|
+
`maxOutputLength` equal to the declared size (declared sizes are never trusted:
|
|
68
|
+
a mismatch either way is `size_mismatch`), checks CRC32 and re-checks the total
|
|
69
|
+
on actual output. Stored and deflate only. Refusals (`CourseBundleRefusalReason`)
|
|
70
|
+
cover: not a zip, truncated, ambiguous end record (two end records that both fit
|
|
71
|
+
the file, for example one forged in a comment), zip64, multi-disk, central
|
|
72
|
+
directory not ending at the end record, entries out of range or overlapping,
|
|
73
|
+
local header name/method mismatch, encrypted entries (flags 0/6/13, AES method),
|
|
74
|
+
symlink and special files (unix mode; DOS reparse flag), unsupported method,
|
|
75
|
+
bad CRC, ratio, entry count, totals, duplicate names (case/NFC-insensitive,
|
|
76
|
+
backslashes normalised first) and bad paths (`..`, `.`, empty segments, absolute,
|
|
77
|
+
drive letter or `:` anywhere, NUL/control/bidi characters, lone surrogates,
|
|
78
|
+
too long or deep). An **honestly** oversize member (declared over
|
|
79
|
+
`maxEntryBytes`) is not inflated and is recorded `refused / entry_too_large`.
|
|
80
|
+
|
|
81
|
+
## Members and statuses
|
|
82
|
+
|
|
83
|
+
By content (never by extension alone): `pdf` (`%PDF-` in the first 1 KiB),
|
|
84
|
+
`image` (PNG, JPEG, WebP), `text` (`.txt`/`.md`, strict UTF-8, no control
|
|
85
|
+
characters, body-sized), `manifest` (root `manifest.json`), else `unsupported`.
|
|
86
|
+
Status: `accepted` (handed to extraction), `duplicate` (same sha256 as an earlier
|
|
87
|
+
member in canonical order; recorded once as evidence, `duplicateOfPath`, never
|
|
88
|
+
extracted again), `unsupported` (`unsupported_type`, `nested_archive`,
|
|
89
|
+
`unreadable_text`), `refused` (`encrypted_pdf`, `entry_too_large`), `metadata`
|
|
90
|
+
(the manifest). Nothing is dropped. An encrypted PDF is any PDF with an
|
|
91
|
+
`/Encrypt` token anywhere in its bytes (trailer, xref stream): a false positive
|
|
92
|
+
on literal text inside an uncompressed stream is refused, never read.
|
|
93
|
+
|
|
94
|
+
## Manifest (`manifest.json`, version 1, closed)
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "version": 1, "course": { "subject": "Math" },
|
|
98
|
+
"sections": [ { "key": "unit-1", "title": "Unit One",
|
|
99
|
+
"lessons": [ { "key": "sums", "title": "Sums",
|
|
100
|
+
"members": [ { "path": "sheets/sums.pdf", "role": "exercise" } ] } ] } ] }
|
|
101
|
+
```
|
|
102
|
+
Every object is closed: an unknown key anywhere is `manifest_unknown_key` (with
|
|
103
|
+
its path in `entry`); another version is `manifest_version`. Keys match
|
|
104
|
+
`[A-Za-z0-9._:-]{1,120}` and are unique; roles are the extraction page roles
|
|
105
|
+
(`exercise`, `example`, `instructions`, `cover`, `answer_key`, `mixed`,
|
|
106
|
+
`unknown`); a listed path must exist and be listed once. The manifest is a source
|
|
107
|
+
fact: roles are `source`, anything the extractor infers stays `inferred`, and a
|
|
108
|
+
manifest/extraction disagreement belongs in extraction review, not here. Without
|
|
109
|
+
a manifest each accepted member is one lesson, sections come from the top-level
|
|
110
|
+
directory and order is natural path order (digit runs compare numerically, no
|
|
111
|
+
locale). Manifest order: the manifest, declared members, then the rest by path.
|
|
112
|
+
Lesson bodies are the readable text members of a lesson, joined; PDFs and images
|
|
113
|
+
are never inlined. Members whose declared role is `answer_key` or `mixed` never
|
|
114
|
+
contribute text to a lesson body (they remain recorded evidence members); the
|
|
115
|
+
proposal rationale only counts how many were withheld.
|
|
116
|
+
|
|
117
|
+
## Readers
|
|
118
|
+
|
|
119
|
+
`BundleReader { formatId, detect, prepare }` in a `BundleReaderRegistry`;
|
|
120
|
+
built-ins are `manifest-v1` and `plain-files`. SCORM and Common Cartridge are
|
|
121
|
+
**not implemented** (their manifests describe launch resources and activity
|
|
122
|
+
trees that do not map to sections/lessons without design); a host can register
|
|
123
|
+
a reader ahead of the built-ins (`register(reader, { first: true })`).
|
|
124
|
+
|
|
125
|
+
## Identity and replay
|
|
126
|
+
|
|
127
|
+
`bundleIdentity` (`cbi1-...`) digests the sorted (path, sha256) pairs, so
|
|
128
|
+
re-zipping, reordering and timestamps replay. One `CourseBundleImport` per
|
|
129
|
+
(tenant, identity); a concurrent first import replays once on the unique index.
|
|
130
|
+
|
|
131
|
+
## Models (closed, required tenant, no REST/CLI/MCP/AI, append-only)
|
|
132
|
+
|
|
133
|
+
`CourseBundleImport` (`course_bundle_imports`: identity, format, reader,
|
|
134
|
+
manifest version, counts, actor) and `CourseBundleMember` (`course_bundle_members`:
|
|
135
|
+
unique tenant/import/path; ordinal, sha256, size, media type, kind, status,
|
|
136
|
+
reason, duplicate-of, role, section and lesson key). Hashes and placements only:
|
|
137
|
+
bytes stay with the host's ingestion receipts. Rewrites and deletes throw.
|
|
138
|
+
Normal manifest migrations create both tables.
|
|
139
|
+
|
|
140
|
+
## Not in this release
|
|
141
|
+
|
|
142
|
+
- Ingestion's receipt contract is unchanged: there is no `application/zip` part
|
|
143
|
+
type or `parentEvidenceId` in ingestion. The education side unpacks and the
|
|
144
|
+
host receives members one by one, so archive-to-member lineage lives in
|
|
145
|
+
`course_bundle_members` (and the host's receipts), not in ingestion evidence.
|
|
146
|
+
Changing ingestion's published receipt is a separate design decision.
|
|
147
|
+
- A manifest/extraction disagreement is not compared automatically.
|
|
148
|
+
- SCORM / Common Cartridge readers; zip64; nested archives; encrypted zips.
|
package/agents/group-sharing.md
CHANGED
|
@@ -276,3 +276,23 @@ family kind. The helper provides no arbitrary cross-tenant directory.
|
|
|
276
276
|
| Behavior/invariant | Trigger | Positive | Negative/failure | Actor/context | Executor/transaction | Runtime/dialect | External edge | Level | Command |
|
|
277
277
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
278
278
|
| G33 workspace kind | trusted exact resource lookup | existing family false; active/inactive group true | blank/malformed/absent tenant; ambiguous group rows; PostgreSQL RLS-hidden or indeterminate zero refuses family; database failures propagate | host-selected exact tenant, no synthetic session/context | caller-supplied executor, fresh reads; no writes | Node SQLite/PostgreSQL | status/membership/role absence never implies family or grants | integration | `pnpm --filter @happyvertical/smrt-education exec vitest run src/__tests__/group-sharing-workspace.test.ts` (same file with TEST_DB_ADAPTER=postgres and isolated DATABASE_URL) |
|
|
279
|
+
|
|
280
|
+
## Authored course visibility
|
|
281
|
+
|
|
282
|
+
Authored-course `visibility` (see [course authoring](course-authoring-service.md))
|
|
283
|
+
only ever narrows group sharing; it never adds a reader, a tenant or a facet.
|
|
284
|
+
|
|
285
|
+
Before: every authored release that was not personal-use was eligible (policy
|
|
286
|
+
options, `listSharedWork`, preview) subject to the policy, edge and review rules.
|
|
287
|
+
After: the same, except an authored release whose identity is explicitly recorded
|
|
288
|
+
as `family` (including any unrecognized stored value) is `course-private`: it is
|
|
289
|
+
absent from the policy options and its work evaluates `course-private`, also when
|
|
290
|
+
only another release of that course is in the enrollment's pin lineage.
|
|
291
|
+
`groups` and `public` change nothing relative to before. A course whose visibility
|
|
292
|
+
was never recorded (everything authored before visibility existed) also behaves as
|
|
293
|
+
before, so existing contracts and tests are unchanged; hosts that want a default
|
|
294
|
+
of "family only" call `setVisibility('family')` explicitly. `public` is a listing
|
|
295
|
+
label only; no reviewed cross-tenant read path exists for it, so it grants nothing.
|
|
296
|
+
Non-authored courses, answer keys and personal-use rules are untouched, and a
|
|
297
|
+
database without the visibility column or release tables reads as never recorded.
|
|
298
|
+
The gate is `authoredVisibilityPermitsGroups(db, tenantId, releaseCourseId)`.
|
|
@@ -101,6 +101,41 @@ Same-tenant membership alone never grants sibling access.
|
|
|
101
101
|
submissions and history never cascade. Bad input throws
|
|
102
102
|
`LearnerValidationError`; a host without `enrollment_status_changes` gets
|
|
103
103
|
`LearnerHistoryUnavailableError`.
|
|
104
|
+
- `completeOwnLesson({ enrollmentId, lessonId, policyBasis, evidence?: { submissionId } })`
|
|
105
|
+
moves progress to `complete` (any lesson kind, `completedAt` set, a missing
|
|
106
|
+
row created) under `education.progress.complete-own`; the host decides per
|
|
107
|
+
setting and may allow a guardian too (own is not blocked). It appends an
|
|
108
|
+
immutable `LessonCompletionRecord` (actor, `policyBasis`, prior status,
|
|
109
|
+
optional evidence submission, at most one per progress) and a `complete`
|
|
110
|
+
`LessonProgressTransition` when that optional table exists. `evidence`
|
|
111
|
+
must be a non-rejected submission of the same tenant, learner, lesson and
|
|
112
|
+
progress, else `LearnerAuthorizationError`. Counters, score and start time
|
|
113
|
+
are untouched; no score is invented. Returns `{ changed, progress }`.
|
|
114
|
+
Repeat rule: a lesson already completed through this path (a record exists)
|
|
115
|
+
returns `{ changed: false }`; one complete any other way (guardian
|
|
116
|
+
`attestLesson`/grade, workbook completion) throws `LessonAlreadyCompleteError`.
|
|
117
|
+
Also `EnrollmentNotActiveError` (`.status`; only `active` enrollments),
|
|
118
|
+
`LessonSkippedError`, `WorkbookOwnedLessonError` (a workbook lesson completes
|
|
119
|
+
through its workbook decision), and `LearnerHistoryUnavailableError` without
|
|
120
|
+
`lesson_completion_records`. Guardian `attestLesson` is unchanged.
|
|
121
|
+
- `setEnrollmentPace({ enrollmentId, pace: { lessonsPerWeek } | null, policyBasis })`
|
|
122
|
+
sets or clears the enrollment's planned lessons per week under
|
|
123
|
+
`education.enrollment.set-pace` (the host decides whether a learner may act
|
|
124
|
+
on their own enrollment). `lessonsPerWeek` is a whole number from 1 to 14
|
|
125
|
+
(`MIN_/MAX_LESSONS_PER_WEEK`), else `EnrollmentPaceError`. It is stored as the
|
|
126
|
+
integer column `enrollments.pace_lessons_per_week` (`0` = no pace, like
|
|
127
|
+
`targetMinutesPerWeek`) and surfaces as `EnrollmentView.pace` (`null` when
|
|
128
|
+
unset). Each change appends an immutable `EnrollmentPaceChange` (actor,
|
|
129
|
+
`policyBasis`, previous and new value, `0` = none); an unchanged pace is
|
|
130
|
+
`{ changed: false }` with no row. Status and progress are untouched. A host
|
|
131
|
+
without `enrollment_pace_changes` gets `LearnerHistoryUnavailableError`.
|
|
132
|
+
Apply the additive migration (new column plus table) before deploying writes;
|
|
133
|
+
reads of an un-migrated table report no pace.
|
|
134
|
+
- `nextLessons({ enrollmentId?, count? })`: `count` is an integer from 1 to
|
|
135
|
+
14 (`MAX_NEXT_LESSONS_COUNT`, default 1, else `LearnerValidationError`) and
|
|
136
|
+
returns up to that many unfinished (not complete or skipped) lessons per
|
|
137
|
+
enrollment in lesson order, as the same flat entries; the default is
|
|
138
|
+
unchanged.
|
|
104
139
|
- `listProgress` reads only the validated learner/enrollment and lessons still
|
|
105
140
|
visible in that enrollment's course. Inconsistent persisted links fail
|
|
106
141
|
closed or are omitted from the list.
|
|
@@ -104,5 +104,8 @@ Both ship through normal manifest migrations. The originals stay in ingestion
|
|
|
104
104
|
and are never copied. Imported material keeps its course licence; extraction
|
|
105
105
|
does not change sharing.
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
Zip and manifest bundles are handled before extraction by
|
|
108
|
+
[CourseBundleImportService](course-bundle-import.md) (SMRT #4578).
|
|
109
|
+
|
|
110
|
+
Not in this release: automatic new
|
|
108
111
|
revisions on `source_changed`, and media assets for extracted diagrams.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Tutor context and sessions
|
|
2
|
+
|
|
3
|
+
No AI call, prompt text or provider lives here. The host builds prompts from
|
|
4
|
+
`TutorContext` and owns the model call and spend ceiling; this package records
|
|
5
|
+
what was said.
|
|
6
|
+
|
|
7
|
+
## TutorContextService.compose
|
|
8
|
+
|
|
9
|
+
`compose({ ...LearnerRequest, audience: 'learner' | 'guardian', enrollmentId?,
|
|
10
|
+
lessonId?, questionRevisionId? })` extends `QuestionResponseService`; every read uses
|
|
11
|
+
the service executor (the transaction's inside `withTransaction`). The host
|
|
12
|
+
authorizer is asked `education.tutor.read` with `own` and `relationshipTypeId`
|
|
13
|
+
as for `LearnerService`. A lesson needs its enrollment and must be in the
|
|
14
|
+
enrollment's effective set; a focus question must belong to that lesson
|
|
15
|
+
(otherwise `LearnerAuthorizationError`). The guardian audience is refused for
|
|
16
|
+
the learner's own actor whatever the host answers.
|
|
17
|
+
|
|
18
|
+
Result: learner (name, level), enrollment (status, pace; no notes), course,
|
|
19
|
+
section and lesson titles, learner-facing lesson body (withheld for
|
|
20
|
+
personal-use-copy), objectives, own progress status, the learner-visible
|
|
21
|
+
question rendering (prompt, response type, choices, subpart prompts, points; no
|
|
22
|
+
media or anchors), submission count with latest status only, and the newest
|
|
23
|
+
**released** feedback text (guardian: newest guardian-confirmed revision), plus
|
|
24
|
+
`answerKeyAvailable`, `truncated` and `contextHash`.
|
|
25
|
+
|
|
26
|
+
Learner audience never carries answer keys, rubrics, key assets or their ids,
|
|
27
|
+
storage URIs, enrollment parent notes, grades, other learners or grading
|
|
28
|
+
internals: it is built from an allowlist and keys are read only to set the
|
|
29
|
+
boolean. The guardian audience adds capped answer-key and rubric text, still no
|
|
30
|
+
storage locators or notes. Do not add fields without a sentinel test in
|
|
31
|
+
`tutor-context.test.ts`.
|
|
32
|
+
|
|
33
|
+
Bounds (exported constants): body 8000, prompt/choice/subpart 2000, key 2000,
|
|
34
|
+
feedback 2000, titles 300, 30 questions (the focus question is kept), 12 choices
|
|
35
|
+
and subparts, 10 objectives, 48000 serialized characters overall (trailing
|
|
36
|
+
questions drop first). Any cut sets `truncated`.
|
|
37
|
+
|
|
38
|
+
## TutorSessionService
|
|
39
|
+
|
|
40
|
+
Append-only `TutorSession`, `TutorSessionClosure` and `TutorExchange` (new
|
|
41
|
+
tables `tutor_sessions`, `tutor_session_closures`, `tutor_exchanges`;
|
|
42
|
+
tenant-scoped, closed generated surfaces, update/delete rejected). A session is
|
|
43
|
+
open until its single closure row exists. Sessions store ids, audience, mode,
|
|
44
|
+
actor, voice consent and the context digest, never context text.
|
|
45
|
+
|
|
46
|
+
- `open` (`education.tutor.exchange`): `requestKey` idempotent per learner; a
|
|
47
|
+
replay with a changed payload is `TutorRequestKeyConflictError`. Voice needs
|
|
48
|
+
`voiceConsent: true` the host recorded (`TutorConsentRequiredError`). Guardian
|
|
49
|
+
audience is refused for the learner's own actor.
|
|
50
|
+
- `append` (`education.tutor.exchange`): role `learner|tutor|system`; `learner`
|
|
51
|
+
only from the learner themself. Text 1 to 4000 characters else
|
|
52
|
+
`TutorExchangeError`. Idempotent per session `requestKey`; sequence is
|
|
53
|
+
server-assigned from 1; closed session is `TutorSessionClosedError`. Optional
|
|
54
|
+
`marker` (`none|filtered|redacted|flagged`), `toolSummary`, token counts.
|
|
55
|
+
- `close`: appends the closure; closing twice returns `changed: false`.
|
|
56
|
+
- `listForLearner` (`education.tutor.read`): sessions newest first, `limit`
|
|
57
|
+
(<=50) and `cursor`, per-session exchanges bounded by `exchangeLimit` (<=200)
|
|
58
|
+
with `exchangesTruncated`; `sessionId` + `afterSequence` page one transcript.
|
|
59
|
+
A guardian reads through the host's decision on the relationship.
|
|
60
|
+
|
|
61
|
+
Missing tables raise `LearnerHistoryUnavailableError`. Concurrent appends rely on
|
|
62
|
+
the unique `(tenant, session, sequence)` index.
|
package/dist/catalog.d.ts
CHANGED
|
@@ -29,7 +29,19 @@ export declare class Course extends SmrtObject {
|
|
|
29
29
|
}
|
|
30
30
|
export declare class ExternalCourse extends Course {
|
|
31
31
|
}
|
|
32
|
+
/** Who an authored course is meant for: its family, its family's groups, a public listing label. */
|
|
33
|
+
export declare const COURSE_VISIBILITIES: readonly ["family", "groups", "public"];
|
|
34
|
+
export type CourseVisibility = (typeof COURSE_VISIBILITIES)[number];
|
|
32
35
|
export declare class AuthoredCourse extends Course {
|
|
36
|
+
/**
|
|
37
|
+
* Authored-course visibility; '' means never set (reads as 'family'). Only the
|
|
38
|
+
* course-authoring service writes it, with an append-only change row.
|
|
39
|
+
*/
|
|
40
|
+
visibility: CourseVisibility | '';
|
|
41
|
+
/** Education-owned image asset shown as the course cover; '' means none. */
|
|
42
|
+
coverAssetId: string;
|
|
43
|
+
constructor(options?: Record<string, unknown>);
|
|
44
|
+
save(options?: SmrtSaveOptions): Promise<this>;
|
|
33
45
|
}
|
|
34
46
|
export declare class CourseSection extends SmrtObject {
|
|
35
47
|
tenantId: string | null;
|
package/dist/catalog.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,UAAU,EACV,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAGlC,MAAM,MAAM,iBAAiB,GACzB,UAAU,GACV,cAAc,GACd,mBAAmB,GACnB,sBAAsB,CAAC;AAC3B,MAAM,MAAM,cAAc,GACtB,QAAQ,GACR,WAAW,GACX,KAAK,GACL,OAAO,GACP,MAAM,GACN,OAAO,CAAC;AACZ,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC;AAC9D,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAwB9E,qBAEa,MAAO,SAAQ,UAAU;IACN,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAClC,KAAK,SAAM;IACtC,OAAO,SAAM;IACb,WAAW,SAAM;IACjB,SAAS,SAAY;IACrB,SAAS,SAAM;IACf,SAAS,SAAM;IACf,OAAO,SAAM;IACb,WAAW,EAAE,iBAAiB,CAAc;IAC5C,WAAW,SAAM;IACjB,UAAU,SAAM;IAChB,OAAO,SAAM;IACb,IAAI,SAAM;IACV,QAAQ,SAAK;IACb,QAAQ,SAAK;IACb,UAAU,SAAO;IACjB,gBAAgB,SAAK;IACrB,QAAQ,EAAE,cAAc,CAAY;IACpC,MAAM,EAAE,YAAY,CAAW;gBACnB,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;IAKjD,yEAAyE;IAC1D,IAAI,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;CAyC9D;AAED,qBACa,cAAe,SAAQ,MAAM;CAAG;AAE7C,qBACa,cAAe,SAAQ,MAAM;
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,UAAU,EACV,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAGlC,MAAM,MAAM,iBAAiB,GACzB,UAAU,GACV,cAAc,GACd,mBAAmB,GACnB,sBAAsB,CAAC;AAC3B,MAAM,MAAM,cAAc,GACtB,QAAQ,GACR,WAAW,GACX,KAAK,GACL,OAAO,GACP,MAAM,GACN,OAAO,CAAC;AACZ,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC;AAC9D,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAwB9E,qBAEa,MAAO,SAAQ,UAAU;IACN,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAClC,KAAK,SAAM;IACtC,OAAO,SAAM;IACb,WAAW,SAAM;IACjB,SAAS,SAAY;IACrB,SAAS,SAAM;IACf,SAAS,SAAM;IACf,OAAO,SAAM;IACb,WAAW,EAAE,iBAAiB,CAAc;IAC5C,WAAW,SAAM;IACjB,UAAU,SAAM;IAChB,OAAO,SAAM;IACb,IAAI,SAAM;IACV,QAAQ,SAAK;IACb,QAAQ,SAAK;IACb,UAAU,SAAO;IACjB,gBAAgB,SAAK;IACrB,QAAQ,EAAE,cAAc,CAAY;IACpC,MAAM,EAAE,YAAY,CAAW;gBACnB,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;IAKjD,yEAAyE;IAC1D,IAAI,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;CAyC9D;AAED,qBACa,cAAe,SAAQ,MAAM;CAAG;AAE7C,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,yCAA0C,CAAC;AAC3E,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE,qBACa,cAAe,SAAQ,MAAM;IACxC;;;OAGG;IACH,UAAU,EAAE,gBAAgB,GAAG,EAAE,CAAM;IACvC,4EAA4E;IAC5E,YAAY,SAAM;gBACN,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;IAIlC,IAAI,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;CAQ9D;AAED,qBAEa,aAAc,SAAQ,UAAU;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IACnB,QAAQ,SAAM;IAC7B,KAAK,SAAM;IACtC,SAAS,SAAK;gBACF,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;IAIlC,IAAI,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;CAmB9D;AAED,qBAEa,MAAO,SAAQ,UAAU;IACN,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IACZ,SAAS,SAAM;IACrC,KAAK,SAAM;IACtC,IAAI,SAAM;IACV,SAAS,SAAK;IACd,gBAAgB,SAAK;IACrB,UAAU,SAAO;IACjB,QAAQ,EAAE,cAAc,CAAY;IACpC,cAAc,EAAE,oBAAoB,CAAY;IAChD,WAAW,SAAM;IACjB,WAAW,SAAM;gBACL,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;IAIlC,IAAI,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;CAmB9D;AAED,qBACa,OAAQ,SAAQ,MAAM;CAAG;AACtC,qBACa,KAAM,SAAQ,MAAM;IAC/B,QAAQ,SAAM;gBACF,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;CAIlD;AACD,qBACa,SAAU,SAAQ,MAAM;IACnC,YAAY,SAAM;IAClB,SAAS,SAAM;IACf,KAAK,SAAK;IACD,cAAc,EAAE,oBAAoB,CAAY;gBAC7C,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;CAIlD;AACD,qBACa,QAAS,SAAQ,MAAM;CAAG"}
|