@happyvertical/smrt-education 0.57.46 → 0.57.47

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.
Files changed (41) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +7 -1
  3. package/agents/learner-service.md +31 -2
  4. package/agents/question-extraction.md +108 -0
  5. package/agents/submission-service.md +15 -0
  6. package/dist/chunks/{workbook-photo-service-DKgoUHeb.js → workbook-photo-service-B7Tujv__.js} +6494 -1153
  7. package/dist/chunks/workbook-photo-service-B7Tujv__.js.map +1 -0
  8. package/dist/collections.d.ts +4 -1
  9. package/dist/collections.d.ts.map +1 -1
  10. package/dist/course-authoring-contracts.d.ts +1 -1
  11. package/dist/course-authoring-contracts.d.ts.map +1 -1
  12. package/dist/index.d.ts +11 -5
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1049 -4628
  15. package/dist/index.js.map +1 -1
  16. package/dist/ingestion.d.ts +2 -0
  17. package/dist/ingestion.d.ts.map +1 -1
  18. package/dist/ingestion.js +197 -2
  19. package/dist/ingestion.js.map +1 -1
  20. package/dist/learner-history.d.ts +50 -0
  21. package/dist/learner-history.d.ts.map +1 -0
  22. package/dist/learner-service.d.ts +78 -2
  23. package/dist/learner-service.d.ts.map +1 -1
  24. package/dist/manifest.json +4255 -1444
  25. package/dist/question-extraction-contracts.d.ts +231 -0
  26. package/dist/question-extraction-contracts.d.ts.map +1 -0
  27. package/dist/question-extraction-ingestion.d.ts +44 -0
  28. package/dist/question-extraction-ingestion.d.ts.map +1 -0
  29. package/dist/question-extraction-models.d.ts +56 -0
  30. package/dist/question-extraction-models.d.ts.map +1 -0
  31. package/dist/question-extraction-service.d.ts +71 -0
  32. package/dist/question-extraction-service.d.ts.map +1 -0
  33. package/dist/question-extraction.d.ts +317 -0
  34. package/dist/question-extraction.d.ts.map +1 -0
  35. package/dist/smrt-knowledge.json +961 -143
  36. package/dist/submission-service.d.ts +34 -1
  37. package/dist/submission-service.d.ts.map +1 -1
  38. package/dist/submissions.d.ts +25 -1
  39. package/dist/submissions.d.ts.map +1 -1
  40. package/package.json +10 -10
  41. package/dist/chunks/workbook-photo-service-DKgoUHeb.js.map +0 -1
package/AGENTS.md CHANGED
@@ -47,6 +47,11 @@ render ids and exact-revision references use [QuestionAuthoringService](agents/q
47
47
  Keep keys out of learner bundles; a missing key stays null. `attachGeneratedPrintable`
48
48
  stores the rendered PDFs; the guardian copy is always an `answer-key`, never `printable`.
49
49
 
50
+ Structured questions from scans and imported course files use
51
+ [QuestionExtractionService](agents/question-extraction.md) and the `extract-questions`
52
+ ingestion handler. Anchors come from provider region catalogs only; match by worksheet
53
+ id and layout, never text; ambiguous matches stay unattached; source-keyed replays.
54
+
50
55
  Source import: [host boundary and transaction contract](agents/source-import.md).
51
56
  `SourceImportService` requires host opt-in, matching active actor/tenant and
52
57
  explicit licensing authorization; never add a system/global write bypass.
package/README.md CHANGED
@@ -98,6 +98,10 @@ can `getLearner`, `startLesson`, and `listProgress` for their own work.
98
98
  Guardian access requires a same-tenant directed `ProfileRelationship` plus the
99
99
  host's permission decision. Sibling membership does not grant access.
100
100
 
101
+ `putBackLesson` returns started work to `not_started` and `setEnrollmentStatus`
102
+ starts, pauses, resumes or drops an enrollment along a fixed transition table;
103
+ both append immutable history (actor, and for status the host policy basis)
104
+ and never touch submissions, grades or other progress.
101
105
  Repeated starts preserve the existing progress, including attempts and scores.
102
106
  A new curriculum/year creates a distinct enrollment and fresh progress history.
103
107
  An unscored lesson has `bestScore: null`, distinct from zero. The service does
@@ -114,7 +118,9 @@ integration on both SQLite and PostgreSQL.
114
118
 
115
119
  `SubmissionService` captures tenant-owned uploads, reviews or attests work with
116
120
  optional guardian scores, signs authorized asset claims, and splits private
117
- PDF packets by page range. It reuses learner ownership and transaction checks.
121
+ PDF packets by page range. `attestOwnLesson` lets a learner mark their own
122
+ Reading or Video lesson done as a self-reported, ungraded attestation that a
123
+ guardian then confirms. It reuses learner ownership and transaction checks.
118
124
  See the [submission integration contract](agents/submission-service.md) for
119
125
  inputs, retry rules, signing requirements and external-storage compensation.
120
126
 
@@ -71,7 +71,36 @@ Same-tenant membership alone never grants sibling access.
71
71
  - `startLesson` allows the learner or authorized guardian to create their own
72
72
  enrollment's lesson progress in `in_progress`. It derives learner identity
73
73
  from the validated enrollment. Repeated starts return existing state without
74
- resetting counters, scores, completion, or timestamps.
74
+ resetting counters, scores, completion, or timestamps. A `not_started` row
75
+ (a put-back lesson) moves to `in_progress` again with its counters, score and
76
+ first `startedAt` kept.
77
+ - `putBackLesson({ enrollmentId, lessonId })` takes `in_progress` work back to
78
+ `not_started` for the learner or an authorized guardian under
79
+ `education.progress.put-back` (the host decides; own is not blocked). The
80
+ progress row keeps its one (enrollment, lesson) identity, so each change
81
+ appends an immutable `LessonProgressTransition` (from, to, kind, actor,
82
+ time) in the same transaction; restarting appends a `start` row when the
83
+ table exists. Counters, score, submissions, grades, workbook and evidence
84
+ rows are never touched. It returns `{ changed, progress }`: no progress or
85
+ already `not_started` is an idempotent no-op. `submitted`, `needs_review`,
86
+ `complete`, `skipped` (stored or workbook-projected) refuse with
87
+ `LessonPutBackError` (`.status`); completion is undone only through guardian
88
+ paths. A host without `lesson_progress_transitions` gets
89
+ `LearnerHistoryUnavailableError` from put-back; other operations still run.
90
+ - `setEnrollmentStatus({ enrollmentId, status, policyBasis })` moves an
91
+ enrollment along `ENROLLMENT_STATUS_TRANSITIONS`: planned→active,
92
+ active→paused|dropped, paused→active|dropped. `completed` is reached only by
93
+ the completion path; completed and dropped are final. The authorizer gets
94
+ `education.enrollment.set-status` and decides whether a learner may act on
95
+ their own enrollment. `policyBasis` (1-200 characters, no control characters,
96
+ `requirePolicyBasis`) names the host setting relied on; it and the actor are
97
+ appended to an immutable `EnrollmentStatusChange` with the change, as
98
+ `pinEnrollment` records its basis. The first activation sets `startedAt`.
99
+ The current status is an idempotent `{ changed: false }`; any other move
100
+ throws `EnrollmentStatusTransitionError` (`.from`, `.to`). Progress,
101
+ submissions and history never cascade. Bad input throws
102
+ `LearnerValidationError`; a host without `enrollment_status_changes` gets
103
+ `LearnerHistoryUnavailableError`.
75
104
  - `listProgress` reads only the validated learner/enrollment and lessons still
76
105
  visible in that enrollment's course. Inconsistent persisted links fail
77
106
  closed or are omitted from the list.
@@ -91,7 +120,7 @@ Same-tenant membership alone never grants sibling access.
91
120
  Service results are data-only views, with no persistence or database methods.
92
121
  Extra request fields are never model patches: a learner cannot set score,
93
122
  attempt count, or completion status through this API. Curriculum activation,
94
- completion transitions and dashboard aggregation are not supported by this
123
+ enrollment completion and dashboard aggregation are not supported by this
95
124
  base service. SubmissionService extends its owning transaction/auth boundary for
96
125
  capture and guardian grading; see [submission contract](submission-service.md).
97
126
  Direct model writes require trusted authority.
@@ -0,0 +1,108 @@
1
+ # Question extraction from scans and imported courses
2
+
3
+ `QuestionExtractionService` (SMRT #4517, extends `QuestionAuthoringService`,
4
+ consumed by Hearth #80 and #94) turns ingestion proposals into structured,
5
+ versioned question records. Risk is high: new schema, a public contract and
6
+ idempotent writes. It reuses the course authorizer: preserve and apply need
7
+ `education.course.author`; `readExtraction`, `findExtraction` and
8
+ `resolveWorksheet` need `education.course.read-draft`. Operations:
9
+ `preserve-question-extraction`, `apply-question-extraction`,
10
+ `read-question-extraction`, `read-worksheet`.
11
+
12
+ ## Flow
13
+
14
+ 1. Receive the scan or course file through ingestion and extract it (OCR or
15
+ vision, with `observeAnswerRegions` for image region catalogs).
16
+ 2. Offer `educationQuestionExtractionHandler({ extractions, request })` (from
17
+ `@happyvertical/smrt-education/ingestion`) in the execution catalog and
18
+ generate proposals. Its closed `QUESTION_EXTRACTION_ARGS_SCHEMA` is the
19
+ extracted question set. Keep `requireReview: true` on every policy layer.
20
+ Size the policy `maxBytes` and proposal output limits for whole worksheets:
21
+ up to 200 questions, 400 student-work entries and 1000 pages.
22
+ 3. `preserveQuestionExtractionAnalysis({ ingestion, extractions, request,
23
+ itemId, expectedWorksheetVersionId?, worksheets? })` reads the completed
24
+ proposal, validates it against retained evidence, matches it and records a
25
+ `QuestionExtractionRecord`. It returns `QuestionExtractionView` for
26
+ extraction review. Not ready, no proposal or competing proposals throw
27
+ `QuestionExtractionPendingError` (`reason`); invalid output throws
28
+ `QuestionExtractionValidationError`. No question records are written.
29
+ 4. The reviewer previews and decides through ingestion. The handler apply
30
+ calls `applyExtraction` on the ingestion transaction and returns the
31
+ per-question outcomes (`ExtractedQuestionResult[]`, also on `view.applied`).
32
+
33
+ Hosts without an ingestion proposal (for example a trusted bundle importer)
34
+ call `preserveExtraction` directly with their own grounding and extractor
35
+ identity, then `applyExtraction`.
36
+
37
+ ## Extraction contract
38
+
39
+ - Every retained source, and each page the provider reported, has a `pages`
40
+ entry with a role (`exercise`, `example`, `instructions`, `cover`,
41
+ `answer_key`, `mixed`, `unknown`) and readability. A missing page is invalid,
42
+ so unreadable or unsupported content is marked, never dropped. Items that
43
+ cannot be read keep their key and numbering with `status` `unreadable` or
44
+ `unsupported` and a null prompt; other gaps go in `omissions`.
45
+ - What ingestion itself did not extract stays visible. A retained original with
46
+ no result, and pages that ingestion omitted or failed, are kept in the
47
+ grounding: the extractor may cite them, and any it does not cover are added as
48
+ `unreadable` pages with a `not_extracted` omission. Truncation and
49
+ `source_incomplete` warnings are carried to `extraction.sourceWarnings` and
50
+ the handler preview. More than 1000 pages is refused, never trimmed.
51
+ - Questions hold printed text only. Learner handwriting goes in `studentWork`
52
+ (null `observed` when illegible) and never becomes question content.
53
+ - Anchors cite an original `evidenceId`, a physical page the provider
54
+ reported (null when unknown) and a `regionId` from that evidence's
55
+ `observedRegions` catalog. The rect comes from the catalog only; an unknown
56
+ region is invalid. Coordinates are never accepted from the extractor.
57
+ - `origin: 'source' | 'inferred'` marks response type, keys, rubrics and points.
58
+ On apply, printed facts get `extracted` provenance and inferred facts get
59
+ `inferred`. An empty key is invalid; a missing key stays null.
60
+ - Diagram and media references are kept on the link with their anchor. They
61
+ are not question `media`, which must name a tenant asset.
62
+
63
+ ## Matching
64
+
65
+ `matchExtractedQuestions(extraction, worksheets, { expectedWorksheetVersionId })`
66
+ is pure. A match needs a worksheet identity (the `wsv1-` id generated printables
67
+ print in their footer, or a host-supplied expectation), plus the printed
68
+ position (`1`, `1.`, `Q1`) of an item on that worksheet. Every claimant of a
69
+ position counts, including one that fails a layout check. Section label, a
70
+ source-printed response type, and choice and subpart counts must agree. Prompt
71
+ text is never used. A printed id that disagrees with the expectation, any layout
72
+ conflict, or two items claiming one position is `ambiguous` and stays
73
+ unattached. Examples are never matched. `resolveWorksheet` finds a worksheet by
74
+ id through `generated_printables`; host-supplied worksheets must be this
75
+ tenant's revisions. Student work gets a `questionRevisionId` only when its
76
+ question matched. Recording attempts and grades stays with the workbook and
77
+ response services.
78
+
79
+ ## Idempotency
80
+
81
+ `sourceIdentity` is a digest of the source kind and the sorted retained evidence
82
+ hashes, never text. Preserve is idempotent per (tenant, intake item, analysis
83
+ attempt). A different payload for the same attempt throws
84
+ `CourseRequestKeyConflictError`. The payload includes the grounding, the
85
+ extractor identity and the expected and supplied worksheets. Apply writes questions through `createQuestion`
86
+ with request key `qx:<sourceIdentity>:<questionKey>`, so re-ingesting the same
87
+ bytes replays the same questions (`replayed`). Identical prompts under different
88
+ keys stay separate records. If the same source is extracted again with different
89
+ content, the outcome is `source_changed`: nothing is overwritten. A new scan has
90
+ new bytes, so it is a new source, and it links to known questions only by
91
+ matching. A repeated apply replays the stored `ExtractedQuestionLink` rows.
92
+
93
+ Outcomes: `created`, `replayed`, `matched` (these carry a revision),
94
+ `needs_review` (ambiguous), `source_changed`, `invalid_content` (fails question
95
+ validation), `unreadable`, `unsupported`, `not_an_exercise`.
96
+
97
+ ## Models (closed, required tenant, no REST/CLI/MCP/AI, append-only)
98
+
99
+ `QuestionExtractionRecord` (`question_extractions`: unique tenant/item/attempt,
100
+ indexed source identity, extractor identity, validated extraction and match JSON,
101
+ proposal digest) and `ExtractedQuestionLink` (`extracted_question_links`: unique
102
+ tenant/extraction/key; outcome, question and revision ids, anchors and media).
103
+ Both ship through normal manifest migrations. The originals stay in ingestion
104
+ and are never copied. Imported material keeps its course licence; extraction
105
+ does not change sharing.
106
+
107
+ Not in this release: zip/bundle manifests (SMRT #4578), automatic new
108
+ revisions on `source_changed`, and media assets for extracted diagrams.
@@ -51,6 +51,21 @@ completion decision; grading alone leaves it in progress, including through a
51
51
  plain SubmissionService instance. Existing explicit completion on the current
52
52
  attempt is retained. The owning workbook join determines this policy, never a
53
53
  caller flag.
54
+ `attestOwnLesson({ enrollmentId, lessonId, policyBasis, notes? })` is the
55
+ learner's own "I read it" / "I watched it" for `SELF_ATTESTABLE_LESSON_KINDS`
56
+ (Reading, Video) under `education.learner.attest-own`. Any other actor is
57
+ refused before the host authorizer; the host decides from its own setting
58
+ (passed as `policyBasis`, 1-200 characters) whether the learner may. It writes
59
+ an ungraded `Attestation` and an insert-only `LessonSelfAttestation` (learner
60
+ actor, basis), moves progress to `submitted` with one attempt, and creates no
61
+ Grade: it is not a grade and never completes the lesson. A repeat returns the
62
+ same attestation. Other kinds throw `OwnAttestationLessonKindError`;
63
+ submitted, review, complete, skipped or already-attested progress throws
64
+ `OwnAttestationConflictError`. Submission views carry `selfReported: true` for
65
+ it; a guardian confirms with `attestLesson` (which grades that attestation) or
66
+ `reviewSubmission`, and the guardian grade then distinguishes confirmed from
67
+ self-reported evidence. A host without `lesson_self_attestations` gets
68
+ `LearnerHistoryUnavailableError`.
54
69
  Omitted/null score is unscored; zero is a real score. The latest guardian grade
55
70
  wins over auto/AI/instructor proposals, and its score replaces progress's score.
56
71
  Review history is retained. ExternalCompletion is available for trusted server