@happyvertical/smrt-education 0.57.36 → 0.57.38

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 (47) hide show
  1. package/AGENTS.md +4 -0
  2. package/agents/family-intake.md +97 -0
  3. package/dist/catalog.d.ts +4 -4
  4. package/dist/catalog.d.ts.map +1 -1
  5. package/dist/chunks/{workbook-photo-service-1wcnf-Xf.js → workbook-photo-service-DCmqeWLd.js} +448 -202
  6. package/dist/chunks/workbook-photo-service-DCmqeWLd.js.map +1 -0
  7. package/dist/course-source.d.ts +2 -2
  8. package/dist/course-source.d.ts.map +1 -1
  9. package/dist/family-intake-contracts.d.ts +56 -0
  10. package/dist/family-intake-contracts.d.ts.map +1 -0
  11. package/dist/family-intake-ingestion.d.ts +29 -0
  12. package/dist/family-intake-ingestion.d.ts.map +1 -0
  13. package/dist/family-intake-models.d.ts +58 -0
  14. package/dist/family-intake-models.d.ts.map +1 -0
  15. package/dist/family-intake-service.d.ts +70 -0
  16. package/dist/family-intake-service.d.ts.map +1 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +5 -5
  20. package/dist/index.js.map +1 -1
  21. package/dist/ingestion.d.ts +2 -0
  22. package/dist/ingestion.d.ts.map +1 -1
  23. package/dist/ingestion.js +426 -13
  24. package/dist/ingestion.js.map +1 -1
  25. package/dist/intake-original-evidence.d.ts +10 -0
  26. package/dist/intake-original-evidence.d.ts.map +1 -0
  27. package/dist/learner-service.d.ts +2 -0
  28. package/dist/learner-service.d.ts.map +1 -1
  29. package/dist/learners.d.ts +5 -5
  30. package/dist/learners.d.ts.map +1 -1
  31. package/dist/lesson-assets.d.ts +2 -2
  32. package/dist/lesson-assets.d.ts.map +1 -1
  33. package/dist/manifest.json +1319 -78
  34. package/dist/smrt-knowledge.json +524 -10
  35. package/dist/submission-service.d.ts +1 -1
  36. package/dist/submission-service.d.ts.map +1 -1
  37. package/dist/submissions.d.ts +7 -7
  38. package/dist/submissions.d.ts.map +1 -1
  39. package/dist/workbook-models.d.ts +8 -8
  40. package/dist/workbook-models.d.ts.map +1 -1
  41. package/dist/workbook-photo-ingestion.d.ts.map +1 -1
  42. package/dist/workbook-photo-models.d.ts +6 -5
  43. package/dist/workbook-photo-models.d.ts.map +1 -1
  44. package/dist/workbook-photo-service.d.ts +6 -0
  45. package/dist/workbook-photo-service.d.ts.map +1 -1
  46. package/package.json +10 -10
  47. package/dist/chunks/workbook-photo-service-1wcnf-Xf.js.map +0 -1
@@ -136,5 +136,7 @@ export declare function preserveWorkbookIntakeAnalysis(input: {
136
136
  submissionId: string;
137
137
  workbookVocabulary?: WorkbookAssessment['vocabulary'];
138
138
  }): Promise<WorkbookPreservationResult>;
139
+ export { receiveFamilyIntake, retainFamilyIntakeReceipt, } from './family-intake-ingestion.js';
140
+ export { FamilyIntakeService } from './family-intake-service.js';
139
141
  export { preserveWorkbookPhotoIdentification, processWorkbookPhotoAnalysis, receiveWorkbookPhotoIntake, retainWorkbookPhotoReceipt, WORKBOOK_PHOTO_HANDLER, workbookPhotoIntakeHandlers, workbookPhotoIntakeTargetResolver, } from './workbook-photo-ingestion.js';
140
142
  //# sourceMappingURL=ingestion.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ingestion.d.ts","sourceRoot":"","sources":["../src/ingestion.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ingestion.d.ts","sourceRoot":"","sources":["../src/ingestion.ts"],"names":[],"mappings":"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"}
package/dist/ingestion.js CHANGED
@@ -1,10 +1,35 @@
1
- import { F as WorkbookAssessmentValidationError, I as aggregateWorkbookQuestions, L as validateWorkbookAssessment, P as WORKBOOK_MATH_VOCABULARY, R as workbookAggregateFeedback, f as validateWorkbookPhotoIdentification, m as normalizeWorkbookAnswerRegions, n as preservePhotoProposal, p as normalizeWorkbookObservedLayout, r as WorkbookService } from "./chunks/workbook-photo-service-1wcnf-Xf.js";
1
+ import { F as WorkbookAssessmentValidationError, G as familyIntakeScopeId, H as FamilyIntakeCollection, I as aggregateWorkbookQuestions, L as validateWorkbookAssessment, P as WORKBOOK_MATH_VOCABULARY, R as workbookAggregateFeedback, V as FamilyIntakeAssetCollection, W as FamilyIntakeDecisionCollection, d as WorkbookPhotoIntakeCollection, f as validateWorkbookPhotoIdentification, m as normalizeWorkbookAnswerRegions, n as preservePhotoProposal, p as normalizeWorkbookObservedLayout, r as WorkbookService, s as WorkbookPhotoAssetCollection, t as WorkbookPhotoService, v as LearnerAuthorizationError } from "./chunks/workbook-photo-service-DCmqeWLd.js";
2
2
  import { getCurrentTenant, withTenant } from "@happyvertical/smrt-tenancy";
3
+ import { withPrivateAssetAccess } from "@happyvertical/smrt-assets";
3
4
  import { registerPermissionDefinitions } from "@happyvertical/smrt-users";
4
5
  import { GenerationSnapshotStaleError, ProposalsNotConfiguredError, extractAnalysis, normalizeObservedImageRegions } from "@happyvertical/smrt-ingestion/server";
5
6
  import { readFile } from "node:fs/promises";
6
7
  import { resolve } from "node:path";
7
8
  import { createHash } from "node:crypto";
9
+ //#region src/intake-original-evidence.ts
10
+ var supportedIntakeOriginal = (type) => [
11
+ "image/png",
12
+ "image/jpeg",
13
+ "image/webp",
14
+ "application/pdf"
15
+ ].includes(type);
16
+ function intakeOriginalEvidence(evidence) {
17
+ const byId = new Map(evidence.map((entry) => [entry.id, entry]));
18
+ return evidence.filter((entry) => {
19
+ let parentId = entry.parentEvidenceId;
20
+ const visited = /* @__PURE__ */ new Set([entry.id]);
21
+ while (parentId) {
22
+ if (visited.has(parentId)) throw new Error("Invalid family evidence lineage");
23
+ visited.add(parentId);
24
+ const parent = byId.get(parentId);
25
+ if (!parent) throw new Error("Incomplete family evidence lineage");
26
+ if (supportedIntakeOriginal(parent.mediaType)) return false;
27
+ parentId = parent.parentEvidenceId;
28
+ }
29
+ return true;
30
+ }).sort((a, b) => a.partId.localeCompare(b.partId));
31
+ }
32
+ //#endregion
8
33
  //#region src/ingestion-isolation.ts
9
34
  async function requireIngestionMemoryLimit(maximumBytes) {
10
35
  if (process.platform !== "linux") throw new Error("Extraction requires a Linux cgroup-v2 memory-limited worker");
@@ -31,6 +56,388 @@ async function requireIngestionMemoryLimit(maximumBytes) {
31
56
  return { memoryLimitBytes: limit };
32
57
  }
33
58
  //#endregion
59
+ //#region src/family-intake-ingestion.ts
60
+ function assertScope$1(input) {
61
+ if (input.scope.tenantId !== input.request.tenantId || input.scope.actorId !== input.request.actorUserId || input.scope.confidentialScopeId !== familyIntakeScopeId(input.request.tenantId)) throw new Error("Family intake scope mismatch");
62
+ }
63
+ async function receiveFamilyIntake(input) {
64
+ assertScope$1(input);
65
+ await input.family.authorizeFamilyIntake(input.request, "education.family-intake.receive");
66
+ const receipt = await input.ingestion.receive(input.receive);
67
+ if (!("itemId" in receipt) || !receipt.itemId) return {
68
+ receipt,
69
+ intake: null
70
+ };
71
+ return {
72
+ receipt,
73
+ intake: await retainFamilyIntakeReceipt({
74
+ ...input,
75
+ itemId: receipt.itemId
76
+ })
77
+ };
78
+ }
79
+ async function retainFamilyIntakeReceipt(input) {
80
+ assertScope$1(input);
81
+ await input.family.authorizeFamilyIntake(input.request, "education.family-intake.receive");
82
+ const identity = await input.ingestion.getReceiptSource(input.itemId);
83
+ const evidence = await input.ingestion.getEvidence(input.itemId);
84
+ if (evidence.length > 200) throw new Error("Family receipt evidence exceeds limit");
85
+ const source = {
86
+ ...identity,
87
+ evidence: evidence.map((e) => ({
88
+ evidenceId: e.id,
89
+ partId: e.partId,
90
+ parentEvidenceId: e.parentEvidenceId,
91
+ mimeType: e.mediaType,
92
+ sha256: e.contentHash
93
+ }))
94
+ };
95
+ const supported = supportedIntakeOriginal;
96
+ const originalEvidence = intakeOriginalEvidence(evidence);
97
+ if (originalEvidence.some((e) => e.mediaType !== "application/json" && !supported(e.mediaType))) throw new Error("Unsupported family original: only JPEG, PNG, WebP and PDF are supported");
98
+ const originals = originalEvidence.filter((e) => supported(e.mediaType));
99
+ if (!originals.length || originals.length > 100 || originals.some((e) => e.byteLength < 1 || e.byteLength > 25 * 1024 * 1024) || originals.reduce((n, e) => n + e.byteLength, 0) > 100 * 1024 * 1024) throw new Error("Unsupported or oversized family intake");
100
+ const pages = [];
101
+ const extensions = {
102
+ "image/png": "png",
103
+ "image/jpeg": "jpg",
104
+ "image/webp": "webp",
105
+ "application/pdf": "pdf"
106
+ };
107
+ for (const original of originals) {
108
+ if (!Object.hasOwn(extensions, original.mediaType)) throw new Error("Unsupported family original");
109
+ const data = await input.ingestion.readEvidence(input.itemId, original.id);
110
+ if (data.length !== original.byteLength || createHash("sha256").update(data).digest("hex") !== original.contentHash) throw new Error("Family receipt integrity mismatch");
111
+ pages.push({
112
+ name: original.filename || `${original.id}.${extensions[original.mediaType]}`,
113
+ mimeType: original.mediaType,
114
+ data: Buffer.from(data)
115
+ });
116
+ }
117
+ return input.family.retainFamilyIntake({
118
+ ...input.request,
119
+ itemId: input.itemId,
120
+ source,
121
+ pages
122
+ });
123
+ }
124
+ //#endregion
125
+ //#region src/family-intake-service.ts
126
+ var canonical$1 = (value) => JSON.stringify(value, (_key, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b))) : v);
127
+ function digest(value) {
128
+ const crypto = typeof process !== "undefined" && process.getBuiltinModule?.("node:crypto");
129
+ if (!crypto) throw new Error("Family intake requires a Node server");
130
+ return crypto.createHash("sha256").update(value).digest("hex");
131
+ }
132
+ function key(value) {
133
+ if (typeof value !== "string" || !value.trim() || value.length > 200 || Array.from(value).some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127)) throw new Error("Invalid family intake key");
134
+ }
135
+ var FamilyIntakeService = class FamilyIntakeService extends WorkbookPhotoService {
136
+ constructor(db, authorize, authorizeCatalog, familyOptions, authorizeFamily) {
137
+ super(db, authorize, authorizeCatalog, familyOptions);
138
+ this.familyOptions = familyOptions;
139
+ this.authorizeFamily = authorizeFamily;
140
+ }
141
+ familyOptions;
142
+ authorizeFamily;
143
+ createTransactionService(db) {
144
+ return new FamilyIntakeService(db, this.authorize, this.authorizeCatalog, this.familyOptions, this.authorizeFamily);
145
+ }
146
+ async authorizeFamilyIntake(request, action = "education.family-intake.read") {
147
+ this.assertServiceOpen();
148
+ const context = getCurrentTenant();
149
+ if (!context || context.superAdminBypass || context.tenantId !== request.tenantId || context.userId !== request.actorUserId || !request.tenantId || !request.actorUserId || !await this.db.get("memberships", {
150
+ tenant_id: request.tenantId,
151
+ user_id: request.actorUserId,
152
+ status: "active"
153
+ }) || !await this.db.get("users", {
154
+ id: request.actorUserId,
155
+ status: "active"
156
+ }) || await this.authorizeFamily({
157
+ ...request,
158
+ executor: this.db,
159
+ action
160
+ }) !== true) throw new LearnerAuthorizationError();
161
+ }
162
+ async familyRow(request) {
163
+ await this.authorizeFamilyIntake(request);
164
+ const row = await (await FamilyIntakeCollection.create({ db: this.db })).get({
165
+ id: request.intakeId,
166
+ tenantId: request.tenantId
167
+ }, { cache: false });
168
+ if (!row) throw new LearnerAuthorizationError();
169
+ return row;
170
+ }
171
+ async familyDecisions(request) {
172
+ return (await (await FamilyIntakeDecisionCollection.create({ db: this.db })).list({
173
+ where: {
174
+ tenantId: request.tenantId,
175
+ intakeId: request.intakeId
176
+ },
177
+ limit: 1e4
178
+ })).sort((a, b) => a.sequence - b.sequence);
179
+ }
180
+ async lockFamily(request) {
181
+ await this.db.query("UPDATE family_intakes SET id=id WHERE id=? AND tenant_id=?", [request.intakeId, request.tenantId]);
182
+ }
183
+ async appendFamily(request, requestKey, body) {
184
+ return (await FamilyIntakeDecisionCollection.create({ db: this.db })).create({
185
+ _insertOnly: true,
186
+ tenantId: request.tenantId,
187
+ intakeId: request.intakeId,
188
+ actorUserId: request.actorUserId,
189
+ requestKey,
190
+ sequence: (await this.familyDecisions(request)).length + 1,
191
+ bodyJson: canonical$1(body)
192
+ });
193
+ }
194
+ async retainFamilyIntake(request) {
195
+ return this.composeMutation(async (service) => {
196
+ if (service.executorBorrowed) throw new Error("Family storage requires an owned transaction");
197
+ await service.authorizeFamilyIntake(request, "education.family-intake.receive");
198
+ key(request.itemId);
199
+ for (const value of [
200
+ request.source?.sourceId,
201
+ request.source?.sourceVersion,
202
+ request.source?.deliveryKey
203
+ ]) key(value);
204
+ if (Buffer.byteLength(canonical$1(request.source)) > 128 * 1024) throw new Error("Family source provenance exceeds limit");
205
+ if (request.source.evidence !== void 0) {
206
+ if (!Array.isArray(request.source.evidence) || request.source.evidence.length > 200) throw new Error("Invalid family source evidence");
207
+ for (const evidence of request.source.evidence) {
208
+ for (const value of [
209
+ evidence.evidenceId,
210
+ evidence.partId,
211
+ evidence.mimeType
212
+ ]) key(value);
213
+ if (evidence.parentEvidenceId !== null) key(evidence.parentEvidenceId);
214
+ if (typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256)) throw new Error("Invalid family source evidence hash");
215
+ }
216
+ }
217
+ const source = {
218
+ sourceId: request.source.sourceId,
219
+ sourceVersion: request.source.sourceVersion,
220
+ deliveryKey: request.source.deliveryKey,
221
+ ...request.source.evidence ? { evidence: request.source.evidence.map((e) => ({
222
+ evidenceId: e.evidenceId,
223
+ partId: e.partId,
224
+ parentEvidenceId: e.parentEvidenceId,
225
+ mimeType: e.mimeType,
226
+ sha256: e.sha256
227
+ })) } : {}
228
+ };
229
+ service.validatePages(request.pages);
230
+ if (request.pages.reduce((n, p) => n + p.data.length, 0) > 100 * 1024 * 1024) throw new Error("Family intake exceeds total byte limit");
231
+ await service.db.query("UPDATE tenants SET id=id WHERE id=?", [request.tenantId]);
232
+ const contentDigest = digest(canonical$1({
233
+ source,
234
+ pages: request.pages.map((p) => ({
235
+ name: p.name,
236
+ mimeType: p.mimeType,
237
+ sha256: digest(p.data)
238
+ }))
239
+ }));
240
+ const collection = await FamilyIntakeCollection.create({ db: service.db });
241
+ const prior = await collection.get({
242
+ tenantId: request.tenantId,
243
+ itemId: request.itemId
244
+ }, { cache: false });
245
+ if (prior) {
246
+ if (prior.contentDigest !== contentDigest) throw new Error("Family receipt retry content conflict");
247
+ return service.getFamilyIntake({
248
+ ...request,
249
+ intakeId: String(prior.id)
250
+ });
251
+ }
252
+ const actor = await service.db.get("users", { id: request.actorUserId });
253
+ if (!actor?.profile_id) throw new LearnerAuthorizationError();
254
+ const row = await collection.create({
255
+ _insertOnly: true,
256
+ tenantId: request.tenantId,
257
+ itemId: request.itemId,
258
+ actorUserId: request.actorUserId,
259
+ ownerProfileId: actor.profile_id,
260
+ contentDigest,
261
+ sourceJson: canonical$1(source)
262
+ });
263
+ const links = await FamilyIntakeAssetCollection.create({ db: service.db });
264
+ for (const [position, page] of request.pages.entries()) {
265
+ const asset = await service.storePage(request, actor.profile_id, page);
266
+ await links.create({
267
+ _insertOnly: true,
268
+ tenantId: request.tenantId,
269
+ intakeId: String(row.id),
270
+ assetId: String(asset.id),
271
+ name: page.name,
272
+ mimeType: page.mimeType,
273
+ sha256: digest(page.data),
274
+ position
275
+ });
276
+ }
277
+ return service.getFamilyIntake({
278
+ ...request,
279
+ intakeId: String(row.id)
280
+ });
281
+ });
282
+ }
283
+ async getFamilyIntake(request) {
284
+ const row = await this.familyRow(request);
285
+ const decisions = await this.familyDecisions(request);
286
+ const assignment = decisions.map((d) => JSON.parse(d.bodyJson)).findLast((d) => d.kind === "assign");
287
+ const adoption = decisions.map((d) => JSON.parse(d.bodyJson)).find((d) => d.kind === "adopt");
288
+ const originals = await (await FamilyIntakeAssetCollection.create({ db: this.db })).list({
289
+ where: {
290
+ tenantId: request.tenantId,
291
+ intakeId: request.intakeId
292
+ },
293
+ orderBy: "position",
294
+ limit: 100
295
+ });
296
+ return {
297
+ id: String(row.id),
298
+ itemId: row.itemId,
299
+ source: JSON.parse(row.sourceJson),
300
+ revision: decisions.length,
301
+ learnerId: assignment?.learnerId ?? null,
302
+ photoIntakeId: adoption?.kind === "adopt" ? adoption.photoIntakeId : null,
303
+ originals: originals.map((o) => ({
304
+ assetId: o.assetId,
305
+ name: o.name,
306
+ mimeType: o.mimeType,
307
+ sha256: o.sha256
308
+ }))
309
+ };
310
+ }
311
+ async listFamilyIntakes(request, options = {}) {
312
+ await this.authorizeFamilyIntake(request);
313
+ const limit = options.limit ?? 20, offset = options.offset ?? 0;
314
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger(offset) || offset < 0) throw new Error("Invalid family inbox page");
315
+ const rows = await (await FamilyIntakeCollection.create({ db: this.db })).list({
316
+ where: { tenantId: request.tenantId },
317
+ orderBy: "createdAt DESC",
318
+ limit,
319
+ offset
320
+ });
321
+ return Promise.all(rows.map((row) => this.getFamilyIntake({
322
+ ...request,
323
+ intakeId: String(row.id)
324
+ })));
325
+ }
326
+ async countFamilyIntakes(request) {
327
+ await this.authorizeFamilyIntake(request);
328
+ const rows = await this.db.query("SELECT COUNT(*) AS total FROM family_intakes f WHERE f.tenant_id=? AND NOT EXISTS (SELECT 1 FROM workbook_photo_intakes p WHERE p.tenant_id=f.tenant_id AND p.family_intake_id=f.id)", [request.tenantId]);
329
+ return Number(rows.rows[0]?.total ?? 0);
330
+ }
331
+ async getFamilyIntakeTimeline(request) {
332
+ await this.familyRow(request);
333
+ return (await this.familyDecisions(request)).map((d) => ({
334
+ id: String(d.id),
335
+ revision: d.sequence,
336
+ actorUserId: d.actorUserId,
337
+ requestKey: d.requestKey,
338
+ decision: JSON.parse(d.bodyJson)
339
+ }));
340
+ }
341
+ async readFamilyIntakeOriginal(request) {
342
+ const row = await this.familyRow(request);
343
+ const original = (await this.getFamilyIntake(request)).originals.find((o) => o.assetId === request.assetId);
344
+ if (!original) throw new LearnerAuthorizationError();
345
+ return withPrivateAssetAccess({
346
+ tenantId: request.tenantId,
347
+ actorUserId: request.actorUserId,
348
+ ownerProfileId: row.ownerProfileId,
349
+ assetIds: [request.assetId]
350
+ }, async () => {
351
+ const runtime = await this.assets();
352
+ const asset = await runtime.collection.get({
353
+ id: request.assetId,
354
+ tenantId: request.tenantId,
355
+ ownerProfileId: row.ownerProfileId
356
+ }, { cache: false });
357
+ if (asset?.accessMode !== "private") throw new LearnerAuthorizationError();
358
+ const data = await runtime.store.read(asset);
359
+ if (digest(data) !== original.sha256) throw new Error("Retained family original integrity mismatch");
360
+ return {
361
+ data,
362
+ mimeType: original.mimeType
363
+ };
364
+ });
365
+ }
366
+ async assignFamilyIntake(request) {
367
+ return this.composeMutation(async (service) => {
368
+ await service.authorizeFamilyIntake(request, "education.family-intake.assign");
369
+ await service.lockFamily(request);
370
+ await service.requireActor(request, "education.learner.plan");
371
+ key(request.requestKey);
372
+ key(request.reason);
373
+ const view = await service.getFamilyIntake(request);
374
+ const body = {
375
+ kind: "assign",
376
+ learnerId: request.learnerId,
377
+ reason: request.reason
378
+ };
379
+ const prior = (await service.familyDecisions(request)).find((d) => d.requestKey === request.requestKey);
380
+ if (prior) {
381
+ if (prior.bodyJson !== canonical$1(body)) throw new Error("Family decision retry content conflict");
382
+ return view;
383
+ }
384
+ if (view.photoIntakeId) throw new Error("Adopted family intake cannot be retargeted");
385
+ if (!Number.isSafeInteger(request.expectedRevision) || request.expectedRevision !== view.revision) throw new Error("Family intake revision conflict");
386
+ await service.appendFamily(request, request.requestKey, body);
387
+ return service.getFamilyIntake(request);
388
+ });
389
+ }
390
+ /** Freeze the explicit assignment and borrow exact originals; no capture or grading. */
391
+ async adoptFamilyIntake(request) {
392
+ return this.composeMutation(async (service) => {
393
+ await service.authorizeFamilyIntake(request, "education.family-intake.assign");
394
+ await service.lockFamily(request);
395
+ await service.requireActor(request, "education.learner.plan");
396
+ key(request.requestKey);
397
+ const view = await service.getFamilyIntake(request);
398
+ const prior = (await service.familyDecisions(request)).find((d) => d.requestKey === request.requestKey);
399
+ if (prior) {
400
+ const body = JSON.parse(prior.bodyJson);
401
+ if (body.kind !== "adopt" || body.learnerId !== request.learnerId) throw new Error("Family decision retry content conflict");
402
+ return view;
403
+ }
404
+ if (view.photoIntakeId || view.learnerId !== request.learnerId) throw new Error("Family intake assignment mismatch");
405
+ if (!Number.isSafeInteger(request.expectedRevision) || request.expectedRevision !== view.revision) throw new Error("Family intake revision conflict");
406
+ const row = await service.familyRow(request);
407
+ for (const original of view.originals) await service.readFamilyIntakeOriginal({
408
+ ...request,
409
+ assetId: original.assetId
410
+ });
411
+ const photoIntakeId = globalThis.crypto.randomUUID();
412
+ await service.appendFamily(request, request.requestKey, {
413
+ kind: "adopt",
414
+ learnerId: request.learnerId,
415
+ photoIntakeId
416
+ });
417
+ const photo = await (await WorkbookPhotoIntakeCollection.create({ db: service.db })).create({
418
+ id: photoIntakeId,
419
+ _insertOnly: true,
420
+ tenantId: request.tenantId,
421
+ learnerId: request.learnerId,
422
+ familyIntakeId: request.intakeId,
423
+ itemId: row.itemId,
424
+ actorUserId: row.actorUserId,
425
+ contentDigest: row.contentDigest
426
+ });
427
+ const links = await WorkbookPhotoAssetCollection.create({ db: service.db });
428
+ for (const [position, original] of view.originals.entries()) await links.create({
429
+ _insertOnly: true,
430
+ tenantId: request.tenantId,
431
+ learnerId: request.learnerId,
432
+ intakeId: String(photo.id),
433
+ ...original,
434
+ position
435
+ });
436
+ return service.getFamilyIntake(request);
437
+ });
438
+ }
439
+ };
440
+ //#endregion
34
441
  //#region src/workbook-photo-ingestion.ts
35
442
  var MODEL = "@happyvertical/smrt-education:WorkbookPhotoIntake";
36
443
  var WORKBOOK_PHOTO_HANDLER = "@happyvertical/smrt-education:identify-workbook-photo";
@@ -128,9 +535,12 @@ var schema = {
128
535
  };
129
536
  function workbookPhotoIntakeHandlers(input) {
130
537
  const bound = async (context, work) => {
131
- assertScope(context.scope, input.request);
538
+ const checked = async (service) => {
539
+ await service.assertWorkbookPhotoScope(input.request, context.scope, context.itemId);
540
+ return work(service);
541
+ };
132
542
  if (!context.policy.requireReview) throw new Error("Photo intake requires review policy");
133
- if (!context.principal) return input.workbooks.withExecutor(context.db, work);
543
+ if (!context.principal) return input.workbooks.withExecutor(context.db, checked);
134
544
  const tenant = getCurrentTenant();
135
545
  if (!tenant || tenant.tenantId !== input.request.tenantId || tenant.userId !== context.principal.context.userId || context.principal.context.tenantId !== input.request.tenantId) throw new Error("Photo principal tenant mismatch");
136
546
  return withTenant({
@@ -138,7 +548,7 @@ function workbookPhotoIntakeHandlers(input) {
138
548
  tenantId: input.request.tenantId,
139
549
  userId: input.request.actorUserId,
140
550
  superAdminBypass: false
141
- }, () => input.workbooks.withExecutor(context.db, work));
551
+ }, () => input.workbooks.withExecutor(context.db, checked));
142
552
  };
143
553
  const authorize = async (context) => bound(context, async (service) => {
144
554
  await service.authorizePhotoIntake(input.request);
@@ -220,15 +630,17 @@ function workbookRevision(book) {
220
630
  return `workbook:${createHash("sha256").update(canonical(book)).digest("hex")}`;
221
631
  }
222
632
  function workbookPhotoIntakeTargetResolver(input) {
223
- return async ({ db, scope, model, id, revision }) => {
224
- assertScope(scope, input.request);
633
+ return async ({ db, scope, itemId, model, id, revision }) => {
225
634
  if (model !== "@happyvertical/smrt-education:Workbook") throw new Error("Photo target unavailable");
226
635
  const book = (await withTenant({
227
636
  ...getCurrentTenant(),
228
637
  tenantId: input.request.tenantId,
229
638
  userId: input.request.actorUserId,
230
639
  superAdminBypass: false
231
- }, () => input.workbooks.withExecutor(db, (service) => service.listWorkbookPhotoWorkbooks(input.request)))).find((book2) => book2.id === id);
640
+ }, () => input.workbooks.withExecutor(db, async (service) => {
641
+ await service.assertWorkbookPhotoScope(input.request, scope, itemId);
642
+ return service.listWorkbookPhotoWorkbooks(input.request);
643
+ }))).find((book2) => book2.id === id);
232
644
  if (!book) throw new Error("Photo target unavailable");
233
645
  const current = workbookRevision(book);
234
646
  if (revision !== void 0 && revision !== current) throw new Error("Photo target changed");
@@ -279,8 +691,8 @@ async function retainWorkbookPhotoReceipt(input) {
279
691
  });
280
692
  }
281
693
  async function preserveWorkbookPhotoIdentification(input) {
282
- assertScope(input.scope, input.request);
283
694
  const view = await input.workbooks.getWorkbookPhotoIntake(input.request);
695
+ await input.workbooks.assertWorkbookPhotoScope(input.request, input.scope, view.itemId);
284
696
  let snapshot;
285
697
  try {
286
698
  snapshot = await input.ingestion.getCompletedAnalysis(view.itemId);
@@ -306,7 +718,7 @@ async function preserveWorkbookPhotoIdentification(input) {
306
718
  requestKey: `analysis:${snapshot.attemptId}`,
307
719
  proposal,
308
720
  allowAutomaticResolution: generation.outcome === "proposals" && generation.suggestions.length === 1 && suggestions[0].alternatives.length === 0 && generation.splits.length === 0,
309
- sourceHashes: snapshot.evidence.filter((e) => e.parentEvidenceId === null).sort((a, b) => a.partId.localeCompare(b.partId)).map((e) => e.contentHash),
721
+ sourceHashes: intakeOriginalEvidence(snapshot.evidence).filter((e) => e.mediaType !== "application/json").map((e) => e.contentHash),
310
722
  provenance: {
311
723
  attemptId: snapshot.attemptId,
312
724
  revision: snapshot.revision,
@@ -320,8 +732,9 @@ async function preserveWorkbookPhotoIdentification(input) {
320
732
  });
321
733
  }
322
734
  async function processWorkbookPhotoAnalysis(input) {
323
- assertScope(input.scope, input.request);
324
- if ((await input.workbooks.getWorkbookPhotoIntake(input.request)).itemId !== input.itemId) throw new Error("Photo worker receipt mismatch");
735
+ const view = await input.workbooks.getWorkbookPhotoIntake(input.request);
736
+ await input.workbooks.assertWorkbookPhotoScope(input.request, input.scope, view.itemId);
737
+ if (view.itemId !== input.itemId) throw new Error("Photo worker receipt mismatch");
325
738
  const processing = await processEducationAnalysis(input);
326
739
  const intake = await preserveWorkbookPhotoIdentification(input);
327
740
  const capture = intake.status === "resolved" && ["lesson", "work"].includes(intake.identification?.kind ?? "") && intake.identification?.workState === "completed" ? await input.workbooks.captureWorkbookPhotoIntake({
@@ -1010,7 +1423,7 @@ async function preserveWorkbookIntakeAnalysis(input) {
1010
1423
  activityId,
1011
1424
  submissionId
1012
1425
  });
1013
- const originals = snapshot.evidence.filter((e) => e.parentEvidenceId === null).sort((a, b) => a.partId.localeCompare(b.partId));
1426
+ const originals = intakeOriginalEvidence(snapshot.evidence).filter((e) => e.mediaType !== "application/json");
1014
1427
  if (originals.length !== sources.length || originals.some((e, i) => e.contentHash !== sources[i].sha256)) throw new Error("Workbook retained source mismatch");
1015
1428
  let assessment;
1016
1429
  try {
@@ -1168,6 +1581,6 @@ async function preserveWorkbookIntakeAnalysis(input) {
1168
1581
  }
1169
1582
  }
1170
1583
  //#endregion
1171
- export { WORKBOOK_PHOTO_HANDLER, captureEducationIntake, educationIntakeHandlers, educationIntakeTargetResolver, preserveWorkbookIntakeAnalysis, preserveWorkbookPhotoIdentification, processEducationAnalysis, processWorkbookPhotoAnalysis, queueEducationExtraction, receiveWorkbookPhotoIntake, requireIngestionMemoryLimit, retainWorkbookPhotoReceipt, workbookPhotoIntakeHandlers, workbookPhotoIntakeTargetResolver };
1584
+ export { FamilyIntakeService, WORKBOOK_PHOTO_HANDLER, captureEducationIntake, educationIntakeHandlers, educationIntakeTargetResolver, preserveWorkbookIntakeAnalysis, preserveWorkbookPhotoIdentification, processEducationAnalysis, processWorkbookPhotoAnalysis, queueEducationExtraction, receiveFamilyIntake, receiveWorkbookPhotoIntake, requireIngestionMemoryLimit, retainFamilyIntakeReceipt, retainWorkbookPhotoReceipt, workbookPhotoIntakeHandlers, workbookPhotoIntakeTargetResolver };
1172
1585
 
1173
1586
  //# sourceMappingURL=ingestion.js.map