@gitroll/tacitt-cli 0.6.10 → 0.6.12

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 (63) hide show
  1. package/README.md +47 -4
  2. package/bin/INDEX.md +6 -4
  3. package/bin/tacitt.cjs +1 -0
  4. package/dist/{agent-setup-LPd8WEsN.mjs → agent-setup-B12VbvN_.mjs} +2 -2
  5. package/dist/agent-setup-worker.mjs +5 -5
  6. package/dist/cli.mjs +39 -25
  7. package/dist/codex-title-worker.mjs +2 -2
  8. package/dist/configured-preview-worker.mjs +5 -5
  9. package/dist/configured-session-worker.mjs +27 -27
  10. package/dist/{frame-ByT1xayJ.mjs → frame-B3He21G8.mjs} +1 -1
  11. package/dist/{heap-CM298Yu0.mjs → heap-Blrat--4.mjs} +469 -30
  12. package/dist/inspection-worker.mjs +4 -4
  13. package/dist/{job-BHIwElNF.mjs → job-CBuIMTQ0.mjs} +57 -22
  14. package/dist/{locate-Cwv6WH8n.mjs → locate-D-ZaHpI0.mjs} +184 -18
  15. package/dist/{model-zvn5GUJ9.mjs → model-DR9odhWP.mjs} +1 -1
  16. package/dist/operation-executor.mjs +3 -3
  17. package/dist/preview-worker.mjs +1 -1
  18. package/dist/privacy-validator.mjs +49 -2
  19. package/dist/{query-By41gaKm.mjs → query-BYDcNKy2.mjs} +1869 -716
  20. package/dist/{run-ZQx7p4Rw.mjs → run-DQ9xyKSt.mjs} +4 -4
  21. package/dist/session-process.mjs +20 -0
  22. package/dist/{src-D_DfY--2.mjs → src-CKQlTA2Z.mjs} +297 -207
  23. package/dist/{src-3jvMvSQn.mjs → src-DeNJ8vOD.mjs} +535 -140
  24. package/dist/{src-BqtdFVq7.mjs → src-Do-qzuMn.mjs} +2329 -1786
  25. package/dist/store-database-worker.mjs +1 -1
  26. package/dist/web/assets/index-BgDdPL1c.js +172 -0
  27. package/dist/web/assets/index-DzOozqms.css +1 -0
  28. package/dist/web/index.html +3 -2
  29. package/package.json +2 -1
  30. package/skills/INDEX.md +6 -4
  31. package/skills/tacitt/INDEX.md +7 -6
  32. package/skills/tacitt/SKILL.md +11 -11
  33. package/skills/tacitt/references/INDEX.md +14 -11
  34. package/skills/tacitt/references/b-method.md +4 -4
  35. package/skills/tacitt/references/bootstrap.md +19 -5
  36. package/skills/tacitt/references/classification.md +4 -4
  37. package/skills/tacitt/references/commands.md +51 -34
  38. package/skills/tacitt/references/journey.md +7 -7
  39. package/skills/tacitt/references/parallel-review.md +35 -35
  40. package/skills/tacitt/references/send.md +15 -6
  41. package/skills/tacitt/scripts/INDEX.md +40 -37
  42. package/skills/tacitt/scripts/answer.mjs +91 -15
  43. package/skills/tacitt/scripts/journey-card-consolidation.mjs +1 -1
  44. package/skills/tacitt/scripts/journey-classification-metadata.mjs +75 -26
  45. package/skills/tacitt/scripts/journey-classification-progress.mjs +1 -2
  46. package/skills/tacitt/scripts/journey-classification-tasks.mjs +28 -8
  47. package/skills/tacitt/scripts/journey-classification.mjs +6 -1
  48. package/skills/tacitt/scripts/journey-consolidation.mjs +1 -0
  49. package/skills/tacitt/scripts/journey-panel-host.d.mts +2 -0
  50. package/skills/tacitt/scripts/journey-panel-host.mjs +25 -2
  51. package/skills/tacitt/scripts/journey-scan-timing.d.mts +5 -0
  52. package/skills/tacitt/scripts/journey-scan-timing.mjs +31 -4
  53. package/skills/tacitt/scripts/journey-state.d.mts +9 -1
  54. package/skills/tacitt/scripts/journey-state.mjs +95 -8
  55. package/skills/tacitt/scripts/journey-tasks.d.mts +1 -0
  56. package/skills/tacitt/scripts/journey-tasks.mjs +24 -9
  57. package/skills/tacitt/scripts/journey.d.mts +44 -31
  58. package/skills/tacitt/scripts/journey.mjs +34 -7
  59. package/skills/tacitt/scripts/privacy-validator.mjs +188 -11
  60. package/skills/tacitt/scripts/review.d.mts +7 -6
  61. package/skills/tacitt/scripts/review.mjs +922 -205
  62. package/dist/web/assets/index-B14e6_-z.js +0 -174
  63. package/dist/web/assets/index-CYJBq1px.css +0 -1
@@ -1,6 +1,6 @@
1
- import { S as readPrivateReviewPayload, at as windowsOwnerOnly, it as restrictToOwner, rt as openPrivateFile, tt as exclusiveLock, x as writePrivateReviewPayload } from "./src-3jvMvSQn.mjs";
2
- import { At as journeyTermSchema, It as termTypeSchema, Mt as reviewTermsResultSchema, Xt as ApplicationError, _ as semanticFindingSchema, b as validateBReview, c as textDigest, d as reviewAlignmentAudit, dt as nativeJourneySchema, er as isStaticCategory, ft as journeyGuidanceSchema, g as validateReviewAlignment, gt as journeyWorkSuggestionSchema, ht as journeyDescribeSchema, i as reviewBatchSchema, it as journeyDigestSchema, jt as reviewTermsInputSchema, kt as privacyTermSchema, l as validateReview, m as reviewAlignmentSchema, nn as reviewFamilies, o as reviewDigest, ot as journeyIdSchema, r as reviewBatchChunks, s as reviewSubmitSchema, sn as fileIdentitySchema, tn as familySelected, tr as staticCategories, u as findingReferenceKey, v as bContextEvidence, wn as createDetectionSummary } from "./locate-Cwv6WH8n.mjs";
3
- import { $ as validateDetectionSummary, $t as searchScopeDigest, At as obsoleteJourney, Bn as confirmedHideKey, Cn as journeyEventSchema, Cr as validatePreparationFacts, Ft as journeyGuidance, Gn as maskedHitsStale, Hn as confirmedHideTerms, In as bMaskedHits, It as journeyBatchReadSchema, J as validateInspectionFence, Jn as workDecisionDigest, Jt as hideBeginSchema, Kn as syncTermGroups, Kr as journeySendSchema, Kt as journeyContextQuerySchema, Lt as journeyReviewStorageQuerySchema, Mr as applyCardAnswers, Nt as attemptBudgetCodePoints, On as settleConfirmedHides, Ot as settleCorrections, Pt as validateAttemptAppend, Q as validateInspectionContribution, Qn as reviewCandidateResourceSchema, Qt as journeySearchSaveSchema, Rr as mergePrivacyTerms, Rt as applyClassificationProposal, Sn as journeyCommitSchema, Sr as preparationStartSchema, Tn as confirmedHideDeltaSchema, U as validateInspectionBatch, Un as dictionaryHides, Vr as staticSessionId, W as validateInspectionBatches, Wt as newJourneyWork, X as validateInspectionInputs, Xt as hidePublishSchema, Y as validateInspectionHead, Yn as workPrivacyDecisions, Yt as hideFinishSchema, Z as validateInspectionQuery, Zt as journeySearchResultSchema, _n as sessionReviewResourceSchema, _r as journeyInventorySchema, ai as withReadingProgress, an as journeyPlaceSchema, ar as scanAliasResourceSchema, bn as sessionUsefulnessResourceSchema, br as preparationBeginSchema, c as reconstructBFields, cn as reviewAttemptsQuerySchema, cr as scanSessionResourceSchema, d as resolveGroupSplit, dn as sessionReconcileResultSchema, en as journeyPolicyRespondSchema, er as reviewRepairResourceSchema, et as inspectionInputs, fi as stateSchema, fn as sessionReconciliationResourceSchema, g as semanticGroupKey, gn as sessionReviewQuerySchema, gr as journeyInventoryReadSchema, gt as manualUploadSchema, h as hideScopeDigest, ii as selectedWorks, in as journeyIsolateSchema, l as chooseCheapPlacement, ln as reviewAttemptsResultSchema, lr as scanGitSeedsId, mn as sessionRereadResourceSchema, ni as isSelectedWork, nn as journeyResourceQuerySchema, nr as bReviewDigest, o as bRereadDigest, on as sortingApplySchema, or as scanCanonicalResourceSchema, pr as validateScanExclusion, pt as manualUploadBindingSchema, q as validateInspectionClaim, qn as retireWorkDescription, qt as journeyContextSchema, ri as isSkippedWork, rn as journeyResourceSchema, rr as mergeBRepair, s as reconcileBSession, sn as reviewAttemptResourceSchema, t as scanBatchContextDigest, ti as isReadingComplete, tn as projectPreparationProgress, u as reviseGroupMembership, v as projectJourneyInventory, vn as sessionReviewStatusSchema, vr as journeyPreparationProgressSchema, vt as candidateSortAvailable, x as bReviewScope, xn as journeyCommitResultSchema, xr as preparationSettlementSchema, y as preparationScope, yn as sessionUsefulnessInputSchema, zn as confirmedHideGroupIds, zt as classificationCommitSchema } from "./src-BqtdFVq7.mjs";
1
+ import { C as writePrivateReviewPayload, ct as windowsOwnerOnly, it as exclusiveLock, ot as openPrivateFile, st as restrictToOwner, w as readPrivateReviewPayload } from "./src-DeNJ8vOD.mjs";
2
+ import { An as createDetectionSummary, Ft as emptyReviewCorrectionSchema, Ht as termTypeSchema, It as journeyTermSchema, Lt as reviewTermsInputSchema, Nt as privacyTermSchema, Pt as emptyReviewCorrectionResultSchema, Rt as reviewTermsResultSchema, S as validateBReview, a as reviewBatchChunks, b as bContextEvidence, bt as journeyWorkSuggestionSchema, c as reviewDigest, cn as reviewFamilies, d as validateReview, f as findingReferenceKey, g as reviewAlignmentSchema, gt as journeyGuidanceSchema, l as reviewSubmitSchema, lt as journeyIdSchema, mt as nativeJourneySchema, nn as ApplicationError, o as reviewBatchSchema, or as isStaticCategory, p as reviewAlignmentAudit, pn as fileIdentitySchema, sn as familySelected, sr as staticCategories, st as journeyDigestSchema, u as textDigest, v as validateReviewAlignment, y as semanticFindingSchema, yt as journeyDescribeSchema } from "./locate-D-ZaHpI0.mjs";
3
+ import { $ as validateDetectionSummary, $t as journeyPlaceSchema, Ai as stateSchema, At as validateAttemptAppend, Bn as journeySearchSaveSchema, Bt as newJourneyWork, Ci as withReadingProgress, Cn as scanAliasResourceSchema, Cr as preparationStateSchema, Dn as scanGitSeedsId, En as scanSessionResourceSchema, Fn as journeyInventoryReadSchema, Fr as assertJourneyPrivacyReady, Ft as classificationCommitSchema, Gt as hideFinishSchema, Hn as journeyCommitResultSchema, Ht as journeyContextQuerySchema, In as journeyInventorySchema, J as validateInspectionFence, Jr as obsoleteJourney, Jt as sendOutputQuerySchema, Kn as confirmedHideDeltaSchema, Kt as hidePublishSchema, Ln as journeyPreparationProgressSchema, Lr as reconcileReaderKeeps, Mt as journeyBatchReadSchema, Nn as projectPreparationProgress, Nt as journeyReviewStorageQuerySchema, Ot as settleCorrections, Pr as applyCardAnswers, Pt as applyClassificationProposal, Q as validateInspectionContribution, Qt as journeyIsolateSchema, Sr as preparationStartSchema, Tr as validatePreparationFacts, U as validateInspectionBatch, Un as journeyCommitSchema, Ut as journeyContextSchema, Vn as searchScopeDigest, W as validateInspectionBatches, Wn as journeyEventSchema, Wt as hideBeginSchema, X as validateInspectionInputs, Xr as sendReachedPassport, Xt as journeyResourceQuerySchema, Y as validateInspectionHead, Yn as settleConfirmedHides, Yr as sendOwesPassport, Yt as sendOutputSchema, Z as validateInspectionQuery, Zt as journeyResourceSchema, an as sessionReconcileResultSchema, bi as isSkippedWork, bn as bReviewDigest, br as preparationPermissionSchema, c as reconstructBFields, ci as journeySendSchema, cn as sessionRereadResourceSchema, d as resolveGroupSplit, dn as sessionReviewResourceSchema, ei as mergePrivacyTerms, en as sortingApplySchema, et as inspectionInputs, fn as sessionReviewStatusSchema, fr as maskedHitsStale, g as semanticGroupKey, gn as reviewCandidateResourceSchema, gr as workPrivacyDecisions, gt as manualUploadSchema, h as hideScopeDigest, hi as hasCompleteWorkDescription, hr as workDecisionDigest, jn as validateScanExclusion, jt as journeyGuidance, kt as attemptBudgetCodePoints, l as chooseCheapPlacement, lr as confirmedHideTerms, mn as sessionUsefulnessResourceSchema, mr as retireWorkDescription, nn as reviewAttemptsQuerySchema, o as bRereadDigest, on as sessionReconciliationResourceSchema, or as confirmedHideGroupIds, pi as sendSessionUploadSchema, pn as sessionUsefulnessInputSchema, pr as syncTermGroups, pt as manualUploadBindingSchema, q as validateInspectionClaim, qt as journeyPolicyRespondSchema, ri as staticSessionId, rn as reviewAttemptsResultSchema, rr as bMaskedHits, s as reconcileBSession, sr as confirmedHideKey, t as scanBatchContextDigest, tn as reviewAttemptResourceSchema, u as reviseGroupMembership, un as sessionReviewQuerySchema, ur as dictionaryHides, v as projectJourneyInventory, vi as isReadingComplete, vn as reviewRepairResourceSchema, vt as candidateSortAvailable, wn as scanCanonicalResourceSchema, wr as preparationUpdateSchema, x as bReviewScope, xi as selectedWorks, xn as mergeBRepair, xr as preparationSettlementSchema, y as preparationScope, yi as isSelectedWork, yr as preparationBeginSchema, zn as journeySearchResultSchema, zt as isolatedWorkId } from "./src-Do-qzuMn.mjs";
4
4
  import { constants } from "node:fs";
5
5
  import { z } from "zod";
6
6
  import { copyFile, link, lstat, mkdir, mkdtemp, open, readFile, realpath, rm, unlink } from "node:fs/promises";
@@ -346,9 +346,26 @@ function readJourney(db, journeyId) {
346
346
  if (typeof text !== "string") throw new ApplicationError("CORRUPTED_STATE");
347
347
  const state = JSON.parse(text);
348
348
  if (typeof state === "object" && state !== null && "formatVersion" in state && state.formatVersion !== 10) throw new StoreError("OBSOLETE_SCHEMA", "Stored native Journey state uses an obsolete candidate format and has been preserved.");
349
+ let value = reconcileReaderKeeps(nativeJourneySchema.parse(state));
350
+ if (value.outputVersion !== void 0 && value.outputVersion > 1) throw new StoreError("OBSOLETE_SCHEMA", "This journey requires a newer final writer.");
351
+ if (value.outputVersion !== 1 && value.phase !== "completed" && !obsoleteJourney(value)) value = {
352
+ ...value,
353
+ outputVersion: 1,
354
+ phase: value.phase === "ready" ? "review" : value.phase,
355
+ privacy: {
356
+ ...value.privacy,
357
+ packageApproval: null
358
+ },
359
+ works: value.works.map((work) => ({
360
+ ...work,
361
+ revision: work.revision + 1,
362
+ status: work.status === "ready" ? "review" : work.status,
363
+ artifact: null
364
+ }))
365
+ };
349
366
  return {
350
367
  text,
351
- value: nativeJourneySchema.parse(state)
368
+ value
352
369
  };
353
370
  });
354
371
  }
@@ -362,23 +379,29 @@ function insertCorrectionReport(db, resource) {
362
379
  }
363
380
  db.prepare("INSERT INTO journey_resources VALUES(?,?,?,?)").run(resource.journeyId, resource.kind, resource.id, json);
364
381
  }
365
- /**
366
- * A snapshot of an earlier release cannot render, so a new Journey may replace it, but never
367
- * one with a Send record or a sent Work: that Send stays for the person to finish.
368
- */
369
- /** A receipt or persisted session mutation intent means a Send may have reached Passport. */
382
+ /** Whether any Send of this Journey may have reached Passport (`sendReachedPassport`). */
370
383
  function reachedPassport(db, journeyId) {
371
384
  return db.prepare("SELECT json FROM journey_sends WHERE journey_id=?").all(journeyId).some((row) => {
372
385
  if (typeof row["json"] !== "string") throw new ApplicationError("CORRUPTED_STATE");
373
- return journeySendSchema.parse(JSON.parse(row["json"])).items.some((item) => item.receipt || item.previousReceipts.length || item.sessionUpload !== void 0 || item.sessionMembership || item.sessionAssembly);
386
+ return sendReachedPassport(journeySendSchema.parse(JSON.parse(row["json"])));
374
387
  });
375
388
  }
376
389
  /** An abandoned Journey gives way to a new one unless one of its Sends reached Passport. */
377
390
  function abandoned(db, current) {
378
391
  return current.abandonedAt !== void 0 && !reachedPassport(db, current.journeyId);
379
392
  }
393
+ /**
394
+ * A snapshot of an earlier release cannot render, so a new Journey replaces it unless its current
395
+ * Send still owes Passport something its next advance sends without the person
396
+ * (`sendOwesPassport`, ADR 0284, owner answer A): sent Works stay on Passport, and the new Journey
397
+ * reads every Work again. Replacement cancels its unfinished Sends.
398
+ */
380
399
  function replaceable(db, current) {
381
- return obsoleteJourney(current) && !current.works.some((work) => work.binding) && !db.prepare("SELECT 1 FROM journey_sends WHERE journey_id=? LIMIT 1").get(current.journeyId);
400
+ if (!obsoleteJourney(current)) return false;
401
+ const row = db.prepare("SELECT json FROM journey_sends JOIN journey_current_send ON journey_sends.id=journey_current_send.send_id WHERE journey_current_send.journey_id=?").get(current.journeyId);
402
+ if (!row) return true;
403
+ if (typeof row["json"] !== "string") throw new ApplicationError("CORRUPTED_STATE");
404
+ return !sendOwesPassport(current, journeySendSchema.parse(JSON.parse(row["json"])));
382
405
  }
383
406
  /** An addition counts only when one of its findings was assigned to a confirmed Hide category. */
384
407
  function joinedHideValues(db, state, delta) {
@@ -439,6 +462,7 @@ function commitJourney(db, input) {
439
462
  ...input,
440
463
  state: {
441
464
  ...input.state,
465
+ outputVersion: 1,
442
466
  privacy: {
443
467
  ...input.state.privacy,
444
468
  rememberedRules
@@ -453,7 +477,25 @@ function commitJourney(db, input) {
453
477
  state: current
454
478
  };
455
479
  }
480
+ if (input.expectedRevision === null && current && replaceable(db, current)) db.prepare("DELETE FROM journey_current_send WHERE journey_id=?").run(current.journeyId);
456
481
  if (input.state.journeyId !== input.event.journeyId) throw new ApplicationError("INVALID_ARGUMENTS");
482
+ if (input.closedReader) {
483
+ const hold = input.closedReader;
484
+ const unread = db.prepare(`SELECT 1 FROM journey_batches
485
+ WHERE id=? AND journey_id=? AND work_id=? AND input_revision=? AND review_ref_json IS NULL`);
486
+ if (hold.batchIds.some((id) => !unread.get(id, input.state.journeyId, hold.workId, hold.inputRevision))) throw new ApplicationError("REVISION_CONFLICT");
487
+ const save = db.prepare("INSERT OR IGNORE INTO journey_resources VALUES(?,?,?,?)");
488
+ for (const batchId of hold.batchIds) {
489
+ const fence = {
490
+ kind: "closed-reader",
491
+ journeyId: input.state.journeyId,
492
+ id: batchId + ":" + hold.packId,
493
+ packId: hold.packId,
494
+ batchId
495
+ };
496
+ save.run(fence.journeyId, fence.kind, fence.id, JSON.stringify(fence));
497
+ }
498
+ }
457
499
  if (input.state.abandonedAt && current?.journeyId === input.state.journeyId && !current.abandonedAt) {
458
500
  if (reachedPassport(db, current.journeyId)) throw new ApplicationError("INVALID_TRANSITION");
459
501
  db.prepare("DELETE FROM journey_current_send WHERE journey_id=?").run(current.journeyId);
@@ -2265,6 +2307,14 @@ function putSessionUsefulness(db, value) {
2265
2307
  const privateReviewHash = (value) => textDigest(JSON.stringify(value));
2266
2308
  //#endregion
2267
2309
  //#region packages/local-host/src/store/journey/terms/aliases.ts
2310
+ function scanAliasRequestKey(alias, requestKey) {
2311
+ const digest = textDigest(JSON.stringify([
2312
+ "scan-alias-v1",
2313
+ alias,
2314
+ requestKey
2315
+ ]));
2316
+ return `${digest.slice(0, 8)}-${digest.slice(8, 12)}-4${digest.slice(13, 16)}-8${digest.slice(17, 20)}-${digest.slice(20, 32)}`;
2317
+ }
2268
2318
  const scanAliasQuerySchema = z.object({ journeyId: z.uuid() }).strict();
2269
2319
  const scanAliasSelectionSchema = z.array(z.object({
2270
2320
  alias: scanAliasResourceSchema,
@@ -2288,7 +2338,8 @@ function selectScanAliases(db, input) {
2288
2338
  requireActiveBatch(state, canonical.header);
2289
2339
  if (canonical.header.digest !== alias.canonicalBatchDigest || target.header.digest !== alias.targetBatchDigest || canonical.header.workId !== target.header.workId || target.header.id !== alias.targetBatchId) throw new ApplicationError("CORRUPTED_STATE");
2290
2340
  if (!canonical.termPack) return [];
2291
- const packs = db.prepare(`SELECT p.payload_ref_json,p.request_hash,r.request_hash AS receipt_hash FROM journey_term_pack_batches m JOIN journey_term_packs p ON p.request_key=m.request_key LEFT JOIN journey_receipts r ON r.request_key=p.request_key AND r.journey_id=p.journey_id WHERE m.batch_id=? AND p.journey_id=? ORDER BY p.rowid`).all(canonical.header.id, input.journeyId).map((pack) => {
2341
+ const packs = db.prepare(`SELECT p.payload_ref_json,p.request_hash,r.request_hash AS receipt_hash FROM journey_term_pack_batches m JOIN journey_term_packs p ON p.request_key=m.request_key LEFT JOIN journey_receipts r ON r.request_key=p.request_key AND r.journey_id=p.journey_id WHERE m.batch_id=? AND p.journey_id=?
2342
+ AND NOT EXISTS (SELECT 1 FROM journey_resources w WHERE w.journey_id=p.journey_id AND w.kind='withdrawn-term-pack' AND w.id=m.batch_id||':'||json_extract(p.payload_ref_json,'$.packId')) ORDER BY p.rowid`).all(canonical.header.id, input.journeyId).map((pack) => {
2292
2343
  if (typeof pack["payload_ref_json"] !== "string" || typeof pack["receipt_hash"] !== "string") throw new ApplicationError("CORRUPTED_STATE");
2293
2344
  const reference = termPackReferenceSchema.parse(JSON.parse(pack["payload_ref_json"]));
2294
2345
  if (reference.requestHash !== pack["request_hash"] || reference.requestHash !== pack["receipt_hash"]) throw new ApplicationError("CORRUPTED_STATE");
@@ -2307,6 +2358,107 @@ function selectScanAliases(db, input) {
2307
2358
  });
2308
2359
  }
2309
2360
  //#endregion
2361
+ //#region packages/local-host/src/store/journey/terms/reopen.ts
2362
+ /** A permanent fence for an accepted pack whose empty coverage was withdrawn. */
2363
+ function withdrawnTermPack(db, journeyId, batchId, packId) {
2364
+ return Boolean(db.prepare("SELECT 1 FROM journey_resources WHERE journey_id=? AND kind='withdrawn-term-pack' AND id=?").get(journeyId, batchId + ":" + packId));
2365
+ }
2366
+ function reopenEmptyReview(db, input) {
2367
+ const state = requireReviewState(db, input.journeyId);
2368
+ if (state.scope.status !== "confirmed" || ![
2369
+ "processing",
2370
+ "review",
2371
+ "ready"
2372
+ ].includes(state.phase) || db.prepare("SELECT 1 FROM journey_current_send WHERE journey_id=?").get(input.journeyId)) throw new ApplicationError("INVALID_TRANSITION");
2373
+ const row = db.prepare("SELECT * FROM journey_batches WHERE journey_id=? AND id=?").get(input.journeyId, input.batchId);
2374
+ if (!row) throw new ApplicationError("UNKNOWN_ITEM");
2375
+ const batch = parseStoredBatch(row);
2376
+ const work = requireActiveBatch(state, batch.header);
2377
+ if (work.access === "drop" || batch.header.digest !== input.batchDigest) throw new ApplicationError("REVISION_CONFLICT");
2378
+ if (input.kind === "apply" && withdrawnTermPack(db, input.journeyId, input.batchId, input.expectedPackId)) return {
2379
+ status: "replayed",
2380
+ packId: input.expectedPackId
2381
+ };
2382
+ const ref = batch.termPack;
2383
+ if (!ref || input.kind === "apply" && ref.packId !== input.expectedPackId) throw new ApplicationError("REVISION_CONFLICT");
2384
+ if (db.prepare("SELECT 1 FROM journey_resources WHERE journey_id=? AND kind='scan-alias' AND id=?").get(input.journeyId, input.batchId)) throw new ApplicationError("INVALID_SELECTION");
2385
+ const aliases = db.prepare(`SELECT b.*,a.json AS alias_json FROM journey_batches b ${activeBatchJoin}
2386
+ JOIN journey_resources a ON a.journey_id=b.journey_id AND a.kind='scan-alias' AND a.id=b.id
2387
+ WHERE b.journey_id=? AND json_extract(a.json,'$.canonicalBatchId')=?`).all(input.journeyId, input.batchId).map((row) => {
2388
+ if (typeof row["alias_json"] !== "string") throw new ApplicationError("CORRUPTED_STATE");
2389
+ const alias = scanAliasResourceSchema.parse(JSON.parse(row["alias_json"]));
2390
+ const stored = parseStoredBatch(row);
2391
+ const packId = scanAliasRequestKey(alias, ref.idempotencyKey);
2392
+ if (stored.termPack && stored.termPack.packId !== packId) throw new ApplicationError("REVISION_CONFLICT");
2393
+ return {
2394
+ batch: stored,
2395
+ packId
2396
+ };
2397
+ });
2398
+ const attempts = [{
2399
+ batch,
2400
+ packId: ref.packId
2401
+ }, ...aliases];
2402
+ const targets = attempts.map((a) => a.batch).filter((b) => b.termPack);
2403
+ for (const target of targets) {
2404
+ const pointer = target.termPack;
2405
+ if (!pointer || target.header.workId !== work.id) throw new ApplicationError("CORRUPTED_STATE");
2406
+ const receipt = db.prepare(`SELECT p.request_hash, r.request_hash AS confirmed_hash, json_extract(p.json,'$.terms.kept') AS kept
2407
+ FROM journey_term_packs p JOIN journey_receipts r ON r.journey_id=p.journey_id AND r.request_key=p.request_key
2408
+ WHERE p.journey_id=? AND p.request_key=?`).get(input.journeyId, pointer.idempotencyKey);
2409
+ if (!receipt || receipt["request_hash"] !== pointer.requestHash || receipt["confirmed_hash"] !== pointer.requestHash) throw new ApplicationError("CORRUPTED_STATE");
2410
+ if (receipt["kept"] !== 0 || db.prepare("SELECT 1 FROM journey_term_occurrences WHERE journey_id=? AND batch_id=? LIMIT 1").get(input.journeyId, target.header.id)) throw new ApplicationError("INVALID_SELECTION");
2411
+ }
2412
+ if (input.kind === "inspect") return {
2413
+ status: "eligible",
2414
+ packId: ref.packId
2415
+ };
2416
+ if (work.completedBatches < targets.length) throw new ApplicationError("CORRUPTED_STATE");
2417
+ for (const { batch: target, packId } of attempts) {
2418
+ const id = target.header.id + ":" + packId;
2419
+ db.prepare("INSERT INTO journey_resources VALUES(?,?,?,?)").run(input.journeyId, "withdrawn-term-pack", id, JSON.stringify({
2420
+ kind: "withdrawn-term-pack",
2421
+ id,
2422
+ journeyId: input.journeyId,
2423
+ batchId: target.header.id,
2424
+ packId
2425
+ }));
2426
+ db.prepare("UPDATE journey_batches SET review_ref_json=NULL,review_sequence=NULL WHERE id=?").run(target.header.id);
2427
+ }
2428
+ if (commitJourney(db, {
2429
+ state: {
2430
+ ...state,
2431
+ readingComplete: false,
2432
+ phase: "processing",
2433
+ privacy: {
2434
+ ...state.privacy,
2435
+ packageApproval: null,
2436
+ confirmedAnswersRevision: null
2437
+ },
2438
+ works: state.works.map((item) => item.id === work.id ? {
2439
+ ...item,
2440
+ status: "reading",
2441
+ revision: item.revision + 1,
2442
+ completedBatches: item.completedBatches - targets.length,
2443
+ artifact: null
2444
+ } : item)
2445
+ },
2446
+ expectedRevision: state.revision,
2447
+ requestKey: input.idempotencyKey,
2448
+ requestHash: textDigest(JSON.stringify(input)),
2449
+ event: {
2450
+ journeyId: input.journeyId,
2451
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2452
+ kind: "batch",
2453
+ source: "conversation"
2454
+ }
2455
+ }).status !== "applied") throw new ApplicationError("REVISION_CONFLICT");
2456
+ return {
2457
+ status: "reopened",
2458
+ packId: ref.packId
2459
+ };
2460
+ }
2461
+ //#endregion
2310
2462
  //#region packages/local-host/src/store/journey/finding-groups.ts
2311
2463
  function findingPreference(db, key) {
2312
2464
  const choice = db.prepare("SELECT choice FROM journey_preferences WHERE group_key=?").get(key)?.["choice"];
@@ -2442,6 +2594,7 @@ function selectTerms(db, query) {
2442
2594
  const batches = input.units.map((unit) => {
2443
2595
  const row = db.prepare("SELECT * FROM journey_batches WHERE journey_id=? AND id=?").get(input.journeyId, unit.batchId);
2444
2596
  if (!row) throw new ApplicationError("UNKNOWN_ITEM");
2597
+ if (withdrawnTermPack(db, input.journeyId, unit.batchId, input.packId)) throw new ApplicationError("REVISION_CONFLICT");
2445
2598
  const batch = parseStoredBatch(row);
2446
2599
  const alias = db.prepare("SELECT json FROM journey_resources WHERE journey_id=? AND kind='scan-alias' AND id=?").get(input.journeyId, unit.batchId);
2447
2600
  if (alias) {
@@ -2449,7 +2602,9 @@ function selectTerms(db, query) {
2449
2602
  const source = db.prepare("SELECT b.review_ref_json FROM journey_batches b WHERE b.journey_id=? AND b.id=json_extract(?,'$.canonicalBatchId')").get(input.journeyId, alias["json"]);
2450
2603
  if (!source || source["review_ref_json"] === null) throw new ApplicationError("SCOPE_NOT_READY");
2451
2604
  }
2452
- requireActiveBatch(state, batch.header);
2605
+ const work = requireActiveBatch(state, batch.header);
2606
+ if (work.access === "drop") throw new ApplicationError("REVISION_CONFLICT");
2607
+ if (work.reviewInterruptions?.some((hold) => hold.inputRevision === work.inputRevision && hold.batchIds.includes(unit.batchId)) || db.prepare("SELECT 1 FROM journey_resources WHERE journey_id=? AND kind='closed-reader' AND id=?").get(state.journeyId, unit.batchId + ":" + input.packId)) throw new ApplicationError("REVISION_CONFLICT");
2453
2608
  if (batch.header.digest !== unit.batchDigest) throw new ApplicationError("REVISION_CONFLICT");
2454
2609
  return batch;
2455
2610
  });
@@ -2613,7 +2768,8 @@ const termReuseSelectionSchema = z.object({
2613
2768
  */
2614
2769
  function selectTermReuse(db, input) {
2615
2770
  const state = requireReviewState(db, input.journeyId);
2616
- const targets = db.prepare(`SELECT b.* FROM journey_batches b ${activeBatchJoin} WHERE b.journey_id=? AND b.work_id=? AND b.review_ref_json IS NULL ${notScanAlias} AND json_extract(b.header_json,'\$.unit.sessionId')=? ORDER BY b.rowid`).all(input.journeyId, input.workId, input.sessionId).map(parseStoredBatch).filter((batch) => isLegacyBatch(db, batch.header));
2771
+ const targets = db.prepare(`SELECT b.* FROM journey_batches b ${activeBatchJoin} WHERE b.journey_id=? AND b.work_id=? AND b.review_ref_json IS NULL ${notScanAlias}
2772
+ AND NOT EXISTS (SELECT 1 FROM journey_resources wr WHERE wr.journey_id=b.journey_id AND wr.kind='withdrawn-term-pack' AND json_extract(wr.json,'$.batchId')=b.id) AND json_extract(b.header_json,'\$.unit.sessionId')=? ORDER BY b.rowid`).all(input.journeyId, input.workId, input.sessionId).map(parseStoredBatch).filter((batch) => isLegacyBatch(db, batch.header));
2617
2773
  if (!targets.length) return {
2618
2774
  targets,
2619
2775
  origins: []
@@ -3557,7 +3713,7 @@ function readInventoryFamilies(db, state) {
3557
3713
  //#endregion
3558
3714
  //#region packages/local-host/src/store/journey/resources.ts
3559
3715
  function saveJourneyResource(db, resource) {
3560
- if (resource.kind === "scan-canonical" || resource.kind === "scan-alias" || resource.kind === "b-attempt" || resource.kind === "preparation-start" || resource.kind === "preparation-settlement" || resource.kind === "b-usefulness" || resource.kind === "b-candidate" || resource.kind === "b-repair" || resource.kind === "b-session" || resource.kind === "b-reread" || resource.kind === "b-reconciliation" || resource.kind === "correction-report") throw new ApplicationError("INVALID_TRANSITION");
3716
+ if (resource.kind === "closed-reader" || resource.kind === "withdrawn-term-pack" || resource.kind === "scan-canonical" || resource.kind === "scan-alias" || resource.kind === "b-attempt" || resource.kind === "preparation-start" || resource.kind === "preparation-settlement" || resource.kind === "b-usefulness" || resource.kind === "b-candidate" || resource.kind === "b-repair" || resource.kind === "b-session" || resource.kind === "b-reread" || resource.kind === "b-reconciliation" || resource.kind === "correction-report") throw new ApplicationError("INVALID_TRANSITION");
3561
3717
  const state = readJourney(db, resource.journeyId);
3562
3718
  if (!state?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
3563
3719
  if (state.pendingHideId && (resource.kind === "base" || resource.kind === "scan-session" || resource.kind === "static-session" || resource.kind === "output")) throw new ApplicationError("SCOPE_NOT_READY");
@@ -3644,6 +3800,64 @@ function readJourneyResources(db, input) {
3644
3800
  }
3645
3801
  //#endregion
3646
3802
  //#region packages/local-host/src/store/journey/preparation.ts
3803
+ function projection(fields) {
3804
+ const sql = `SELECT state_json ->> '$.formatVersion' AS format, ${fields.map((key) => `state_json -> '$.${key}' AS "${key}"`).join(",")} FROM journey_runs WHERE id=?`;
3805
+ return (db, journeyId) => {
3806
+ const row = db.prepare(sql).get(journeyId);
3807
+ if (!row) return null;
3808
+ if (row["format"] !== 10) throw new StoreError("OBSOLETE_SCHEMA", "Stored Journey preparation format is obsolete.");
3809
+ const result = {};
3810
+ for (const key of fields) {
3811
+ const value = row[key];
3812
+ if (typeof value === "string") result[key] = JSON.parse(value);
3813
+ }
3814
+ return result;
3815
+ };
3816
+ }
3817
+ const selectPermission = projection(preparationPermissionSchema.keyof().options);
3818
+ const selectState = projection(preparationStateSchema.keyof().options);
3819
+ function readPreparationPermission(db, journeyId) {
3820
+ return preparationPermissionSchema.nullable().parse(selectPermission(db, journeyId));
3821
+ }
3822
+ function readPreparationState(db, journeyId) {
3823
+ return preparationStateSchema.nullable().parse(selectState(db, journeyId));
3824
+ }
3825
+ /** Merge the small set of changed Work rows into the current state under its revision fence.
3826
+ * The normal commit owns its receipts, input generations, hide and correction invalidation. */
3827
+ function updatePreparation(db, input) {
3828
+ const current = readJourney(db);
3829
+ if (!current || current.journeyId !== input.journeyId) throw new ApplicationError("REVISION_CONFLICT");
3830
+ const replacements = new Map(input.works.map((work) => [work.id, work]));
3831
+ if (current.revision === input.expectedRevision && (replacements.size !== input.works.length || input.works.some((work) => !current.works.some((item) => item.id === work.id)))) throw new ApplicationError("INVALID_ARGUMENTS");
3832
+ const result = commitJourney(db, {
3833
+ expectedRevision: input.expectedRevision,
3834
+ requestKey: input.requestKey,
3835
+ requestHash: textDigest(JSON.stringify(input)),
3836
+ event: {
3837
+ journeyId: input.journeyId,
3838
+ kind: input.kind,
3839
+ source: "system",
3840
+ at: input.at
3841
+ },
3842
+ state: {
3843
+ ...current,
3844
+ works: current.works.map((work) => replacements.get(work.id) ?? work),
3845
+ phase: input.phase,
3846
+ readingComplete: input.readingComplete,
3847
+ readingStartedAt: input.readingStartedAt,
3848
+ privacy: input.clearPackageApproval ? {
3849
+ ...current.privacy,
3850
+ packageApproval: null
3851
+ } : current.privacy
3852
+ }
3853
+ });
3854
+ const state = readPreparationState(db, result.state.journeyId);
3855
+ if (!state) throw new ApplicationError("CORRUPTED_STATE");
3856
+ return {
3857
+ status: result.status,
3858
+ state
3859
+ };
3860
+ }
3647
3861
  function readPreparationReceipts(db, journeyId) {
3648
3862
  const starts = [], settlements = [];
3649
3863
  for (const row of db.prepare("SELECT kind,id,json FROM journey_resources WHERE journey_id=? AND kind IN ('preparation-start','preparation-settlement')").all(journeyId)) {
@@ -3665,9 +3879,9 @@ function activeScope(db, input, starting) {
3665
3879
  if (state.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
3666
3880
  const work = state.works.find((item) => item.id === input.workId);
3667
3881
  if (!work || input.inputRevision !== work.inputRevision || input.snapshotId !== state.snapshotId) throw new ApplicationError("REVISION_CONFLICT");
3668
- const plan = readInventoryPlan(db, state);
3882
+ const { plan, families } = readInventoryFamilies(db, state);
3669
3883
  if (!plan.approval.inventory.complete) throw new ApplicationError("CORRUPTED_STATE");
3670
- return preparationScope(plan.approval.inventory, work);
3884
+ return preparationScope(plan.approval.inventory, work, families);
3671
3885
  }
3672
3886
  function insert(db, record) {
3673
3887
  db.prepare("INSERT INTO journey_resources VALUES(?,?,?,?)").run(record.journeyId, record.kind, record.id, JSON.stringify(record));
@@ -3738,7 +3952,16 @@ function readJourneyPreparationProgress(db, input) {
3738
3952
  const state = readFencedJourney(db, input);
3739
3953
  if (!state.consent || !state.snapshotId) return null;
3740
3954
  const { starts, settlements } = readPreparationReceipts(db, state.journeyId);
3741
- return projectPreparationProgress(state, readInventoryPlan(db, state), readCaptures(db, state.journeyId), starts, settlements);
3955
+ const captures = db.prepare(`SELECT id, json_extract(json,'$.capture.partId') AS partId,
3956
+ json_extract(json,'$.capture.facts.byteLength') AS bytes FROM journey_resources
3957
+ WHERE journey_id=? AND kind='capture'`).all(state.journeyId).map(({ id, partId, bytes }) => {
3958
+ if (id !== partId || typeof partId !== "string" || typeof bytes !== "number" || !Number.isSafeInteger(bytes) || bytes < 0) throw new ApplicationError("CORRUPTED_STATE");
3959
+ return {
3960
+ partId,
3961
+ bytes
3962
+ };
3963
+ });
3964
+ return projectPreparationProgress(state, readInventoryFamilies(db, state).families, captures, starts, settlements);
3742
3965
  }
3743
3966
  //#endregion
3744
3967
  //#region packages/local-host/src/store/journey/hide.ts
@@ -4544,14 +4767,6 @@ function finishJourneyPlacement(db, packet) {
4544
4767
  //#endregion
4545
4768
  //#region packages/local-host/src/store/journey/isolation.ts
4546
4769
  const reason = " – couldn't prepare";
4547
- /** Stable blocked Work IDs per parent, so later failing roots join one instead of multiplying. */
4548
- function isolatedWorkId(parentId, ordinal) {
4549
- const bytes = createHash("sha256").update(`isolated-roots:${parentId}:${ordinal}`).digest().subarray(0, 16);
4550
- bytes[6] = (bytes[6] ?? 0) & 15 | 64;
4551
- bytes[8] = (bytes[8] ?? 0) & 63 | 128;
4552
- const hex = bytes.toString("hex");
4553
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
4554
- }
4555
4770
  /**
4556
4771
  * Moves identified failing roots out of a pending multi-root Work into its visible blocked
4557
4772
  * Work, carrying their public-safe codes; the parent returns to pending for the rest.
@@ -4595,8 +4810,15 @@ function isolateJourneyRoots(db, input) {
4595
4810
  })),
4596
4811
  blocks: /* @__PURE__ */ new Map([[targetId, [...codes]]])
4597
4812
  });
4813
+ const classified = !existing && parent.classificationRecommendation && hasCompleteWorkDescription(parent) && !parent.suggestion ? {
4814
+ description: parent.description,
4815
+ classificationRecommendation: parent.classificationRecommendation,
4816
+ reason: parent.classificationRecommendation.reason,
4817
+ projectType: parent.projectType?.origin === "model" ? parent.projectType : null
4818
+ } : {};
4598
4819
  const works = next.works.map((work) => work.id === targetId ? {
4599
4820
  ...work,
4821
+ ...classified,
4600
4822
  ...access
4601
4823
  } : work);
4602
4824
  return commitJourney(db, commit(input, {
@@ -4724,9 +4946,197 @@ function finishJourneyDescription(db, packet) {
4724
4946
  }
4725
4947
  //#endregion
4726
4948
  //#region packages/local-host/src/store/journey/send.ts
4949
+ /** Run the privacy fence against the store's current immutable row on every request.
4950
+ * Only the approved output and this Work's frozen upload identities cross the worker boundary. */
4951
+ function readSendOutput(db, input) {
4952
+ const { journeyId, workId, revision } = input;
4953
+ const state = readJourney(db, journeyId);
4954
+ if (!state?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
4955
+ if (state.phase === "completed" && state.outputVersion !== 1) throw new ApplicationError("SCOPE_NOT_READY");
4956
+ assertJourneyPrivacyReady(state, true);
4957
+ if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
4958
+ const work = state.works.find((item) => item.id === workId);
4959
+ if (!work?.artifact || work.revision !== revision || work.artifact.revision !== revision) throw new ApplicationError("REVISION_CONFLICT");
4960
+ const [output] = readJourneyResources(db, {
4961
+ journeyId,
4962
+ kind: "output",
4963
+ id: `${workId}:${revision}`
4964
+ });
4965
+ if (output?.kind !== "output" || output.workId !== workId || output.revision !== revision || output.primary.sha256 !== work.artifact.sha256) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
4966
+ if (!input.sessions) return {
4967
+ output,
4968
+ identities: []
4969
+ };
4970
+ const [base] = readJourneyResources(db, {
4971
+ journeyId,
4972
+ kind: "base",
4973
+ id: `${workId}:${output.finalMaskLedger.inputRevision}`
4974
+ });
4975
+ if (base?.kind !== "base" || base.workId !== workId || base.inputRevision !== output.finalMaskLedger.inputRevision || base.primary.sha256 !== output.finalMaskLedger.basePrimarySha256) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
4976
+ const plan = readInventoryPlan(db, state);
4977
+ const sessions = new Map(plan.approval.inventory.sessions.map((session) => [session.id, session]));
4978
+ const groups = /* @__PURE__ */ new Map();
4979
+ const identities = base.sessions.map((binding) => {
4980
+ const session = sessions.get(binding.applicationSessionId);
4981
+ const part = session?.parts[0];
4982
+ const lastAt = session?.lastInteractionAt ?? session?.createdAt;
4983
+ if (!session || !part || !lastAt || !Number.isFinite(Date.parse(lastAt))) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
4984
+ const native = part.nativeEvidence?.identity;
4985
+ if (native?.sessionId?.trim() && native.role !== "unknown" && (native.role !== "worker" || native.agentId?.trim() && session.harness === "claude")) {
4986
+ const key = JSON.stringify([
4987
+ session.harness,
4988
+ native.sessionId,
4989
+ native.role === "worker" ? native.agentId : null
4990
+ ]);
4991
+ const group = groups.get(key) ?? [];
4992
+ group.push(binding.archiveSessionId);
4993
+ groups.set(key, group);
4994
+ }
4995
+ const nativeId = part.nativeEvidence?.identity?.sessionId ?? session.sourceSessionId;
4996
+ const path = part.harnessRelativePath ?? "";
4997
+ if (!path && !nativeId?.trim() || isAbsolute(path) || path.includes("\\") || path.split("/").some((segment) => segment === ".." || segment === ".")) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
4998
+ return {
4999
+ archiveSessionId: binding.archiveSessionId,
5000
+ relativePath: path,
5001
+ lastAt: new Date(lastAt).toISOString()
5002
+ };
5003
+ });
5004
+ const duplicates = [...groups.values()].filter((group) => group.length > 1);
5005
+ if (!duplicates.length) return {
5006
+ output,
5007
+ identities
5008
+ };
5009
+ const duplicateIds = new Set(duplicates.flat());
5010
+ const owners = new Map(base.sessions.map((s) => [s.archiveSessionId, s.applicationSessionId]));
5011
+ const sources = base.sourceBindings.flatMap((binding) => {
5012
+ if (!duplicateIds.has(binding.sessionId) || binding.bundleId !== base.bundleId || owners.get(binding.sessionId) !== binding.applicationSessionId) return [];
5013
+ const part = sessions.get(binding.applicationSessionId)?.parts.find((p) => p.id === binding.sourcePartId);
5014
+ return part ? [{
5015
+ session: binding.sessionId,
5016
+ source: binding.bundleSourceId,
5017
+ part
5018
+ }] : [];
5019
+ });
5020
+ const outputs = state.works.filter(isSelectedWork).flatMap((w) => w.artifact ? [{
5021
+ workId: w.id,
5022
+ revision: w.revision,
5023
+ sha256: w.artifact.sha256,
5024
+ bundleId: w.artifact.bundleId
5025
+ }] : []);
5026
+ const approval = state.privacy.packageApproval;
5027
+ if (!approval) throw new ApplicationError("CONFIRMATION_REQUIRED");
5028
+ return {
5029
+ output,
5030
+ identities,
5031
+ extension: {
5032
+ groups: duplicates,
5033
+ sources,
5034
+ outputs,
5035
+ approval
5036
+ }
5037
+ };
5038
+ }
5039
+ const sendCurrentSchema = z.object({
5040
+ journeyId: journeyIdSchema,
5041
+ sendId: journeyIdSchema,
5042
+ revision: z.number().int().nonnegative()
5043
+ }).strict();
5044
+ function isCurrentSend(db, input) {
5045
+ return !!db.prepare(`SELECT 1 FROM journey_runs r JOIN journey_current_send s ON s.journey_id=r.id
5046
+ JOIN journey_current c ON c.journey_id=r.id AND c.singleton=1
5047
+ WHERE r.id=? AND r.revision=? AND s.send_id=?`).get(input.journeyId, input.revision, input.sendId);
5048
+ }
5049
+ const transferCountSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
5050
+ const sendTransferSchema = z.object({
5051
+ journeyId: journeyIdSchema,
5052
+ sendId: journeyIdSchema,
5053
+ workId: journeyIdSchema,
5054
+ expectedJourneyRevision: transferCountSchema,
5055
+ expectedSendRevision: transferCountSchema,
5056
+ storedSessions: transferCountSchema.optional(),
5057
+ sessionUpload: sendSessionUploadSchema,
5058
+ requestKey: journeyIdSchema,
5059
+ at: z.iso.datetime()
5060
+ }).strict();
5061
+ const sendTransferResultSchema = z.discriminatedUnion("status", [z.object({
5062
+ status: z.literal("applied"),
5063
+ revision: transferCountSchema,
5064
+ lastEvent: transferCountSchema,
5065
+ sendRevision: transferCountSchema
5066
+ }).strict(), z.object({ status: z.enum(["stale", "replayed"]) }).strict()]);
5067
+ /** Update only transfer fields in the Send document and stamp its Journey revision/event. */
5068
+ function checkpointJourneySendTransfer(db, input) {
5069
+ const requestHash = createHash("sha256").update(JSON.stringify(input)).digest("hex");
5070
+ const receipt = db.prepare("SELECT request_hash,journey_id FROM journey_receipts WHERE request_key=?").get(input.requestKey);
5071
+ if (receipt) {
5072
+ if (receipt["request_hash"] !== requestHash || receipt["journey_id"] !== input.journeyId) throw new ApplicationError("OPERATION_CONFLICT");
5073
+ return { status: "replayed" };
5074
+ }
5075
+ const row = db.prepare(`SELECT r.revision,r.state_json ->> '$.formatVersion' AS formatVersion,
5076
+ r.state_json ->> '$.phase' AS phase,
5077
+ json_type(r.state_json,'$.consent') AS consentType,
5078
+ r.state_json ->> '$.pendingHideId' AS pendingHideId,
5079
+ json_type(r.state_json,'$.abandonedAt') AS abandonedType,s.json
5080
+ FROM journey_runs r
5081
+ JOIN journey_current c ON c.singleton=1 AND c.journey_id=r.id
5082
+ JOIN journey_current_send cs ON cs.journey_id=r.id AND cs.send_id=?
5083
+ JOIN journey_sends s ON s.journey_id=r.id AND s.id=cs.send_id
5084
+ WHERE r.id=?`).get(input.sendId, input.journeyId);
5085
+ if (!row) return { status: "stale" };
5086
+ const revisionValue = row["revision"];
5087
+ if (typeof revisionValue !== "number" || !Number.isSafeInteger(revisionValue) || revisionValue < 0) throw new ApplicationError("CORRUPTED_STATE");
5088
+ if (revisionValue !== input.expectedJourneyRevision) return { status: "stale" };
5089
+ if (row["formatVersion"] !== 10) throw new StoreError("OBSOLETE_SCHEMA", "Stored native Journey state uses an obsolete candidate format and has been preserved.");
5090
+ if (row["consentType"] !== "object") throw new ApplicationError("CONFIRMATION_REQUIRED");
5091
+ if (row["phase"] !== "sending") throw new ApplicationError(row["phase"] === "paused" ? "OPERATION_CANCELED" : "INVALID_TRANSITION");
5092
+ if (row["pendingHideId"] !== null && row["pendingHideId"] !== void 0) throw new ApplicationError("REVISION_CONFLICT");
5093
+ if (row["abandonedType"] !== null && row["abandonedType"] !== void 0) throw new ApplicationError("INVALID_TRANSITION");
5094
+ if (typeof row["json"] !== "string") throw new ApplicationError("CORRUPTED_STATE");
5095
+ const send = journeySendSchema.parse(JSON.parse(row["json"]));
5096
+ if (send.revision !== input.expectedSendRevision) return { status: "stale" };
5097
+ if (!send.subject || !send.confirmedAt) throw new ApplicationError("CONFIRMATION_REQUIRED");
5098
+ if (send.status === "paused") throw new ApplicationError("OPERATION_CANCELED");
5099
+ if (!["sending", "waiting"].includes(send.status)) throw new ApplicationError("INVALID_TRANSITION");
5100
+ const index = send.items.findIndex((item) => item.workId === input.workId);
5101
+ if (index < 0) throw new ApplicationError("INVALID_SELECTION");
5102
+ const item = send.items[index];
5103
+ if (!item) throw new ApplicationError("CORRUPTED_STATE");
5104
+ if (item.status !== "uploading") throw new ApplicationError("INVALID_TRANSITION");
5105
+ const revision = revisionValue + 1;
5106
+ const sendRevision = send.revision + 1;
5107
+ const next = journeySendSchema.parse({
5108
+ ...send,
5109
+ revision: sendRevision,
5110
+ items: send.items.map((candidate, candidateIndex) => candidateIndex === index ? {
5111
+ ...candidate,
5112
+ sessionUpload: input.sessionUpload,
5113
+ ...input.storedSessions === void 0 ? {} : { storedSessions: input.storedSessions }
5114
+ } : candidate)
5115
+ });
5116
+ if (db.prepare("UPDATE journey_sends SET json=? WHERE journey_id=? AND id=?").run(JSON.stringify(next), input.journeyId, input.sendId).changes !== 1) throw new ApplicationError("REVISION_CONFLICT");
5117
+ const inserted = db.prepare("INSERT INTO journey_events(journey_id,event_json) VALUES(?,?)").run(input.journeyId, JSON.stringify({
5118
+ journeyId: input.journeyId,
5119
+ revision,
5120
+ kind: "send",
5121
+ source: "system",
5122
+ at: input.at
5123
+ }));
5124
+ const lastEvent = Number(inserted.lastInsertRowid);
5125
+ if (!Number.isSafeInteger(lastEvent) || lastEvent < 1) throw new ApplicationError("CORRUPTED_STATE");
5126
+ if (db.prepare(`UPDATE journey_runs SET revision=?,state_json=json_set(
5127
+ state_json,'$.revision',?,'$.lastEvent',?) WHERE id=? AND revision=?`).run(revision, revision, lastEvent, input.journeyId, input.expectedJourneyRevision).changes !== 1) throw new ApplicationError("REVISION_CONFLICT");
5128
+ db.prepare("INSERT INTO journey_receipts VALUES(?,?,?,?)").run(input.requestKey, requestHash, input.journeyId, revision);
5129
+ return {
5130
+ status: "applied",
5131
+ revision,
5132
+ lastEvent,
5133
+ sendRevision
5134
+ };
5135
+ }
4727
5136
  const sendLoadSchema = z.object({
4728
5137
  journeyId: journeyIdSchema,
4729
- sendId: journeyIdSchema.optional()
5138
+ sendId: journeyIdSchema.optional(),
5139
+ observedRevision: z.number().int().nonnegative().optional()
4730
5140
  }).strict();
4731
5141
  const sendSnapshotSchema = z.object({
4732
5142
  journey: nativeJourneySchema,
@@ -4747,6 +5157,7 @@ const sendCommitResultSchema = sendSnapshotSchema.extend({ status: z.enum([
4747
5157
  "stale"
4748
5158
  ]) });
4749
5159
  function loadJourneySend(db, input) {
5160
+ if (input.observedRevision !== void 0 && !input.sendId && db.prepare("SELECT 1 AS same FROM journey_runs WHERE id=? AND revision=?").get(input.journeyId, input.observedRevision)) return null;
4750
5161
  const journey = readJourney(db, input.journeyId);
4751
5162
  if (!journey) return null;
4752
5163
  const row = input.sendId ? db.prepare("SELECT json FROM journey_sends WHERE journey_id=? AND id=?").get(input.journeyId, input.sendId) : db.prepare("SELECT json FROM journey_sends JOIN journey_current_send ON journey_sends.id=journey_current_send.send_id WHERE journey_current_send.journey_id=?").get(input.journeyId);
@@ -4784,6 +5195,7 @@ function commitJourneySend(db, input) {
4784
5195
  ...input.state,
4785
5196
  revision
4786
5197
  });
5198
+ if (!prior && obsoleteJourney(current.journey)) throw new ApplicationError("JOURNEY_OBSOLETE");
4787
5199
  const skipped = (workId) => current.journey.works.some((work) => work.id === workId && isSkippedWork(work));
4788
5200
  const finished = send.status === "completed" && send.omitted.every((work) => [
4789
5201
  "drop",
@@ -4858,7 +5270,10 @@ function admitScanSession(db, input) {
4858
5270
  const state = readJourney(db, input.resource.journeyId);
4859
5271
  if (!state) throw new ApplicationError("UNKNOWN_ITEM");
4860
5272
  return {
4861
- state,
5273
+ state: preparationStateSchema.strip().parse({
5274
+ ...state,
5275
+ works: state.works.filter((work) => work.id === input.resource.workId)
5276
+ }),
4862
5277
  priors: input.headers.map((header) => admitReviewBatch(db, header))
4863
5278
  };
4864
5279
  }
@@ -6867,6 +7282,9 @@ const operations = {
6867
7282
  "journey.policy.respond": define(true, policyRespondSchema, respondJourneyPolicy, (value) => journeyCommitResultSchema.parse(value)),
6868
7283
  "journey.policy.batches": define(false, policyBatchesSchema, selectPolicyBatches, (value) => z.array(z.uuid()).parse(value)),
6869
7284
  "journey.send.load": define(false, sendLoadSchema, loadJourneySend, (value) => sendSnapshotSchema.nullable().parse(value)),
7285
+ "journey.send.current": define(false, sendCurrentSchema, isCurrentSend, (value) => z.boolean().parse(value)),
7286
+ "journey.send.transfer": define(true, sendTransferSchema, checkpointJourneySendTransfer, (value) => sendTransferResultSchema.parse(value)),
7287
+ "journey.send.output": define(false, sendOutputQuerySchema, readSendOutput, (value) => sendOutputSchema.parse(value)),
6870
7288
  "journey.send.commit": define(true, sendCommitSchema, commitJourneySend, (value) => sendCommitResultSchema.parse(value)),
6871
7289
  "manual.upload.load": define(false, manualUploadLoadSchema, loadManualUpload, (value) => manualUploadSchema.nullable().parse(value)),
6872
7290
  "manual.upload.commit": define(true, manualUploadCommitSchema, commitManualUpload, (value) => manualUploadResultSchema.parse(value)),
@@ -6884,13 +7302,34 @@ const operations = {
6884
7302
  "journey.batch.put": define(true, batchPublicationSchema, putStoredBatch, nothing),
6885
7303
  "journey.batch.admit": define(false, batchHeaderSchema, admitReviewBatch, (value) => storedBatchSchema.nullable().parse(value)),
6886
7304
  "journey.scan.admit": define(true, scanAdmissionSchema, admitScanSession, (value) => z.object({
6887
- state: nativeJourneySchema,
7305
+ state: preparationStateSchema,
6888
7306
  priors: z.array(storedBatchSchema.nullable())
6889
7307
  }).strict().parse(value)),
6890
7308
  "journey.scan.put": define(true, scanBatchesSchema, putScanBatches, nothing),
6891
7309
  "journey.batch.read": define(false, journeyBatchReadSchema, selectReviewBatch, (value) => batchSelectionSchema.parse(value)),
6892
7310
  "journey.review.attempts": define(false, reviewAttemptsQuerySchema, readReviewAttempts, (value) => reviewAttemptsResultSchema.parse(value)),
7311
+ "journey.preparation.state": define(false, z.object({ journeyId: z.uuid() }), (db, { journeyId }) => readPreparationState(db, journeyId), (value) => preparationStateSchema.nullable().parse(value)),
7312
+ "journey.preparation.permission": define(false, z.object({ journeyId: z.uuid() }), (db, { journeyId }) => readPreparationPermission(db, journeyId), (value) => preparationPermissionSchema.nullable().parse(value)),
7313
+ "journey.preparation.update": define(true, preparationUpdateSchema, updatePreparation, (value) => z.object({
7314
+ status: z.enum([
7315
+ "applied",
7316
+ "stale",
7317
+ "replayed"
7318
+ ]),
7319
+ state: preparationStateSchema
7320
+ }).strict().parse(value)),
7321
+ "journey.receipt.revision": define(false, z.object({
7322
+ journeyId: z.uuid(),
7323
+ key: z.uuid(),
7324
+ hash: z.string().regex(/^[0-9a-f]{64}$/u)
7325
+ }).strict(), (db, { journeyId, key, hash }) => {
7326
+ const row = db.prepare("SELECT journey_id,request_hash,revision FROM journey_receipts WHERE request_key=?").get(key);
7327
+ if (!row) return null;
7328
+ if (row["journey_id"] !== journeyId || row["request_hash"] !== hash) throw new ApplicationError("OPERATION_CONFLICT");
7329
+ return z.number().int().nonnegative().parse(row["revision"]);
7330
+ }, (value) => z.number().int().nonnegative().nullable().parse(value)),
6893
7331
  "journey.current": define(false, z.object({ journeyId: z.uuid().optional() }), (db, input) => readJourney(db, input.journeyId), (value) => nativeJourneySchema.nullable().parse(value)),
7332
+ "journey.terms.reopen-empty": define(true, emptyReviewCorrectionSchema, reopenEmptyReview, (value) => emptyReviewCorrectionResultSchema.parse(value)),
6894
7333
  "journey.terms.select": define(false, termsQuerySchema, selectTerms, (value) => termsSelectionSchema.parse(value)),
6895
7334
  "journey.terms.finish": define(true, termsFinishSchema, finishTerms, (value) => reviewTermsResultSchema.parse(value)),
6896
7335
  "journey.terms.read": define(false, journeyTermsQuerySchema, readJourneyTerms, (value) => z.array(journeyTermSchema).parse(value)),
@@ -7412,4 +7851,4 @@ function assertStoreHeap(limitBytes = getHeapStatistics().heap_size_limit) {
7412
7851
  return limitBytes;
7413
7852
  }
7414
7853
  //#endregion
7415
- export { checkVersion as $, resourceSelectionQuerySchema as A, termReuseQuerySchema as B, loadAcceptedReview as C, aggregateUsefulness as D, loadPrivateResource as E, validateUnitCandidate as F, requireActiveBatch as G, batchHeader as H, reuseQuerySchema as I, validateDatabase as J, initializeAndClaim as K, batchQuery as L, evaluateUnitEvidence as M, sameCandidateRequest as N, sessionUsefulnessRecommendation as O, validateCandidateAttempt as P, validateState as Q, isPrivateResourceKind as R, validateSessionBindings as S, loadBatch as T, createJourneyPayloadFiles as U, privateReviewHash as V, validateBatchBody as W, readBundleDirectory as X, withDatabase as Y, readStoredState as Z, loadReconciliation as _, isCoordinationBusy as _t, decodeError as a, hasResetMarker as at, validatePreparationSelection as b, dropsAnchor as c, privatePath as ct, parseDelivery as d, stateDirectory as dt, schemaVersion as et, validateCandidate as f, syncDirectory as ft, loadPreparedBodies as g, errorCode as gt, loadPreparation as h, StoreError as ht, createExecutor as i, finishReset as it, inspectionQuerySchema as j, sessionQuery as k, replySchema as l, readResetMarker as lt, loadStatus as m, withFileCheckTimings as mt, releaseInheritedHeapFlags as n, createDatabaseFile as nt, ProtocolError as o, inspectStateFiles as ot, classificationJourneyCommit as p, withStateSnapshot as pt, ownership as q, storeHeapLimits as r, databaseName as rt, allowedReplies as s, present as st, assertStoreHeap as t, beginReset as tt, operations as u, recheckPrivatePath as ut, loadRereadBodies as v, isSqliteCorruption as vt, loadAuthenticatedReview as w, validateSessionReconciliation as x, loadSessionBodies as y, privateResourceHeader as z };
7854
+ export { validateState as $, resourceSelectionQuerySchema as A, termReuseQuerySchema as B, loadAcceptedReview as C, aggregateUsefulness as D, loadPrivateResource as E, validateUnitCandidate as F, validateBatchBody as G, privateReviewHash as H, reuseQuerySchema as I, ownership as J, requireActiveBatch as K, batchQuery as L, evaluateUnitEvidence as M, sameCandidateRequest as N, sessionUsefulnessRecommendation as O, validateCandidateAttempt as P, readStoredState as Q, isPrivateResourceKind as R, validateSessionBindings as S, loadBatch as T, batchHeader as U, scanAliasRequestKey as V, createJourneyPayloadFiles as W, withDatabase as X, validateDatabase as Y, readBundleDirectory as Z, loadReconciliation as _, errorCode as _t, decodeError as a, finishReset as at, validatePreparationSelection as b, dropsAnchor as c, present as ct, parseDelivery as d, recheckPrivatePath as dt, checkVersion as et, validateCandidate as f, stateDirectory as ft, loadPreparedBodies as g, StoreError as gt, loadPreparation as h, withFileCheckTimings as ht, createExecutor as i, databaseName as it, inspectionQuerySchema as j, sessionQuery as k, replySchema as l, privatePath as lt, loadStatus as m, withStateSnapshot as mt, releaseInheritedHeapFlags as n, beginReset as nt, ProtocolError as o, hasResetMarker as ot, classificationJourneyCommit as p, syncDirectory as pt, initializeAndClaim as q, storeHeapLimits as r, createDatabaseFile as rt, allowedReplies as s, inspectStateFiles as st, assertStoreHeap as t, schemaVersion as tt, operations as u, readResetMarker as ut, loadRereadBodies as v, isCoordinationBusy as vt, loadAuthenticatedReview as w, validateSessionReconciliation as x, loadSessionBodies as y, isSqliteCorruption as yt, privateResourceHeader as z };