@happyvertical/smrt-education 0.57.33 → 0.57.34

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 (55) hide show
  1. package/AGENTS.md +8 -1
  2. package/README.md +117 -13
  3. package/agents/catalog-sync.md +46 -0
  4. package/agents/photo-intake.md +249 -0
  5. package/agents/workbook-service.md +355 -0
  6. package/agents/workbook-test-matrix.md +74 -0
  7. package/dist/catalog-acquisition-service.d.ts.map +1 -1
  8. package/dist/catalog-capability.d.ts +21 -0
  9. package/dist/catalog-capability.d.ts.map +1 -0
  10. package/dist/catalog-contracts.d.ts +40 -0
  11. package/dist/catalog-contracts.d.ts.map +1 -1
  12. package/dist/catalog-database-sync-store.d.ts.map +1 -1
  13. package/dist/catalog-models.d.ts +5 -1
  14. package/dist/catalog-models.d.ts.map +1 -1
  15. package/dist/catalog-read-service.d.ts +2 -1
  16. package/dist/catalog-read-service.d.ts.map +1 -1
  17. package/dist/catalog-service.d.ts +5 -0
  18. package/dist/catalog-service.d.ts.map +1 -1
  19. package/dist/chunks/workbook-photo-service-1wcnf-Xf.js +27359 -0
  20. package/dist/chunks/workbook-photo-service-1wcnf-Xf.js.map +1 -0
  21. package/dist/index.d.ts +11 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +339 -24864
  24. package/dist/index.js.map +1 -1
  25. package/dist/ingestion.d.ts +37 -0
  26. package/dist/ingestion.d.ts.map +1 -1
  27. package/dist/ingestion.js +829 -20
  28. package/dist/ingestion.js.map +1 -1
  29. package/dist/learner-service.d.ts +15 -0
  30. package/dist/learner-service.d.ts.map +1 -1
  31. package/dist/manifest.json +4735 -6
  32. package/dist/smrt-knowledge.json +1760 -26
  33. package/dist/sources/public-illustrative-mathematics-adapter.d.ts.map +1 -1
  34. package/dist/sources/public-illustrative-mathematics-parser.d.ts.map +1 -1
  35. package/dist/submission-service.d.ts +38 -11
  36. package/dist/submission-service.d.ts.map +1 -1
  37. package/dist/workbook-answer-regions.d.ts +11 -0
  38. package/dist/workbook-answer-regions.d.ts.map +1 -0
  39. package/dist/workbook-contracts.d.ts +224 -0
  40. package/dist/workbook-contracts.d.ts.map +1 -0
  41. package/dist/workbook-layout.d.ts +13 -0
  42. package/dist/workbook-layout.d.ts.map +1 -0
  43. package/dist/workbook-models.d.ts +129 -0
  44. package/dist/workbook-models.d.ts.map +1 -0
  45. package/dist/workbook-photo-contracts.d.ts +56 -0
  46. package/dist/workbook-photo-contracts.d.ts.map +1 -0
  47. package/dist/workbook-photo-ingestion.d.ts +70 -0
  48. package/dist/workbook-photo-ingestion.d.ts.map +1 -0
  49. package/dist/workbook-photo-models.d.ts +63 -0
  50. package/dist/workbook-photo-models.d.ts.map +1 -0
  51. package/dist/workbook-photo-service.d.ts +72 -0
  52. package/dist/workbook-photo-service.d.ts.map +1 -0
  53. package/dist/workbook-service.d.ts +158 -0
  54. package/dist/workbook-service.d.ts.map +1 -0
  55. package/package.json +8 -8
package/AGENTS.md CHANGED
@@ -3,7 +3,8 @@
3
3
  Catalog objects are optional-tenant scoped and have no generated REST, CLI, MCP,
4
4
  or AI surface. Applications use `CatalogService`, supplying their actor-bound
5
5
  authorization decision and the trusted `TenantCollection.withTenantCatalogReadScope`
6
- adapter from smrt-users. That read scope may widen only an allowlisted catalog
6
+ adapter from smrt-users. Catalog authorization receives the exact shared collection
7
+ executor; live session/permission checks must use that executor. That read scope may widen only an allowlisted catalog
7
8
  collection's `list` to self, ancestors, and globals; it never authorizes a get,
8
9
  write, raw query, sibling, or descendant read.
9
10
 
@@ -27,6 +28,12 @@ files after rollback; never expose source URIs or accept learner grade patches.
27
28
  Validation: `pnpm --filter @happyvertical/smrt-education test`,
28
29
  `test:postgres`, `typecheck`, and `build`.
29
30
 
31
+ Physical workbook capture, immutable assessment/feedback history and descriptive
32
+ evidence use [WorkbookService](agents/workbook-service.md). Keep provisional
33
+ sharing separate from guardian Grade authority. Photo-first receipt/context
34
+ capture uses [WorkbookPhotoService](agents/photo-intake.md); covers/references
35
+ never create learning evidence, and blank exercises need explicit work intent.
36
+
30
37
  Source import: [host boundary and transaction contract](agents/source-import.md).
31
38
  `SourceImportService` requires host opt-in, matching active actor/tenant and
32
39
  explicit licensing authorization; never add a system/global write bypass.
package/README.md CHANGED
@@ -146,20 +146,41 @@ regressions, dialect coverage, and the existing DuckDB setup limitations.
146
146
  `CatalogSyncAdapter` and `CatalogAcquireAdapter`; `fetch` is optional and defaults
147
147
  to the standard global Fetch API. Use the same factory for discovery and explicit
148
148
  private acquisition through `CatalogAcquisitionService`. No credential, mounted
149
- provider module, schema extension, or PDF fetch is needed for discovery.
150
-
151
- The declared scope is public PDFs with explicit student/family labels on the
152
- first-edition Kendall Hunt K–5, 6–8, and high-school teacher/family hierarchy,
153
- including Algebra 1 Supports and linked Spanish counterparts. Traversal starts
154
- at the official homepage and fixed curriculum roots; it follows direct course,
155
- unit, resources, family-materials, and lesson-preparation links. Teacher guides,
156
- answers, assessments, presentations, login-only links, unknown labels, HTML-only
157
- student lessons, non-PDF formats, and AccessIM/v.360 are excluded. Completion
158
- means this declared scope only. Policy omissions appear in the bounded, invocation-local
159
- `CatalogSyncReport.exclusionSummary`, rather than the durable failure ledger.
149
+ provider module or PDF fetch is needed for discovery. Apply the additive catalog
150
+ representation column migration before using this version.
151
+
152
+ The declared metadata scope is public artifact links on the fixed official CMS
153
+ host, with explicit student/family labels in the first-edition Kendall Hunt
154
+ K–5, 6–8, and high-school teacher/family hierarchy, including Algebra 1 Supports
155
+ and linked Spanish counterparts. Traversal starts at the official homepage and
156
+ fixed curriculum roots; it follows direct course, unit, resources,
157
+ family-materials, and lesson-preparation links. An icon or file-name label never
158
+ establishes the observed format or grants role/rights. Teacher guides, answers,
159
+ assessments, teacher presentations, login-only links, unknown roles, HTML-only
160
+ student lessons, external-host artifacts and AccessIM/v.360 remain outside this
161
+ metadata scope. External links are not followed or HEADed. Known PDF, ZIP and
162
+ DOCX MIME types are retained. Otherwise eligible artifacts with an absent or
163
+ unrecognized Content-Type remain explicit unknown metadata with acquisition
164
+ denied; malformed MIME or invalid validators pause enumeration rather than
165
+ silently omitting an artifact. Only PDF bytes can be acquired.
166
+ Completion means this declared scope only. Policy omissions appear in the
167
+ bounded, invocation-local `CatalogSyncReport.exclusionSummary`, rather than the
168
+ durable failure ledger.
160
169
 
161
170
  Each `enumerate` reads one HTML page (2 MB maximum) and at most four asset HEADs.
162
- It never GETs PDF content. Version-1 JSON cursors contain the remaining validated
171
+ It never GETs asset content. Known PDF/ZIP/DOCX HEAD metadata is retained even when
172
+ the file exceeds the PDF acquisition cap. Missing/weak validators, malformed MIME
173
+ or access/transport failures pause the page without advancing. Each observed record has
174
+ optional `representation` metadata with actual `format`, `mediaType`, positive safe
175
+ `byteLength`, strong ETag/Last-Modified `validators`, and `acquisition` capability.
176
+ A PDF is `supported` or `size-limit` with its observed `maxBytes`; ZIP and DOCX are always
177
+ `unsupported-format`. An unknown representation has `format: 'unknown'`,
178
+ `mediaType: null` for an absent header or the actual unrecognized canonical MIME,
179
+ `availableFormats: []`, and `acquisition.status: 'unknown-format'` with a
180
+ `missing-content-type` or `unrecognized-content-type` reason. Its observed size,
181
+ validators and legal evidence remain intact; the evaluator denies new acquisition.
182
+ Unknown v2 revisions encode MIME absence or the observed token explicitly.
183
+ The source icon does not override the actual MIME. Version-1 JSON cursors contain the remaining validated
163
184
  hierarchy paths and an artifact offset plus source fingerprint when needed;
164
185
  persist the returned cursor unchanged. A fresh adapter can resume it. The cursor
165
186
  is capped at 64 KB and 384 pending paths; source pages are capped at 128 children
@@ -181,7 +202,8 @@ the adapter hashes the bytes with SHA-256. The generic service retains attributi
181
202
  and image/trademark notices and provides tenant, selection and idempotence checks.
182
203
 
183
204
  `timeoutMs` bounds the entire call, including streaming (default 20 seconds,
184
- maximum 60 seconds). `maxPdfBytes` defaults to 32 MiB (maximum 64 MiB). Requests
205
+ maximum 60 seconds). `maxPdfBytes` bounds acquisition only and defaults to 32 MiB
206
+ (maximum 64 MiB). Changing it does not change an existing PDF content revision. Requests
185
207
  use fixed official hosts and strict paths, reject redirects, and carry no source
186
208
  credentials. Rate limiting returns a resumable pause with Retry-After information;
187
209
  hosts must honor that delay before resuming rather than loop immediately.
@@ -225,3 +247,85 @@ history without guessed reclassification. An old coarse six-grade checkpoint
225
247
  with no cursor starts the new adapter at its root, preserves its imported count
226
248
  and history, and may complete/withdraw only at the new traversal's actual terminal
227
249
  page. History alone does not create an active pause.
250
+
251
+
252
+ Before creating a new selection or writing files, hosts call
253
+ `evaluateCatalogAcquisitionCapability(resource, format, { maxBytes })` on the
254
+ fresh authoritative record inside their transaction, separately from legal
255
+ `CatalogPolicy` authorization. The optional current limit can tighten the
256
+ persisted limit, never loosen it. Denials distinguish unknown format, unsupported
257
+ format, size limit, unavailable format and invalid representation metadata. It does not
258
+ control printing already pinned bytes. `CatalogAcquisitionService` repeats the
259
+ capability guard for new fetches after its existing pinned-acquisition return;
260
+ current actor, selection, revision, available-format and legal checks still precede that return.
261
+ The adapter independently revalidates format, size, validators and content bytes.
262
+
263
+ `normalizeCatalogRepresentation(unknown)` parses persisted JSON and returns a
264
+ validated defensive copy. Missing, null and serialized JSON null mean legacy
265
+ absence; the evaluator reports `verification: 'legacy'` without inventing measured
266
+ support, and adapters still enforce their strict acquisition checks. Malformed
267
+ present metadata throws `CatalogRepresentationError`; the evaluator explicitly
268
+ denies `invalid-representation-metadata`. Never catch this error and replace it
269
+ with absence in a read projection. `CatalogResource.getRepresentation()` and
270
+ `setRepresentation()` use the same guard. `CatalogDatabaseSyncStore` persists the
271
+ field and `CatalogReadService` preserves it in its metadata DTO. The nullable TEXT column migration leaves existing rows SQL NULL; new model/store
272
+ values use serialized JSON null. Both mean legacy absence; runtime code
273
+ does not create or alter schema.
274
+
275
+
276
+ ### Recovering an old partial inventory after a semantic scope change
277
+
278
+ Including previously omitted non-PDF links changes the canonical page semantics.
279
+ This causes a one-time catalog revision change even when the publisher's HTML
280
+ and PDF validators did not change. PDF v1 encoding remains unchanged, but its
281
+ page fingerprint can change. An old chunk cursor must pause on a fingerprint
282
+ mismatch; never reinterpret its artifact offset against the expanded inventory,
283
+ remove its fingerprint, or mark it complete. An old coarse checkpoint with no
284
+ cursor already starts root discovery naturally and needs no reset.
285
+
286
+ There is no ordinary UI or cross-process restart operation. The following is an
287
+ **operator-only procedure requiring proven quiescence**. Fence new catalog
288
+ requests, disable/drain or terminate every catalog producer, job and automated
289
+ continuation across **every pod/process**, and await all in-flight provider
290
+ operations/writers, including successful and failing callbacks. Keep new writers
291
+ fenced until the reset finishes. Browser Stop or a paused status alone is not
292
+ quiescence: a late success or failure can overwrite a reset or withdraw old-run
293
+ rows. The fresh run-ID check below does not provide concurrent CAS protection.
294
+
295
+ With trusted `db`, `providerId` and the operator's `expectedRunId` (explicit
296
+ `undefined` only for a legacy generation without one), use the existing public
297
+ store API after establishing that precondition:
298
+
299
+ ```ts
300
+ const store = new CatalogDatabaseSyncStore(db);
301
+ const current = await store.getCheckpoint(providerId);
302
+ if (!current || current.complete || !current.paused ||
303
+ current.runId !== expectedRunId) {
304
+ throw new Error('Expected paused catalog generation is no longer current.');
305
+ }
306
+ await store.saveCheckpoint({
307
+ ...current,
308
+ runId: globalThis.crypto.randomUUID(),
309
+ cursor: undefined,
310
+ importedCount: 0,
311
+ complete: false,
312
+ paused: false,
313
+ pauseReason: undefined,
314
+ failures: structuredClone(current.failures),
315
+ });
316
+ ```
317
+
318
+ Record the quiescence and reset in the operator's audit. This changes traversal
319
+ state only: retain all source rows, bounded failure history/counts,
320
+ `lastSuccessfulAt`, selections, acquisitions and private files. Do not delete
321
+ rows/bytes or claim completion. After writers are re-enabled, ordinary sync
322
+ starts at the current adapter's root under the fresh run ID; rows remain until
323
+ an actual successful terminal generation permits withdrawal. A subsequent real
324
+ source failure still pauses and cannot authorize withdrawal.
325
+
326
+ Old catalog selection/acquisition retries may be stale after the one-time
327
+ semantic revision change and still require current revision/rights checks.
328
+ Previously assigned/materialized private bytes are not changed or deleted by
329
+ catalog discovery; their private printing path is independent of later catalog
330
+ revisions. A new selection through the current adapter keeps its revision and
331
+ cached acquisition across a same-adapter refresh with unchanged source semantics.
@@ -55,3 +55,49 @@ pauses for unconfirmed coverage, without inventing an operational failure or
55
55
  withdrawing unseen rows. In Drive's catalog adapter, definite unsupported or
56
56
  out-of-boundary shortcut omissions are policy; missing revisions and inaccessible
57
57
  targets remain operational failures. The direct Drive discovery API is unchanged.
58
+
59
+
60
+ Observed representation metadata and legal permissions are separate. Use the
61
+ public `evaluateCatalogAcquisitionCapability` on a fresh record before creating
62
+ new selection/file state, then apply `CatalogPolicy` independently. The
63
+ `representation` serialized JSON field uses a nullable TEXT column (existing rows
64
+ remain SQL NULL; new model/store values use JSON null); apply its
65
+ additive manifest migration before runtime use. Missing/null remains legacy,
66
+ but malformed present metadata must never be erased into legacy absence:
67
+ `normalizeCatalogRepresentation` and the model getter throw a typed error and
68
+ the evaluator returns an explicit denial. Known ZIP/DOCX and over-cap PDF metadata
69
+ are retained; technical acquisition restrictions never remove legal evidence.
70
+ Current configured limits may tighten a recorded limit. Existing pinned-byte
71
+ retries retain their actor/selection/revision/legal checks and do not become
72
+ unavailable merely because future acquisition limits tightened.
73
+
74
+
75
+ The public IM adapter enumerates strictly hosted artifact links independent of
76
+ icons; only actual PDF/ZIP/DOCX MIME is recognized and only PDF is acquirable.
77
+ An eligible public HEAD with valid size and strong validators retains an explicit
78
+ `format: 'unknown'` representation when Content-Type is absent (`mediaType: null`,
79
+ `missing-content-type`) or syntactically valid but unrecognized (the bounded
80
+ canonical MIME token, `unrecognized-content-type`). Its acquisition status is
81
+ `unknown-format`, `availableFormats` is empty, and the public evaluator denies
82
+ before new selections, files or asset GETs. Legal evidence, size and validators
83
+ remain intact and metadata enumeration advances. Unknown MIME does not establish
84
+ technical support or change legal permission. Malformed present MIME and invalid
85
+ validators still pause without advancing. Unknown revisions encode MIME absence
86
+ or the observed token, so later recognition changes identity; ordinary PDF v1
87
+ revision encoding remains stable. Role/local ownership/malformed
88
+ markup checks apply to every format; external hosts remain outside coverage.
89
+ Expanded parsed inventory changes page fingerprints once: old offset cursors
90
+ must pause, never be reinterpreted against a different artifact list.
91
+
92
+ Recovery is quiescent **operator-only**, using existing public
93
+ `CatalogDatabaseSyncStore.getCheckpoint/saveCheckpoint`; see the concrete
94
+ [procedure](../README.md#recovering-an-old-partial-inventory-after-a-semantic-scope-change).
95
+ Fence/disable/drain every catalog producer, automatic continuation and job across
96
+ every pod/process and await all in-flight successes/failures before the fresh
97
+ paused/expected-run check and reset; browser Stop/paused status is insufficient.
98
+ Keep writers fenced through reset. Preserve rows, bounded ledger/counts,
99
+ last-success time and private bytes; use a fresh run UUID/no cursor/imported0,
100
+ incomplete/unpaused state. Only actual terminal success may later withdraw.
101
+ This is not an ordinary UI restart or multi-writer CAS guarantee. Old catalog
102
+ retries can be stale while already assigned private files remain unchanged;
103
+ new current-adapter pins remain stable on a same-adapter semantic refresh.
@@ -0,0 +1,249 @@
1
+ # Photo-first workbook intake
2
+
3
+ `WorkbookPhotoService` extends `WorkbookService` with the same constructor,
4
+ transaction executor, actor/tenant checks, private storage and rollback ledger.
5
+ Import server integration from `@happyvertical/smrt-education/ingestion`.
6
+ Apply manifest migrations for `WorkbookPhotoIntake`, `WorkbookPhotoAsset`,
7
+ `WorkbookPhotoDecision` and `WorkbookContextRevision` before deploying. There is
8
+ no runtime schema creation and these models expose no generated operations. Each
9
+ concrete table declares its own tenant/learner fields; a custom abstract model
10
+ base does not supply persisted columns to independent manifest tables.
11
+
12
+ ## Receipt before assignment
13
+
14
+ Construct the service and `IngestionService` from one authenticated server session.
15
+ Use `scope.confidentialScopeId = request.learnerId`, not an enrollment or model
16
+ inference. `authorizePhotoIntake(request)` authorizes a learner's own upload or a
17
+ currently authorized guardian before `receiveWorkbookPhotoIntake({ingestion,
18
+ scope,workbooks,request,receive})` calls ingestion. Its result is `{receipt,intake}`;
19
+ `intake:null` means no successful durable receipt. `retainWorkbookPhotoReceipt`
20
+ repairs an interrupted education copy from an existing authorized receipt.
21
+
22
+ The immutable education receipt retains the original bytes as private Assets
23
+ through the sole `WorkbookPhotoAsset` owner join, preserving ordered SHA-256
24
+ hashes. Copy retries reject changed bytes, filenames, media types or learner
25
+ ownership. Storage uses the inherited fresh-file compensation ledger; borrowed
26
+ host executors cannot perform file writes. Provider failure does not delete the
27
+ receipt or originals. `getWorkbookPhotoIntake`, `listWorkbookPhotoIntakes` and
28
+ `readWorkbookPhotoOriginal` reauthorize exact ownership independently of scratch
29
+ retention. A learner cannot list/read guardian-uploaded pending or context-only
30
+ material. Guardian-origin lesson source remains private until an explicit
31
+ feedback release on its exact captured attempt, regardless of automatic material
32
+ classification or the workbook provisional-feedback setting. Guardian reads remain
33
+ subject to the current relationship and grants. Child-owned uploads retain the
34
+ normal learner boundary.
35
+
36
+ ## Provider identification and human decisions
37
+
38
+ Configure `workbookPhotoIntakeHandlers({workbooks,request})` and
39
+ `workbookPhotoIntakeTargetResolver({workbooks,request})` on ingestion's execution
40
+ catalog, and the normal proposal generator/read configuration. Handler
41
+ `@happyvertical/smrt-education:identify-workbook-photo` uses
42
+ `@happyvertical/smrt-education:WorkbookPhotoIntake:get@1` and
43
+ `workbookphotointakes.read`. This operation classifies only; applying it never
44
+ creates a Grade. Keep `requireReview:true`. Candidate freshness checks use the
45
+ supplied executor and the exact authorized workbook/context digest.
46
+
47
+ The initial immutable captured ceiling must include all intended later stages
48
+ and a provisioned stable execution principal; for marking, include the existing
49
+ review-submission/Grade operation as well. Current guardian authorization still
50
+ controls every Grade. Never widen the receipt ceiling after upload or create a
51
+ second receipt to evade that boundary.
52
+
53
+ Queue with `queueEducationExtraction`, process extraction/interpretation with
54
+ `processEducationAnalysis`, then call `preserveWorkbookPhotoIdentification`.
55
+ Alternatively `processWorkbookPhotoAnalysis` combines processing, preservation
56
+ and capture of a confidently resolved **completed** lesson. It returns
57
+ `{processing,intake,capture,assessmentRequired}`. The provider runs outside the
58
+ education transaction. Preservation reads the current public immutable ingestion
59
+ snapshot, exact offered schema and original hashes; it retains analysis IDs,
60
+ revision, digests and provider/configuration provenance in append-only decisions.
61
+ Unavailable, failed, stale, malformed and ambiguous results leave durable intake
62
+ pending. A provider needs-review outcome, alternatives or splits never authorize
63
+ automatic resolution; its original proposal remains inspectable. Literal page
64
+ numbers cannot establish lesson identity. Authorization/integrity/database failures still throw.
65
+
66
+ Provider output distinguishes lesson, cover, reference (including contents and
67
+ answer keys), and uncertain material, plus blank/completed/unknown work state.
68
+ Automatic book identity requires a printed ISBN or complete printed
69
+ publisher/title/edition quotations. Title alone never merges. An existing book may instead match exact current printed title/edition to one
70
+ authorized contextual record with retained publisher provenance; that publisher
71
+ is context, never a fabricated current quotation. Conflicting or ambiguous
72
+ matches stay pending. Provider
73
+ candidates contain book metadata only, never reference text or answer keys.
74
+ `listWorkbookPhotoWorkbooks(request)` includes cover-only books for the provider
75
+ and guardian picker. A printed lesson identity supplies one stable activity key;
76
+ multiple page files may belong to that same lesson. Page number alone cannot
77
+ automatically establish lesson identity.
78
+
79
+ A guardian may call `resolveWorkbookPhotoIntake({...request,intakeId,requestKey,
80
+ identification})` after upload. It requires planning authority and appends a new
81
+ decision. Stable request-key retries replay; changed retries conflict. Covers and
82
+ references create/reuse the Workbook and append contextual metadata only: no
83
+ Course, Lesson, progress, attempt, Grade or concept observations. Existing
84
+ Workbook title/edition identity never changes; context versions retain original
85
+ metadata and source receipt identity. A captured intake cannot be retargeted.
86
+
87
+ ## Existing marking pipeline
88
+
89
+ `captureWorkbookPhotoIntake({...request,intakeId,requestKey,assistance?,
90
+ previousSubmissionId?,completedWork?})` copies the retained originals through
91
+ `captureWorkbookWork` and returns its immutable attempt. Blank or unknown work
92
+ requires explicit `completedWork:true`; no blank exercise is automatically marked
93
+ as unanswered learner work. Corrections require the explicit current predecessor
94
+ and preserve the existing practice episode. Existing attempts reject automatic
95
+ replacement; provider classification cannot silently turn a new photo into a
96
+ correction.
97
+
98
+ After capture, configure the existing `educationIntakeHandlers` with the returned
99
+ submission ID, `view.activity.id`, and authoritative activity enrollment ID.
100
+ Queue `queueEducationExtraction(ingestion,view.itemId,configuration,submissionId)`
101
+ on the **same learner-scoped receipt**, process normally, then use
102
+ `preserveWorkbookIntakeAnalysis`. That bridge already verifies exact education
103
+ ownership and source binding independently of the receipt's confidential scope.
104
+ Guardian Grade/release policy and immutable assessment history are unchanged.
105
+
106
+ ## Verification matrix
107
+
108
+ `src/__tests__/photo-intake-workflows.ts` runs on SQLite and available PostgreSQL
109
+ (`photo-intake.test.ts` / `photo-intake.optional.test.ts`).
110
+ `photo-intake-manifest.test.ts` separately pins all concrete ownership/provenance
111
+ columns and UUID/FK definitions in the generated structured manifest. It covers actor/resource
112
+ ownership and active context, private originals, sibling/foreign asset denial,
113
+ retry conflicts, concurrent deduplication, rollback and borrowed executor denial,
114
+ cover-only context and immutable identity, ambiguous titles, source integrity,
115
+ blank work intent, multi-page capture, real ingestion generation for cover then
116
+ lesson, provider failure, and guardian-only pending/reference originals. The same
117
+ transaction executor is used for authorization and mutations. Real provider
118
+ handwriting accuracy and deployment worker isolation remain host acceptance.
119
+
120
+ Run package test/typecheck/build; integration owns PostgreSQL provisioning and
121
+ full-suite validation. Workers never start containers. This is additive feature
122
+ work; a base regression run is N/A because no pre-activity capture API existed.
123
+
124
+ ## Guardian-origin source privacy
125
+
126
+ The immutable photo receipt actor, compared with the persisted learner user,
127
+ determines source origin. Classification as a lesson does not grant learner
128
+ access. `Submission.intakeItemId` joins the exact tenant/learner receipt; only a
129
+ `WorkbookFeedbackRelease` on that submission's exact `WorkbookAttempt` releases
130
+ its source. Another attempt or context-only page cannot borrow that release.
131
+
132
+ The base `SubmissionService` applies this policy too: own learners receive no
133
+ pages, notes, Grade/feedback or grade-derived status before release, and signing
134
+ a guessed source asset is denied. The DTO exposes `awaitingParentRelease:true`
135
+ for an own learner awaiting release, and `sourceRequiresParentRelease:true` to
136
+ all authorized actors for guardian-origin material. The latter remains true
137
+ after release: every new derived revision still needs its own explicit release.
138
+ `WorkbookAttemptView.reviewRequired` and persisted capture policy snapshots
139
+ retain their historical workbook setting; combine that setting with
140
+ `submission.sourceRequiresParentRelease` when describing effective visibility. Workbook timeline and
141
+ feedback delivery acknowledgment require exact revision release for this source,
142
+ even when the general workbook policy permits provisional AI feedback.
143
+
144
+ Existing hosts without photo schema retain ordinary submission behavior through
145
+ optional table-existence checks; no application schema is created at runtime.
146
+ The shared SQLite/PostgreSQL privacy case covers guardian-origin mistaken lesson
147
+ capture, policy off, automatic AI and guardian assessment before release, direct
148
+ base submission reads/signing, exact release, later unreleased reanalysis and a
149
+ different attempt that stays private. Existing own-learner capture cases remain
150
+ readable, and legacy submission/workbook suites protect consumers without photo
151
+ tables.
152
+
153
+ ## Existing candidate identification
154
+
155
+ `workbookId` selects an exact authorized candidate; it does not replace the
156
+ required `identification.workbook` contract. Copy that candidate's `workbookKey`,
157
+ `title` and `edition` into the required object. Those are catalog identity fields,
158
+ not invented IDs or claims about what is printed on the new photograph.
159
+ `identityEvidence` quotes only current visible material. A unique current
160
+ title/edition may match retained publisher provenance without publisher/ISBN
161
+ being reprinted. Conflicting identity, duplicate candidates and title-only
162
+ matching remain pending; malformed model output is never repaired automatically.
163
+
164
+ ## Exact receipt authorization
165
+
166
+ Processing and retries resolve `context.itemId` through the service-owned
167
+ `getWorkbookPhotoIntakeByItem` lookup, scoped to the current tenant and learner
168
+ on the supplied executor. It delegates to the same current actor and private
169
+ source/release checks as the intake-ID getter. The inbox list remains capped at
170
+ 1,000 rows for display; that pagination window is never an authorization or
171
+ lifetime receipt limit.
172
+
173
+ Hosts may opt into `listWorkbookPhotoIntakes(request, { newestFirst: true })`
174
+ for a current inbox. Default ordering remains unchanged and both orders keep the
175
+ 1,000-row display bound and the same per-receipt visibility checks. Exact selected
176
+ receipt reads remain independent of either display window.
177
+
178
+ ## Marking before filing
179
+
180
+ Completed learner work does not require a printed lesson number or workbook
181
+ identity. Identification `kind:'work', workState:'completed'` deliberately omits
182
+ `workbook`, `activity` and `workbookId`. Its confidence concerns observable learner
183
+ completion/material kind, independently of bibliographic identity. Printed worked
184
+ examples, answer keys, blank pages and uncertain material are not completed learner
185
+ work. `kind:'lesson'` retains the existing strict printed identity contract.
186
+
187
+ Confident work preservation creates one receipt-bound **unfiled** work unit.
188
+ `processWorkbookPhotoAnalysis` captures it and returns the existing
189
+ `capture/assessmentRequired` result. Queue the normal assessment stage on that
190
+ same receipt, submission and returned activity/enrollment IDs; do not re-upload or
191
+ create another receipt. Visible responses can be assessed while illegible or
192
+ ambiguous responses remain unassessed. The extraction observation contract keeps
193
+ printed examples separate from learner marks and reports matching endpoints only
194
+ when observed. Adopt it with a new extraction configuration revision.
195
+
196
+ The internal private Course/Enrollment/Worksheet are routing records, not a claim
197
+ about a canonical book or lesson. `WorkbookActivity.workbookId` is null only for
198
+ explicit `captureKind:'unfiled'` with an exact owned `photoIntakeId`; all ordinary
199
+ identified activity invariants remain. No Workbook is invented. Activity DTO
200
+ `filing.state` is `unfiled`, `filed` or `identified`; empty workbook title/key/edition
201
+ on unfiled work are intentional. Use the work label as the primary display.
202
+ `listWorkbookActivities` includes these work units. Exact
203
+ `getWorkbookPhotoTimeline({...request,intakeId,limit?,cursor?,revisionId?})`
204
+ returns existing authorized feedback/history; the normal activity timeline also
205
+ works. `getWorkbookActivity` refreshes exact filing presentation without loading
206
+ history. The receipt view refreshes filing rather than replaying its old label.
207
+
208
+ Optional guardian `fileWorkbookWork({...request,activityId,workbookId,
209
+ canonicalActivityId?,expectedRevisionId,requestKey,reason})` appends an association
210
+ to a real authorized workbook and optionally a real identified activity. Initial
211
+ expected revision is null. Stable retries replay their original accepted result;
212
+ changed retries/stale heads conflict. Filing does not move or recreate activity,
213
+ submission, attempt, assessment, release, source or correction episode IDs. It
214
+ cannot change captured review policy, create a Grade, duplicate concept evidence,
215
+ or release guardian-origin material. Future corrections continue against the
216
+ original work-unit ID and explicit current predecessor. Existing identified
217
+ captures cannot be retargeted through filing.
218
+
219
+ Apply normal manifest migrations before using this contract. Existing activities
220
+ receive `capture_kind='identified'` through the declared database default; the
221
+ workbook FK becomes nullable and photo-intake FK plus filing table are added.
222
+ Old populated SQLite requires the explicit supported nullability-relaxation
223
+ migration described by #4413; no runtime DDL or manual SQL workaround is allowed.
224
+
225
+ When no book record exists, the same optional filing operation accepts
226
+ `newWorkbook:{title,edition?,workbookKey?}` instead of `workbookId`. Exactly one is
227
+ required. Parents need only title/edition; an omitted key becomes an opaque stable
228
+ manual identity for this work/request, never an automatic title-based match.
229
+ An explicit existing key must retain its exact title/edition. New metadata cannot
230
+ also claim `canonicalActivityId`. Book creation and filing share the guardian's
231
+ serialized transaction: stale/unauthorized/conflicting requests create nothing,
232
+ and a later failure rolls both back. No cover re-upload, canonical lesson,
233
+ submission replacement or second assessment is needed. New books keep the default
234
+ review policy; filing never changes the source attempt's captured policy.
235
+
236
+ ## Exact cover context for header selection
237
+
238
+ `getWorkbookPhotoIntake` adds optional nullable `workbookId`, loaded from the latest
239
+ retained resolution's exact WorkbookContextRevision under the same tenant/learner
240
+ and decision ID. Pending/uncertain or unfiled-work resolution returns null; this is
241
+ independent of a later activity filing. It never guesses from title or an inbox
242
+ list. Existing guardian-origin receipt privacy still applies before lookup.
243
+
244
+ Header cover camera/upload uses the normal receive/process photo flow. A confident
245
+ cover resolves book/context only, with no attempt, lesson or Grade. Read that exact
246
+ intake, then let the guardian explicitly choose `fileWorkbookWork({workbookId,...})`.
247
+ An uncertain cover can use the existing guardian `resolveWorkbookPhotoIntake` with
248
+ manual cover metadata, followed by the same exact getter and explicit filing.
249
+ Neither cover identification nor context alone publishes learner source/feedback.