@happyvertical/smrt-education 0.57.33 → 0.57.35
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 +8 -1
- package/README.md +117 -13
- package/agents/catalog-sync.md +46 -0
- package/agents/photo-intake.md +249 -0
- package/agents/workbook-service.md +355 -0
- package/agents/workbook-test-matrix.md +74 -0
- package/dist/catalog-acquisition-service.d.ts.map +1 -1
- package/dist/catalog-capability.d.ts +21 -0
- package/dist/catalog-capability.d.ts.map +1 -0
- package/dist/catalog-contracts.d.ts +40 -0
- package/dist/catalog-contracts.d.ts.map +1 -1
- package/dist/catalog-database-sync-store.d.ts.map +1 -1
- package/dist/catalog-models.d.ts +5 -1
- package/dist/catalog-models.d.ts.map +1 -1
- package/dist/catalog-read-service.d.ts +2 -1
- package/dist/catalog-read-service.d.ts.map +1 -1
- package/dist/catalog-service.d.ts +5 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/chunks/workbook-photo-service-1wcnf-Xf.js +27359 -0
- package/dist/chunks/workbook-photo-service-1wcnf-Xf.js.map +1 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +339 -24864
- package/dist/index.js.map +1 -1
- package/dist/ingestion.d.ts +37 -0
- package/dist/ingestion.d.ts.map +1 -1
- package/dist/ingestion.js +829 -20
- package/dist/ingestion.js.map +1 -1
- package/dist/learner-service.d.ts +15 -0
- package/dist/learner-service.d.ts.map +1 -1
- package/dist/manifest.json +4735 -6
- package/dist/smrt-knowledge.json +1760 -26
- package/dist/sources/public-illustrative-mathematics-adapter.d.ts.map +1 -1
- package/dist/sources/public-illustrative-mathematics-parser.d.ts.map +1 -1
- package/dist/submission-service.d.ts +38 -11
- package/dist/submission-service.d.ts.map +1 -1
- package/dist/workbook-answer-regions.d.ts +11 -0
- package/dist/workbook-answer-regions.d.ts.map +1 -0
- package/dist/workbook-contracts.d.ts +224 -0
- package/dist/workbook-contracts.d.ts.map +1 -0
- package/dist/workbook-layout.d.ts +13 -0
- package/dist/workbook-layout.d.ts.map +1 -0
- package/dist/workbook-models.d.ts +129 -0
- package/dist/workbook-models.d.ts.map +1 -0
- package/dist/workbook-photo-contracts.d.ts +56 -0
- package/dist/workbook-photo-contracts.d.ts.map +1 -0
- package/dist/workbook-photo-ingestion.d.ts +70 -0
- package/dist/workbook-photo-ingestion.d.ts.map +1 -0
- package/dist/workbook-photo-models.d.ts +63 -0
- package/dist/workbook-photo-models.d.ts.map +1 -0
- package/dist/workbook-photo-service.d.ts +72 -0
- package/dist/workbook-photo-service.d.ts.map +1 -0
- package/dist/workbook-service.d.ts +158 -0
- package/dist/workbook-service.d.ts.map +1 -0
- 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.
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
|
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.
|
package/agents/catalog-sync.md
CHANGED
|
@@ -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.
|