@gitroll/tacitt-cli 0.6.10 → 0.6.11

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 (53) hide show
  1. package/README.md +43 -4
  2. package/dist/{agent-setup-LPd8WEsN.mjs → agent-setup-DdfF3zch.mjs} +2 -2
  3. package/dist/agent-setup-worker.mjs +5 -5
  4. package/dist/cli.mjs +22 -8
  5. package/dist/codex-title-worker.mjs +1 -1
  6. package/dist/configured-preview-worker.mjs +5 -5
  7. package/dist/configured-session-worker.mjs +9 -7
  8. package/dist/{frame-ByT1xayJ.mjs → frame-Bq2zRh-i.mjs} +1 -1
  9. package/dist/{heap-CM298Yu0.mjs → heap-CV8Eca8V.mjs} +212 -10
  10. package/dist/inspection-worker.mjs +4 -4
  11. package/dist/{job-BHIwElNF.mjs → job-BKsifhyv.mjs} +37 -17
  12. package/dist/{locate-Cwv6WH8n.mjs → locate-Cx52Zqxv.mjs} +179 -18
  13. package/dist/{model-zvn5GUJ9.mjs → model-C1UXkHh9.mjs} +1 -1
  14. package/dist/operation-executor.mjs +3 -3
  15. package/dist/preview-worker.mjs +1 -1
  16. package/dist/privacy-validator.mjs +29 -2
  17. package/dist/{query-By41gaKm.mjs → query-AMZerUh8.mjs} +312 -94
  18. package/dist/{run-ZQx7p4Rw.mjs → run-CaPktPhH.mjs} +4 -4
  19. package/dist/{src-BqtdFVq7.mjs → src-D1iONla3.mjs} +1902 -1606
  20. package/dist/{src-3jvMvSQn.mjs → src-DlVbGB32.mjs} +74 -37
  21. package/dist/{src-D_DfY--2.mjs → src-wZx_v0Ih.mjs} +297 -207
  22. package/dist/store-database-worker.mjs +1 -1
  23. package/dist/web/assets/index-CYl7eDUu.js +172 -0
  24. package/dist/web/assets/index-DzOozqms.css +1 -0
  25. package/dist/web/index.html +2 -2
  26. package/package.json +1 -1
  27. package/skills/tacitt/SKILL.md +11 -11
  28. package/skills/tacitt/references/b-method.md +2 -2
  29. package/skills/tacitt/references/bootstrap.md +2 -2
  30. package/skills/tacitt/references/classification.md +3 -3
  31. package/skills/tacitt/references/commands.md +29 -30
  32. package/skills/tacitt/references/journey.md +6 -6
  33. package/skills/tacitt/references/parallel-review.md +27 -33
  34. package/skills/tacitt/references/send.md +9 -4
  35. package/skills/tacitt/scripts/INDEX.md +37 -37
  36. package/skills/tacitt/scripts/answer.mjs +73 -15
  37. package/skills/tacitt/scripts/journey-classification-metadata.mjs +75 -26
  38. package/skills/tacitt/scripts/journey-classification-progress.mjs +1 -2
  39. package/skills/tacitt/scripts/journey-classification.mjs +6 -1
  40. package/skills/tacitt/scripts/journey-consolidation.mjs +1 -0
  41. package/skills/tacitt/scripts/journey-scan-timing.d.mts +1 -0
  42. package/skills/tacitt/scripts/journey-scan-timing.mjs +19 -4
  43. package/skills/tacitt/scripts/journey-state.d.mts +9 -1
  44. package/skills/tacitt/scripts/journey-state.mjs +84 -8
  45. package/skills/tacitt/scripts/journey-tasks.d.mts +1 -0
  46. package/skills/tacitt/scripts/journey-tasks.mjs +13 -5
  47. package/skills/tacitt/scripts/journey.d.mts +44 -31
  48. package/skills/tacitt/scripts/journey.mjs +24 -5
  49. package/skills/tacitt/scripts/privacy-validator.mjs +167 -11
  50. package/skills/tacitt/scripts/review.d.mts +7 -6
  51. package/skills/tacitt/scripts/review.mjs +562 -145
  52. package/dist/web/assets/index-B14e6_-z.js +0 -174
  53. package/dist/web/assets/index-CYJBq1px.css +0 -1
@@ -1,4 +1,4 @@
1
- import { $ as bCountSchema, $n as detectStatic, A as bDuplicateMirror, B as bEvidenceReader, Bt as sessionPeriodSchema, C as validateBUnit, Cn as inspectionSemanticVersions, Ct as cardsSubmitSchema, D as uncoveredBObligations, Dn as createStaticProcessor, Dt as privacyResolveSchema, E as bLiterals, Et as packageConfirmActionSchema, Fn as entityDictionaryVersion, Ft as termTypeInfo, G as subtractBRanges, Gn as validateProfile, H as codePointLength, Ht as journeyProfileAddressActionSchema, I as bIssue, It as termTypeSchema, J as bRetainedGroupSchema, Jn as validatePlan, Jt as respondProfileLinks, Kn as orderProfile, L as bOverlaps, Lt as termTypes, M as bLegacyBindings, Mt as reviewTermsResultSchema, N as bUnitFragments, Nt as termAssessmentSchema, O as indexBRanges, On as staticDefaults, Ot as privacyTermKey, P as bAdoptedObligations, Pn as cleanupImplementationVersion, Pt as termOverrideValueSchema, Q as bAnchorSchema, Qt as safeCode, R as unownedBObligations, Rt as deselectOutsidePeriod, S as validateBFields, Sn as currentDetectorVersion, St as cardsConfirmActionSchema, T as bLiteralTerms, Tn as gapReasons, Tt as emptyJourneyPrivacy, U as codePointSlice, Ut as journeyProfileLinksActionSchema, V as bDigest, Vt as emptyJourneyProfile, W as createCodePointIndex, X as bSessionDecisionSchema, Xt as ApplicationError, Y as bReviewSubmissionSchema, Yn as copyData, Yt as classificationTitleMaskSchema, Z as bTermSchema, Zt as UnfinishedJourneyError, _ as semanticFindingSchema, _t as hideConfirmSchema, a as reviewChunkSchema, an as observeSource, at as journeyExceptionSchema, b as validateBReview, bt as journeyConfigurationSchema, c as textDigest, cn as inventorySchema, cr as scalarBefore, ct as journeyPreferenceScopeSchema, dt as nativeJourneySchema, en as familyNeedsReview, et as bIdSchema, f as reviewAlignmentAuditSchema, fn as nativeSourceIdentitySchema, ft as journeyGuidanceSchema, g as validateReviewAlignment, h as reviewFindingsDigest, ht as journeyDescribeSchema, i as reviewBatchSchema, in as sourceKey, ir as createEntityMatcher, it as journeyDigestSchema, jt as reviewTermsInputSchema, kn as staticOptions, l as validateReview, lr as defaultDetectionLimits, lt as journeyProjectTypeSchema, m as reviewAlignmentSchema, mt as delegatedDecisionSchema, n as compileBFindings, nn as reviewFamilies, nt as journeyAccessSchema, o as reviewDigest, on as sourceSnapshotId, ot as journeyIdSchema, p as reviewAlignmentInputSchema, pt as journeyInstructionInputSchema, q as bRepresentationSchema, r as reviewBatchChunks, rn as sourceGraph, rt as journeyChoiceSchema, s as reviewSubmitSchema, sn as fileIdentitySchema, sr as scalarAfter, st as journeyKindSchema, tn as familySelected, tr as staticCategories, tt as bMethod, u as findingReferenceKey, un as sourcePartSchema, ut as journeyWorkTypeSchema, vt as hidePlanInputSchema, w as bLiteralAdmitted, x as createBAnalysisUnits, xt as cardAnswerActionSchema, y as validateBClassification, yt as hidePlanResourceSchema, z as bAnchorKey } from "./locate-Cwv6WH8n.mjs";
1
+ import { $ as bTermSchema, A as indexBRanges, At as packageConfirmActionSchema, B as unownedBObligations, Bn as cleanupImplementationVersion, Bt as termOverrideValueSchema, C as createBAnalysisUnits, Ct as hidePlanResourceSchema, D as bLiteralTerms, Dt as cardsSubmitSchema, E as bLiteralAdmitted, Et as cardsConfirmActionSchema, F as bUnitFragments, Fn as staticOptions, Ft as emptyReviewCorrectionSchema, G as codePointSlice, H as bEvidenceReader, Ht as termTypeSchema, I as bAdoptedObligations, Jt as journeyProfileAddressActionSchema, K as createCodePointIndex, Kt as sessionPeriodSchema, Lt as reviewTermsInputSchema, M as bDuplicateMirror, Mt as privacyTermKey, Nn as createStaticProcessor, O as bLiterals, On as currentDetectorVersion, P as bLegacyBindings, Pn as staticDefaults, Q as bSessionDecisionSchema, Qn as orderProfile, R as bIssue, Rt as reviewTermsResultSchema, S as validateBReview, St as hidePlanInputSchema, T as validateBUnit, Tt as cardAnswerActionSchema, U as bDigest, Ut as termTypes, V as bAnchorKey, Vn as entityDictionaryVersion, Vt as termTypeInfo, W as codePointLength, Wt as deselectOutsidePeriod, X as bRetainedGroupSchema, Y as bRepresentationSchema, Yt as journeyProfileLinksActionSchema, Z as bReviewSubmissionSchema, Zn as validateProfile, _ as reviewFindingsDigest, _t as journeyInstructionInputSchema, a as reviewBatchChunks, ar as detectStatic, at as journeyAccessSchema, c as reviewDigest, cn as reviewFamilies, ct as journeyExceptionSchema, d as validateReview, dn as observeSource, dt as journeyPreferenceScopeSchema, en as respondProfileLinks, er as validatePlan, et as bAnchorSchema, f as findingReferenceKey, fn as sourceSnapshotId, ft as journeyProjectTypeSchema, g as reviewAlignmentSchema, gn as sourcePartSchema, gt as journeyGuidanceSchema, h as reviewAlignmentInputSchema, hr as defaultDetectionLimits, ht as reviewInterruptionSchema, i as compileBFindings, in as safeCode, it as consentedWithoutAccount, jn as gapReasons, jt as privacyResolveSchema, k as uncoveredBObligations, kn as inspectionSemanticVersions, kt as emptyJourneyPrivacy, l as reviewSubmitSchema, ln as sourceGraph, lt as journeyIdSchema, m as reviewAlignmentAuditSchema, mn as inventorySchema, mr as scalarBefore, mt as nativeJourneySchema, nn as ApplicationError, nt as bIdSchema, o as reviewBatchSchema, on as familyNeedsReview, ot as journeyChoiceSchema, pn as fileIdentitySchema, pr as scalarAfter, pt as journeyWorkTypeSchema, q as subtractBRanges, qt as emptyJourneyProfile, rn as UnfinishedJourneyError, rt as bMethod, s as reviewChunkSchema, sn as familySelected, sr as staticCategories, st as journeyDigestSchema, tn as classificationTitleMaskSchema, tr as copyData, tt as bCountSchema, u as textDigest, un as sourceKey, ur as createEntityMatcher, ut as journeyKindSchema, v as validateReviewAlignment, vn as nativeSourceIdentitySchema, vt as delegatedDecisionSchema, w as validateBFields, wt as journeyConfigurationSchema, x as validateBClassification, xt as hideConfirmSchema, y as semanticFindingSchema, yt as journeyDescribeSchema, z as bOverlaps, zt as termAssessmentSchema } from "./locate-Cx52Zqxv.mjs";
2
2
  import { z } from "zod";
3
3
  import { createHash, createHmac, createSecretKey, randomUUID } from "node:crypto";
4
4
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
@@ -440,7 +440,7 @@ const staticPresets = Object.freeze([
440
440
  /** Browser-safe fixed metadata and definitions; no recognizers, native sources or session state. */
441
441
  const staticCatalogRef = Object.freeze({
442
442
  id: "static-catalog",
443
- version: "11"
443
+ version: "12"
444
444
  });
445
445
  //#endregion
446
446
  //#region packages/application/src/processing/settings/catalog.ts
@@ -1221,7 +1221,7 @@ function isClassifying(state) {
1221
1221
  * not skipped. The Skill helpers mirror this rule in `journey-state.mjs`.
1222
1222
  */
1223
1223
  function isSkippedWork(work) {
1224
- return work.status === "blocked" && !work.blockedCodes.includes("PRIVACY_CHANGE_PENDING");
1224
+ return work.status === "blocked" && !work.blockedCodes.includes("PRIVACY_CHANGE_PENDING") && currentReviewInterruptions(work).length === 0;
1225
1225
  }
1226
1226
  /** A selected Work that is not skipped (ADR 0236). */
1227
1227
  function isIncludedWork(work) {
@@ -1239,6 +1239,12 @@ function hasPublicChoice(work) {
1239
1239
  if (!work.accessConfirmed || !work.accessChoice) return false;
1240
1240
  return work.accessChoice.channel === "account" ? work.accessOrigin === "account" : work.accessOrigin === "explicit";
1241
1241
  }
1242
+ /** ADR 0277 slice 2 display projection. Send consent remains a separate gate. */
1243
+ function publicSource(work) {
1244
+ if (!isIncludedWork(work) || work.access !== "public" || work.accountAccessConflict) return null;
1245
+ if (!hasPublicChoice(work)) return "suggestion";
1246
+ return work.accessChoice?.channel === "account" ? "account" : "choice";
1247
+ }
1242
1248
  function selectedWorks(state) {
1243
1249
  return state.works.filter(isSelectedWork);
1244
1250
  }
@@ -1254,9 +1260,13 @@ function hasCertifiedNoBatchReview(work, snapshotId) {
1254
1260
  const roots = proofs.flatMap((proof) => proof.rootSessionIds);
1255
1261
  return roots.length === work.rootSessionIds.length && new Set(roots).size === roots.length && work.rootSessionIds.every((id) => roots.includes(id));
1256
1262
  }
1263
+ /** A reused Work ID cannot carry an old input generation's failed review forward. */
1264
+ function currentReviewInterruptions(work) {
1265
+ return (work.reviewInterruptions ?? []).filter((item) => item.inputRevision === work.inputRevision);
1266
+ }
1257
1267
  /** Reading completes over included Works once the person has confirmed the scan scope. */
1258
1268
  function isReadingComplete(state) {
1259
- return state.scope.status === "confirmed" && state.works.length > 0 && !isClassifying(state) && state.works.filter(isIncludedWork).every((work) => work.status !== "pending" && work.status !== "blocked" && (work.totalBatches > 0 || hasCertifiedNoBatchReview(work, state.snapshotId)) && work.completedBatches === work.totalBatches);
1269
+ return state.scope.status === "confirmed" && state.works.length > 0 && !isClassifying(state) && state.works.filter(isIncludedWork).every((work) => currentReviewInterruptions(work).length === 0 && work.status !== "pending" && work.status !== "blocked" && (work.totalBatches > 0 || hasCertifiedNoBatchReview(work, state.snapshotId)) && work.completedBatches === work.totalBatches);
1260
1270
  }
1261
1271
  /** Recompute completion after a selection or reading change without leaving a paused/send phase. */
1262
1272
  function withReadingProgress(state) {
@@ -1910,7 +1920,23 @@ const nativeStartSchema = z.object({
1910
1920
  period: sessionPeriodSchema.optional()
1911
1921
  }).strict();
1912
1922
  const nativeGetSchema = z.object({ journeyId: journeyIdSchema.optional() }).strict();
1923
+ const retryReviewActionSchema = reviewInterruptionSchema.pick({
1924
+ packId: true,
1925
+ inputRevision: true
1926
+ }).extend({
1927
+ kind: z.literal("retry-review"),
1928
+ workId: journeyIdSchema
1929
+ });
1913
1930
  const journeyActionSchema = z.discriminatedUnion("kind", [
1931
+ reviewInterruptionSchema.extend({
1932
+ kind: z.literal("review-interrupted"),
1933
+ workId: journeyIdSchema
1934
+ }),
1935
+ reviewInterruptionSchema.omit({ reason: true }).extend({
1936
+ kind: z.literal("review-reader-closed"),
1937
+ workId: journeyIdSchema
1938
+ }),
1939
+ retryReviewActionSchema,
1914
1940
  journeySplitActionSchema,
1915
1941
  journeyMergeActionSchema,
1916
1942
  journeyPropagateActionSchema,
@@ -1923,6 +1949,7 @@ const journeyActionSchema = z.discriminatedUnion("kind", [
1923
1949
  kind: z.literal("consent"),
1924
1950
  allow: z.boolean()
1925
1951
  }).strict(),
1952
+ z.object({ kind: z.literal("account-read") }).strict(),
1926
1953
  z.object({ kind: z.literal("abandon") }).strict(),
1927
1954
  z.object({
1928
1955
  kind: z.literal("delegate"),
@@ -2104,7 +2131,7 @@ function privacyDecisionDigest(state) {
2104
2131
  return digest;
2105
2132
  }
2106
2133
  function assertJourneyScanned(state) {
2107
- if (!state.consent || state.scope.status !== "confirmed" || !state.readingComplete || state.works.some((work) => isIncludedWork(work) && (!work.classificationMetadata || work.totalBatches === 0 && !hasCertifiedNoBatchReview(work, state.snapshotId) || work.completedBatches !== work.totalBatches || work.status === "blocked"))) throw new ApplicationError("SCOPE_NOT_READY");
2134
+ if (!state.consent || state.scope.status !== "confirmed" || !state.readingComplete || state.works.some((work) => isIncludedWork(work) && (currentReviewInterruptions(work).length > 0 || !work.classificationMetadata || work.totalBatches === 0 && !hasCertifiedNoBatchReview(work, state.snapshotId) || work.completedBatches !== work.totalBatches || work.status === "blocked"))) throw new ApplicationError("SCOPE_NOT_READY");
2108
2135
  }
2109
2136
  /** Legacy model, delegate or unrecorded confirmation is not the person's Public choice. */
2110
2137
  function assertPublicChoices(state) {
@@ -2606,6 +2633,45 @@ const preparationSettlementSchema = z.object({
2606
2633
  ...binding,
2607
2634
  facts: preparationFactsSchema
2608
2635
  }).strict();
2636
+ const preparationStateSchema = nativeJourneySchema.pick({
2637
+ journeyId: true,
2638
+ revision: true,
2639
+ classificationRevision: true,
2640
+ phase: true,
2641
+ snapshotId: true,
2642
+ scope: true,
2643
+ consent: true,
2644
+ pendingHideId: true,
2645
+ configurationId: true,
2646
+ works: true,
2647
+ exceptions: true,
2648
+ instructions: true,
2649
+ instructionsRevision: true,
2650
+ readingComplete: true,
2651
+ readingStartedAt: true,
2652
+ period: true,
2653
+ adapters: true,
2654
+ reviewMethod: true
2655
+ });
2656
+ const preparationPermissionSchema = nativeJourneySchema.pick({
2657
+ destination: true,
2658
+ consent: true,
2659
+ phase: true,
2660
+ scope: true,
2661
+ pendingHideId: true
2662
+ });
2663
+ const preparationUpdateSchema = z.object({
2664
+ journeyId: journeyIdSchema,
2665
+ expectedRevision: preparationCountSchema,
2666
+ requestKey: journeyIdSchema,
2667
+ at: z.iso.datetime(),
2668
+ kind: z.enum(["snapshot", "batch"]),
2669
+ works: z.array(nativeJourneySchema.shape.works.element),
2670
+ phase: nativeJourneySchema.shape.phase,
2671
+ readingComplete: z.boolean(),
2672
+ readingStartedAt: nativeJourneySchema.shape.readingStartedAt,
2673
+ clearPackageApproval: z.boolean()
2674
+ }).strict();
2609
2675
  function emptyPreparationFacts() {
2610
2676
  return {
2611
2677
  stage: "capture",
@@ -2632,1335 +2698,1727 @@ function validatePreparationFacts(start, facts) {
2632
2698
  if (facts.unsupported.evaluated > start.sourceParts || facts.emptyReviewRoots.evaluated > start.reviewRoots || facts.unsupported.known + facts.resourceLimited.known + facts.excluded.known > facts.unsupported.evaluated) throw new ApplicationError("CORRUPTED_STATE");
2633
2699
  }
2634
2700
  //#endregion
2635
- //#region packages/application/src/journey/inventory/schema.ts
2636
- const journeyInventoryQuerySchema = z.object({ journeyId: journeyIdSchema }).strict();
2637
- /** Trusted host binding, never accepted by the public command schema. */
2638
- const journeyInventoryReadSchema = journeyInventoryQuerySchema.extend({ expectedDestination: z.string().min(1).optional() }).strict();
2639
- const discoveryWarningCodeSchema = z.enum([
2640
- "UNSUPPORTED_SOURCE_ENCODING",
2641
- "SKIPPED_NONREGULAR_SOURCE",
2642
- "WORKFLOW_JOURNAL_EXCLUDED",
2643
- "AUTOMATIC_HELPER_EXCLUDED",
2644
- "UNRESOLVED_WORKERS_EXCLUDED",
2645
- "METADATA_LIMIT",
2646
- "METADATA_DEPTH_LIMIT",
2647
- "INCOMPLETE_DISCOVERY",
2648
- "DISCOVERY_ENTRY_LIMIT",
2649
- "DISCOVERY_DEPTH_LIMIT",
2650
- "BROKEN_SOURCE_LINK"
2651
- ]);
2652
- const evidence = z.object({
2653
- known: preparationCountSchema,
2654
- total: preparationCountSchema.nullable()
2655
- }).strict().refine((value) => value.total === null || value.total === value.known);
2656
- const workPreparationObservationSchema = z.object({
2701
+ //#region packages/application/src/journey/correction/model.ts
2702
+ /** Original field-relative code-point coordinates, end-exclusive, exactly as B anchors. */
2703
+ const correctionAnchorSchema = z.object({
2704
+ fieldId: bIdSchema,
2705
+ recordId: bIdSchema,
2706
+ fieldHash: journeyDigestSchema,
2707
+ start: bCountSchema,
2708
+ end: bCountSchema,
2709
+ quote: z.string().min(1).max(4096)
2710
+ }).strict();
2711
+ const correctionReportSchema = z.object({
2712
+ id: journeyIdSchema,
2657
2713
  workId: journeyIdSchema,
2658
- inputRevision: preparationCountSchema.min(1),
2659
- observation: z.enum([
2660
- "unavailable",
2661
- "incomplete",
2662
- "settled"
2663
- ]),
2664
- stage: preparationStageSchema.nullable(),
2665
- sourceParts: z.object({
2666
- unsupported: evidence,
2667
- resourceLimited: evidence,
2668
- excluded: evidence,
2669
- unclassified: preparationCountSchema
2670
- }).strict(),
2671
- emptyReviewRoots: evidence,
2672
- unevaluatedReviewRoots: preparationCountSchema
2714
+ inputRevision: z.number().int().min(1),
2715
+ batchId: journeyIdSchema,
2716
+ groupId: journeyIdSchema,
2717
+ kind: journeyKindSchema,
2718
+ reason: z.string().trim().min(1).max(1e3),
2719
+ anchors: z.array(correctionAnchorSchema).min(1).max(256)
2673
2720
  }).strict();
2674
- const journeyInventorySchema = z.object({
2721
+ const reviewCorrectSchema = z.object({
2675
2722
  journeyId: journeyIdSchema,
2676
- revision: preparationCountSchema,
2677
- snapshotId: journeyDigestSchema.nullable(),
2678
- frozen: z.object({
2679
- retainedSessions: preparationCountSchema,
2680
- reviewRoots: preparationCountSchema,
2681
- identifiedMainRoots: preparationCountSchema,
2682
- unknownRoleRoots: preparationCountSchema,
2683
- attachedSubagents: preparationCountSchema,
2684
- unresolvedWorkerSessions: preparationCountSchema,
2685
- sourceParts: preparationCountSchema,
2686
- frozenSourceBytes: preparationCountSchema,
2687
- discoveryUnsupportedInputs: z.null(),
2688
- discoverySkippedInputs: z.null(),
2689
- discoverySkippedSourceBytes: z.null(),
2690
- warningOccurrences: z.array(z.object({
2691
- code: discoveryWarningCodeSchema,
2692
- occurrences: preparationCountSchema
2693
- }).strict())
2694
- }).strict().nullable(),
2695
- captures: z.object({
2696
- targetParts: preparationCountSchema,
2697
- capturedParts: preparationCountSchema,
2698
- capturedBytes: preparationCountSchema,
2699
- completeCapturedBytes: preparationCountSchema.nullable()
2700
- }).strict().nullable(),
2701
- workScope: z.object({
2702
- total: preparationCountSchema,
2703
- byStatus: z.object({
2704
- pending: preparationCountSchema,
2705
- reading: preparationCountSchema,
2706
- review: preparationCountSchema,
2707
- ready: preparationCountSchema,
2708
- blocked: preparationCountSchema
2709
- }).strict(),
2710
- byAccess: z.object({
2711
- public: preparationCountSchema,
2712
- private: preparationCountSchema,
2713
- drop: preparationCountSchema
2714
- }).strict(),
2715
- assignedReviewRoots: preparationCountSchema,
2716
- unassignedReviewRoots: preparationCountSchema.nullable(),
2717
- excludedAutomationRoots: preparationCountSchema.nullable(),
2718
- unassignedOtherRoots: preparationCountSchema.nullable()
2719
- }).strict(),
2720
- preparation: z.array(workPreparationObservationSchema)
2723
+ expectedRevision: z.number().int().nonnegative(),
2724
+ idempotencyKey: journeyIdSchema,
2725
+ report: correctionReportSchema
2721
2726
  }).strict();
2722
- /** Panel preparation counts over the selected Works: opaque Work IDs, parts and bytes only. */
2723
- const journeyPreparationProgressSchema = z.object({
2724
- currentWorkIds: z.array(journeyIdSchema),
2725
- targetParts: preparationCountSchema,
2726
- capturedParts: preparationCountSchema,
2727
- targetBytes: preparationCountSchema,
2728
- capturedBytes: preparationCountSchema
2727
+ /** Immutable private report receipt; the only place where correction quotes are retained. */
2728
+ const correctionReportResourceSchema = z.object({
2729
+ journeyId: journeyIdSchema,
2730
+ kind: z.literal("correction-report"),
2731
+ id: journeyIdSchema,
2732
+ requestHash: journeyDigestSchema,
2733
+ at: z.iso.datetime(),
2734
+ snapshotId: journeyDigestSchema,
2735
+ batchDigest: journeyDigestSchema,
2736
+ decisionDigest: journeyDigestSchema,
2737
+ report: correctionReportSchema
2738
+ }).strict();
2739
+ const correctionAuditSchema = z.object({
2740
+ reportId: journeyIdSchema,
2741
+ action: z.enum([
2742
+ "report",
2743
+ "hide",
2744
+ "dismiss"
2745
+ ]),
2746
+ actor: z.enum([
2747
+ "observer",
2748
+ "human",
2749
+ "delegate"
2750
+ ]),
2751
+ authorityId: journeyIdSchema.nullable(),
2752
+ decisionDigest: journeyDigestSchema
2729
2753
  }).strict();
2730
2754
  //#endregion
2731
- //#region packages/application/src/source/resolve.ts
2732
- /** Queue-based ancestry resolution cannot leak deep descendants through a recursion cutoff. */
2733
- function resolveParts(inventory, route) {
2734
- const nodes = sourceGraph(inventory);
2735
- const families = reviewFamilies(inventory, nodes);
2736
- const resolved = /* @__PURE__ */ new Map();
2737
- const depths = /* @__PURE__ */ new Map();
2738
- const dependents = /* @__PURE__ */ new Map();
2739
- const remaining = /* @__PURE__ */ new Map();
2740
- const queue = [];
2741
- const finish = (node, included, unresolved, reason, depth = 0) => {
2742
- resolved.set(node.key, {
2743
- sessionId: node.session.id,
2744
- partId: node.part?.id ?? "",
2745
- included,
2746
- unresolved,
2747
- reason
2748
- });
2749
- depths.set(node.key, depth);
2750
- queue.push(node);
2751
- };
2752
- const pending = (node, reason) => finish(node, route === "original", true, reason);
2753
- for (const node of nodes) {
2754
- const { identity, reason } = node.observation;
2755
- if (families.unresolved.has(node.session.id)) finish(node, false, node.ancestry !== "missing", node.ancestry === "missing" ? "missing-parent" : "ancestry-unresolved");
2756
- else if (!familySelected(inventory, families, node.session)) finish(node, false, false, families.owners.get(node.session.id)?.id !== node.session.id ? "deselected-parent" : node.session.selected ? "workspace-not-selected" : "not-selected");
2757
- else if (reason !== null) pending(node, reason);
2758
- else if (identity?.role === "guardian") finish(node, false, false, "guardian-excluded");
2759
- else if (identity?.role !== "worker") finish(node, true, false, null);
2760
- else if (node.ancestry === "missing") finish(node, false, false, "missing-parent");
2761
- else if (node.ancestry !== "parents") finish(node, false, true, "ancestry-unresolved");
2762
- else {
2763
- remaining.set(node.key, node.parents.length);
2764
- for (const parent of node.parents) {
2765
- const children = dependents.get(parent.key) ?? [];
2766
- children.push(node);
2767
- dependents.set(parent.key, children);
2768
- }
2769
- }
2770
- }
2771
- for (let cursor = 0; cursor < queue.length; cursor++) {
2772
- const parent = queue[cursor];
2773
- if (!parent) continue;
2774
- const value = resolved.get(parent.key);
2775
- for (const node of dependents.get(parent.key) ?? []) {
2776
- if (resolved.has(node.key)) continue;
2777
- const outstanding = (remaining.get(node.key) ?? 1) - 1;
2778
- remaining.set(node.key, outstanding);
2779
- const depth = (depths.get(parent.key) ?? 0) + 1;
2780
- if (value?.included && !value.unresolved && (route === "original" || depth <= 256)) {
2781
- finish(node, true, false, null, depth);
2782
- continue;
2783
- }
2784
- if (outstanding > 0) continue;
2785
- const results = node.parents.map((entry) => resolved.get(entry.key));
2786
- if (results.every((entry) => entry && [
2787
- "not-selected",
2788
- "workspace-not-selected",
2789
- "deselected-parent"
2790
- ].includes(entry.reason ?? ""))) finish(node, false, false, "deselected-parent");
2791
- else if (results.every((entry) => entry?.reason === "missing-parent" || entry?.reason === "deselected-parent")) finish(node, false, false, "missing-parent");
2792
- else finish(node, false, true, "ancestry-unresolved");
2793
- }
2794
- }
2795
- for (const node of nodes) if (!resolved.has(node.key)) finish(node, false, true, "ancestry-unresolved");
2796
- return resolved;
2755
+ //#region packages/application/src/journey/correction/binding.ts
2756
+ function workCorrections(state, workId) {
2757
+ return (state.corrections ?? []).filter((item) => item.workId === workId);
2758
+ }
2759
+ /** Binds rendered output and generated metadata to every correction disposition of the Work. */
2760
+ function workCorrectionBinding(state, workId) {
2761
+ const corrections = workCorrections(state, workId);
2762
+ return corrections.length ? corrections.map(({ id, groupId, inputRevision, batchId, status, disposition }) => ({
2763
+ id,
2764
+ groupId,
2765
+ inputRevision,
2766
+ batchId,
2767
+ status,
2768
+ choice: disposition?.choice ?? null,
2769
+ decisionDigest: disposition?.decisionDigest ?? null
2770
+ })) : null;
2797
2771
  }
2798
2772
  //#endregion
2799
- //#region packages/application/src/source/index.ts
2800
- function resolveSourceSelection(inventory, route = "original") {
2801
- const parts = resolveParts(inventory, route);
2802
- const sessions = /* @__PURE__ */ new Map();
2803
- const families = reviewFamilies(inventory);
2804
- const includedWorkspaces = /* @__PURE__ */ new Set();
2805
- const counts = {
2806
- workspaces: 0,
2807
- sessions: 0,
2808
- parts: 0,
2809
- sourceBytes: 0
2810
- };
2811
- const coverage = {
2812
- includedSessions: 0,
2813
- excludedSessions: 0,
2814
- unresolvedSessions: 0,
2815
- unresolvedParts: 0
2816
- };
2817
- for (const session of inventory.sessions) {
2818
- const decisions = session.parts.flatMap((part) => {
2819
- const value = parts.get(sourceKey(session.id, part.id));
2820
- return value ? [{
2821
- part,
2822
- value
2823
- }] : [];
2824
- });
2825
- const included = decisions.filter(({ value }) => value.included);
2826
- const unresolved = decisions.filter(({ value }) => value.unresolved);
2827
- const excluded = decisions.filter(({ value }) => !value.included && !value.unresolved);
2828
- const selected = familySelected(inventory, families, session);
2829
- const missing = selected && session.parts.length === 0;
2830
- const reasons = [...new Set(decisions.flatMap(({ value }) => value.reason ? [value.reason] : []))].sort();
2831
- if (missing) reasons.push("unreadable-source");
2832
- const summary = Object.freeze({
2833
- status: unresolved.length || missing ? "unresolved" : included.length ? excluded.length ? "partial" : "included" : "excluded",
2834
- includedParts: included.length,
2835
- excludedParts: excluded.length,
2836
- unresolvedParts: unresolved.length,
2837
- sourceBytes: included.reduce((sum, { part }) => sum + part.snapshot.size, 0),
2838
- reasons: Object.freeze(reasons)
2839
- });
2840
- sessions.set(session.id, summary);
2841
- if (!selected) continue;
2842
- if (summary.includedParts) {
2843
- counts.sessions++;
2844
- counts.parts += summary.includedParts;
2845
- counts.sourceBytes += summary.sourceBytes;
2846
- includedWorkspaces.add(session.workspaceId);
2847
- coverage.includedSessions++;
2848
- }
2849
- if (summary.status === "excluded") coverage.excludedSessions++;
2850
- if (summary.status === "unresolved") coverage.unresolvedSessions++;
2851
- coverage.unresolvedParts += summary.unresolvedParts;
2852
- }
2853
- counts.workspaces = includedWorkspaces.size;
2854
- return Object.freeze({
2855
- counts: Object.freeze(counts),
2856
- coverage: Object.freeze(coverage),
2857
- sessions,
2858
- session: (sessionId) => {
2859
- const value = sessions.get(sessionId);
2860
- if (!value) throw new ApplicationError("UNKNOWN_ITEM");
2861
- return value;
2862
- },
2863
- part: (sessionId, partId) => parts.get(sourceKey(sessionId, partId))
2864
- });
2773
+ //#region packages/application/src/journey/placement/decisions.ts
2774
+ function workPrivacyDecisions(state, work) {
2775
+ return state.groups.filter((group) => work.groupIds.includes(group.id)).map(({ id, key, kind, label, condition, choice, confirmed }) => ({
2776
+ id,
2777
+ key,
2778
+ kind,
2779
+ label,
2780
+ condition,
2781
+ choice,
2782
+ confirmed
2783
+ })).sort((a, b) => a.id.localeCompare(b.id));
2865
2784
  }
2866
- //#endregion
2867
- //#region packages/application/src/journey/scan-exclusion.ts
2868
- /** Manual source-policy proof; never an assertion that unknown content was scanned. */
2869
- const scanExclusionResourceSchema = z.object({
2870
- kind: z.literal("scan-exclusion"),
2871
- proofVersion: z.literal(1),
2872
- journeyId: journeyIdSchema,
2873
- workId: journeyIdSchema,
2874
- inputRevision: z.number().int().positive(),
2875
- snapshotId: journeyDigestSchema,
2876
- configurationDigest: journeyDigestSchema,
2877
- rootSessionId: z.string().min(1),
2878
- sessionId: z.string().min(1),
2879
- sourceId: z.string().min(1),
2880
- sourcePartId: z.string().min(1),
2881
- native: nativeSourceIdentitySchema,
2882
- adapterVersion: z.string().min(1),
2883
- reason: z.enum([
2884
- "guardian-excluded",
2885
- "missing-parent",
2886
- "deselected-parent",
2887
- "ancestry-unresolved",
2888
- "non-interactive-launch"
2889
- ]),
2890
- facts: z.object({
2891
- representation: z.enum(["jsonl", "jsonl.zst"]),
2892
- byteLength: z.number().int().nonnegative(),
2893
- sha256: journeyDigestSchema
2894
- }).strict()
2895
- }).strict();
2896
- /** Validate policy against frozen identity evidence, never a caller-asserted source role. */
2897
- function validateScanExclusion(resource, state, inventory, capture) {
2898
- const work = state.works.find((item) => item.id === resource.workId);
2899
- if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
2900
- if (!work || state.scope.status !== "confirmed" || work.access === "drop" || state.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
2901
- if (state.journeyId !== resource.journeyId || work.inputRevision !== resource.inputRevision || state.snapshotId !== resource.snapshotId || !work.rootSessionIds.includes(resource.rootSessionId)) throw new ApplicationError("REVISION_CONFLICT");
2902
- const families = reviewFamilies(inventory);
2903
- const part = (families.members.get(resource.rootSessionId)?.find((item) => item.id === resource.sessionId))?.parts.find((item) => item.id === resource.sourcePartId);
2904
- const evidence = capture.source.nativeEvidence;
2905
- const launch = resource.reason === "non-interactive-launch";
2906
- const ancestry = resource.reason === "ancestry-unresolved";
2907
- if (ancestry && resource.native.role !== "worker") throw new ApplicationError("CORRUPTED_STATE");
2908
- const prefixRoot = launch && resource.rootSessionId === resource.sessionId && resource.native.role === "main" && evidence?.recognition === "unknown" && evidence.completeness === "prefix" && JSON.stringify(evidence.identity) === JSON.stringify({
2909
- ...resource.native,
2910
- role: "unknown"
2911
- });
2912
- if (!part || capture.partId !== resource.sourcePartId || capture.facts.sha256 !== resource.facts.sha256 || capture.facts.byteLength !== resource.facts.byteLength || capture.facts.representation !== resource.facts.representation || !evidence || !prefixRoot && (evidence.recognition !== "recognized" || JSON.stringify(evidence.identity) !== JSON.stringify(resource.native)) || evidence.integrity !== "valid" || evidence.adapterVersion !== resource.adapterVersion || evidence.snapshotId !== sourceSnapshotId(capture.source) || JSON.stringify(part.nativeEvidence) !== JSON.stringify(evidence)) throw new ApplicationError("CORRUPTED_STATE");
2913
- const ids = new Set(work.rootSessionIds.flatMap((id) => (families.members.get(id) ?? []).map((item) => item.id)));
2914
- const sessions = inventory.sessions.filter((item) => ids.has(item.id));
2915
- const workspaceIds = new Set(sessions.map((item) => item.workspaceId));
2916
- const decision = resolveSourceSelection({
2917
- ...inventory,
2918
- sessions,
2919
- workspaces: inventory.workspaces.filter((item) => workspaceIds.has(item.id))
2920
- }, "configured").part(resource.sessionId, resource.sourcePartId);
2921
- if (!decision || (prefixRoot ? decision.included || decision.reason !== "unknown-source" : ancestry ? decision.included ? decision.reason !== null : decision.reason !== resource.reason : decision.included !== launch || (launch ? decision.reason !== null : decision.reason !== resource.reason))) throw new ApplicationError("CORRUPTED_STATE");
2785
+ /** Corrections are additive under an unchanged Hide group, so they bind metadata review too. */
2786
+ function workDecisionDigest(state, work) {
2787
+ const decisions = workPrivacyDecisions(state, work);
2788
+ const corrections = workCorrectionBinding(state, work.id);
2789
+ return textDigest(JSON.stringify(corrections ? {
2790
+ decisions,
2791
+ corrections
2792
+ } : decisions));
2793
+ }
2794
+ /** Retire generated text until it is reviewed; the processed project name keeps rows distinct. */
2795
+ function retireWorkDescription(work) {
2796
+ return {
2797
+ ...work,
2798
+ suggestion: null
2799
+ };
2922
2800
  }
2923
2801
  //#endregion
2924
- //#region packages/application/src/journey/scan-git-seeds.ts
2925
- /**
2926
- * Owner-only Git detector input fixed once per frozen snapshot and canonical root set.
2927
- * Later Git activity cannot change or invalidate a scan of the same frozen sources.
2928
- */
2929
- const scanGitSeedsResourceSchema = z.object({
2930
- kind: z.literal("git-seeds"),
2931
- journeyId: journeyIdSchema,
2932
- snapshotId: journeyDigestSchema,
2933
- roots: z.array(z.string()),
2934
- seeds: z.object({
2935
- names: z.array(z.string().min(1)),
2936
- snapshots: z.array(z.object({
2937
- id: z.string().min(1),
2938
- sha256: z.string()
2939
- }).strict())
2940
- }).strict(),
2941
- sha256: journeyDigestSchema
2942
- }).strict();
2943
- /** Storage key; equal root sets in any order share one receipt. */
2944
- const scanGitSeedsId = (snapshotId, roots) => `${snapshotId}:${textDigest(JSON.stringify([...new Set(roots)].sort()))}`;
2945
- function scanGitSeedsResource(journeyId, snapshotId, roots, seeds) {
2946
- const value = {
2947
- names: [...seeds.names],
2948
- snapshots: seeds.snapshots.map((row) => ({ ...row }))
2802
+ //#region packages/application/src/journey/terms/groups.ts
2803
+ const keyPrefix = "type:";
2804
+ const termGroupKey = (type) => `${keyPrefix}${type}`;
2805
+ /** RFC 9562 UUID version 5 of `name` in the `namespace` UUID. */
2806
+ function uuidV5(namespace, name) {
2807
+ const bytes = createHash("sha1").update(Buffer.from(namespace.replaceAll("-", ""), "hex")).update(name, "utf8").digest().subarray(0, 16);
2808
+ bytes.writeUInt8(bytes.readUInt8(6) & 15 | 80, 6);
2809
+ bytes.writeUInt8(bytes.readUInt8(8) & 63 | 128, 8);
2810
+ const hex = bytes.toString("hex");
2811
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
2812
+ }
2813
+ /** Deterministic: every reader and writer of a journey names a type's group alike. */
2814
+ const termGroupId = (journeyId, type) => uuidV5(journeyId, type);
2815
+ /** The term type a group answers, or null for any other category. */
2816
+ function termGroupType(group) {
2817
+ if (!group.key.startsWith(keyPrefix)) return null;
2818
+ const parsed = termTypeSchema.safeParse(group.key.slice(5));
2819
+ return parsed.success ? parsed.data : null;
2820
+ }
2821
+ const typeGroup = (state, type) => state.groups.find((group) => termGroupType(group) === type);
2822
+ function newTypeGroup(state, type, preference) {
2823
+ const info = termTypeInfo[type];
2824
+ const base = {
2825
+ id: termGroupId(state.journeyId, type),
2826
+ kind: info.kind,
2827
+ key: termGroupKey(type),
2828
+ label: info.label,
2829
+ condition: info.condition,
2830
+ count: 0,
2831
+ preferenceScope: null,
2832
+ representative: null
2833
+ };
2834
+ if (!info.asked) return {
2835
+ ...base,
2836
+ belief: .97,
2837
+ choice: "hide",
2838
+ confirmed: true,
2839
+ origin: "policy"
2840
+ };
2841
+ if (state.questionsPaused) return {
2842
+ ...base,
2843
+ belief: .97,
2844
+ choice: "hide",
2845
+ confirmed: false,
2846
+ origin: "paused"
2949
2847
  };
2950
2848
  return {
2951
- kind: "git-seeds",
2952
- journeyId,
2953
- snapshotId,
2954
- roots: [...new Set(roots)].sort(),
2955
- seeds: value,
2956
- sha256: textDigest(JSON.stringify(value))
2849
+ ...base,
2850
+ belief: preference === null ? .5 : preference === "hide" ? .97 : .03,
2851
+ choice: preference,
2852
+ confirmed: false,
2853
+ origin: preference === null ? "model" : "preference"
2957
2854
  };
2958
2855
  }
2959
- /** A missing, tampered or foreign receipt is corrupted state, never a cue to reread live Git. */
2960
- function verifiedScanGitSeeds(resource, snapshotId, roots) {
2961
- if (resource.snapshotId !== snapshotId || scanGitSeedsId(resource.snapshotId, resource.roots) !== scanGitSeedsId(snapshotId, roots) || textDigest(JSON.stringify(resource.seeds)) !== resource.sha256) throw new ApplicationError("CORRUPTED_STATE");
2962
- return resource.seeds;
2856
+ /**
2857
+ * One group per type with at least one dictionary value, `count` its distinct values. Existing
2858
+ * groups keep their identity and answer; other groups are unchanged.
2859
+ */
2860
+ function syncTermGroups(state, counts, preference) {
2861
+ const groups = state.groups.map((group) => {
2862
+ const type = termGroupType(group);
2863
+ return type === null ? group : {
2864
+ ...group,
2865
+ count: counts.get(type) ?? group.count
2866
+ };
2867
+ });
2868
+ for (const type of termTypes) {
2869
+ const count = counts.get(type) ?? 0;
2870
+ if (count === 0 || groups.some((group) => termGroupType(group) === type)) continue;
2871
+ groups.push({
2872
+ ...newTypeGroup(state, type, preference(termGroupKey(type))),
2873
+ count
2874
+ });
2875
+ }
2876
+ return groups;
2963
2877
  }
2964
2878
  //#endregion
2965
- //#region packages/application/src/journey/scan-session.ts
2966
- const count$9 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
2967
- const scanFileSchema = z.object({
2968
- path: z.string().min(1),
2969
- identity: fileIdentitySchema,
2970
- sha256: journeyDigestSchema,
2971
- byteLength: count$9
2972
- }).strict();
2879
+ //#region packages/application/src/journey/review/confirmed-hides.ts
2880
+ /** Kinds from lowest to highest mask priority. */
2881
+ const confirmedHideKinds = [
2882
+ "internal",
2883
+ "identity",
2884
+ "private",
2885
+ "credentials"
2886
+ ];
2887
+ const kinds = confirmedHideKinds;
2888
+ const rank$1 = (kind) => kinds.indexOf(kind);
2973
2889
  /**
2974
- * Track A source-bound prose (`file`) with built-in static protection spans; no delivery archive
2975
- * exists during scanning (ADR 0172). Obsolete version 3 inputs with detector evidence are refused.
2890
+ * ADR 0086: a type's values are hidden unless its group is answered Keep; before an answer the
2891
+ * default is Hide. Credentials and contact details are always hidden (D06).
2976
2892
  */
2977
- const sessionScanInputSchema = z.object({
2978
- version: z.literal(4),
2979
- sessionId: z.string().min(1),
2980
- file: scanFileSchema,
2981
- sources: z.array(z.object({
2982
- sourceId: z.string().min(1),
2983
- sourcePartId: z.string().min(1),
2984
- native: nativeSourceIdentitySchema,
2985
- facts: z.object({
2986
- representation: z.enum(["jsonl", "jsonl.zst"]),
2987
- byteLength: count$9,
2988
- sha256: journeyDigestSchema
2989
- }).strict()
2990
- }).strict()),
2991
- recordCount: count$9,
2992
- fieldCount: count$9,
2993
- inputUtf16: count$9,
2994
- mainReviewVerified: z.literal(true),
2995
- emptyReviewVerified: z.boolean()
2996
- }).strict().refine((input) => new Set(input.sources.map((source) => source.sourceId)).size === input.sources.length);
2997
- const scanSessionResourceSchema = z.object({
2998
- kind: z.literal("scan-session"),
2999
- journeyId: journeyIdSchema,
3000
- workId: journeyIdSchema,
3001
- inputRevision: count$9.min(1),
3002
- snapshotId: journeyDigestSchema,
3003
- configurationDigest: journeyDigestSchema,
3004
- rootSessionId: z.string().min(1),
3005
- input: sessionScanInputSchema
3006
- }).strict();
3007
- /** Complete scan-set evidence for certified empty main input; never an output digest. */
3008
- function scanPreparationDigest(resources) {
3009
- return textDigest(JSON.stringify(resources.map((resource) => resource.kind === "scan-exclusion" ? resource : [
3010
- resource.input.sessionId,
3011
- resource.rootSessionId,
3012
- resource.configurationDigest,
3013
- resource.input.file.sha256,
3014
- resource.input.sources
3015
- ]).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))));
2893
+ function typeHides(state, type) {
2894
+ const group = typeGroup(state, type);
2895
+ return !termTypeInfo[type].asked || !group?.confirmed || group.choice !== "keep";
2896
+ }
2897
+ /** B review (superseded by ADR 0086): IDs of the categories confirmed as Hide. */
2898
+ const confirmedHideGroupIds = (state) => state.groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => group.id);
2899
+ const overrideKey = (state) => (state.valueOverrides ?? []).map((item) => JSON.stringify([
2900
+ item.type,
2901
+ item.value,
2902
+ item.choice
2903
+ ])).toSorted();
2904
+ /**
2905
+ * Identifies the hidden set (ADR 0079, 0086). The dictionary only grows and a type's group
2906
+ * counts its distinct values, so each type's count and Hide answer, with the value overrides,
2907
+ * name exactly one hidden set.
2908
+ */
2909
+ function confirmedHideKey(state) {
2910
+ return createHash("sha256").update(JSON.stringify({
2911
+ types: termTypes.map((type) => [
2912
+ type,
2913
+ typeGroup(state, type)?.count ?? 0,
2914
+ typeHides(state, type)
2915
+ ]),
2916
+ overrides: overrideKey(state),
2917
+ terms: state.privacy.terms.filter((term) => term.disposition === "hidden").map((term) => [term.type, term.value]).toSorted((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))
2918
+ })).digest("hex");
2919
+ }
2920
+ /** Whether two states hold the same value overrides. */
2921
+ const sameValueOverrides = (left, right) => JSON.stringify(overrideKey(left)) === JSON.stringify(overrideKey(right));
2922
+ /**
2923
+ * A preparation's waived hits stay waived only while every category recorded for them is still a
2924
+ * confirmed Hide; otherwise render may no longer mask them, so the session needs reconciliation.
2925
+ */
2926
+ function maskedHitsStale(maskedHits, hiding) {
2927
+ const current = new Set(hiding);
2928
+ return !!maskedHits?.some((hit) => hit.groupIds.some((id) => !current.has(id)));
2929
+ }
2930
+ /**
2931
+ * ADR 0086 hidden set: values of types not answered Keep, minus Keep overrides, plus Hide
2932
+ * overrides, plus every value of an always-hidden type. A value of several types takes the
2933
+ * highest mask kind among the types hiding it.
2934
+ */
2935
+ function dictionaryHides(state, terms) {
2936
+ const overrides = state.valueOverrides ?? [];
2937
+ const kept = new Set(overrides.flatMap((item) => item.choice === "keep" ? [item.value] : []));
2938
+ const hidden = /* @__PURE__ */ new Map();
2939
+ const add = (value, type) => {
2940
+ const prior = hidden.get(value);
2941
+ const kind = termTypeInfo[type].kind;
2942
+ hidden.set(value, {
2943
+ kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
2944
+ groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], termGroupId(state.journeyId, type)])].sort()
2945
+ });
2946
+ };
2947
+ const all = [...terms];
2948
+ const hiddenValues = new Set(state.privacy.terms.filter((item) => item.disposition === "hidden").map((item) => item.value));
2949
+ for (const term of all) if (hiddenValues.has(term.value) && !kept.has(term.value)) add(term.value, term.type);
2950
+ for (const item of overrides) if (item.choice === "hide") add(item.value, item.type);
2951
+ for (const term of all) if (!termTypeInfo[term.type].asked) add(term.value, term.type);
2952
+ return hidden;
2953
+ }
2954
+ /** The journey's current hidden set, applied to every selected Work (ADR 0086, D03). */
2955
+ async function journeyConfirmedHides(reader, state) {
2956
+ return dictionaryHides(state, [...await reader.journeyTerms({ journeyId: state.journeyId }), ...state.privacy.terms]);
2957
+ }
2958
+ function confirmedHideTerms(groups, records) {
2959
+ const confirmed = new Map(groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => [group.id, group]));
2960
+ const fields = /* @__PURE__ */ new Map();
2961
+ for (const record of records) {
2962
+ const assigned = new Map(record.assignments.map((item) => [item.findingId, item.groupId]));
2963
+ const chunks = new Map(record.chunks.map((chunk) => [chunk.id, chunk]));
2964
+ for (const finding of record.findings) {
2965
+ const group = confirmed.get(assigned.get(finding.id) ?? "");
2966
+ const chunk = chunks.get(finding.chunkId);
2967
+ if (!group || !chunk || finding.class !== "gray") continue;
2968
+ const key = JSON.stringify([chunk.fieldId, chunk.fieldHash]);
2969
+ fields.set(key, [...fields.get(key) ?? [], {
2970
+ start: chunk.offset + finding.start,
2971
+ end: chunk.offset + finding.end,
2972
+ text: chunk.text.slice(finding.start, finding.end),
2973
+ split: [finding.start === 0, finding.end === chunk.text.length],
2974
+ group
2975
+ }]);
2976
+ }
2977
+ }
2978
+ const terms = /* @__PURE__ */ new Map();
2979
+ const add = (text, groupIds, kind) => {
2980
+ for (const term of bLiteralTerms(text)) {
2981
+ const prior = terms.get(term);
2982
+ terms.set(term, {
2983
+ kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
2984
+ groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], ...groupIds])].sort()
2985
+ });
2986
+ }
2987
+ };
2988
+ for (const spans of fields.values()) {
2989
+ let open;
2990
+ for (const span of spans.toSorted((a, b) => a.start - b.start || b.end - a.end)) {
2991
+ if (open && span.split[0] && span.start === open.end) {
2992
+ open.text += span.text;
2993
+ open.end = span.end;
2994
+ open.groupIds.push(span.group.id);
2995
+ } else {
2996
+ if (open) add(open.text, open.groupIds, open.kind);
2997
+ open = {
2998
+ end: span.end,
2999
+ text: span.text,
3000
+ groupIds: [span.group.id],
3001
+ kind: span.group.kind
3002
+ };
3003
+ }
3004
+ if (!span.split[1]) {
3005
+ add(open.text, open.groupIds, open.kind);
3006
+ open = void 0;
3007
+ }
3008
+ }
3009
+ if (open) add(open.text, open.groupIds, open.kind);
3010
+ }
3011
+ return terms;
3016
3012
  }
3017
- const scanBatchBinding = {
3018
- journeyId: journeyIdSchema,
3019
- workId: journeyIdSchema,
3020
- inputRevision: count$9.min(1),
3021
- snapshotId: journeyDigestSchema,
3022
- configurationDigest: journeyDigestSchema
3023
- };
3024
- const scanCanonicalResourceSchema = z.object({
3025
- ...scanBatchBinding,
3026
- kind: z.literal("scan-canonical"),
3027
- id: journeyDigestSchema,
3028
- batchId: journeyIdSchema,
3029
- batchDigest: journeyDigestSchema
3030
- }).strict();
3031
- const scanAliasResourceSchema = z.object({
3032
- ...scanBatchBinding,
3033
- kind: z.literal("scan-alias"),
3034
- id: journeyIdSchema,
3035
- canonicalBatchId: journeyIdSchema,
3036
- canonicalBatchDigest: journeyDigestSchema,
3037
- targetBatchId: journeyIdSchema,
3038
- targetBatchDigest: journeyDigestSchema,
3039
- equivalenceDigest: journeyDigestSchema
3040
- }).strict();
3041
3013
  //#endregion
3042
- //#region packages/application/src/journey/b/repair.ts
3043
- const bRepairPlanSchema = z.object({
3044
- unitId: journeyDigestSchema,
3045
- unitDigest: journeyDigestSchema,
3046
- priorDigest: journeyDigestSchema,
3047
- round: z.literal(1),
3048
- targetFields: z.array(bIdSchema).min(1).max(24),
3049
- remainingFields: z.array(bIdSchema)
3050
- }).strict();
3051
- const bReviewDigest = (submission) => bDigest(JSON.stringify(bReviewSubmissionSchema.parse(submission)));
3052
- const repairMeaning = (part) => JSON.stringify([part.category, part.classification]);
3053
- function planBRepair(unit, review, completedRepairs = 0) {
3054
- if (completedRepairs !== 0) throw new ApplicationError("INVALID_OPERATION");
3055
- const validated = validateBReview(unit, review.submission);
3056
- const needed = new Set(validated.issues.flatMap((issue) => issue.fieldId === null ? [] : [issue.fieldId]));
3057
- const targets = [...new Set(unit.fragments.filter((part) => part.core).map((part) => part.fieldId))].filter((fieldId) => needed.has(fieldId));
3058
- if (!targets.length) return null;
3059
- return bRepairPlanSchema.parse({
3060
- unitId: unit.id,
3061
- unitDigest: unit.digest,
3062
- priorDigest: bReviewDigest(review.submission),
3063
- round: 1,
3064
- targetFields: targets.slice(0, 24),
3065
- remainingFields: [...needed].filter((fieldId) => !targets.slice(0, 24).includes(fieldId))
3066
- });
3014
+ //#region packages/application/src/journey/b/matches.ts
3015
+ function bDictionaryHits(fields, literals, signal = new AbortController().signal) {
3016
+ const matcher = createEntityMatcher([...new Set(literals.map((part) => part.term))].map((value) => ({
3017
+ kind: "custom-term",
3018
+ value
3019
+ })));
3020
+ const hits = [];
3021
+ for (const field of fields) {
3022
+ const index = createCodePointIndex(field.text);
3023
+ const base = field.start ?? 0;
3024
+ for (const match of matcher.find(field.text, signal, 1e5)) {
3025
+ if (hits.length >= 1e6) throw new ApplicationError("PROCESSING_LIMIT");
3026
+ const body = {
3027
+ fieldId: field.id,
3028
+ fieldHash: field.fieldHash,
3029
+ recordId: field.recordId,
3030
+ start: base + index.codePointOffset(match.range.start),
3031
+ end: base + index.codePointOffset(match.range.end),
3032
+ exactQuote: field.text.slice(match.range.start, match.range.end),
3033
+ term: match.entity.value
3034
+ };
3035
+ hits.push({
3036
+ ...body,
3037
+ digest: bDigest(JSON.stringify(body))
3038
+ });
3039
+ }
3040
+ }
3041
+ return [...new Map(hits.map((hit) => [hit.digest, hit])).values()];
3067
3042
  }
3068
- function mergeBRepair(unit, previous, inputPlan, raw) {
3069
- const plan = bRepairPlanSchema.parse(inputPlan);
3070
- const expected = planBRepair(unit, previous);
3071
- if (JSON.stringify(plan) !== JSON.stringify(expected)) throw new ApplicationError("REVISION_CONFLICT");
3072
- const correction = bReviewSubmissionSchema.parse(raw);
3073
- const targeted = (fieldId) => plan.targetFields.includes(fieldId);
3074
- if ([
3075
- ...correction.terms.flatMap((term) => term.occurrences),
3076
- ...correction.retainedSourceGroups,
3077
- ...correction.representationLimits,
3078
- ...correction.uncertainOccurrences
3079
- ].some((part) => !targeted(part.fieldId))) throw new ApplicationError("INVALID_REVIEW_SPAN");
3080
- const repaired = validateBReview(unit, correction, plan.targetFields);
3081
- const prior = validateBReview(unit, previous.submission);
3082
- const correctedMeanings = /* @__PURE__ */ new Map();
3083
- for (const part of repaired.sensitive) {
3084
- const key = bAnchorKey(part);
3085
- const meanings = correctedMeanings.get(key) ?? /* @__PURE__ */ new Set();
3086
- meanings.add(repairMeaning(part));
3087
- correctedMeanings.set(key, meanings);
3043
+ function unclassifiedBHits(hits, literals, negative) {
3044
+ const issues = [];
3045
+ const positiveFields = new Map([...Map.groupBy(literals, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
3046
+ const negativeFields = new Map([...Map.groupBy(negative, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
3047
+ for (const hit of hits) {
3048
+ const positive = positiveFields.get(hit.fieldId)?.relation(hit).covers ?? false;
3049
+ const negative = negativeFields.get(hit.fieldId)?.relation(hit);
3050
+ if (!positive && !negative?.covers || positive && negative?.overlaps) issues.push(bIssue(positive ? "CONFLICTING_PRIVACY_DECISIONS" : "UNCLASSIFIED_MATCH", hit));
3088
3051
  }
3089
- const preservedKeys = /* @__PURE__ */ new Set();
3090
- const conflicts = [];
3091
- const preserved = prior.sensitive.filter((part) => targeted(part.fieldId)).flatMap((part) => {
3092
- const retentions = part.classification.class === "certain" ? [] : repaired.retained.filter((retained) => retained.fieldId === part.fieldId);
3093
- return subtractBRanges([part], retentions).map((range) => ({
3094
- fieldId: part.fieldId,
3095
- recordId: part.recordId,
3096
- fieldHash: part.fieldHash,
3097
- ...range,
3098
- exactQuote: codePointSlice(part.exactQuote, range.start - part.start, range.end - part.start),
3099
- category: part.category,
3100
- classification: part.classification,
3101
- reason: "Prior sensitive obligation preserved during bounded repair."
3102
- }));
3103
- }).filter((part) => {
3104
- const anchor = bAnchorKey(part), meaning = repairMeaning(part), corrected = correctedMeanings.get(anchor);
3105
- if (corrected && (corrected.size > 1 || !corrected.has(meaning))) conflicts.push(bIssue("CONFLICTING_PRIVACY_DECISIONS", part));
3106
- if (corrected?.has(meaning)) return false;
3107
- const key = JSON.stringify([anchor, meaning]);
3108
- if (preservedKeys.has(key)) return false;
3109
- preservedKeys.add(key);
3110
- return true;
3111
- });
3112
- const original = previous.submission;
3113
- const merged = {
3114
- ...original,
3115
- terms: [...original.terms.map((term) => ({
3116
- ...term,
3117
- occurrences: term.occurrences.filter((part) => !targeted(part.fieldId))
3118
- })).filter((term) => term.occurrences.length > 0), ...correction.terms],
3119
- retainedSourceGroups: [...original.retainedSourceGroups.filter((part) => !targeted(part.fieldId)), ...correction.retainedSourceGroups],
3120
- representationLimits: [
3121
- ...original.representationLimits.filter((part) => !targeted(part.fieldId)),
3122
- ...correction.representationLimits,
3123
- ...preserved
3124
- ],
3125
- uncertainOccurrences: [...original.uncertainOccurrences.filter((part) => !targeted(part.fieldId)), ...correction.uncertainOccurrences],
3126
- coverage: [...original.coverage.filter((part) => !targeted(part.fieldId)), ...correction.coverage],
3127
- sessionDecision: correction.sessionDecision
3052
+ return issues;
3053
+ }
3054
+ const bSpacedWord = (value) => /[\p{L}\p{N}_]/u.test(value) && !/[\p{sc=Han}\p{sc=Hiragana}\p{sc=Katakana}\p{sc=Thai}\p{sc=Lao}\p{sc=Khmer}\p{sc=Myanmar}]/u.test(value);
3055
+ /** A confirmed Hide value matches only where it does not run into a neighbouring word (ADR 0069). */
3056
+ function bWordBounded(text, start, end) {
3057
+ return !(bSpacedWord(scalarBefore(text, start)) && bSpacedWord(scalarAfter(text, start)) || bSpacedWord(scalarBefore(text, end)) && bSpacedWord(scalarAfter(text, end)));
3058
+ }
3059
+ /**
3060
+ * Occurrences of a Work's explicit exceptions, matched like Hide everywhere: exact NFC values
3061
+ * over complete graphemes. A confirmed Hide value inside one stays visible there.
3062
+ */
3063
+ function bExceptionRanges(values) {
3064
+ const matcher = createEntityMatcher([...new Set([...values].map((value) => value.normalize("NFC")))].map((value) => ({
3065
+ kind: "custom-term",
3066
+ value
3067
+ })));
3068
+ return (text, signal = new AbortController().signal) => matcher.find(text, signal, 1e5).filter(({ range, entity }) => text.slice(range.start, range.end).normalize("NFC") === entity.value).map(({ range }) => range);
3069
+ }
3070
+ /** Whether a range lies inside one of `ranges`: a prefix maximum of ends, O(log n) per query. */
3071
+ function bContainment(ranges) {
3072
+ const sorted = ranges.toSorted((a, b) => a.start - b.start);
3073
+ const reach = [];
3074
+ for (const [index, range] of sorted.entries()) reach.push(Math.max(range.end, reach[index - 1] ?? range.end));
3075
+ return (range) => {
3076
+ let low = 0, high = sorted.length;
3077
+ while (low < high) {
3078
+ const middle = low + high >> 1;
3079
+ if ((sorted[middle]?.start ?? Infinity) <= range.start) low = middle + 1;
3080
+ else high = middle;
3081
+ }
3082
+ return low > 0 && (reach[low - 1] ?? -1) >= range.end;
3083
+ };
3084
+ }
3085
+ /**
3086
+ * Word-bounded, exact, case-sensitive NFC occurrences of the terms, in UTF-16 coordinates,
3087
+ * except where an explicit exception occurrence contains one.
3088
+ */
3089
+ function bTermMatcher(terms, exceptions = []) {
3090
+ const matcher = createEntityMatcher([...new Set(terms)].map((value) => ({
3091
+ kind: "custom-term",
3092
+ value
3093
+ })));
3094
+ const excepted = bExceptionRanges(exceptions);
3095
+ return (text, signal = new AbortController().signal) => {
3096
+ const hits = matcher.find(text, signal, 1e5).filter(({ range }) => bWordBounded(text, range.start, range.end));
3097
+ const kept = hits.length ? bContainment(excepted(text, signal)) : () => false;
3098
+ return hits.filter(({ range }) => !kept(range)).map(({ range, entity }) => ({
3099
+ ...range,
3100
+ term: entity.value
3101
+ }));
3128
3102
  };
3129
- const review = validateBReview(unit, merged);
3130
- review.issues = [...new Map([...review.issues, ...conflicts].map((issue) => [JSON.stringify(issue), issue])).values()];
3131
- return {
3132
- review,
3133
- receipt: {
3134
- ...plan,
3135
- resultDigest: bReviewDigest(merged),
3136
- priorObligationsPreserved: true,
3137
- unresolvedIssues: review.issues
3103
+ }
3104
+ /** Dictionary hits that a confirmed Hide value masks at render, so they need no classification. */
3105
+ function bMaskedHits(fields, terms) {
3106
+ const byId = new Map(fields.map((field) => [field.id, {
3107
+ text: field.text,
3108
+ index: createCodePointIndex(field.text),
3109
+ base: field.start ?? 0
3110
+ }]));
3111
+ return (hit) => {
3112
+ const field = byId.get(hit.fieldId);
3113
+ return !!field && terms.has(hit.term) && bWordBounded(field.text, field.index.offset(hit.start - field.base), field.index.offset(hit.end - field.base));
3114
+ };
3115
+ }
3116
+ const segmenter = new Intl.Segmenter("und", { granularity: "grapheme" });
3117
+ /** Beyond this many inner match edges in one word the index is not kept (retire instead). */
3118
+ const maximumEdges = 16;
3119
+ /** Beyond this many distinct pieces the index is not kept (retire instead). */
3120
+ const maximumHideIndexTokens = 2e6;
3121
+ /**
3122
+ * Adds the word pieces of `text` that an occurrence of a confirmed Hide value can consist of.
3123
+ * The matcher compares per-grapheme NFC text and checks word edges on the original characters
3124
+ * at grapheme edges only, so a match may start or end inside a run of word characters there,
3125
+ * or inside a grapheme. Every piece between such positions is added. Returns false when a run
3126
+ * has too many such positions to list.
3127
+ */
3128
+ function addHideIndexTokens(text, add) {
3129
+ let complete = true;
3130
+ let from = 0;
3131
+ for (const { index } of text.matchAll(/[^\p{ASCII}]+/gu)) {
3132
+ if (index < from) continue;
3133
+ let start = index;
3134
+ while (start > from && asciiWord(text.charCodeAt(start - 1))) start--;
3135
+ let end = index + 1;
3136
+ while (end < text.length && (text.charCodeAt(end) > 127 || asciiWord(text.charCodeAt(end)))) end++;
3137
+ asciiTokens(text.slice(from, start), add);
3138
+ complete = graphemeTokens(text.slice(start, end), add) && complete;
3139
+ from = end;
3140
+ }
3141
+ asciiTokens(text.slice(from), add);
3142
+ return complete;
3143
+ }
3144
+ const asciiWord = (code) => code >= 48 && code <= 57 || code >= 65 && code <= 90 || code === 95 || code >= 97 && code <= 122;
3145
+ function asciiTokens(text, add) {
3146
+ for (const [token] of text.matchAll(/[A-Za-z0-9_]+/gu)) add(token);
3147
+ }
3148
+ function graphemeTokens(text, add) {
3149
+ let complete = true, run = [], edges = [];
3150
+ const flush = () => {
3151
+ if (edges.length > maximumEdges) complete = false;
3152
+ else {
3153
+ const cuts = [
3154
+ 0,
3155
+ ...edges,
3156
+ run.length
3157
+ ];
3158
+ for (let from = 0; from < cuts.length; from++) for (let to = from + 1; to < cuts.length; to++) add(run.slice(cuts[from], cuts[to]).join(""));
3138
3159
  }
3160
+ run = [];
3161
+ edges = [];
3139
3162
  };
3163
+ let previous = "";
3164
+ for (const { segment } of segmenter.segment(text)) {
3165
+ const original = Array.from(segment);
3166
+ Array.from(segment.normalize("NFC")).forEach((scalar, index) => {
3167
+ if (!bSpacedWord(scalar)) {
3168
+ if (run.length) flush();
3169
+ return;
3170
+ }
3171
+ if (run.length && (index > 0 || !(bSpacedWord(previous) && bSpacedWord(original[0] ?? "")))) edges.push(run.length);
3172
+ run.push(scalar);
3173
+ });
3174
+ previous = original.at(-1) ?? "";
3175
+ }
3176
+ if (run.length) flush();
3177
+ return complete;
3140
3178
  }
3141
- //#endregion
3142
- //#region packages/application/src/journey/review/limits.ts
3143
- /** Raw provider bytes stay bounded independently of JSON escaping and compiled findings. */
3144
- const reviewResponseBytes = 4194304;
3145
- /** Fits the escaped raw-response envelope and evidence plus compiled occurrence findings. */
3146
- const reviewTransportBytes = 33554432;
3147
- //#endregion
3148
- //#region packages/application/src/journey/review/candidate-schema.ts
3149
- const reviewIssueSchema = z.object({
3150
- code: z.string().regex(/^[A-Z_]{3,80}$/u),
3151
- fieldId: z.string().min(1).nullable(),
3152
- start: z.number().int().nonnegative().nullable(),
3153
- end: z.number().int().nonnegative().nullable()
3154
- }).strict();
3155
- const reviewCandidateInputSchema = z.object({
3156
- journeyId: journeyIdSchema,
3157
- batchId: journeyIdSchema,
3158
- batchDigest: journeyDigestSchema,
3159
- idempotencyKey: journeyIdSchema,
3160
- attemptId: journeyIdSchema,
3161
- response: z.string().min(1).refine((value) => Buffer.byteLength(value, "utf8") <= reviewResponseBytes),
3162
- repairId: journeyIdSchema.nullable(),
3163
- sessionReviewId: journeyIdSchema.nullable()
3164
- }).strict();
3165
- const reviewCandidateResourceSchema = reviewCandidateInputSchema.omit({ idempotencyKey: true }).extend({
3166
- kind: z.literal("b-candidate"),
3167
- id: journeyIdSchema,
3168
- requestHash: journeyDigestSchema,
3169
- at: z.iso.datetime(),
3170
- workId: journeyIdSchema,
3171
- inputRevision: z.number().int().positive(),
3172
- snapshotId: journeyDigestSchema,
3173
- responseDigest: journeyDigestSchema,
3174
- status: z.enum([
3175
- "valid",
3176
- "pending",
3177
- "invalid"
3178
- ]),
3179
- issues: z.array(reviewIssueSchema),
3180
- evidence: bReviewSubmissionSchema.nullable()
3181
- }).strict();
3182
- const reviewCandidateResultSchema = reviewCandidateResourceSchema.pick({
3183
- responseDigest: true,
3184
- status: true,
3185
- issues: true,
3186
- evidence: true
3187
- }).extend({ candidateId: journeyIdSchema }).strict();
3188
- const reviewRepairInputSchema = z.object({
3189
- journeyId: journeyIdSchema,
3190
- candidateId: journeyIdSchema,
3191
- idempotencyKey: journeyIdSchema
3192
- }).strict();
3193
- const reviewRepairResourceSchema = z.object({
3194
- kind: z.literal("b-repair"),
3195
- id: journeyIdSchema,
3196
- journeyId: journeyIdSchema,
3197
- workId: journeyIdSchema,
3198
- batchId: journeyIdSchema,
3199
- batchDigest: journeyDigestSchema,
3200
- inputRevision: z.number().int().positive(),
3201
- snapshotId: journeyDigestSchema,
3202
- candidateId: journeyIdSchema,
3203
- requestHash: journeyDigestSchema,
3204
- at: z.iso.datetime(),
3205
- plan: bRepairPlanSchema
3206
- }).strict();
3207
- //#endregion
3208
- //#region packages/application/src/journey/correction/model.ts
3209
- /** Original field-relative code-point coordinates, end-exclusive, exactly as B anchors. */
3210
- const correctionAnchorSchema = z.object({
3211
- fieldId: bIdSchema,
3212
- recordId: bIdSchema,
3213
- fieldHash: journeyDigestSchema,
3214
- start: bCountSchema,
3215
- end: bCountSchema,
3216
- quote: z.string().min(1).max(4096)
3217
- }).strict();
3218
- const correctionReportSchema = z.object({
3219
- id: journeyIdSchema,
3220
- workId: journeyIdSchema,
3221
- inputRevision: z.number().int().min(1),
3222
- batchId: journeyIdSchema,
3223
- groupId: journeyIdSchema,
3224
- kind: journeyKindSchema,
3225
- reason: z.string().trim().min(1).max(1e3),
3226
- anchors: z.array(correctionAnchorSchema).min(1).max(256)
3227
- }).strict();
3228
- const reviewCorrectSchema = z.object({
3229
- journeyId: journeyIdSchema,
3230
- expectedRevision: z.number().int().nonnegative(),
3231
- idempotencyKey: journeyIdSchema,
3232
- report: correctionReportSchema
3233
- }).strict();
3234
- /** Immutable private report receipt; the only place where correction quotes are retained. */
3235
- const correctionReportResourceSchema = z.object({
3236
- journeyId: journeyIdSchema,
3237
- kind: z.literal("correction-report"),
3238
- id: journeyIdSchema,
3239
- requestHash: journeyDigestSchema,
3240
- at: z.iso.datetime(),
3241
- snapshotId: journeyDigestSchema,
3242
- batchDigest: journeyDigestSchema,
3243
- decisionDigest: journeyDigestSchema,
3244
- report: correctionReportSchema
3245
- }).strict();
3246
- const correctionAuditSchema = z.object({
3247
- reportId: journeyIdSchema,
3248
- action: z.enum([
3249
- "report",
3250
- "hide",
3251
- "dismiss"
3252
- ]),
3253
- actor: z.enum([
3254
- "observer",
3255
- "human",
3256
- "delegate"
3257
- ]),
3258
- authorityId: journeyIdSchema.nullable(),
3259
- decisionDigest: journeyDigestSchema
3260
- }).strict();
3261
- //#endregion
3262
- //#region packages/application/src/journey/correction/binding.ts
3263
- function workCorrections(state, workId) {
3264
- return (state.corrections ?? []).filter((item) => item.workId === workId);
3179
+ /** The words of an NFC value, or null when it has none and cannot be checked by words. */
3180
+ function hideValueTokens(term) {
3181
+ const tokens = [];
3182
+ let run = "";
3183
+ for (const scalar of term) if (bSpacedWord(scalar)) run += scalar;
3184
+ else if (run) {
3185
+ tokens.push(run);
3186
+ run = "";
3187
+ }
3188
+ if (run) tokens.push(run);
3189
+ return tokens.length ? [...new Set(tokens)] : null;
3265
3190
  }
3266
- /** Binds rendered output and generated metadata to every correction disposition of the Work. */
3267
- function workCorrectionBinding(state, workId) {
3268
- const corrections = workCorrections(state, workId);
3269
- return corrections.length ? corrections.map(({ id, groupId, inputRevision, batchId, status, disposition }) => ({
3270
- id,
3271
- groupId,
3272
- inputRevision,
3273
- batchId,
3274
- status,
3275
- choice: disposition?.choice ?? null,
3276
- decisionDigest: disposition?.decisionDigest ?? null
3277
- })) : null;
3191
+ /** A keyed, truncated digest; a collision can only retire an unaffected Work. */
3192
+ function hideTokenDigest(key, token) {
3193
+ return createHmac("sha256", key).update(token, "utf8").digest("base64url").slice(0, 11);
3194
+ }
3195
+ /** Identifies the key an index or delta was made with, without revealing it. */
3196
+ const hideKeyId = (key) => hideTokenDigest(key, "\0key");
3197
+ /** Sorted, distinct, fixed-width digests in one string. */
3198
+ function packHideIndex(key, tokens) {
3199
+ const secret = createSecretKey(key);
3200
+ return [...new Set([...tokens].map((token) => hideTokenDigest(secret, token)))].sort().join("");
3201
+ }
3202
+ /** One digest; an index is a run of them, checked by length and alphabet. */
3203
+ const hideDigestPattern = /^[A-Za-z0-9_-]{11}$/u;
3204
+ const isHideIndex = (value) => value.length % 11 === 0 && /^[A-Za-z0-9_-]*$/u.test(value);
3205
+ function hideIndexHas(index, digest) {
3206
+ let low = 0, high = index.length / 11 - 1;
3207
+ while (low <= high) {
3208
+ const middle = low + high >> 1;
3209
+ const value = index.slice(middle * 11, (middle + 1) * 11);
3210
+ if (value === digest) return true;
3211
+ if (value < digest) low = middle + 1;
3212
+ else high = middle - 1;
3213
+ }
3214
+ return false;
3278
3215
  }
3279
3216
  //#endregion
3280
- //#region packages/application/src/journey/placement/decisions.ts
3281
- function workPrivacyDecisions(state, work) {
3282
- return state.groups.filter((group) => work.groupIds.includes(group.id)).map(({ id, key, kind, label, condition, choice, confirmed }) => ({
3283
- id,
3284
- key,
3217
+ //#region packages/application/src/journey/review/retirement.ts
3218
+ /**
3219
+ * ADR 0079: the confirmed Hide values a commit adds, removes or recategorizes, with the kind
3220
+ * each then has (null once removed; for additions, the lowest it can have). Terms exist only in
3221
+ * memory for this commit. `rendered` holds the value's keyed digest for each kind, to compare
3222
+ * with the values a Work was rendered with; `tokens` holds its words' keyed digests, or null
3223
+ * when it has none. A partial value is a literal that may join a neighbour across a chunk edge,
3224
+ * so its whole text, and every Work it may reach, is unknown.
3225
+ */
3226
+ const deltaValueSchema = z.object({
3227
+ term: z.string().min(1),
3228
+ kind: journeyKindSchema.nullable(),
3229
+ partial: z.boolean(),
3230
+ rendered: z.array(z.string().regex(hideDigestPattern)).length(confirmedHideKinds.length),
3231
+ tokens: z.array(z.string().regex(hideDigestPattern)).min(1).nullable(),
3232
+ /** The new findings an addition comes from; the store keeps it only if one joins a Hide. */
3233
+ sources: z.array(z.object({
3234
+ batchId: z.uuid(),
3235
+ findingId: z.uuid()
3236
+ }).strict()).min(1).optional()
3237
+ }).strict();
3238
+ /** `keyId` names the journey key the digests were made with; an index under another key is unusable. */
3239
+ const keyId = z.string().regex(hideDigestPattern);
3240
+ const confirmedHideDeltaSchema = z.discriminatedUnion("scope", [z.object({
3241
+ scope: z.literal("complete"),
3242
+ keyId,
3243
+ from: z.string(),
3244
+ to: z.string(),
3245
+ values: z.array(deltaValueSchema)
3246
+ }).strict(), z.object({
3247
+ scope: z.literal("additions"),
3248
+ keyId,
3249
+ values: z.array(deltaValueSchema)
3250
+ }).strict()]);
3251
+ /** What a value was rendered as: its kind, in the digests a render keeps (ADR 0079). */
3252
+ const renderedHideValue = (kind, term) => `${kind}\u0000${term}`;
3253
+ function hideDeltaValue(key, term, kind, partial = false) {
3254
+ const tokens = partial ? null : hideValueTokens(term);
3255
+ return {
3256
+ term,
3285
3257
  kind,
3286
- label,
3287
- condition,
3288
- choice,
3289
- confirmed
3290
- })).sort((a, b) => a.id.localeCompare(b.id));
3291
- }
3292
- /** Corrections are additive under an unchanged Hide group, so they bind metadata review too. */
3293
- function workDecisionDigest(state, work) {
3294
- const decisions = workPrivacyDecisions(state, work);
3295
- const corrections = workCorrectionBinding(state, work.id);
3296
- return textDigest(JSON.stringify(corrections ? {
3297
- decisions,
3298
- corrections
3299
- } : decisions));
3258
+ partial,
3259
+ rendered: confirmedHideKinds.map((item) => hideTokenDigest(key, renderedHideValue(item, term))),
3260
+ tokens: tokens && tokens.map((token) => hideTokenDigest(key, token))
3261
+ };
3300
3262
  }
3301
- /** Retire generated text until it is reviewed; the processed project name keeps rows distinct. */
3302
- function retireWorkDescription(work) {
3263
+ const kindOf = (kind) => journeyKindSchema.nullable().parse(kind ?? null);
3264
+ const rank = (kind) => kind === null ? -1 : confirmedHideKinds.indexOf(kind);
3265
+ /**
3266
+ * Every value whose hidden kind differs between two states (ADR 0086): both are read against the
3267
+ * same dictionary, which only grows and is unchanged by the commit that asks.
3268
+ */
3269
+ async function readConfirmedHideChange(reader, key, previous, next) {
3270
+ const terms = await reader.journeyTerms({ journeyId: previous.journeyId });
3271
+ const before = dictionaryHides(previous, terms), after = dictionaryHides(next, terms);
3303
3272
  return {
3304
- ...work,
3305
- suggestion: null
3273
+ scope: "complete",
3274
+ keyId: hideKeyId(key),
3275
+ from: confirmedHideKey(previous),
3276
+ to: confirmedHideKey(next),
3277
+ values: [.../* @__PURE__ */ new Set([...before.keys(), ...after.keys()])].filter((term) => before.get(term)?.kind !== after.get(term)?.kind).map((term) => hideDeltaValue(key, term, kindOf(after.get(term)?.kind)))
3306
3278
  };
3307
3279
  }
3308
- //#endregion
3309
- //#region packages/application/src/journey/terms/groups.ts
3310
- const keyPrefix = "type:";
3311
- const termGroupKey = (type) => `${keyPrefix}${type}`;
3312
- /** RFC 9562 UUID version 5 of `name` in the `namespace` UUID. */
3313
- function uuidV5(namespace, name) {
3314
- const bytes = createHash("sha1").update(Buffer.from(namespace.replaceAll("-", ""), "hex")).update(name, "utf8").digest().subarray(0, 16);
3315
- bytes.writeUInt8(bytes.readUInt8(6) & 15 | 80, 6);
3316
- bytes.writeUInt8(bytes.readUInt8(8) & 63 | 128, 8);
3317
- const hex = bytes.toString("hex");
3318
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
3319
- }
3320
- /** Deterministic: every reader and writer of a journey names a type's group alike. */
3321
- const termGroupId = (journeyId, type) => uuidV5(journeyId, type);
3322
- /** The term type a group answers, or null for any other category. */
3323
- function termGroupType(group) {
3324
- if (!group.key.startsWith(keyPrefix)) return null;
3325
- const parsed = termTypeSchema.safeParse(group.key.slice(5));
3326
- return parsed.success ? parsed.data : null;
3327
- }
3328
- const typeGroup = (state, type) => state.groups.find((group) => termGroupType(group) === type);
3329
- function newTypeGroup(state, type, preference) {
3330
- const info = termTypeInfo[type];
3331
- const base = {
3332
- id: termGroupId(state.journeyId, type),
3333
- kind: info.kind,
3334
- key: termGroupKey(type),
3335
- label: info.label,
3336
- condition: info.condition,
3337
- count: 0,
3338
- preferenceScope: null,
3339
- representative: null
3340
- };
3341
- if (!info.asked) return {
3342
- ...base,
3343
- belief: .97,
3344
- choice: "hide",
3345
- confirmed: true,
3346
- origin: "policy"
3347
- };
3348
- if (state.questionsPaused) return {
3349
- ...base,
3350
- belief: .97,
3351
- choice: "hide",
3352
- confirmed: false,
3353
- origin: "paused"
3354
- };
3280
+ /** Dictionary values a commit may add to the hidden set, each with its type's kind (ADR 0086). */
3281
+ function termHideAdditions(key, values) {
3282
+ const unique = /* @__PURE__ */ new Map();
3283
+ for (const { term, kind } of values) unique.set(JSON.stringify([term, kind]), hideDeltaValue(key, term, kindOf(kind)));
3355
3284
  return {
3356
- ...base,
3357
- belief: preference === null ? .5 : preference === "hide" ? .97 : .03,
3358
- choice: preference,
3359
- confirmed: false,
3360
- origin: preference === null ? "model" : "preference"
3285
+ scope: "additions",
3286
+ keyId: hideKeyId(key),
3287
+ values: [...unique.values()]
3361
3288
  };
3362
3289
  }
3363
3290
  /**
3364
- * One group per type with at least one dictionary value, `count` its distinct values. Existing
3365
- * groups keep their identity and answer; other groups are unchanged.
3291
+ * The values new Gray findings can add. A finding at a chunk edge may join a neighbouring
3292
+ * literal of the same field, so its words are not checked.
3366
3293
  */
3367
- function syncTermGroups(state, counts, preference) {
3368
- const groups = state.groups.map((group) => {
3369
- const type = termGroupType(group);
3370
- return type === null ? group : {
3371
- ...group,
3372
- count: counts.get(type) ?? group.count
3373
- };
3374
- });
3375
- for (const type of termTypes) {
3376
- const count = counts.get(type) ?? 0;
3377
- if (count === 0 || groups.some((group) => termGroupType(group) === type)) continue;
3378
- groups.push({
3379
- ...newTypeGroup(state, type, preference(termGroupKey(type))),
3380
- count
3381
- });
3294
+ function confirmedHideAdditions(key, records) {
3295
+ const values = /* @__PURE__ */ new Map();
3296
+ for (const record of records) {
3297
+ const chunks = new Map(record.chunks.map((chunk) => [chunk.id, chunk]));
3298
+ for (const finding of record.findings) {
3299
+ const chunk = chunks.get(finding.chunkId);
3300
+ if (!chunk || finding.class !== "gray") continue;
3301
+ const whole = chunk.offset === 0 && chunk.textHash === chunk.fieldHash;
3302
+ const joins = finding.start === 0 && chunk.offset > 0 || finding.end === chunk.text.length && !whole;
3303
+ const source = {
3304
+ batchId: record.batchId,
3305
+ findingId: finding.id
3306
+ };
3307
+ for (const term of bLiteralTerms(chunk.text.slice(finding.start, finding.end))) {
3308
+ const id = JSON.stringify([
3309
+ term,
3310
+ finding.kind,
3311
+ joins
3312
+ ]);
3313
+ const value = values.get(id);
3314
+ if (value) value.sources.push(source);
3315
+ else values.set(id, {
3316
+ ...hideDeltaValue(key, term, finding.kind, joins),
3317
+ sources: [source]
3318
+ });
3319
+ }
3320
+ }
3382
3321
  }
3383
- return groups;
3384
- }
3385
- //#endregion
3386
- //#region packages/application/src/journey/review/confirmed-hides.ts
3387
- /** Kinds from lowest to highest mask priority. */
3388
- const confirmedHideKinds = [
3389
- "internal",
3390
- "identity",
3391
- "private",
3392
- "credentials"
3393
- ];
3394
- const kinds = confirmedHideKinds;
3395
- const rank$1 = (kind) => kinds.indexOf(kind);
3396
- /**
3397
- * ADR 0086: a type's values are hidden unless its group is answered Keep; before an answer the
3398
- * default is Hide. Credentials and contact details are always hidden (D06).
3399
- */
3400
- function typeHides(state, type) {
3401
- const group = typeGroup(state, type);
3402
- return !termTypeInfo[type].asked || !group?.confirmed || group.choice !== "keep";
3322
+ return {
3323
+ scope: "additions",
3324
+ keyId: hideKeyId(key),
3325
+ values: [...values.values()]
3326
+ };
3403
3327
  }
3404
- /** B review (superseded by ADR 0086): IDs of the categories confirmed as Hide. */
3405
- const confirmedHideGroupIds = (state) => state.groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => group.id);
3406
- const overrideKey = (state) => (state.valueOverrides ?? []).map((item) => JSON.stringify([
3407
- item.type,
3408
- item.value,
3409
- item.choice
3410
- ])).toSorted();
3411
3328
  /**
3412
- * Identifies the hidden set (ADR 0079, 0086). The dictionary only grows and a type's group
3413
- * counts its distinct values, so each type's count and Hide answer, with the value overrides,
3414
- * name exactly one hidden set.
3329
+ * How hidden values moved when the value overrides and type answers stay the same: not at all,
3330
+ * only added (new dictionary values of hiding types), only removed, or otherwise. A type's
3331
+ * hidden values are its distinct values while it hides; an answer that changes whether a type
3332
+ * hides is otherwise, so only a complete delta covers it.
3415
3333
  */
3416
- function confirmedHideKey(state) {
3417
- return createHash("sha256").update(JSON.stringify({
3418
- types: termTypes.map((type) => [
3419
- type,
3420
- typeGroup(state, type)?.count ?? 0,
3421
- typeHides(state, type)
3422
- ]),
3423
- overrides: overrideKey(state),
3424
- terms: state.privacy.terms.filter((term) => term.disposition === "hidden").map((term) => [term.type, term.value]).toSorted((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))
3425
- })).digest("hex");
3334
+ function memberChange(previous, next) {
3335
+ if (!sameValueOverrides(previous, next)) return "other";
3336
+ if (termTypes.some((type) => typeGroup(previous, type) !== void 0 && typeHides(previous, type) !== typeHides(next, type))) return "other";
3337
+ const hidden = (state, type) => typeHides(state, type) ? typeGroup(state, type)?.count ?? 0 : 0;
3338
+ const counts = termTypes.map((type) => hidden(next, type) - hidden(previous, type));
3339
+ if (counts.every((count) => count === 0)) return "same";
3340
+ return counts.every((count) => count >= 0) ? "additions" : counts.every((count) => count <= 0) ? "removals" : "other";
3426
3341
  }
3427
- /** Whether two states hold the same value overrides. */
3428
- const sameValueOverrides = (left, right) => JSON.stringify(overrideKey(left)) === JSON.stringify(overrideKey(right));
3429
3342
  /**
3430
- * A preparation's waived hits stay waived only while every category recorded for them is still a
3431
- * confirmed Hide; otherwise render may no longer mask them, so the session needs reconciliation.
3343
+ * Whether a Work's output may change with the delta (Work metadata is never masked, so a
3344
+ * generated description never retires on its own). Without a delta that covers this exact change every Work is affected, as ADR 0069 first decided, except when
3345
+ * members were only removed: no value is then added, so nothing can be under-hidden, and an
3346
+ * output may keep masking a value that left its category until it is rendered again.
3432
3347
  */
3433
- function maskedHitsStale(maskedHits, hiding) {
3434
- const current = new Set(hiding);
3435
- return !!maskedHits?.some((hit) => hit.groupIds.some((id) => !current.has(id)));
3348
+ function impact(previous, next, delta, outputIndex) {
3349
+ const change = memberChange(previous, next);
3350
+ const covered = delta && (delta.scope === "complete" ? delta.from === confirmedHideKey(previous) && delta.to === confirmedHideKey(next) : change === "additions" || change === "same");
3351
+ if (!delta || !covered) return change === "removals" || change === "same" ? () => false : () => true;
3352
+ const unchanged = (index, value) => {
3353
+ const at = value.rendered.findIndex((digest) => hideIndexHas(index.values, digest));
3354
+ const was = at < 0 ? null : confirmedHideKinds[at] ?? null;
3355
+ return delta.scope === "additions" ? rank(was) >= rank(value.kind) : was === value.kind;
3356
+ };
3357
+ const reaches = (index, value) => {
3358
+ const digests = index.digests;
3359
+ return !value.tokens || !digests || value.tokens.every((token) => hideIndexHas(digests, token));
3360
+ };
3361
+ return (work) => {
3362
+ if (!work.artifact) return false;
3363
+ const found = outputIndex(work);
3364
+ const index = found?.keyId === delta.keyId ? found : null;
3365
+ return !index || delta.values.some((value) => value.partial || !unchanged(index, value) && reaches(index, value));
3366
+ };
3436
3367
  }
3437
3368
  /**
3438
- * ADR 0086 hidden set: values of types not answered Keep, minus Keep overrides, plus Hide
3439
- * overrides, plus every value of an always-hidden type. A value of several types takes the
3440
- * highest mask kind among the types hiding it.
3369
+ * Applied with every Journey commit. When confirmed Hide categories or their members change,
3370
+ * a Work whose output or generated description may change is retired until both are produced
3371
+ * again (ADR 0069); ADR 0079 limits this to Works the changed values can reach.
3441
3372
  */
3442
- function dictionaryHides(state, terms) {
3443
- const overrides = state.valueOverrides ?? [];
3444
- const kept = new Set(overrides.flatMap((item) => item.choice === "keep" ? [item.value] : []));
3445
- const hidden = /* @__PURE__ */ new Map();
3446
- const add = (value, type) => {
3447
- const prior = hidden.get(value);
3448
- const kind = termTypeInfo[type].kind;
3449
- hidden.set(value, {
3450
- kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
3451
- groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], termGroupId(state.journeyId, type)])].sort()
3452
- });
3373
+ function settleConfirmedHides(previous, next, delta, outputIndex = () => null) {
3374
+ if (!previous || confirmedHideKey(previous) === confirmedHideKey(next)) return next;
3375
+ const before = new Map(previous.works.map((work) => [work.id, work.revision]));
3376
+ const affected = impact(previous, next, delta, outputIndex);
3377
+ return {
3378
+ ...next,
3379
+ works: next.works.map((work) => work.artifact === null && work.revision > (before.get(work.id) ?? work.revision) || !affected(work) ? work : {
3380
+ ...retireWorkDescription(work),
3381
+ artifact: null,
3382
+ revision: work.revision + 1,
3383
+ status: work.status === "ready" ? "review" : work.status
3384
+ })
3453
3385
  };
3454
- const all = [...terms];
3455
- const hiddenValues = new Set(state.privacy.terms.filter((item) => item.disposition === "hidden").map((item) => item.value));
3456
- for (const term of all) if (hiddenValues.has(term.value) && !kept.has(term.value)) add(term.value, term.type);
3457
- for (const item of overrides) if (item.choice === "hide") add(item.value, item.type);
3458
- for (const term of all) if (!termTypeInfo[term.type].asked) add(term.value, term.type);
3459
- return hidden;
3460
- }
3461
- /** The journey's current hidden set, applied to every selected Work (ADR 0086, D03). */
3462
- async function journeyConfirmedHides(reader, state) {
3463
- return dictionaryHides(state, [...await reader.journeyTerms({ journeyId: state.journeyId }), ...state.privacy.terms]);
3464
3386
  }
3465
- function confirmedHideTerms(groups, records) {
3466
- const confirmed = new Map(groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => [group.id, group]));
3467
- const fields = /* @__PURE__ */ new Map();
3468
- for (const record of records) {
3469
- const assigned = new Map(record.assignments.map((item) => [item.findingId, item.groupId]));
3470
- const chunks = new Map(record.chunks.map((chunk) => [chunk.id, chunk]));
3471
- for (const finding of record.findings) {
3472
- const group = confirmed.get(assigned.get(finding.id) ?? "");
3473
- const chunk = chunks.get(finding.chunkId);
3474
- if (!group || !chunk || finding.class !== "gray") continue;
3475
- const key = JSON.stringify([chunk.fieldId, chunk.fieldHash]);
3476
- fields.set(key, [...fields.get(key) ?? [], {
3477
- start: chunk.offset + finding.start,
3478
- end: chunk.offset + finding.end,
3479
- text: chunk.text.slice(finding.start, finding.end),
3480
- split: [finding.start === 0, finding.end === chunk.text.length],
3481
- group
3482
- }]);
3483
- }
3484
- }
3485
- const terms = /* @__PURE__ */ new Map();
3486
- const add = (text, groupIds, kind) => {
3487
- for (const term of bLiteralTerms(text)) {
3488
- const prior = terms.get(term);
3489
- terms.set(term, {
3490
- kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
3491
- groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], ...groupIds])].sort()
3492
- });
3493
- }
3494
- };
3495
- for (const spans of fields.values()) {
3496
- let open;
3497
- for (const span of spans.toSorted((a, b) => a.start - b.start || b.end - a.end)) {
3498
- if (open && span.split[0] && span.start === open.end) {
3499
- open.text += span.text;
3500
- open.end = span.end;
3501
- open.groupIds.push(span.group.id);
3502
- } else {
3503
- if (open) add(open.text, open.groupIds, open.kind);
3504
- open = {
3505
- end: span.end,
3506
- text: span.text,
3507
- groupIds: [span.group.id],
3508
- kind: span.group.kind
3509
- };
3510
- }
3511
- if (!span.split[1]) {
3512
- add(open.text, open.groupIds, open.kind);
3513
- open = void 0;
3514
- }
3515
- }
3516
- if (open) add(open.text, open.groupIds, open.kind);
3517
- }
3518
- return terms;
3387
+ //#endregion
3388
+ //#region packages/application/src/journey/storage.ts
3389
+ const journeyEventSchema = z.object({
3390
+ sequence: z.number().int().nonnegative(),
3391
+ journeyId: journeyIdSchema,
3392
+ revision: z.number().int().nonnegative(),
3393
+ kind: z.enum([
3394
+ "started",
3395
+ "response",
3396
+ "snapshot",
3397
+ "batch",
3398
+ "output",
3399
+ "send",
3400
+ "search"
3401
+ ]),
3402
+ source: z.enum([
3403
+ "conversation",
3404
+ "panel",
3405
+ "system"
3406
+ ]),
3407
+ at: z.iso.datetime(),
3408
+ alignment: reviewAlignmentAuditSchema.optional(),
3409
+ preferenceScopes: z.array(z.object({
3410
+ groupId: journeyIdSchema,
3411
+ preferenceScope: journeyPreferenceScopeSchema
3412
+ }).strict()).min(1).max(1e4).optional(),
3413
+ correction: correctionAuditSchema.optional(),
3414
+ decision: delegatedDecisionSchema.extend({
3415
+ action: z.enum([
3416
+ "group",
3417
+ "groups-hide",
3418
+ "work-access",
3419
+ "instruction",
3420
+ "correction",
3421
+ "works-access",
3422
+ "work-type",
3423
+ "scope-confirm",
3424
+ "account-read",
3425
+ "card-answer",
3426
+ "cards-confirm"
3427
+ ]),
3428
+ targetIds: z.array(journeyIdSchema).min(1),
3429
+ choice: z.string().min(1).max(16)
3430
+ }).optional()
3431
+ }).strict();
3432
+ const journeyCommitSchema = z.object({
3433
+ expectedRevision: z.number().int().nonnegative().nullable(),
3434
+ requestKey: journeyIdSchema,
3435
+ requestHash: journeyDigestSchema,
3436
+ state: nativeJourneySchema,
3437
+ event: journeyEventSchema.omit({
3438
+ sequence: true,
3439
+ revision: true
3440
+ }),
3441
+ /** Inserted in the same transaction as this state; never through the generic resource save. */
3442
+ correctionReport: correctionReportResourceSchema.optional(),
3443
+ /** Verify unread batch ownership in the same transaction as a reader interruption. */
3444
+ closedReader: reviewInterruptionSchema.pick({
3445
+ inputRevision: true,
3446
+ packId: true,
3447
+ batchIds: true
3448
+ }).extend({ workId: journeyIdSchema }).optional(),
3449
+ /** Set by the store for this commit only and never persisted (ADR 0079). */
3450
+ confirmedHideDelta: confirmedHideDeltaSchema.optional(),
3451
+ /** Dictionary values a Hide override adds (ADR 0086), inserted with this state. */
3452
+ terms: z.array(z.object({
3453
+ type: termTypeSchema,
3454
+ value: z.string().min(1)
3455
+ }).strict()).max(2e4).optional()
3456
+ }).strict();
3457
+ const journeyCommitResultSchema = z.object({
3458
+ status: z.enum([
3459
+ "applied",
3460
+ "replayed",
3461
+ "stale"
3462
+ ]),
3463
+ state: nativeJourneySchema
3464
+ }).strict();
3465
+ //#endregion
3466
+ //#region packages/application/src/journey/search.ts
3467
+ const count$9 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
3468
+ const term = z.string().min(1).max(1024).refine((value) => Buffer.byteLength(value) <= 1024 && !/[\0\r\n]/u.test(value));
3469
+ const journeySearchSchema = z.object({
3470
+ journeyId: journeyIdSchema,
3471
+ terms: z.array(z.object({
3472
+ id: z.string().min(1).max(64),
3473
+ text: term,
3474
+ variants: z.array(term).max(7).default([])
3475
+ }).strict()).min(1).max(16)
3476
+ }).strict();
3477
+ const journeySearchResultSchema = z.object({
3478
+ binding: z.object({
3479
+ journeyId: journeyIdSchema,
3480
+ snapshotId: journeyDigestSchema,
3481
+ revision: count$9,
3482
+ scopeDigest: journeyDigestSchema,
3483
+ outputs: z.array(z.object({
3484
+ workId: journeyIdSchema,
3485
+ outputRevision: count$9,
3486
+ sha256: journeyDigestSchema
3487
+ }).strict())
3488
+ }).strict(),
3489
+ status: z.enum(["complete", "partial"]),
3490
+ scope: z.object({
3491
+ retainedText: z.literal(true),
3492
+ metadata: z.literal(true),
3493
+ setupText: z.literal(true),
3494
+ attachments: z.literal(false),
3495
+ images: z.literal(false),
3496
+ originals: z.literal("frozen-native-records-and-discovery-metadata"),
3497
+ matching: z.literal("case-sensitive-literal-leftmost-longest")
3498
+ }).strict(),
3499
+ terms: z.array(z.object({
3500
+ id: z.string(),
3501
+ tried: z.array(z.string()),
3502
+ sentMatches: count$9.nullable(),
3503
+ originalMatches: count$9.nullable(),
3504
+ sentSessions: count$9.nullable(),
3505
+ originalSessions: count$9.nullable()
3506
+ }).strict()),
3507
+ hits: z.array(z.object({
3508
+ termId: z.string(),
3509
+ basis: z.enum(["sent", "original"]),
3510
+ workId: journeyIdSchema,
3511
+ locator: z.object({
3512
+ kind: z.enum([
3513
+ "retained-text",
3514
+ "metadata",
3515
+ "setup-text"
3516
+ ]),
3517
+ entryPath: z.string().optional(),
3518
+ archiveSessionId: z.string().optional(),
3519
+ sessionId: z.string().optional(),
3520
+ sourcePartId: z.string().optional(),
3521
+ recordIndex: count$9,
3522
+ fieldId: z.string(),
3523
+ line: count$9
3524
+ }).strict(),
3525
+ occurrences: count$9,
3526
+ modelContext: z.string().max(4096).optional(),
3527
+ match: z.object({
3528
+ variantIndex: count$9.max(7),
3529
+ start: count$9,
3530
+ end: count$9
3531
+ }).strict().optional()
3532
+ }).strict()),
3533
+ failures: z.array(z.object({
3534
+ workId: journeyIdSchema,
3535
+ basis: z.enum(["sent", "original"]),
3536
+ code: z.enum(["SEARCH_INPUT_UNAVAILABLE", "SEARCH_ENGINE_FAILED"])
3537
+ }).strict()),
3538
+ truncated: z.boolean()
3539
+ }).strict().refine((result) => result.hits.every((hit) => {
3540
+ if (hit.basis === "original") return hit.modelContext === void 0 && hit.match === void 0;
3541
+ if (!hit.match || hit.modelContext === void 0) return false;
3542
+ const { start, end, variantIndex } = hit.match;
3543
+ const variant = result.terms.find((term) => term.id === hit.termId)?.tried[variantIndex];
3544
+ if (!variant || end <= start || end > hit.modelContext.length || hit.modelContext.slice(start, end) !== variant) return false;
3545
+ return [start, end].every((offset) => {
3546
+ const before = hit.modelContext?.charCodeAt(offset - 1) ?? 0;
3547
+ const after = hit.modelContext?.charCodeAt(offset) ?? 0;
3548
+ return !(before >= 55296 && before <= 56319 && after >= 56320 && after <= 57343);
3549
+ });
3550
+ }));
3551
+ const journeySearchSaveSchema = z.object({
3552
+ journeyId: journeyIdSchema,
3553
+ expectedRevision: count$9,
3554
+ idempotencyKey: journeyIdSchema,
3555
+ result: journeySearchResultSchema
3556
+ }).strict();
3557
+ function searchScopeDigest(state) {
3558
+ return textDigest(JSON.stringify({
3559
+ snapshot: state.snapshotId,
3560
+ configurationId: state.configurationId,
3561
+ pendingHideId: state.pendingHideId,
3562
+ works: state.works.map((work) => ({
3563
+ id: work.id,
3564
+ revision: work.revision,
3565
+ inputRevision: work.inputRevision,
3566
+ access: work.access,
3567
+ artifact: work.artifact?.sha256 ?? null
3568
+ }))
3569
+ }));
3519
3570
  }
3571
+ z.object({
3572
+ journeyId: journeyIdSchema.optional(),
3573
+ workIds: z.array(journeyIdSchema).min(1).max(1e3).optional()
3574
+ }).strict();
3575
+ const categories = z.object({
3576
+ identity: z.number().int().nonnegative(),
3577
+ credentials: z.number().int().nonnegative(),
3578
+ internal: z.number().int().nonnegative(),
3579
+ private: z.number().int().nonnegative()
3580
+ }).strict();
3581
+ z.object({
3582
+ journeyId: journeyIdSchema,
3583
+ nonAdditiveMechanismCounts: z.literal(true),
3584
+ total: z.number().int().nonnegative(),
3585
+ categories,
3586
+ byMechanism: z.object({
3587
+ staticNodes: z.record(z.string(), z.number().int().nonnegative()),
3588
+ semantic: z.number().int().nonnegative(),
3589
+ correction: z.number().int().nonnegative()
3590
+ }).strict(),
3591
+ works: z.array(z.object({
3592
+ workId: journeyIdSchema,
3593
+ revision: z.number().int().positive(),
3594
+ inputRevision: z.number().int().positive(),
3595
+ primarySha256: z.string().length(64),
3596
+ companionSha256: z.string().length(64),
3597
+ ledgerSha256: z.string().length(64),
3598
+ decisionDigest: z.string().length(64),
3599
+ reviewInputDigest: z.string().length(64),
3600
+ total: z.number().int().nonnegative(),
3601
+ categories,
3602
+ byMechanism: z.object({
3603
+ staticNodes: z.record(z.string(), z.number().int().nonnegative()),
3604
+ semantic: z.number().int().nonnegative(),
3605
+ correction: z.number().int().nonnegative()
3606
+ }).strict()
3607
+ }).strict())
3608
+ }).strict();
3520
3609
  //#endregion
3521
- //#region packages/application/src/journey/b/matches.ts
3522
- function bDictionaryHits(fields, literals, signal = new AbortController().signal) {
3523
- const matcher = createEntityMatcher([...new Set(literals.map((part) => part.term))].map((value) => ({
3524
- kind: "custom-term",
3525
- value
3526
- })));
3527
- const hits = [];
3528
- for (const field of fields) {
3529
- const index = createCodePointIndex(field.text);
3530
- const base = field.start ?? 0;
3531
- for (const match of matcher.find(field.text, signal, 1e5)) {
3532
- if (hits.length >= 1e6) throw new ApplicationError("PROCESSING_LIMIT");
3533
- const body = {
3534
- fieldId: field.id,
3535
- fieldHash: field.fieldHash,
3536
- recordId: field.recordId,
3537
- start: base + index.codePointOffset(match.range.start),
3538
- end: base + index.codePointOffset(match.range.end),
3539
- exactQuote: field.text.slice(match.range.start, match.range.end),
3540
- term: match.entity.value
3541
- };
3542
- hits.push({
3543
- ...body,
3544
- digest: bDigest(JSON.stringify(body))
3545
- });
3546
- }
3547
- }
3548
- return [...new Map(hits.map((hit) => [hit.digest, hit])).values()];
3549
- }
3550
- function unclassifiedBHits(hits, literals, negative) {
3551
- const issues = [];
3552
- const positiveFields = new Map([...Map.groupBy(literals, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
3553
- const negativeFields = new Map([...Map.groupBy(negative, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
3554
- for (const hit of hits) {
3555
- const positive = positiveFields.get(hit.fieldId)?.relation(hit).covers ?? false;
3556
- const negative = negativeFields.get(hit.fieldId)?.relation(hit);
3557
- if (!positive && !negative?.covers || positive && negative?.overlaps) issues.push(bIssue(positive ? "CONFLICTING_PRIVACY_DECISIONS" : "UNCLASSIFIED_MATCH", hit));
3558
- }
3559
- return issues;
3560
- }
3561
- const bSpacedWord = (value) => /[\p{L}\p{N}_]/u.test(value) && !/[\p{sc=Han}\p{sc=Hiragana}\p{sc=Katakana}\p{sc=Thai}\p{sc=Lao}\p{sc=Khmer}\p{sc=Myanmar}]/u.test(value);
3562
- /** A confirmed Hide value matches only where it does not run into a neighbouring word (ADR 0069). */
3563
- function bWordBounded(text, start, end) {
3564
- return !(bSpacedWord(scalarBefore(text, start)) && bSpacedWord(scalarAfter(text, start)) || bSpacedWord(scalarBefore(text, end)) && bSpacedWord(scalarAfter(text, end)));
3610
+ //#region packages/application/src/journey/scope/respond.ts
3611
+ /**
3612
+ * Account-seeded access (ADR 0274 D3(b)). With `held`, the time the account inventory was read,
3613
+ * a Work whose every root is held Public is confirmed Public with the account as its recorded
3614
+ * choice (ADR 0275); without it, Public stays a suggestion.
3615
+ */
3616
+ function accountSeededAccess(account, held) {
3617
+ const accessChoice = account === "public-all" && held !== void 0 ? {
3618
+ channel: "account",
3619
+ at: held
3620
+ } : void 0;
3621
+ return {
3622
+ access: account === "public" || account === "public-all" ? "public" : "private",
3623
+ accessOrigin: "account",
3624
+ accessConfirmed: account === "private" || accessChoice !== void 0,
3625
+ accountAccessConflict: account === "mixed",
3626
+ accessChoice
3627
+ };
3565
3628
  }
3566
3629
  /**
3567
- * Occurrences of a Work's explicit exceptions, matched like Hide everywhere: exact NFC values
3568
- * over complete graphemes. A confirmed Hide value inside one stays visible there.
3630
+ * The scope-confirmation stop (ADR 0277 section 5), or null when the read lets it confirm. A
3631
+ * failed read always stops, with Retry only. A signed-out read stops too, except when the
3632
+ * journey's consent has no subject (Q11 a and Q13 a), regardless of caller configuration.
3569
3633
  */
3570
- function bExceptionRanges(values) {
3571
- const matcher = createEntityMatcher([...new Set([...values].map((value) => value.normalize("NFC")))].map((value) => ({
3572
- kind: "custom-term",
3573
- value
3574
- })));
3575
- return (text, signal = new AbortController().signal) => matcher.find(text, signal, 1e5).filter(({ range, entity }) => text.slice(range.start, range.end).normalize("NFC") === entity.value).map(({ range }) => range);
3634
+ function accountReadStop(state, read) {
3635
+ if (!read || read.status === "read") return null;
3636
+ if (read.status === "unavailable") return "ACCOUNT_ACCESS_UNAVAILABLE";
3637
+ return consentedWithoutAccount(state) ? null : "ACCOUNT_SIGNED_OUT";
3576
3638
  }
3577
- /** Whether a range lies inside one of `ranges`: a prefix maximum of ends, O(log n) per query. */
3578
- function bContainment(ranges) {
3579
- const sorted = ranges.toSorted((a, b) => a.start - b.start);
3580
- const reach = [];
3581
- for (const [index, range] of sorted.entries()) reach.push(Math.max(range.end, reach[index - 1] ?? range.end));
3582
- return (range) => {
3583
- let low = 0, high = sorted.length;
3584
- while (low < high) {
3585
- const middle = low + high >> 1;
3586
- if ((sorted[middle]?.start ?? Infinity) <= range.start) low = middle + 1;
3587
- else high = middle;
3639
+ /** The journey with this read recorded; the subject is kept only for a `read`. */
3640
+ function withAccountRead(state, read, now) {
3641
+ return {
3642
+ ...state,
3643
+ accountRead: read.status === "read" ? {
3644
+ status: "read",
3645
+ subject: read.subject,
3646
+ at: now
3647
+ } : {
3648
+ status: read.status,
3649
+ at: now
3588
3650
  }
3589
- return low > 0 && (reach[low - 1] ?? -1) >= range.end;
3590
3651
  };
3591
3652
  }
3592
3653
  /**
3593
- * Word-bounded, exact, case-sensitive NFC occurrences of the terms, in UTF-16 coordinates,
3594
- * except where an explicit exception occurrence contains one.
3654
+ * Retry at the Send screen (ADR 0277 section 5): record the read and, on `read`, re-apply the
3655
+ * account while keeping confirmed person choices and every Private or dropped Work. An earlier choice
3656
+ * that cannot carry falls back to Private. Access changes get a new revision and clear the
3657
+ * output and package approval.
3595
3658
  */
3596
- function bTermMatcher(terms, exceptions = []) {
3597
- const matcher = createEntityMatcher([...new Set(terms)].map((value) => ({
3598
- kind: "custom-term",
3599
- value
3600
- })));
3601
- const excepted = bExceptionRanges(exceptions);
3602
- return (text, signal = new AbortController().signal) => {
3603
- const hits = matcher.find(text, signal, 1e5).filter(({ range }) => bWordBounded(text, range.start, range.end));
3604
- const kept = hits.length ? bContainment(excepted(text, signal)) : () => false;
3605
- return hits.filter(({ range }) => !kept(range)).map(({ range, entity }) => ({
3606
- ...range,
3607
- term: entity.value
3608
- }));
3659
+ function retryAccountRead(state, read, now) {
3660
+ if (state.scope.status !== "confirmed" || state.phase === "sending" || state.phase === "completed") throw new ApplicationError("INVALID_TRANSITION");
3661
+ const recorded = withAccountRead(state, read, now);
3662
+ if (read.status !== "read") return recorded;
3663
+ let changed = false;
3664
+ const works = state.works.map((work) => {
3665
+ if (work.placement === "classifying") return work;
3666
+ if (work.accessOrigin === "explicit" && work.accessConfirmed) return work;
3667
+ if (work.access !== "public") return work;
3668
+ const account = read.access.get(work.id);
3669
+ if (!account && !work.accessChoice && work.accessOrigin !== "account") return work;
3670
+ const seeded = accountSeededAccess(account ?? "private", now);
3671
+ if (work.accessChoice && (work.accessChoice.channel !== "account" || !seeded.accessChoice)) {
3672
+ seeded.access = "private";
3673
+ seeded.accessConfirmed = account !== "mixed";
3674
+ seeded.accessChoice = void 0;
3675
+ }
3676
+ if (seeded.access === work.access) return {
3677
+ ...work,
3678
+ ...seeded
3679
+ };
3680
+ changed = true;
3681
+ return {
3682
+ ...work,
3683
+ ...seeded,
3684
+ revision: work.revision + 1,
3685
+ artifact: null,
3686
+ status: work.status === "ready" ? "review" : work.status
3687
+ };
3688
+ });
3689
+ return changed ? withReadingProgress({
3690
+ ...recorded,
3691
+ privacy: {
3692
+ ...state.privacy,
3693
+ packageApproval: null
3694
+ },
3695
+ works
3696
+ }) : {
3697
+ ...recorded,
3698
+ works
3609
3699
  };
3610
3700
  }
3611
- /** Dictionary hits that a confirmed Hide value masks at render, so they need no classification. */
3612
- function bMaskedHits(fields, terms) {
3613
- const byId = new Map(fields.map((field) => [field.id, {
3614
- text: field.text,
3615
- index: createCodePointIndex(field.text),
3616
- base: field.start ?? 0
3617
- }]));
3618
- return (hit) => {
3619
- const field = byId.get(hit.fieldId);
3620
- return !!field && terms.has(hit.term) && bWordBounded(field.text, field.index.offset(hit.start - field.base), field.index.offset(hit.end - field.base));
3701
+ /** Classification confirmation initializes visibility and scan membership for this journey. */
3702
+ function confirmScope(state, now, decision, accountAccess) {
3703
+ if (state.scope.status === "confirmed") return state;
3704
+ if (state.classificationRevision === null || !state.works.length || state.works.some((work) => work.placement !== "placed" && work.placement !== "classifying" || work.placement === "placed" && !(work.status === "blocked" && work.access === "drop" && work.accessConfirmed) && (!work.description.trim() || !work.projectType))) throw new ApplicationError("SCOPE_NOT_READY");
3705
+ return withReadingProgress({
3706
+ ...state,
3707
+ scope: {
3708
+ status: "confirmed",
3709
+ actor: decision ? "delegate" : "human",
3710
+ at: now
3711
+ },
3712
+ works: state.works.map((work) => {
3713
+ if (work.placement === "classifying") return work;
3714
+ if (work.accessOrigin === "explicit" && work.accessConfirmed) return work;
3715
+ if (work.access === "private" && work.accessOrigin === "delegate") return work;
3716
+ const account = accountAccess?.get(work.id);
3717
+ if (account) return {
3718
+ ...work,
3719
+ ...accountSeededAccess(account, now)
3720
+ };
3721
+ if (work.accessOrigin === "account") return work;
3722
+ const access = work.accessConfirmed ? work.access : work.classificationRecommendation?.access ?? work.access;
3723
+ return {
3724
+ ...work,
3725
+ access,
3726
+ accessOrigin: work.accessConfirmed || !work.classificationRecommendation ? work.accessOrigin : "model",
3727
+ accessConfirmed: access !== "public"
3728
+ };
3729
+ })
3730
+ });
3731
+ }
3732
+ function setWorkType(state, workId, type, decision) {
3733
+ if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
3734
+ const projectType = decision ? {
3735
+ type,
3736
+ reason: "Chosen by your delegate for this journey.",
3737
+ origin: "delegate"
3738
+ } : {
3739
+ type,
3740
+ reason: "Chosen by you.",
3741
+ origin: "explicit"
3621
3742
  };
3743
+ return {
3744
+ ...state,
3745
+ scope: {
3746
+ status: "pending",
3747
+ actor: null,
3748
+ at: null
3749
+ },
3750
+ privacy: {
3751
+ ...state.privacy,
3752
+ cards: [],
3753
+ confirmedAnswersRevision: null,
3754
+ packageApproval: null
3755
+ },
3756
+ works: state.works.map((work) => work.id !== workId || decision && work.projectType?.origin === "explicit" ? work : {
3757
+ ...work,
3758
+ projectType
3759
+ })
3760
+ };
3761
+ }
3762
+ /** One rule for single and bulk choices; selection changes recompute reading completion. */
3763
+ function setWorksAccess(state, workIds, access, decision, accessChoice) {
3764
+ if (decision && access === "public") throw new ApplicationError("CONFIRMATION_REQUIRED");
3765
+ if (new Set(workIds).size !== workIds.length) throw new ApplicationError("INVALID_SELECTION");
3766
+ if (workIds.some((id) => !state.works.some((work) => work.id === id))) throw new ApplicationError("UNKNOWN_WORKSPACE");
3767
+ if (state.scope.status === "confirmed") return changeConfirmedAccess(state, workIds, access, decision, accessChoice);
3768
+ return withReadingProgress({
3769
+ ...state,
3770
+ scope: {
3771
+ status: "pending",
3772
+ actor: null,
3773
+ at: null
3774
+ },
3775
+ privacy: {
3776
+ ...state.privacy,
3777
+ cards: [],
3778
+ confirmedAnswersRevision: null,
3779
+ packageApproval: null
3780
+ },
3781
+ works: state.works.map((work) => !workIds.includes(work.id) ? work : {
3782
+ ...work,
3783
+ revision: work.revision + 1,
3784
+ artifact: null,
3785
+ status: work.status === "ready" ? "review" : work.status,
3786
+ access,
3787
+ accessConfirmed: true,
3788
+ accountAccessConflict: false,
3789
+ accessChoice: access === "public" ? accessChoice : void 0,
3790
+ accessOrigin: decision && work.accessOrigin !== "explicit" ? "delegate" : "explicit"
3791
+ })
3792
+ });
3622
3793
  }
3623
- const segmenter = new Intl.Segmenter("und", { granularity: "grapheme" });
3624
- /** Beyond this many inner match edges in one word the index is not kept (retire instead). */
3625
- const maximumEdges = 16;
3626
- /** Beyond this many distinct pieces the index is not kept (retire instead). */
3627
- const maximumHideIndexTokens = 2e6;
3628
3794
  /**
3629
- * Adds the word pieces of `text` that an occurrence of a confirmed Hide value can consist of.
3630
- * The matcher compares per-grapheme NFC text and checks word edges on the original characters
3631
- * at grapheme edges only, so a match may start or end inside a run of word characters there,
3632
- * or inside a grapheme. Every piece between such positions is added. Returns false when a run
3633
- * has too many such positions to list.
3795
+ * After confirmation the person, never a delegate, may still change any Work's visibility
3796
+ * (D2, superseding ADR 0162's lock and ADR 0188's blocked-only Drop). Drop stops that Work's
3797
+ * scan; a Work moved out of Drop is read again; reading progress is recomputed.
3634
3798
  */
3635
- function addHideIndexTokens(text, add) {
3636
- let complete = true;
3637
- let from = 0;
3638
- for (const { index } of text.matchAll(/[^\p{ASCII}]+/gu)) {
3639
- if (index < from) continue;
3640
- let start = index;
3641
- while (start > from && asciiWord(text.charCodeAt(start - 1))) start--;
3642
- let end = index + 1;
3643
- while (end < text.length && (text.charCodeAt(end) > 127 || asciiWord(text.charCodeAt(end)))) end++;
3644
- asciiTokens(text.slice(from, start), add);
3645
- complete = graphemeTokens(text.slice(start, end), add) && complete;
3646
- from = end;
3647
- }
3648
- asciiTokens(text.slice(from), add);
3649
- return complete;
3650
- }
3651
- const asciiWord = (code) => code >= 48 && code <= 57 || code >= 65 && code <= 90 || code === 95 || code >= 97 && code <= 122;
3652
- function asciiTokens(text, add) {
3653
- for (const [token] of text.matchAll(/[A-Za-z0-9_]+/gu)) add(token);
3799
+ function changeConfirmedAccess(state, workIds, access, decision, accessChoice) {
3800
+ if (decision || state.phase === "sending" || state.phase === "completed" || state.works.some((work) => workIds.includes(work.id) && work.access === access && work.accessConfirmed && hasPublicChoice(work))) throw new ApplicationError("INVALID_TRANSITION");
3801
+ if (access === "public" && state.works.every((work) => !workIds.includes(work.id) || work.access === "public" && work.accessConfirmed)) return {
3802
+ ...state,
3803
+ works: state.works.map((work) => workIds.includes(work.id) && accessChoice ? {
3804
+ ...work,
3805
+ accessOrigin: "explicit",
3806
+ accessChoice
3807
+ } : work)
3808
+ };
3809
+ return withReadingProgress({
3810
+ ...state,
3811
+ privacy: {
3812
+ ...state.privacy,
3813
+ packageApproval: null
3814
+ },
3815
+ works: state.works.map((work) => workIds.includes(work.id) ? {
3816
+ ...work,
3817
+ revision: work.revision + 1,
3818
+ artifact: null,
3819
+ status: work.status === "ready" ? "review" : work.status,
3820
+ access,
3821
+ accessConfirmed: true,
3822
+ accountAccessConflict: false,
3823
+ accessChoice: access === "public" ? accessChoice : void 0,
3824
+ accessOrigin: "explicit"
3825
+ } : work)
3826
+ });
3654
3827
  }
3655
- function graphemeTokens(text, add) {
3656
- let complete = true, run = [], edges = [];
3657
- const flush = () => {
3658
- if (edges.length > maximumEdges) complete = false;
3659
- else {
3660
- const cuts = [
3661
- 0,
3662
- ...edges,
3663
- run.length
3664
- ];
3665
- for (let from = 0; from < cuts.length; from++) for (let to = from + 1; to < cuts.length; to++) add(run.slice(cuts[from], cuts[to]).join(""));
3666
- }
3667
- run = [];
3668
- edges = [];
3828
+ /** Model suggestions fill or refresh model types only; a person's or delegate's type stays. */
3829
+ function applyClassification(state, items) {
3830
+ if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
3831
+ const byWork = new Map(items.map((item) => [item.workId, item]));
3832
+ if (byWork.size !== items.length) throw new ApplicationError("INVALID_SELECTION");
3833
+ if (items.some((item) => !state.works.some((work) => work.id === item.workId))) throw new ApplicationError("UNKNOWN_WORKSPACE");
3834
+ return {
3835
+ ...state,
3836
+ works: state.works.map((work) => {
3837
+ const item = byWork.get(work.id);
3838
+ if (!item || work.projectType && work.projectType.origin !== "model") return work;
3839
+ return {
3840
+ ...work,
3841
+ projectType: {
3842
+ type: item.type,
3843
+ reason: item.reason,
3844
+ origin: "model"
3845
+ }
3846
+ };
3847
+ })
3669
3848
  };
3670
- let previous = "";
3671
- for (const { segment } of segmenter.segment(text)) {
3672
- const original = Array.from(segment);
3673
- Array.from(segment.normalize("NFC")).forEach((scalar, index) => {
3674
- if (!bSpacedWord(scalar)) {
3675
- if (run.length) flush();
3676
- return;
3677
- }
3678
- if (run.length && (index > 0 || !(bSpacedWord(previous) && bSpacedWord(original[0] ?? "")))) edges.push(run.length);
3679
- run.push(scalar);
3849
+ }
3850
+ //#endregion
3851
+ //#region packages/application/src/journey/inventory/schema.ts
3852
+ const journeyInventoryQuerySchema = z.object({ journeyId: journeyIdSchema }).strict();
3853
+ /** Trusted host binding, never accepted by the public command schema. */
3854
+ const journeyInventoryReadSchema = journeyInventoryQuerySchema.extend({ expectedDestination: z.string().min(1).optional() }).strict();
3855
+ const discoveryWarningCodeSchema = z.enum([
3856
+ "UNSUPPORTED_SOURCE_ENCODING",
3857
+ "SKIPPED_NONREGULAR_SOURCE",
3858
+ "WORKFLOW_JOURNAL_EXCLUDED",
3859
+ "AUTOMATIC_HELPER_EXCLUDED",
3860
+ "UNRESOLVED_WORKERS_EXCLUDED",
3861
+ "METADATA_LIMIT",
3862
+ "METADATA_DEPTH_LIMIT",
3863
+ "INCOMPLETE_DISCOVERY",
3864
+ "DISCOVERY_ENTRY_LIMIT",
3865
+ "DISCOVERY_DEPTH_LIMIT",
3866
+ "BROKEN_SOURCE_LINK"
3867
+ ]);
3868
+ const evidence = z.object({
3869
+ known: preparationCountSchema,
3870
+ total: preparationCountSchema.nullable()
3871
+ }).strict().refine((value) => value.total === null || value.total === value.known);
3872
+ const workPreparationObservationSchema = z.object({
3873
+ workId: journeyIdSchema,
3874
+ inputRevision: preparationCountSchema.min(1),
3875
+ observation: z.enum([
3876
+ "unavailable",
3877
+ "incomplete",
3878
+ "settled"
3879
+ ]),
3880
+ stage: preparationStageSchema.nullable(),
3881
+ sourceParts: z.object({
3882
+ unsupported: evidence,
3883
+ resourceLimited: evidence,
3884
+ excluded: evidence,
3885
+ unclassified: preparationCountSchema
3886
+ }).strict(),
3887
+ emptyReviewRoots: evidence,
3888
+ unevaluatedReviewRoots: preparationCountSchema
3889
+ }).strict();
3890
+ const journeyInventorySchema = z.object({
3891
+ journeyId: journeyIdSchema,
3892
+ revision: preparationCountSchema,
3893
+ snapshotId: journeyDigestSchema.nullable(),
3894
+ frozen: z.object({
3895
+ retainedSessions: preparationCountSchema,
3896
+ reviewRoots: preparationCountSchema,
3897
+ identifiedMainRoots: preparationCountSchema,
3898
+ unknownRoleRoots: preparationCountSchema,
3899
+ attachedSubagents: preparationCountSchema,
3900
+ unresolvedWorkerSessions: preparationCountSchema,
3901
+ sourceParts: preparationCountSchema,
3902
+ frozenSourceBytes: preparationCountSchema,
3903
+ discoveryUnsupportedInputs: z.null(),
3904
+ discoverySkippedInputs: z.null(),
3905
+ discoverySkippedSourceBytes: z.null(),
3906
+ warningOccurrences: z.array(z.object({
3907
+ code: discoveryWarningCodeSchema,
3908
+ occurrences: preparationCountSchema
3909
+ }).strict())
3910
+ }).strict().nullable(),
3911
+ captures: z.object({
3912
+ targetParts: preparationCountSchema,
3913
+ capturedParts: preparationCountSchema,
3914
+ capturedBytes: preparationCountSchema,
3915
+ completeCapturedBytes: preparationCountSchema.nullable()
3916
+ }).strict().nullable(),
3917
+ workScope: z.object({
3918
+ total: preparationCountSchema,
3919
+ byStatus: z.object({
3920
+ pending: preparationCountSchema,
3921
+ reading: preparationCountSchema,
3922
+ review: preparationCountSchema,
3923
+ ready: preparationCountSchema,
3924
+ blocked: preparationCountSchema
3925
+ }).strict(),
3926
+ byAccess: z.object({
3927
+ public: preparationCountSchema,
3928
+ private: preparationCountSchema,
3929
+ drop: preparationCountSchema
3930
+ }).strict(),
3931
+ assignedReviewRoots: preparationCountSchema,
3932
+ unassignedReviewRoots: preparationCountSchema.nullable(),
3933
+ excludedAutomationRoots: preparationCountSchema.nullable(),
3934
+ unassignedOtherRoots: preparationCountSchema.nullable()
3935
+ }).strict(),
3936
+ preparation: z.array(workPreparationObservationSchema)
3937
+ }).strict();
3938
+ /** Panel preparation counts over the selected Works: opaque Work IDs, parts and bytes only. */
3939
+ const journeyPreparationProgressSchema = z.object({
3940
+ currentWorkIds: z.array(journeyIdSchema),
3941
+ targetParts: preparationCountSchema,
3942
+ capturedParts: preparationCountSchema,
3943
+ targetBytes: preparationCountSchema,
3944
+ capturedBytes: preparationCountSchema
3945
+ }).strict();
3946
+ //#endregion
3947
+ //#region packages/application/src/source/resolve.ts
3948
+ /** Queue-based ancestry resolution cannot leak deep descendants through a recursion cutoff. */
3949
+ function resolveParts(inventory, route) {
3950
+ const nodes = sourceGraph(inventory);
3951
+ const families = reviewFamilies(inventory, nodes);
3952
+ const resolved = /* @__PURE__ */ new Map();
3953
+ const depths = /* @__PURE__ */ new Map();
3954
+ const dependents = /* @__PURE__ */ new Map();
3955
+ const remaining = /* @__PURE__ */ new Map();
3956
+ const queue = [];
3957
+ const finish = (node, included, unresolved, reason, depth = 0) => {
3958
+ resolved.set(node.key, {
3959
+ sessionId: node.session.id,
3960
+ partId: node.part?.id ?? "",
3961
+ included,
3962
+ unresolved,
3963
+ reason
3680
3964
  });
3681
- previous = original.at(-1) ?? "";
3965
+ depths.set(node.key, depth);
3966
+ queue.push(node);
3967
+ };
3968
+ const pending = (node, reason) => finish(node, route === "original", true, reason);
3969
+ for (const node of nodes) {
3970
+ const { identity, reason } = node.observation;
3971
+ if (families.unresolved.has(node.session.id)) finish(node, false, node.ancestry !== "missing", node.ancestry === "missing" ? "missing-parent" : "ancestry-unresolved");
3972
+ else if (!familySelected(inventory, families, node.session)) finish(node, false, false, families.owners.get(node.session.id)?.id !== node.session.id ? "deselected-parent" : node.session.selected ? "workspace-not-selected" : "not-selected");
3973
+ else if (reason !== null) pending(node, reason);
3974
+ else if (identity?.role === "guardian") finish(node, false, false, "guardian-excluded");
3975
+ else if (identity?.role !== "worker") finish(node, true, false, null);
3976
+ else if (node.ancestry === "missing") finish(node, false, false, "missing-parent");
3977
+ else if (node.ancestry !== "parents") finish(node, false, true, "ancestry-unresolved");
3978
+ else {
3979
+ remaining.set(node.key, node.parents.length);
3980
+ for (const parent of node.parents) {
3981
+ const children = dependents.get(parent.key) ?? [];
3982
+ children.push(node);
3983
+ dependents.set(parent.key, children);
3984
+ }
3985
+ }
3682
3986
  }
3683
- if (run.length) flush();
3684
- return complete;
3685
- }
3686
- /** The words of an NFC value, or null when it has none and cannot be checked by words. */
3687
- function hideValueTokens(term) {
3688
- const tokens = [];
3689
- let run = "";
3690
- for (const scalar of term) if (bSpacedWord(scalar)) run += scalar;
3691
- else if (run) {
3692
- tokens.push(run);
3693
- run = "";
3987
+ for (let cursor = 0; cursor < queue.length; cursor++) {
3988
+ const parent = queue[cursor];
3989
+ if (!parent) continue;
3990
+ const value = resolved.get(parent.key);
3991
+ for (const node of dependents.get(parent.key) ?? []) {
3992
+ if (resolved.has(node.key)) continue;
3993
+ const outstanding = (remaining.get(node.key) ?? 1) - 1;
3994
+ remaining.set(node.key, outstanding);
3995
+ const depth = (depths.get(parent.key) ?? 0) + 1;
3996
+ if (value?.included && !value.unresolved && (route === "original" || depth <= 256)) {
3997
+ finish(node, true, false, null, depth);
3998
+ continue;
3999
+ }
4000
+ if (outstanding > 0) continue;
4001
+ const results = node.parents.map((entry) => resolved.get(entry.key));
4002
+ if (results.every((entry) => entry && [
4003
+ "not-selected",
4004
+ "workspace-not-selected",
4005
+ "deselected-parent"
4006
+ ].includes(entry.reason ?? ""))) finish(node, false, false, "deselected-parent");
4007
+ else if (results.every((entry) => entry?.reason === "missing-parent" || entry?.reason === "deselected-parent")) finish(node, false, false, "missing-parent");
4008
+ else finish(node, false, true, "ancestry-unresolved");
4009
+ }
3694
4010
  }
3695
- if (run) tokens.push(run);
3696
- return tokens.length ? [...new Set(tokens)] : null;
3697
- }
3698
- /** A keyed, truncated digest; a collision can only retire an unaffected Work. */
3699
- function hideTokenDigest(key, token) {
3700
- return createHmac("sha256", key).update(token, "utf8").digest("base64url").slice(0, 11);
3701
- }
3702
- /** Identifies the key an index or delta was made with, without revealing it. */
3703
- const hideKeyId = (key) => hideTokenDigest(key, "\0key");
3704
- /** Sorted, distinct, fixed-width digests in one string. */
3705
- function packHideIndex(key, tokens) {
3706
- const secret = createSecretKey(key);
3707
- return [...new Set([...tokens].map((token) => hideTokenDigest(secret, token)))].sort().join("");
4011
+ for (const node of nodes) if (!resolved.has(node.key)) finish(node, false, true, "ancestry-unresolved");
4012
+ return resolved;
3708
4013
  }
3709
- /** One digest; an index is a run of them, checked by length and alphabet. */
3710
- const hideDigestPattern = /^[A-Za-z0-9_-]{11}$/u;
3711
- const isHideIndex = (value) => value.length % 11 === 0 && /^[A-Za-z0-9_-]*$/u.test(value);
3712
- function hideIndexHas(index, digest) {
3713
- let low = 0, high = index.length / 11 - 1;
3714
- while (low <= high) {
3715
- const middle = low + high >> 1;
3716
- const value = index.slice(middle * 11, (middle + 1) * 11);
3717
- if (value === digest) return true;
3718
- if (value < digest) low = middle + 1;
3719
- else high = middle - 1;
4014
+ //#endregion
4015
+ //#region packages/application/src/source/index.ts
4016
+ function resolveSourceSelection(inventory, route = "original") {
4017
+ const parts = resolveParts(inventory, route);
4018
+ const sessions = /* @__PURE__ */ new Map();
4019
+ const families = reviewFamilies(inventory);
4020
+ const includedWorkspaces = /* @__PURE__ */ new Set();
4021
+ const counts = {
4022
+ workspaces: 0,
4023
+ sessions: 0,
4024
+ parts: 0,
4025
+ sourceBytes: 0
4026
+ };
4027
+ const coverage = {
4028
+ includedSessions: 0,
4029
+ excludedSessions: 0,
4030
+ unresolvedSessions: 0,
4031
+ unresolvedParts: 0
4032
+ };
4033
+ for (const session of inventory.sessions) {
4034
+ const decisions = session.parts.flatMap((part) => {
4035
+ const value = parts.get(sourceKey(session.id, part.id));
4036
+ return value ? [{
4037
+ part,
4038
+ value
4039
+ }] : [];
4040
+ });
4041
+ const included = decisions.filter(({ value }) => value.included);
4042
+ const unresolved = decisions.filter(({ value }) => value.unresolved);
4043
+ const excluded = decisions.filter(({ value }) => !value.included && !value.unresolved);
4044
+ const selected = familySelected(inventory, families, session);
4045
+ const missing = selected && session.parts.length === 0;
4046
+ const reasons = [...new Set(decisions.flatMap(({ value }) => value.reason ? [value.reason] : []))].sort();
4047
+ if (missing) reasons.push("unreadable-source");
4048
+ const summary = Object.freeze({
4049
+ status: unresolved.length || missing ? "unresolved" : included.length ? excluded.length ? "partial" : "included" : "excluded",
4050
+ includedParts: included.length,
4051
+ excludedParts: excluded.length,
4052
+ unresolvedParts: unresolved.length,
4053
+ sourceBytes: included.reduce((sum, { part }) => sum + part.snapshot.size, 0),
4054
+ reasons: Object.freeze(reasons)
4055
+ });
4056
+ sessions.set(session.id, summary);
4057
+ if (!selected) continue;
4058
+ if (summary.includedParts) {
4059
+ counts.sessions++;
4060
+ counts.parts += summary.includedParts;
4061
+ counts.sourceBytes += summary.sourceBytes;
4062
+ includedWorkspaces.add(session.workspaceId);
4063
+ coverage.includedSessions++;
4064
+ }
4065
+ if (summary.status === "excluded") coverage.excludedSessions++;
4066
+ if (summary.status === "unresolved") coverage.unresolvedSessions++;
4067
+ coverage.unresolvedParts += summary.unresolvedParts;
3720
4068
  }
3721
- return false;
4069
+ counts.workspaces = includedWorkspaces.size;
4070
+ return Object.freeze({
4071
+ counts: Object.freeze(counts),
4072
+ coverage: Object.freeze(coverage),
4073
+ sessions,
4074
+ session: (sessionId) => {
4075
+ const value = sessions.get(sessionId);
4076
+ if (!value) throw new ApplicationError("UNKNOWN_ITEM");
4077
+ return value;
4078
+ },
4079
+ part: (sessionId, partId) => parts.get(sourceKey(sessionId, partId))
4080
+ });
3722
4081
  }
3723
4082
  //#endregion
3724
- //#region packages/application/src/journey/review/retirement.ts
3725
- /**
3726
- * ADR 0079: the confirmed Hide values a commit adds, removes or recategorizes, with the kind
3727
- * each then has (null once removed; for additions, the lowest it can have). Terms exist only in
3728
- * memory for this commit. `rendered` holds the value's keyed digest for each kind, to compare
3729
- * with the values a Work was rendered with; `tokens` holds its words' keyed digests, or null
3730
- * when it has none. A partial value is a literal that may join a neighbour across a chunk edge,
3731
- * so its whole text, and every Work it may reach, is unknown.
3732
- */
3733
- const deltaValueSchema = z.object({
3734
- term: z.string().min(1),
3735
- kind: journeyKindSchema.nullable(),
3736
- partial: z.boolean(),
3737
- rendered: z.array(z.string().regex(hideDigestPattern)).length(confirmedHideKinds.length),
3738
- tokens: z.array(z.string().regex(hideDigestPattern)).min(1).nullable(),
3739
- /** The new findings an addition comes from; the store keeps it only if one joins a Hide. */
3740
- sources: z.array(z.object({
3741
- batchId: z.uuid(),
3742
- findingId: z.uuid()
3743
- }).strict()).min(1).optional()
4083
+ //#region packages/application/src/journey/scan-exclusion.ts
4084
+ /** Manual source-policy proof; never an assertion that unknown content was scanned. */
4085
+ const scanExclusionResourceSchema = z.object({
4086
+ kind: z.literal("scan-exclusion"),
4087
+ proofVersion: z.literal(1),
4088
+ journeyId: journeyIdSchema,
4089
+ workId: journeyIdSchema,
4090
+ inputRevision: z.number().int().positive(),
4091
+ snapshotId: journeyDigestSchema,
4092
+ configurationDigest: journeyDigestSchema,
4093
+ rootSessionId: z.string().min(1),
4094
+ sessionId: z.string().min(1),
4095
+ sourceId: z.string().min(1),
4096
+ sourcePartId: z.string().min(1),
4097
+ native: nativeSourceIdentitySchema,
4098
+ adapterVersion: z.string().min(1),
4099
+ reason: z.enum([
4100
+ "guardian-excluded",
4101
+ "missing-parent",
4102
+ "deselected-parent",
4103
+ "ancestry-unresolved",
4104
+ "non-interactive-launch"
4105
+ ]),
4106
+ facts: z.object({
4107
+ representation: z.enum(["jsonl", "jsonl.zst"]),
4108
+ byteLength: z.number().int().nonnegative(),
4109
+ sha256: journeyDigestSchema
4110
+ }).strict()
3744
4111
  }).strict();
3745
- /** `keyId` names the journey key the digests were made with; an index under another key is unusable. */
3746
- const keyId = z.string().regex(hideDigestPattern);
3747
- const confirmedHideDeltaSchema = z.discriminatedUnion("scope", [z.object({
3748
- scope: z.literal("complete"),
3749
- keyId,
3750
- from: z.string(),
3751
- to: z.string(),
3752
- values: z.array(deltaValueSchema)
3753
- }).strict(), z.object({
3754
- scope: z.literal("additions"),
3755
- keyId,
3756
- values: z.array(deltaValueSchema)
3757
- }).strict()]);
3758
- /** What a value was rendered as: its kind, in the digests a render keeps (ADR 0079). */
3759
- const renderedHideValue = (kind, term) => `${kind}\u0000${term}`;
3760
- function hideDeltaValue(key, term, kind, partial = false) {
3761
- const tokens = partial ? null : hideValueTokens(term);
3762
- return {
3763
- term,
3764
- kind,
3765
- partial,
3766
- rendered: confirmedHideKinds.map((item) => hideTokenDigest(key, renderedHideValue(item, term))),
3767
- tokens: tokens && tokens.map((token) => hideTokenDigest(key, token))
3768
- };
4112
+ /** Validate policy against frozen identity evidence, never a caller-asserted source role. */
4113
+ function validateScanExclusion(resource, state, inventory, capture) {
4114
+ const work = state.works.find((item) => item.id === resource.workId);
4115
+ if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
4116
+ if (!work || state.scope.status !== "confirmed" || work.access === "drop" || state.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
4117
+ if (state.journeyId !== resource.journeyId || work.inputRevision !== resource.inputRevision || state.snapshotId !== resource.snapshotId || !work.rootSessionIds.includes(resource.rootSessionId)) throw new ApplicationError("REVISION_CONFLICT");
4118
+ const families = reviewFamilies(inventory);
4119
+ const part = (families.members.get(resource.rootSessionId)?.find((item) => item.id === resource.sessionId))?.parts.find((item) => item.id === resource.sourcePartId);
4120
+ const evidence = capture.source.nativeEvidence;
4121
+ const launch = resource.reason === "non-interactive-launch";
4122
+ const ancestry = resource.reason === "ancestry-unresolved";
4123
+ if (ancestry && resource.native.role !== "worker") throw new ApplicationError("CORRUPTED_STATE");
4124
+ const prefixRoot = launch && resource.rootSessionId === resource.sessionId && resource.native.role === "main" && evidence?.recognition === "unknown" && evidence.completeness === "prefix" && JSON.stringify(evidence.identity) === JSON.stringify({
4125
+ ...resource.native,
4126
+ role: "unknown"
4127
+ });
4128
+ if (!part || capture.partId !== resource.sourcePartId || capture.facts.sha256 !== resource.facts.sha256 || capture.facts.byteLength !== resource.facts.byteLength || capture.facts.representation !== resource.facts.representation || !evidence || !prefixRoot && (evidence.recognition !== "recognized" || JSON.stringify(evidence.identity) !== JSON.stringify(resource.native)) || evidence.integrity !== "valid" || evidence.adapterVersion !== resource.adapterVersion || evidence.snapshotId !== sourceSnapshotId(capture.source) || JSON.stringify(part.nativeEvidence) !== JSON.stringify(evidence)) throw new ApplicationError("CORRUPTED_STATE");
4129
+ const ids = new Set(work.rootSessionIds.flatMap((id) => (families.members.get(id) ?? []).map((item) => item.id)));
4130
+ const sessions = inventory.sessions.filter((item) => ids.has(item.id));
4131
+ const workspaceIds = new Set(sessions.map((item) => item.workspaceId));
4132
+ const decision = resolveSourceSelection({
4133
+ ...inventory,
4134
+ sessions,
4135
+ workspaces: inventory.workspaces.filter((item) => workspaceIds.has(item.id))
4136
+ }, "configured").part(resource.sessionId, resource.sourcePartId);
4137
+ if (!decision || (prefixRoot ? decision.included || decision.reason !== "unknown-source" : ancestry ? decision.included ? decision.reason !== null : decision.reason !== resource.reason : decision.included !== launch || (launch ? decision.reason !== null : decision.reason !== resource.reason))) throw new ApplicationError("CORRUPTED_STATE");
3769
4138
  }
3770
- const kindOf = (kind) => journeyKindSchema.nullable().parse(kind ?? null);
3771
- const rank = (kind) => kind === null ? -1 : confirmedHideKinds.indexOf(kind);
4139
+ //#endregion
4140
+ //#region packages/application/src/journey/scan-git-seeds.ts
3772
4141
  /**
3773
- * Every value whose hidden kind differs between two states (ADR 0086): both are read against the
3774
- * same dictionary, which only grows and is unchanged by the commit that asks.
4142
+ * Owner-only Git detector input fixed once per frozen snapshot and canonical root set.
4143
+ * Later Git activity cannot change or invalidate a scan of the same frozen sources.
3775
4144
  */
3776
- async function readConfirmedHideChange(reader, key, previous, next) {
3777
- const terms = await reader.journeyTerms({ journeyId: previous.journeyId });
3778
- const before = dictionaryHides(previous, terms), after = dictionaryHides(next, terms);
3779
- return {
3780
- scope: "complete",
3781
- keyId: hideKeyId(key),
3782
- from: confirmedHideKey(previous),
3783
- to: confirmedHideKey(next),
3784
- values: [.../* @__PURE__ */ new Set([...before.keys(), ...after.keys()])].filter((term) => before.get(term)?.kind !== after.get(term)?.kind).map((term) => hideDeltaValue(key, term, kindOf(after.get(term)?.kind)))
4145
+ const scanGitSeedsResourceSchema = z.object({
4146
+ kind: z.literal("git-seeds"),
4147
+ journeyId: journeyIdSchema,
4148
+ snapshotId: journeyDigestSchema,
4149
+ roots: z.array(z.string()),
4150
+ seeds: z.object({
4151
+ names: z.array(z.string().min(1)),
4152
+ snapshots: z.array(z.object({
4153
+ id: z.string().min(1),
4154
+ sha256: z.string()
4155
+ }).strict())
4156
+ }).strict(),
4157
+ sha256: journeyDigestSchema
4158
+ }).strict();
4159
+ /** Storage key; equal root sets in any order share one receipt. */
4160
+ const scanGitSeedsId = (snapshotId, roots) => `${snapshotId}:${textDigest(JSON.stringify([...new Set(roots)].sort()))}`;
4161
+ function scanGitSeedsResource(journeyId, snapshotId, roots, seeds) {
4162
+ const value = {
4163
+ names: [...seeds.names],
4164
+ snapshots: seeds.snapshots.map((row) => ({ ...row }))
3785
4165
  };
3786
- }
3787
- /** Dictionary values a commit may add to the hidden set, each with its type's kind (ADR 0086). */
3788
- function termHideAdditions(key, values) {
3789
- const unique = /* @__PURE__ */ new Map();
3790
- for (const { term, kind } of values) unique.set(JSON.stringify([term, kind]), hideDeltaValue(key, term, kindOf(kind)));
3791
4166
  return {
3792
- scope: "additions",
3793
- keyId: hideKeyId(key),
3794
- values: [...unique.values()]
4167
+ kind: "git-seeds",
4168
+ journeyId,
4169
+ snapshotId,
4170
+ roots: [...new Set(roots)].sort(),
4171
+ seeds: value,
4172
+ sha256: textDigest(JSON.stringify(value))
3795
4173
  };
3796
4174
  }
3797
- /**
3798
- * The values new Gray findings can add. A finding at a chunk edge may join a neighbouring
3799
- * literal of the same field, so its words are not checked.
3800
- */
3801
- function confirmedHideAdditions(key, records) {
3802
- const values = /* @__PURE__ */ new Map();
3803
- for (const record of records) {
3804
- const chunks = new Map(record.chunks.map((chunk) => [chunk.id, chunk]));
3805
- for (const finding of record.findings) {
3806
- const chunk = chunks.get(finding.chunkId);
3807
- if (!chunk || finding.class !== "gray") continue;
3808
- const whole = chunk.offset === 0 && chunk.textHash === chunk.fieldHash;
3809
- const joins = finding.start === 0 && chunk.offset > 0 || finding.end === chunk.text.length && !whole;
3810
- const source = {
3811
- batchId: record.batchId,
3812
- findingId: finding.id
3813
- };
3814
- for (const term of bLiteralTerms(chunk.text.slice(finding.start, finding.end))) {
3815
- const id = JSON.stringify([
3816
- term,
3817
- finding.kind,
3818
- joins
3819
- ]);
3820
- const value = values.get(id);
3821
- if (value) value.sources.push(source);
3822
- else values.set(id, {
3823
- ...hideDeltaValue(key, term, finding.kind, joins),
3824
- sources: [source]
3825
- });
3826
- }
3827
- }
3828
- }
3829
- return {
3830
- scope: "additions",
3831
- keyId: hideKeyId(key),
3832
- values: [...values.values()]
3833
- };
4175
+ /** A missing, tampered or foreign receipt is corrupted state, never a cue to reread live Git. */
4176
+ function verifiedScanGitSeeds(resource, snapshotId, roots) {
4177
+ if (resource.snapshotId !== snapshotId || scanGitSeedsId(resource.snapshotId, resource.roots) !== scanGitSeedsId(snapshotId, roots) || textDigest(JSON.stringify(resource.seeds)) !== resource.sha256) throw new ApplicationError("CORRUPTED_STATE");
4178
+ return resource.seeds;
3834
4179
  }
4180
+ //#endregion
4181
+ //#region packages/application/src/journey/scan-session.ts
4182
+ const count$8 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
4183
+ const scanFileSchema = z.object({
4184
+ path: z.string().min(1),
4185
+ identity: fileIdentitySchema,
4186
+ sha256: journeyDigestSchema,
4187
+ byteLength: count$8
4188
+ }).strict();
3835
4189
  /**
3836
- * How hidden values moved when the value overrides and type answers stay the same: not at all,
3837
- * only added (new dictionary values of hiding types), only removed, or otherwise. A type's
3838
- * hidden values are its distinct values while it hides; an answer that changes whether a type
3839
- * hides is otherwise, so only a complete delta covers it.
4190
+ * Track A source-bound prose (`file`) with built-in static protection spans; no delivery archive
4191
+ * exists during scanning (ADR 0172). Obsolete version 3 inputs with detector evidence are refused.
3840
4192
  */
3841
- function memberChange(previous, next) {
3842
- if (!sameValueOverrides(previous, next)) return "other";
3843
- if (termTypes.some((type) => typeGroup(previous, type) !== void 0 && typeHides(previous, type) !== typeHides(next, type))) return "other";
3844
- const hidden = (state, type) => typeHides(state, type) ? typeGroup(state, type)?.count ?? 0 : 0;
3845
- const counts = termTypes.map((type) => hidden(next, type) - hidden(previous, type));
3846
- if (counts.every((count) => count === 0)) return "same";
3847
- return counts.every((count) => count >= 0) ? "additions" : counts.every((count) => count <= 0) ? "removals" : "other";
4193
+ const sessionScanInputSchema = z.object({
4194
+ version: z.literal(4),
4195
+ sessionId: z.string().min(1),
4196
+ file: scanFileSchema,
4197
+ sources: z.array(z.object({
4198
+ sourceId: z.string().min(1),
4199
+ sourcePartId: z.string().min(1),
4200
+ native: nativeSourceIdentitySchema,
4201
+ facts: z.object({
4202
+ representation: z.enum(["jsonl", "jsonl.zst"]),
4203
+ byteLength: count$8,
4204
+ sha256: journeyDigestSchema
4205
+ }).strict()
4206
+ }).strict()),
4207
+ recordCount: count$8,
4208
+ fieldCount: count$8,
4209
+ inputUtf16: count$8,
4210
+ mainReviewVerified: z.literal(true),
4211
+ emptyReviewVerified: z.boolean()
4212
+ }).strict().refine((input) => new Set(input.sources.map((source) => source.sourceId)).size === input.sources.length);
4213
+ const scanSessionResourceSchema = z.object({
4214
+ kind: z.literal("scan-session"),
4215
+ journeyId: journeyIdSchema,
4216
+ workId: journeyIdSchema,
4217
+ inputRevision: count$8.min(1),
4218
+ snapshotId: journeyDigestSchema,
4219
+ configurationDigest: journeyDigestSchema,
4220
+ rootSessionId: z.string().min(1),
4221
+ input: sessionScanInputSchema
4222
+ }).strict();
4223
+ /** Complete scan-set evidence for certified empty main input; never an output digest. */
4224
+ function scanPreparationDigest(resources) {
4225
+ return textDigest(JSON.stringify(resources.map((resource) => resource.kind === "scan-exclusion" ? resource : [
4226
+ resource.input.sessionId,
4227
+ resource.rootSessionId,
4228
+ resource.configurationDigest,
4229
+ resource.input.file.sha256,
4230
+ resource.input.sources
4231
+ ]).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))));
3848
4232
  }
3849
- /**
3850
- * Whether a Work's output may change with the delta (Work metadata is never masked, so a
3851
- * generated description never retires on its own). Without a delta that covers this exact change every Work is affected, as ADR 0069 first decided, except when
3852
- * members were only removed: no value is then added, so nothing can be under-hidden, and an
3853
- * output may keep masking a value that left its category until it is rendered again.
3854
- */
3855
- function impact(previous, next, delta, outputIndex) {
3856
- const change = memberChange(previous, next);
3857
- const covered = delta && (delta.scope === "complete" ? delta.from === confirmedHideKey(previous) && delta.to === confirmedHideKey(next) : change === "additions" || change === "same");
3858
- if (!delta || !covered) return change === "removals" || change === "same" ? () => false : () => true;
3859
- const unchanged = (index, value) => {
3860
- const at = value.rendered.findIndex((digest) => hideIndexHas(index.values, digest));
3861
- const was = at < 0 ? null : confirmedHideKinds[at] ?? null;
3862
- return delta.scope === "additions" ? rank(was) >= rank(value.kind) : was === value.kind;
3863
- };
3864
- const reaches = (index, value) => {
3865
- const digests = index.digests;
3866
- return !value.tokens || !digests || value.tokens.every((token) => hideIndexHas(digests, token));
3867
- };
3868
- return (work) => {
3869
- if (!work.artifact) return false;
3870
- const found = outputIndex(work);
3871
- const index = found?.keyId === delta.keyId ? found : null;
3872
- return !index || delta.values.some((value) => value.partial || !unchanged(index, value) && reaches(index, value));
3873
- };
4233
+ const scanBatchBinding = {
4234
+ journeyId: journeyIdSchema,
4235
+ workId: journeyIdSchema,
4236
+ inputRevision: count$8.min(1),
4237
+ snapshotId: journeyDigestSchema,
4238
+ configurationDigest: journeyDigestSchema
4239
+ };
4240
+ const scanCanonicalResourceSchema = z.object({
4241
+ ...scanBatchBinding,
4242
+ kind: z.literal("scan-canonical"),
4243
+ id: journeyDigestSchema,
4244
+ batchId: journeyIdSchema,
4245
+ batchDigest: journeyDigestSchema
4246
+ }).strict();
4247
+ const scanAliasResourceSchema = z.object({
4248
+ ...scanBatchBinding,
4249
+ kind: z.literal("scan-alias"),
4250
+ id: journeyIdSchema,
4251
+ canonicalBatchId: journeyIdSchema,
4252
+ canonicalBatchDigest: journeyDigestSchema,
4253
+ targetBatchId: journeyIdSchema,
4254
+ targetBatchDigest: journeyDigestSchema,
4255
+ equivalenceDigest: journeyDigestSchema
4256
+ }).strict();
4257
+ //#endregion
4258
+ //#region packages/application/src/journey/b/repair.ts
4259
+ const bRepairPlanSchema = z.object({
4260
+ unitId: journeyDigestSchema,
4261
+ unitDigest: journeyDigestSchema,
4262
+ priorDigest: journeyDigestSchema,
4263
+ round: z.literal(1),
4264
+ targetFields: z.array(bIdSchema).min(1).max(24),
4265
+ remainingFields: z.array(bIdSchema)
4266
+ }).strict();
4267
+ const bReviewDigest = (submission) => bDigest(JSON.stringify(bReviewSubmissionSchema.parse(submission)));
4268
+ const repairMeaning = (part) => JSON.stringify([part.category, part.classification]);
4269
+ function planBRepair(unit, review, completedRepairs = 0) {
4270
+ if (completedRepairs !== 0) throw new ApplicationError("INVALID_OPERATION");
4271
+ const validated = validateBReview(unit, review.submission);
4272
+ const needed = new Set(validated.issues.flatMap((issue) => issue.fieldId === null ? [] : [issue.fieldId]));
4273
+ const targets = [...new Set(unit.fragments.filter((part) => part.core).map((part) => part.fieldId))].filter((fieldId) => needed.has(fieldId));
4274
+ if (!targets.length) return null;
4275
+ return bRepairPlanSchema.parse({
4276
+ unitId: unit.id,
4277
+ unitDigest: unit.digest,
4278
+ priorDigest: bReviewDigest(review.submission),
4279
+ round: 1,
4280
+ targetFields: targets.slice(0, 24),
4281
+ remainingFields: [...needed].filter((fieldId) => !targets.slice(0, 24).includes(fieldId))
4282
+ });
3874
4283
  }
3875
- /**
3876
- * Applied with every Journey commit. When confirmed Hide categories or their members change,
3877
- * a Work whose output or generated description may change is retired until both are produced
3878
- * again (ADR 0069); ADR 0079 limits this to Works the changed values can reach.
3879
- */
3880
- function settleConfirmedHides(previous, next, delta, outputIndex = () => null) {
3881
- if (!previous || confirmedHideKey(previous) === confirmedHideKey(next)) return next;
3882
- const before = new Map(previous.works.map((work) => [work.id, work.revision]));
3883
- const affected = impact(previous, next, delta, outputIndex);
4284
+ function mergeBRepair(unit, previous, inputPlan, raw) {
4285
+ const plan = bRepairPlanSchema.parse(inputPlan);
4286
+ const expected = planBRepair(unit, previous);
4287
+ if (JSON.stringify(plan) !== JSON.stringify(expected)) throw new ApplicationError("REVISION_CONFLICT");
4288
+ const correction = bReviewSubmissionSchema.parse(raw);
4289
+ const targeted = (fieldId) => plan.targetFields.includes(fieldId);
4290
+ if ([
4291
+ ...correction.terms.flatMap((term) => term.occurrences),
4292
+ ...correction.retainedSourceGroups,
4293
+ ...correction.representationLimits,
4294
+ ...correction.uncertainOccurrences
4295
+ ].some((part) => !targeted(part.fieldId))) throw new ApplicationError("INVALID_REVIEW_SPAN");
4296
+ const repaired = validateBReview(unit, correction, plan.targetFields);
4297
+ const prior = validateBReview(unit, previous.submission);
4298
+ const correctedMeanings = /* @__PURE__ */ new Map();
4299
+ for (const part of repaired.sensitive) {
4300
+ const key = bAnchorKey(part);
4301
+ const meanings = correctedMeanings.get(key) ?? /* @__PURE__ */ new Set();
4302
+ meanings.add(repairMeaning(part));
4303
+ correctedMeanings.set(key, meanings);
4304
+ }
4305
+ const preservedKeys = /* @__PURE__ */ new Set();
4306
+ const conflicts = [];
4307
+ const preserved = prior.sensitive.filter((part) => targeted(part.fieldId)).flatMap((part) => {
4308
+ const retentions = part.classification.class === "certain" ? [] : repaired.retained.filter((retained) => retained.fieldId === part.fieldId);
4309
+ return subtractBRanges([part], retentions).map((range) => ({
4310
+ fieldId: part.fieldId,
4311
+ recordId: part.recordId,
4312
+ fieldHash: part.fieldHash,
4313
+ ...range,
4314
+ exactQuote: codePointSlice(part.exactQuote, range.start - part.start, range.end - part.start),
4315
+ category: part.category,
4316
+ classification: part.classification,
4317
+ reason: "Prior sensitive obligation preserved during bounded repair."
4318
+ }));
4319
+ }).filter((part) => {
4320
+ const anchor = bAnchorKey(part), meaning = repairMeaning(part), corrected = correctedMeanings.get(anchor);
4321
+ if (corrected && (corrected.size > 1 || !corrected.has(meaning))) conflicts.push(bIssue("CONFLICTING_PRIVACY_DECISIONS", part));
4322
+ if (corrected?.has(meaning)) return false;
4323
+ const key = JSON.stringify([anchor, meaning]);
4324
+ if (preservedKeys.has(key)) return false;
4325
+ preservedKeys.add(key);
4326
+ return true;
4327
+ });
4328
+ const original = previous.submission;
4329
+ const merged = {
4330
+ ...original,
4331
+ terms: [...original.terms.map((term) => ({
4332
+ ...term,
4333
+ occurrences: term.occurrences.filter((part) => !targeted(part.fieldId))
4334
+ })).filter((term) => term.occurrences.length > 0), ...correction.terms],
4335
+ retainedSourceGroups: [...original.retainedSourceGroups.filter((part) => !targeted(part.fieldId)), ...correction.retainedSourceGroups],
4336
+ representationLimits: [
4337
+ ...original.representationLimits.filter((part) => !targeted(part.fieldId)),
4338
+ ...correction.representationLimits,
4339
+ ...preserved
4340
+ ],
4341
+ uncertainOccurrences: [...original.uncertainOccurrences.filter((part) => !targeted(part.fieldId)), ...correction.uncertainOccurrences],
4342
+ coverage: [...original.coverage.filter((part) => !targeted(part.fieldId)), ...correction.coverage],
4343
+ sessionDecision: correction.sessionDecision
4344
+ };
4345
+ const review = validateBReview(unit, merged);
4346
+ review.issues = [...new Map([...review.issues, ...conflicts].map((issue) => [JSON.stringify(issue), issue])).values()];
3884
4347
  return {
3885
- ...next,
3886
- works: next.works.map((work) => work.artifact === null && work.revision > (before.get(work.id) ?? work.revision) || !affected(work) ? work : {
3887
- ...retireWorkDescription(work),
3888
- artifact: null,
3889
- revision: work.revision + 1,
3890
- status: work.status === "ready" ? "review" : work.status
3891
- })
4348
+ review,
4349
+ receipt: {
4350
+ ...plan,
4351
+ resultDigest: bReviewDigest(merged),
4352
+ priorObligationsPreserved: true,
4353
+ unresolvedIssues: review.issues
4354
+ }
3892
4355
  };
3893
4356
  }
3894
4357
  //#endregion
3895
- //#region packages/application/src/journey/storage.ts
3896
- const journeyEventSchema = z.object({
3897
- sequence: z.number().int().nonnegative(),
4358
+ //#region packages/application/src/journey/review/limits.ts
4359
+ /** Raw provider bytes stay bounded independently of JSON escaping and compiled findings. */
4360
+ const reviewResponseBytes = 4194304;
4361
+ /** Fits the escaped raw-response envelope and evidence plus compiled occurrence findings. */
4362
+ const reviewTransportBytes = 33554432;
4363
+ //#endregion
4364
+ //#region packages/application/src/journey/review/candidate-schema.ts
4365
+ const reviewIssueSchema = z.object({
4366
+ code: z.string().regex(/^[A-Z_]{3,80}$/u),
4367
+ fieldId: z.string().min(1).nullable(),
4368
+ start: z.number().int().nonnegative().nullable(),
4369
+ end: z.number().int().nonnegative().nullable()
4370
+ }).strict();
4371
+ const reviewCandidateInputSchema = z.object({
3898
4372
  journeyId: journeyIdSchema,
3899
- revision: z.number().int().nonnegative(),
3900
- kind: z.enum([
3901
- "started",
3902
- "response",
3903
- "snapshot",
3904
- "batch",
3905
- "output",
3906
- "send",
3907
- "search"
3908
- ]),
3909
- source: z.enum([
3910
- "conversation",
3911
- "panel",
3912
- "system"
3913
- ]),
3914
- at: z.iso.datetime(),
3915
- alignment: reviewAlignmentAuditSchema.optional(),
3916
- preferenceScopes: z.array(z.object({
3917
- groupId: journeyIdSchema,
3918
- preferenceScope: journeyPreferenceScopeSchema
3919
- }).strict()).min(1).max(1e4).optional(),
3920
- correction: correctionAuditSchema.optional(),
3921
- decision: delegatedDecisionSchema.extend({
3922
- action: z.enum([
3923
- "group",
3924
- "groups-hide",
3925
- "work-access",
3926
- "instruction",
3927
- "correction",
3928
- "works-access",
3929
- "work-type",
3930
- "scope-confirm",
3931
- "card-answer",
3932
- "cards-confirm"
3933
- ]),
3934
- targetIds: z.array(journeyIdSchema).min(1),
3935
- choice: z.string().min(1).max(16)
3936
- }).optional()
4373
+ batchId: journeyIdSchema,
4374
+ batchDigest: journeyDigestSchema,
4375
+ idempotencyKey: journeyIdSchema,
4376
+ attemptId: journeyIdSchema,
4377
+ response: z.string().min(1).refine((value) => Buffer.byteLength(value, "utf8") <= reviewResponseBytes),
4378
+ repairId: journeyIdSchema.nullable(),
4379
+ sessionReviewId: journeyIdSchema.nullable()
3937
4380
  }).strict();
3938
- const journeyCommitSchema = z.object({
3939
- expectedRevision: z.number().int().nonnegative().nullable(),
3940
- requestKey: journeyIdSchema,
4381
+ const reviewCandidateResourceSchema = reviewCandidateInputSchema.omit({ idempotencyKey: true }).extend({
4382
+ kind: z.literal("b-candidate"),
4383
+ id: journeyIdSchema,
3941
4384
  requestHash: journeyDigestSchema,
3942
- state: nativeJourneySchema,
3943
- event: journeyEventSchema.omit({
3944
- sequence: true,
3945
- revision: true
3946
- }),
3947
- /** Inserted in the same transaction as this state; never through the generic resource save. */
3948
- correctionReport: correctionReportResourceSchema.optional(),
3949
- /** Set by the store for this commit only and never persisted (ADR 0079). */
3950
- confirmedHideDelta: confirmedHideDeltaSchema.optional(),
3951
- /** Dictionary values a Hide override adds (ADR 0086), inserted with this state. */
3952
- terms: z.array(z.object({
3953
- type: termTypeSchema,
3954
- value: z.string().min(1)
3955
- }).strict()).max(2e4).optional()
3956
- }).strict();
3957
- const journeyCommitResultSchema = z.object({
4385
+ at: z.iso.datetime(),
4386
+ workId: journeyIdSchema,
4387
+ inputRevision: z.number().int().positive(),
4388
+ snapshotId: journeyDigestSchema,
4389
+ responseDigest: journeyDigestSchema,
3958
4390
  status: z.enum([
3959
- "applied",
3960
- "replayed",
3961
- "stale"
4391
+ "valid",
4392
+ "pending",
4393
+ "invalid"
3962
4394
  ]),
3963
- state: nativeJourneySchema
4395
+ issues: z.array(reviewIssueSchema),
4396
+ evidence: bReviewSubmissionSchema.nullable()
4397
+ }).strict();
4398
+ const reviewCandidateResultSchema = reviewCandidateResourceSchema.pick({
4399
+ responseDigest: true,
4400
+ status: true,
4401
+ issues: true,
4402
+ evidence: true
4403
+ }).extend({ candidateId: journeyIdSchema }).strict();
4404
+ const reviewRepairInputSchema = z.object({
4405
+ journeyId: journeyIdSchema,
4406
+ candidateId: journeyIdSchema,
4407
+ idempotencyKey: journeyIdSchema
4408
+ }).strict();
4409
+ const reviewRepairResourceSchema = z.object({
4410
+ kind: z.literal("b-repair"),
4411
+ id: journeyIdSchema,
4412
+ journeyId: journeyIdSchema,
4413
+ workId: journeyIdSchema,
4414
+ batchId: journeyIdSchema,
4415
+ batchDigest: journeyDigestSchema,
4416
+ inputRevision: z.number().int().positive(),
4417
+ snapshotId: journeyDigestSchema,
4418
+ candidateId: journeyIdSchema,
4419
+ requestHash: journeyDigestSchema,
4420
+ at: z.iso.datetime(),
4421
+ plan: bRepairPlanSchema
3964
4422
  }).strict();
3965
4423
  //#endregion
3966
4424
  //#region packages/application/src/journey/review/usefulness-schema.ts
@@ -4032,7 +4490,7 @@ const reviewProgressSchema = z.array(z.object({
4032
4490
  }).strict());
4033
4491
  //#endregion
4034
4492
  //#region packages/application/src/journey/review/session-schema.ts
4035
- const count$8 = z.number().int().nonnegative();
4493
+ const count$7 = z.number().int().nonnegative();
4036
4494
  const sessionReviewInputSchema = z.object({
4037
4495
  journeyId: journeyIdSchema,
4038
4496
  workId: journeyIdSchema,
@@ -4053,7 +4511,7 @@ const sessionReviewResourceSchema = z.object({
4053
4511
  workId: journeyIdSchema,
4054
4512
  sessionId: z.string().min(1).max(128),
4055
4513
  snapshotId: journeyDigestSchema,
4056
- inputRevision: count$8.positive(),
4514
+ inputRevision: count$7.positive(),
4057
4515
  requestHash: journeyDigestSchema,
4058
4516
  bindingDigest: journeyDigestSchema,
4059
4517
  at: z.iso.datetime(),
@@ -4067,8 +4525,8 @@ const sessionReviewResourceSchema = z.object({
4067
4525
  issues: z.array(reviewIssueSchema),
4068
4526
  requiresSemantic: z.boolean(),
4069
4527
  usefulness: sessionUsefulnessSchema,
4070
- totalCodePoints: count$8,
4071
- reviewedCodePoints: count$8
4528
+ totalCodePoints: count$7,
4529
+ reviewedCodePoints: count$7
4072
4530
  }).strict();
4073
4531
  const sessionRereadInputSchema = z.object({
4074
4532
  journeyId: journeyIdSchema,
@@ -4077,8 +4535,8 @@ const sessionRereadInputSchema = z.object({
4077
4535
  anchors: z.array(z.object({
4078
4536
  fieldId: z.string().min(1),
4079
4537
  fieldHash: journeyDigestSchema,
4080
- start: count$8,
4081
- end: count$8
4538
+ start: count$7,
4539
+ end: count$7
4082
4540
  }).strict()).min(1).max(128)
4083
4541
  }).strict();
4084
4542
  const sessionRereadResourceSchema = z.object({
@@ -4087,7 +4545,7 @@ const sessionRereadResourceSchema = z.object({
4087
4545
  journeyId: journeyIdSchema,
4088
4546
  workId: journeyIdSchema,
4089
4547
  sessionId: z.string().min(1).max(128),
4090
- inputRevision: count$8.positive(),
4548
+ inputRevision: count$7.positive(),
4091
4549
  snapshotId: journeyDigestSchema,
4092
4550
  sessionReviewId: journeyIdSchema,
4093
4551
  bindingDigest: journeyDigestSchema,
@@ -4105,8 +4563,8 @@ const sessionReconcileInputSchema = z.object({
4105
4563
  alignment: reviewAlignmentSchema
4106
4564
  }).strict();
4107
4565
  const retainedReviewAnchorSchema = bAnchorSchema.extend({
4108
- utf16Start: count$8,
4109
- utf16End: count$8,
4566
+ utf16Start: count$7,
4567
+ utf16End: count$7,
4110
4568
  owned: z.boolean()
4111
4569
  }).strict();
4112
4570
  const sessionOverlaySchema = z.object({
@@ -4120,7 +4578,7 @@ const sessionReconciliationResourceSchema = z.object({
4120
4578
  journeyId: journeyIdSchema,
4121
4579
  workId: journeyIdSchema,
4122
4580
  sessionId: z.string().min(1).max(128),
4123
- inputRevision: count$8.positive(),
4581
+ inputRevision: count$7.positive(),
4124
4582
  snapshotId: journeyDigestSchema,
4125
4583
  sessionReviewId: journeyIdSchema,
4126
4584
  bindingDigest: journeyDigestSchema,
@@ -4148,8 +4606,8 @@ const sessionReviewQuerySchema = z.object({
4148
4606
  workId: journeyIdSchema.optional(),
4149
4607
  sessionId: z.string().min(1).max(128).optional(),
4150
4608
  sessionReviewId: journeyIdSchema.optional(),
4151
- afterHit: count$8.default(0),
4152
- limit: count$8.positive().max(500).default(100)
4609
+ afterHit: count$7.default(0),
4610
+ limit: count$7.positive().max(500).default(100)
4153
4611
  }).strict();
4154
4612
  const sessionReviewStatusSchema = z.object({
4155
4613
  complete: z.boolean(),
@@ -4165,8 +4623,8 @@ const sessionReviewStatusSchema = z.object({
4165
4623
  requiresHumanDecision: z.boolean(),
4166
4624
  requiresSemantic: z.boolean(),
4167
4625
  issues: z.array(reviewIssueSchema),
4168
- totalCodePoints: count$8,
4169
- reviewedCodePoints: count$8,
4626
+ totalCodePoints: count$7,
4627
+ reviewedCodePoints: count$7,
4170
4628
  reconciliationId: journeyIdSchema.nullable(),
4171
4629
  /** ADR 0086: a read unit of the session suggests excluding it; advice, never a gate. */
4172
4630
  suggestExclude: z.boolean(),
@@ -4174,8 +4632,8 @@ const sessionReviewStatusSchema = z.object({
4174
4632
  reason: z.string().min(1).max(2e3).nullable()
4175
4633
  }).strict()),
4176
4634
  hits: z.array(sessionReviewHitSchema),
4177
- nextHit: count$8.nullable(),
4178
- totalHits: count$8
4635
+ nextHit: count$7.nullable(),
4636
+ totalHits: count$7
4179
4637
  }).strict();
4180
4638
  const sessionReviewResultSchema = z.object({
4181
4639
  preparation: sessionReviewResourceSchema,
@@ -4259,13 +4717,13 @@ const metadataFieldsSchema = z.object({
4259
4717
  }).strict();
4260
4718
  //#endregion
4261
4719
  //#region packages/application/src/journey/placement/routing-schema.ts
4262
- const count$7 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
4720
+ const count$6 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
4263
4721
  const bucket = z.object({
4264
4722
  complete: z.boolean(),
4265
- total: count$7,
4723
+ total: count$6,
4266
4724
  repositories: z.array(z.object({
4267
4725
  id: z.string().min(1),
4268
- count: count$7.min(1)
4726
+ count: count$6.min(1)
4269
4727
  }).strict())
4270
4728
  }).strict();
4271
4729
  const placementSignalsSchema = z.object({
@@ -4283,8 +4741,8 @@ const cheapPlacementSchema = z.object({
4283
4741
  "name"
4284
4742
  ]),
4285
4743
  confidence: z.enum(["high", "medium"]),
4286
- matched: count$7.min(1),
4287
- total: count$7.min(1),
4744
+ matched: count$6.min(1),
4745
+ total: count$6.min(1),
4288
4746
  otherRepositoryIds: z.array(z.string())
4289
4747
  }).strict();
4290
4748
  const sortingResourceSchema = z.object({
@@ -4305,26 +4763,26 @@ const sortingResourceSchema = z.object({
4305
4763
  const sortingApplySchema = z.object({
4306
4764
  journeyId: z.uuid(),
4307
4765
  sessionId: z.string().min(1).max(128),
4308
- expectedRevision: count$7,
4766
+ expectedRevision: count$6,
4309
4767
  idempotencyKey: z.uuid()
4310
4768
  }).strict();
4311
4769
  /** Preparation moves identified failing roots of one pending Work into its blocked Work. */
4312
4770
  const journeyIsolateSchema = z.object({
4313
4771
  journeyId: z.uuid(),
4314
4772
  workId: z.uuid(),
4315
- inputRevision: count$7.min(1),
4773
+ inputRevision: count$6.min(1),
4316
4774
  roots: z.array(z.object({
4317
4775
  sessionId: z.string().min(1).max(128),
4318
4776
  code: z.string().regex(/^[A-Z_]{3,80}$/u)
4319
4777
  }).strict()).min(1).max(1e5),
4320
- expectedRevision: count$7,
4778
+ expectedRevision: count$6,
4321
4779
  idempotencyKey: z.uuid()
4322
4780
  }).strict();
4323
4781
  const placement = z.object({
4324
4782
  workId: z.uuid(),
4325
4783
  sessionId: z.string().min(1).max(128),
4326
- inputRevision: count$7.min(1),
4327
- throughEvent: count$7,
4784
+ inputRevision: count$6.min(1),
4785
+ throughEvent: count$6,
4328
4786
  contextDigest: z.string().regex(/^[a-f0-9]{64}$/u),
4329
4787
  decision: z.enum([
4330
4788
  "join",
@@ -4340,13 +4798,13 @@ const placement = z.object({
4340
4798
  }).strict();
4341
4799
  const journeyPlaceSchema = z.object({
4342
4800
  journeyId: z.uuid(),
4343
- expectedRevision: count$7,
4801
+ expectedRevision: count$6,
4344
4802
  idempotencyKey: z.uuid(),
4345
4803
  placements: z.array(placement).min(1).max(1e3)
4346
4804
  }).strict();
4347
4805
  //#endregion
4348
4806
  //#region packages/application/src/journey/scope/progress.ts
4349
- const count$6 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
4807
+ const count$5 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
4350
4808
  const ids = z.array(z.string().min(1).max(128)).max(1e5).refine((values) => new Set(values).size === values.length);
4351
4809
  const journeyClassificationProgressSchema = z.object({
4352
4810
  journeyId: journeyIdSchema,
@@ -4354,8 +4812,8 @@ const journeyClassificationProgressSchema = z.object({
4354
4812
  id: z.literal("current"),
4355
4813
  snapshotId: journeyDigestSchema,
4356
4814
  inputFingerprint: journeyDigestSchema,
4357
- baselineRevision: count$6,
4358
- proposalRevision: count$6.nullable(),
4815
+ baselineRevision: count$5,
4816
+ proposalRevision: count$5.nullable(),
4359
4817
  stage: z.enum(["classification", "proposal"]),
4360
4818
  status: z.enum([
4361
4819
  "running",
@@ -4365,7 +4823,7 @@ const journeyClassificationProgressSchema = z.object({
4365
4823
  ]),
4366
4824
  startedAt: z.iso.datetime(),
4367
4825
  updatedAt: z.iso.datetime(),
4368
- elapsedMs: count$6,
4826
+ elapsedMs: count$5,
4369
4827
  rootIds: ids,
4370
4828
  completedRootIds: ids,
4371
4829
  groupedRootIds: ids,
@@ -4400,9 +4858,23 @@ const journeyCaptureSchema = z.object({
4400
4858
  }).strict();
4401
4859
  const base = { journeyId: journeyIdSchema };
4402
4860
  const journeyResourceSchema = z.discriminatedUnion("kind", [
4861
+ z.object({
4862
+ ...base,
4863
+ kind: z.literal("closed-reader"),
4864
+ id: z.string(),
4865
+ batchId: journeyIdSchema,
4866
+ packId: journeyIdSchema
4867
+ }).strict(),
4403
4868
  scanSessionResourceSchema,
4404
4869
  scanCanonicalResourceSchema,
4405
4870
  scanAliasResourceSchema,
4871
+ z.object({
4872
+ ...base,
4873
+ kind: z.literal("withdrawn-term-pack"),
4874
+ id: z.string(),
4875
+ batchId: journeyIdSchema,
4876
+ packId: journeyIdSchema
4877
+ }).strict(),
4406
4878
  scanExclusionResourceSchema,
4407
4879
  scanGitSeedsResourceSchema,
4408
4880
  staticSessionResourceSchema,
@@ -4620,119 +5092,13 @@ function projectPreparationProgress(state, plan, captures, starts, settlements)
4620
5092
  };
4621
5093
  }
4622
5094
  //#endregion
4623
- //#region packages/application/src/journey/policy/port.ts
4624
- const journeyPolicyRespondSchema = z.object({
4625
- request: nativeRespondSchema,
4626
- source: z.enum(["conversation", "panel"]),
4627
- now: z.iso.datetime()
4628
- }).strict();
4629
- //#endregion
4630
- //#region packages/application/src/journey/search.ts
4631
- const count$5 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
4632
- const term = z.string().min(1).max(1024).refine((value) => Buffer.byteLength(value) <= 1024 && !/[\0\r\n]/u.test(value));
4633
- const journeySearchSchema = z.object({
4634
- journeyId: journeyIdSchema,
4635
- terms: z.array(z.object({
4636
- id: z.string().min(1).max(64),
4637
- text: term,
4638
- variants: z.array(term).max(7).default([])
4639
- }).strict()).min(1).max(16)
4640
- }).strict();
4641
- const journeySearchResultSchema = z.object({
4642
- binding: z.object({
4643
- journeyId: journeyIdSchema,
4644
- snapshotId: journeyDigestSchema,
4645
- revision: count$5,
4646
- scopeDigest: journeyDigestSchema,
4647
- outputs: z.array(z.object({
4648
- workId: journeyIdSchema,
4649
- outputRevision: count$5,
4650
- sha256: journeyDigestSchema
4651
- }).strict())
4652
- }).strict(),
4653
- status: z.enum(["complete", "partial"]),
4654
- scope: z.object({
4655
- retainedText: z.literal(true),
4656
- metadata: z.literal(true),
4657
- setupText: z.literal(true),
4658
- attachments: z.literal(false),
4659
- images: z.literal(false),
4660
- originals: z.literal("frozen-native-records-and-discovery-metadata"),
4661
- matching: z.literal("case-sensitive-literal-leftmost-longest")
4662
- }).strict(),
4663
- terms: z.array(z.object({
4664
- id: z.string(),
4665
- tried: z.array(z.string()),
4666
- sentMatches: count$5.nullable(),
4667
- originalMatches: count$5.nullable(),
4668
- sentSessions: count$5.nullable(),
4669
- originalSessions: count$5.nullable()
4670
- }).strict()),
4671
- hits: z.array(z.object({
4672
- termId: z.string(),
4673
- basis: z.enum(["sent", "original"]),
4674
- workId: journeyIdSchema,
4675
- locator: z.object({
4676
- kind: z.enum([
4677
- "retained-text",
4678
- "metadata",
4679
- "setup-text"
4680
- ]),
4681
- entryPath: z.string().optional(),
4682
- archiveSessionId: z.string().optional(),
4683
- sessionId: z.string().optional(),
4684
- sourcePartId: z.string().optional(),
4685
- recordIndex: count$5,
4686
- fieldId: z.string(),
4687
- line: count$5
4688
- }).strict(),
4689
- occurrences: count$5,
4690
- modelContext: z.string().max(4096).optional(),
4691
- match: z.object({
4692
- variantIndex: count$5.max(7),
4693
- start: count$5,
4694
- end: count$5
4695
- }).strict().optional()
4696
- }).strict()),
4697
- failures: z.array(z.object({
4698
- workId: journeyIdSchema,
4699
- basis: z.enum(["sent", "original"]),
4700
- code: z.enum(["SEARCH_INPUT_UNAVAILABLE", "SEARCH_ENGINE_FAILED"])
4701
- }).strict()),
4702
- truncated: z.boolean()
4703
- }).strict().refine((result) => result.hits.every((hit) => {
4704
- if (hit.basis === "original") return hit.modelContext === void 0 && hit.match === void 0;
4705
- if (!hit.match || hit.modelContext === void 0) return false;
4706
- const { start, end, variantIndex } = hit.match;
4707
- const variant = result.terms.find((term) => term.id === hit.termId)?.tried[variantIndex];
4708
- if (!variant || end <= start || end > hit.modelContext.length || hit.modelContext.slice(start, end) !== variant) return false;
4709
- return [start, end].every((offset) => {
4710
- const before = hit.modelContext?.charCodeAt(offset - 1) ?? 0;
4711
- const after = hit.modelContext?.charCodeAt(offset) ?? 0;
4712
- return !(before >= 55296 && before <= 56319 && after >= 56320 && after <= 57343);
4713
- });
4714
- }));
4715
- const journeySearchSaveSchema = z.object({
4716
- journeyId: journeyIdSchema,
4717
- expectedRevision: count$5,
4718
- idempotencyKey: journeyIdSchema,
4719
- result: journeySearchResultSchema
4720
- }).strict();
4721
- function searchScopeDigest(state) {
4722
- return textDigest(JSON.stringify({
4723
- snapshot: state.snapshotId,
4724
- configurationId: state.configurationId,
4725
- pendingHideId: state.pendingHideId,
4726
- works: state.works.map((work) => ({
4727
- id: work.id,
4728
- revision: work.revision,
4729
- inputRevision: work.inputRevision,
4730
- access: work.access,
4731
- artifact: work.artifact?.sha256 ?? null
4732
- }))
4733
- }));
4734
- }
4735
- //#endregion
5095
+ //#region packages/application/src/journey/policy/port.ts
5096
+ const journeyPolicyRespondSchema = z.object({
5097
+ request: nativeRespondSchema,
5098
+ source: z.enum(["conversation", "panel"]),
5099
+ now: z.iso.datetime()
5100
+ }).strict();
5101
+ //#endregion
4736
5102
  //#region packages/application/src/journey/hide/port.ts
4737
5103
  const hidePublishSchema = z.object({
4738
5104
  expectedRevision: z.number().int().nonnegative(),
@@ -4890,7 +5256,7 @@ function retireScanGeneration(state, workId) {
4890
5256
  }
4891
5257
  /** The same Work with a new scan generation: it is read again from its first session. */
4892
5258
  function resetWorkScan(work) {
4893
- const { emptyReview: _empty, excludedReview: _excluded, staticReview: _static, metadataReviewedDigest: _metadata, reviewReuseFrom: _reuse, renderFailure: _failure, ...retained } = work;
5259
+ const { reviewInterruptions: _reviewInterruptions, emptyReview: _empty, excludedReview: _excluded, staticReview: _static, metadataReviewedDigest: _metadata, reviewReuseFrom: _reuse, renderFailure: _failure, ...retained } = work;
4894
5260
  return {
4895
5261
  ...retained,
4896
5262
  revision: work.revision + 1,
@@ -4905,174 +5271,6 @@ function resetWorkScan(work) {
4905
5271
  };
4906
5272
  }
4907
5273
  //#endregion
4908
- //#region packages/application/src/journey/scope/respond.ts
4909
- /**
4910
- * Account-seeded access (ADR 0274 D3(b)). With `held`, the time the account inventory was read,
4911
- * a Work whose every root is held Public is confirmed Public with the account as its recorded
4912
- * choice (ADR 0275); without it, Public stays a suggestion.
4913
- */
4914
- function accountSeededAccess(account, held) {
4915
- const accessChoice = account === "public-all" && held !== void 0 ? {
4916
- channel: "account",
4917
- at: held
4918
- } : void 0;
4919
- return {
4920
- access: account === "public" || account === "public-all" ? "public" : "private",
4921
- accessOrigin: "account",
4922
- accessConfirmed: account === "private" || accessChoice !== void 0,
4923
- accountAccessConflict: account === "mixed",
4924
- accessChoice
4925
- };
4926
- }
4927
- /** Classification confirmation initializes visibility and scan membership for this journey. */
4928
- function confirmScope(state, now, decision, accountAccess) {
4929
- if (state.scope.status === "confirmed") return state;
4930
- if (state.classificationRevision === null || !state.works.length || state.works.some((work) => work.placement !== "placed" && work.placement !== "classifying" || work.placement === "placed" && !(work.status === "blocked" && work.access === "drop" && work.accessConfirmed) && (!work.description.trim() || !work.projectType))) throw new ApplicationError("SCOPE_NOT_READY");
4931
- return withReadingProgress({
4932
- ...state,
4933
- scope: {
4934
- status: "confirmed",
4935
- actor: decision ? "delegate" : "human",
4936
- at: now
4937
- },
4938
- works: state.works.map((work) => {
4939
- if (work.placement === "classifying") return work;
4940
- if (work.accessOrigin === "explicit" && work.accessConfirmed) return work;
4941
- const account = accountAccess?.get(work.id);
4942
- if (account) return {
4943
- ...work,
4944
- ...accountSeededAccess(account, now)
4945
- };
4946
- if (work.accessOrigin === "account") return work;
4947
- const access = work.accessConfirmed ? work.access : work.classificationRecommendation?.access ?? work.access;
4948
- return {
4949
- ...work,
4950
- access,
4951
- accessOrigin: work.accessConfirmed || !work.classificationRecommendation ? work.accessOrigin : "model",
4952
- accessConfirmed: access !== "public"
4953
- };
4954
- })
4955
- });
4956
- }
4957
- function setWorkType(state, workId, type, decision) {
4958
- if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
4959
- const projectType = decision ? {
4960
- type,
4961
- reason: "Chosen by your delegate for this journey.",
4962
- origin: "delegate"
4963
- } : {
4964
- type,
4965
- reason: "Chosen by you.",
4966
- origin: "explicit"
4967
- };
4968
- return {
4969
- ...state,
4970
- scope: {
4971
- status: "pending",
4972
- actor: null,
4973
- at: null
4974
- },
4975
- privacy: {
4976
- ...state.privacy,
4977
- cards: [],
4978
- confirmedAnswersRevision: null,
4979
- packageApproval: null
4980
- },
4981
- works: state.works.map((work) => work.id !== workId || decision && work.projectType?.origin === "explicit" ? work : {
4982
- ...work,
4983
- projectType
4984
- })
4985
- };
4986
- }
4987
- /** One rule for single and bulk choices; selection changes recompute reading completion. */
4988
- function setWorksAccess(state, workIds, access, decision, accessChoice) {
4989
- if (decision && access === "public") throw new ApplicationError("CONFIRMATION_REQUIRED");
4990
- if (new Set(workIds).size !== workIds.length) throw new ApplicationError("INVALID_SELECTION");
4991
- if (workIds.some((id) => !state.works.some((work) => work.id === id))) throw new ApplicationError("UNKNOWN_WORKSPACE");
4992
- if (state.scope.status === "confirmed") return changeConfirmedAccess(state, workIds, access, decision, accessChoice);
4993
- return withReadingProgress({
4994
- ...state,
4995
- scope: {
4996
- status: "pending",
4997
- actor: null,
4998
- at: null
4999
- },
5000
- privacy: {
5001
- ...state.privacy,
5002
- cards: [],
5003
- confirmedAnswersRevision: null,
5004
- packageApproval: null
5005
- },
5006
- works: state.works.map((work) => !workIds.includes(work.id) ? work : {
5007
- ...work,
5008
- revision: work.revision + 1,
5009
- artifact: null,
5010
- status: work.status === "ready" ? "review" : work.status,
5011
- access,
5012
- accessConfirmed: true,
5013
- accountAccessConflict: false,
5014
- accessChoice: access === "public" ? accessChoice : void 0,
5015
- accessOrigin: decision && work.accessOrigin !== "explicit" ? "delegate" : "explicit"
5016
- })
5017
- });
5018
- }
5019
- /**
5020
- * After confirmation the person, never a delegate, may still change any Work's visibility
5021
- * (D2, superseding ADR 0162's lock and ADR 0188's blocked-only Drop). Drop stops that Work's
5022
- * scan; a Work moved out of Drop is read again; reading progress is recomputed.
5023
- */
5024
- function changeConfirmedAccess(state, workIds, access, decision, accessChoice) {
5025
- if (decision || state.phase === "sending" || state.phase === "completed" || state.works.some((work) => workIds.includes(work.id) && work.access === access && work.accessConfirmed && hasPublicChoice(work))) throw new ApplicationError("INVALID_TRANSITION");
5026
- if (access === "public" && state.works.every((work) => !workIds.includes(work.id) || work.access === "public" && work.accessConfirmed)) return {
5027
- ...state,
5028
- works: state.works.map((work) => workIds.includes(work.id) && accessChoice ? {
5029
- ...work,
5030
- accessOrigin: "explicit",
5031
- accessChoice
5032
- } : work)
5033
- };
5034
- return withReadingProgress({
5035
- ...state,
5036
- privacy: {
5037
- ...state.privacy,
5038
- packageApproval: null
5039
- },
5040
- works: state.works.map((work) => workIds.includes(work.id) ? {
5041
- ...work,
5042
- revision: work.revision + 1,
5043
- artifact: null,
5044
- status: work.status === "ready" ? "review" : work.status,
5045
- access,
5046
- accessConfirmed: true,
5047
- accountAccessConflict: false,
5048
- accessChoice: access === "public" ? accessChoice : void 0,
5049
- accessOrigin: "explicit"
5050
- } : work)
5051
- });
5052
- }
5053
- /** Model suggestions fill or refresh model types only; a person's or delegate's type stays. */
5054
- function applyClassification(state, items) {
5055
- if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
5056
- const byWork = new Map(items.map((item) => [item.workId, item]));
5057
- if (byWork.size !== items.length) throw new ApplicationError("INVALID_SELECTION");
5058
- if (items.some((item) => !state.works.some((work) => work.id === item.workId))) throw new ApplicationError("UNKNOWN_WORKSPACE");
5059
- return {
5060
- ...state,
5061
- works: state.works.map((work) => {
5062
- const item = byWork.get(work.id);
5063
- if (!item || work.projectType && work.projectType.origin !== "model") return work;
5064
- return {
5065
- ...work,
5066
- projectType: {
5067
- type: item.type,
5068
- reason: item.reason,
5069
- origin: "model"
5070
- }
5071
- };
5072
- })
5073
- };
5074
- }
5075
- //#endregion
5076
5274
  //#region packages/application/src/journey/scope/proposal.ts
5077
5275
  const count$3 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
5078
5276
  const workspaces = z.array(z.string().min(1).max(128));
@@ -5095,6 +5293,10 @@ const classificationCommitSchema = z.object({
5095
5293
  partition: journeyDigestSchema,
5096
5294
  late: z.boolean(),
5097
5295
  accountAccess: z.record(z.string(), accountWorkAccessSchema).optional(),
5296
+ /** Binds late account facts to the full read that remains current at commit. */
5297
+ accountSubject: z.string().min(1).max(128).optional(),
5298
+ /** A failed or mismatched late read (ADR 0277 section 5). */
5299
+ accountRead: z.enum(["signed-out", "unavailable"]).optional(),
5098
5300
  items: z.array(journeyClassificationSubmitSchema.shape.items.element.extend({
5099
5301
  workId: journeyIdSchema,
5100
5302
  workspaceIds: workspaces
@@ -5238,7 +5440,12 @@ function regroupTerms(terms, works, changedMetadata, chunkSessions) {
5238
5440
  function applyClassificationProposal(state, plan) {
5239
5441
  const classifying = isClassifying(state);
5240
5442
  if (!state.consent || state.journeyId !== plan.journeyId || (plan.expectedClassificationRevision === null ? state.revision !== plan.expectedRevision : !classifying || state.classificationRevision !== plan.expectedClassificationRevision) || state.snapshotId !== plan.snapshotId || state.scope.status === "confirmed" !== plan.late || plan.late && !classifying || state.phase === "paused" || state.pendingHideId || classificationPartition(state) !== plan.partition) return null;
5241
- const { works, changedMetadata } = classificationWorks(state, plan);
5443
+ const currentPlan = plan.accountAccess !== void 0 && (!plan.accountSubject || state.accountRead?.status !== "read" || state.accountRead.subject !== plan.accountSubject) ? {
5444
+ ...plan,
5445
+ accountAccess: void 0,
5446
+ accountRead: "unavailable"
5447
+ } : plan;
5448
+ const { works, changedMetadata } = classificationWorks(state, currentPlan);
5242
5449
  const retained = new Set(works.map((work) => work.id));
5243
5450
  const regrouped = !plan.late && (state.works.some((work) => !retained.has(work.id)) || changedMetadata.size > 0);
5244
5451
  const cardTerms = new Set(state.privacy.cards.flatMap((card) => card.termKeys));
@@ -5269,6 +5476,10 @@ function applyClassificationProposal(state, plan) {
5269
5476
  confirmedAnswersRevision: null
5270
5477
  } : {}
5271
5478
  },
5479
+ ...currentPlan.accountRead ? { accountRead: {
5480
+ status: currentPlan.accountRead,
5481
+ at: plan.at
5482
+ } } : {},
5272
5483
  readingComplete: !plan.pending && works.filter((work) => work.access !== "drop").every((work) => work.totalBatches > 0 && work.completedBatches === work.totalBatches),
5273
5484
  phase: "processing"
5274
5485
  };
@@ -5334,44 +5545,6 @@ z.object({
5334
5545
  groupId: journeyIdSchema
5335
5546
  }).strict())
5336
5547
  }).strict().nullable();
5337
- z.object({
5338
- journeyId: journeyIdSchema.optional(),
5339
- workIds: z.array(journeyIdSchema).min(1).max(1e3).optional()
5340
- }).strict();
5341
- const categories = z.object({
5342
- identity: z.number().int().nonnegative(),
5343
- credentials: z.number().int().nonnegative(),
5344
- internal: z.number().int().nonnegative(),
5345
- private: z.number().int().nonnegative()
5346
- }).strict();
5347
- z.object({
5348
- journeyId: journeyIdSchema,
5349
- nonAdditiveMechanismCounts: z.literal(true),
5350
- total: z.number().int().nonnegative(),
5351
- categories,
5352
- byMechanism: z.object({
5353
- staticNodes: z.record(z.string(), z.number().int().nonnegative()),
5354
- semantic: z.number().int().nonnegative(),
5355
- correction: z.number().int().nonnegative()
5356
- }).strict(),
5357
- works: z.array(z.object({
5358
- workId: journeyIdSchema,
5359
- revision: z.number().int().positive(),
5360
- inputRevision: z.number().int().positive(),
5361
- primarySha256: z.string().length(64),
5362
- companionSha256: z.string().length(64),
5363
- ledgerSha256: z.string().length(64),
5364
- decisionDigest: z.string().length(64),
5365
- reviewInputDigest: z.string().length(64),
5366
- total: z.number().int().nonnegative(),
5367
- categories,
5368
- byMechanism: z.object({
5369
- staticNodes: z.record(z.string(), z.number().int().nonnegative()),
5370
- semantic: z.number().int().nonnegative(),
5371
- correction: z.number().int().nonnegative()
5372
- }).strict()
5373
- }).strict())
5374
- }).strict();
5375
5548
  //#endregion
5376
5549
  //#region packages/application/src/journey/delegation.ts
5377
5550
  function delegatedDecisionScope(action) {
@@ -5424,6 +5597,11 @@ function delegatedDecisionTargets(action, journeyId) {
5424
5597
  targetIds: [journeyId],
5425
5598
  choice: String(action.answersRevision)
5426
5599
  };
5600
+ if (action.kind === "account-read") return {
5601
+ action: action.kind,
5602
+ targetIds: [journeyId],
5603
+ choice: "retry"
5604
+ };
5427
5605
  return action.kind === "scope-confirm" ? {
5428
5606
  action: action.kind,
5429
5607
  targetIds: [journeyId],
@@ -5455,7 +5633,7 @@ function assertDelegatedDecision(state, action, decision, source) {
5455
5633
  if (work?.projectType?.origin === "explicit" && work.projectType.type !== action.type) throw new ApplicationError("CONFIRMATION_REQUIRED");
5456
5634
  return;
5457
5635
  }
5458
- if (action.kind === "scope-confirm") return;
5636
+ if (action.kind === "scope-confirm" || action.kind === "account-read") return;
5459
5637
  if (action.kind !== "group" && action.kind !== "groups-hide") throw new ApplicationError("CONFIRMATION_REQUIRED");
5460
5638
  const ids = action.kind === "group" ? [action.groupId] : action.groupIds;
5461
5639
  const choice = action.kind === "group" ? action.choice : "hide";
@@ -5528,7 +5706,7 @@ function invalidateAnalysisIntent(state) {
5528
5706
  ...state,
5529
5707
  works: state.works.map((work) => {
5530
5708
  if (!work.rootSessionIds.length) return work;
5531
- const { emptyReview: _empty, excludedReview: _excluded, ...retired } = retireWorkDescription(work);
5709
+ const { reviewInterruptions: _reviewInterruptions, emptyReview: _empty, excludedReview: _excluded, ...retired } = retireWorkDescription(work);
5532
5710
  return {
5533
5711
  ...retired,
5534
5712
  title: work.explicitTitleId ? work.title : retired.title,
@@ -5543,7 +5721,7 @@ function invalidateAnalysisIntent(state) {
5543
5721
  })
5544
5722
  });
5545
5723
  }
5546
- function respondToJourney(state, action, source, now, decision, preferenceScope, accountAccess) {
5724
+ function respondToJourney(state, action, source, now, decision, preferenceScope, accountRead, account) {
5547
5725
  if (preferenceScope && (decision || !["group", "groups-hide"].includes(action.kind))) throw new ApplicationError("INVALID_ARGUMENTS");
5548
5726
  if (state.abandonedAt) throw new ApplicationError("INVALID_TRANSITION");
5549
5727
  if (action.kind === "abandon") {
@@ -5597,12 +5775,14 @@ function respondToJourney(state, action, source, now, decision, preferenceScope,
5597
5775
  ...state,
5598
5776
  phase: "welcome"
5599
5777
  };
5778
+ if (account?.configured && !account.subject) throw new ApplicationError("JOURNEY_SIGN_IN_REQUIRED");
5600
5779
  return {
5601
5780
  ...state,
5602
5781
  consent: {
5603
5782
  provider: state.provider,
5604
5783
  source,
5605
- allowedAt: now
5784
+ allowedAt: now,
5785
+ ...account?.subject ? { subject: account.subject } : {}
5606
5786
  },
5607
5787
  phase: "discovering"
5608
5788
  };
@@ -5657,7 +5837,14 @@ function respondToJourney(state, action, source, now, decision, preferenceScope,
5657
5837
  };
5658
5838
  if (action.kind === "scope-confirm") {
5659
5839
  if (source !== "conversation" || decision) throw new ApplicationError("CONFIRMATION_REQUIRED");
5660
- return confirmScope(state, now, void 0, accountAccess);
5840
+ const stop = accountReadStop(state, accountRead);
5841
+ if (stop) throw new ApplicationError(stop);
5842
+ const confirmed = confirmScope(state, now, void 0, accountRead?.status === "read" ? accountRead.access : void 0);
5843
+ return accountRead && confirmed !== state ? withAccountRead(confirmed, accountRead, now) : confirmed;
5844
+ }
5845
+ if (action.kind === "account-read") {
5846
+ if (!accountRead) throw new ApplicationError("INVALID_TRANSITION");
5847
+ return retryAccountRead(state, accountRead, now);
5661
5848
  }
5662
5849
  if (action.kind === "card-answer" || action.kind === "cards-confirm" || action.kind === "package-confirm") throw new ApplicationError("INVALID_TRANSITION");
5663
5850
  if ((action.kind === "work-access" || action.kind === "works-access") && action.access === "public" && source === "conversation" && (!action.accessChoice || action.accessChoice.channel !== "conversation" || !Number.isFinite(Date.parse(action.accessChoice.at)))) throw new ApplicationError("CONFIRMATION_REQUIRED");
@@ -5678,6 +5865,48 @@ function respondToJourney(state, action, source, now, decision, preferenceScope,
5678
5865
  const workId = action.workId;
5679
5866
  if (!state.works.some((work) => work.id === workId)) throw new ApplicationError("UNKNOWN_WORKSPACE");
5680
5867
  if (action.kind === "work-type") return setWorkType(state, workId, action.type, decision);
5868
+ if (action.kind === "review-interrupted" || action.kind === "review-reader-closed" || action.kind === "retry-review") {
5869
+ const work = state.works.find((item) => item.id === workId);
5870
+ if (!work || work.inputRevision !== action.inputRevision) throw new ApplicationError("REVISION_CONFLICT");
5871
+ if (!isSelectedWork(work) || work.status === "blocked" || state.scope.status !== "confirmed" || state.phase === "sending" || state.phase === "completed") throw new ApplicationError("INVALID_TRANSITION");
5872
+ if (action.kind === "review-reader-closed") {
5873
+ if (source !== "conversation" || decision) throw new ApplicationError("CONFIRMATION_REQUIRED");
5874
+ return state;
5875
+ }
5876
+ const holds = currentReviewInterruptions(work);
5877
+ const previous = holds.find((item) => item.packId === action.packId);
5878
+ let nextHolds;
5879
+ if (action.kind === "review-interrupted") {
5880
+ if (source !== "conversation" || decision) throw new ApplicationError("CONFIRMATION_REQUIRED");
5881
+ if (previous) {
5882
+ if (previous.reason !== action.reason || previous.batchIds.length !== action.batchIds.length || !action.batchIds.every((id) => previous.batchIds.includes(id))) throw new ApplicationError("OPERATION_CONFLICT");
5883
+ return state;
5884
+ }
5885
+ if (work.totalBatches - work.completedBatches < action.batchIds.length || holds.some((hold) => hold.batchIds.some((id) => action.batchIds.includes(id)))) throw new ApplicationError("INVALID_TRANSITION");
5886
+ nextHolds = [...holds, {
5887
+ packId: action.packId,
5888
+ inputRevision: action.inputRevision,
5889
+ batchIds: action.batchIds,
5890
+ reason: action.reason
5891
+ }];
5892
+ } else {
5893
+ if (previous?.reason !== "retry-exhausted") throw new ApplicationError("INVALID_TRANSITION");
5894
+ nextHolds = holds.filter((item) => item.packId !== action.packId);
5895
+ }
5896
+ return withReadingProgress({
5897
+ ...state,
5898
+ privacy: {
5899
+ ...state.privacy,
5900
+ packageApproval: null
5901
+ },
5902
+ works: state.works.map((item) => item.id === workId ? {
5903
+ ...item,
5904
+ reviewInterruptions: nextHolds,
5905
+ revision: item.revision + 1,
5906
+ artifact: null
5907
+ } : item)
5908
+ });
5909
+ }
5681
5910
  if (action.kind === "retry-work") {
5682
5911
  if (state.works.find((item) => item.id === workId)?.status !== "blocked" || state.phase === "sending" || state.phase === "completed") throw new ApplicationError("INVALID_TRANSITION");
5683
5912
  return withReadingProgress({
@@ -6429,10 +6658,19 @@ function digest$1(value) {
6429
6658
  * quotes stay out. The agent reads override values from `journey.get` and the guidance.
6430
6659
  */
6431
6660
  function journeyView(state, now) {
6432
- const { exceptions, instructions, corrections, valueOverrides, ...safe } = state;
6661
+ const { exceptions, instructions, corrections, valueOverrides, consent, accountRead, ...safe } = state;
6433
6662
  const terms = activePrivacyTerms(state);
6434
6663
  return {
6435
6664
  ...safe,
6665
+ consent: consent && {
6666
+ provider: consent.provider,
6667
+ allowedAt: consent.allowedAt,
6668
+ source: consent.source
6669
+ },
6670
+ ...accountRead ? { accountRead: {
6671
+ status: accountRead.status,
6672
+ at: accountRead.at
6673
+ } } : {},
6436
6674
  displayedScopeDigest: displayedScopeDigest(state),
6437
6675
  privacy: {
6438
6676
  ...state.privacy,
@@ -6441,10 +6679,12 @@ function journeyView(state, now) {
6441
6679
  works: state.works.map((work) => {
6442
6680
  const localTitle = work.suggestion || work.explicitTitleId ? work.title : work.classificationMetadata?.title ?? work.title;
6443
6681
  const imagesOmitted = hasCurrentStaticReview(state, work) ? work.staticReview?.imagesOmitted ?? 0 : 0;
6682
+ const source = publicSource(work);
6444
6683
  if (!work.classificationMetadata) return {
6445
6684
  ...work,
6446
6685
  localTitle,
6447
- imagesOmitted
6686
+ imagesOmitted,
6687
+ publicSource: source
6448
6688
  };
6449
6689
  const metadata = work.classificationMetadata;
6450
6690
  const displayed = {
@@ -6458,6 +6698,7 @@ function journeyView(state, now) {
6458
6698
  ...displayed,
6459
6699
  localTitle,
6460
6700
  imagesOmitted,
6701
+ publicSource: source,
6461
6702
  projectType: work.projectType ? {
6462
6703
  ...work.projectType,
6463
6704
  reason: work.projectType.origin === "model" ? displayed.reason : work.projectType.reason
@@ -6576,12 +6817,14 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6576
6817
  status: state.phase === "paused" && classificationProgress.status === "running" ? "paused" : classificationProgress.status
6577
6818
  } : null;
6578
6819
  const packaging = await local?.renderActivity?.read(state.journeyId) ?? null;
6820
+ const subject = state.accountRead?.status === "read" ? await local?.accountState?.().then((account) => account.subject).catch(() => null) : null;
6579
6821
  return {
6580
6822
  ...view,
6581
6823
  ...reads,
6582
6824
  packaging,
6583
6825
  preparationProgress,
6584
- classificationProgress: liveClassificationProgress
6826
+ classificationProgress: liveClassificationProgress,
6827
+ accountReadCurrent: state.accountRead?.status === "read" && typeof subject === "string" && state.accountRead.subject === subject
6585
6828
  };
6586
6829
  }
6587
6830
  }
@@ -6815,19 +7058,50 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6815
7058
  "works-access",
6816
7059
  "card-answer",
6817
7060
  "profile-address",
6818
- "profile-links"
7061
+ "profile-links",
7062
+ "account-read"
6819
7063
  ].includes(input.action.kind);
7064
+ let accountRead;
7065
+ const readAccount = async (state) => {
7066
+ const scope = digest$1(state.works.map((work) => ({
7067
+ id: work.id,
7068
+ roots: work.rootSessionIds.toSorted()
7069
+ })).sort((left, right) => left.id.localeCompare(right.id)));
7070
+ if (!accountRead || accountRead.scope !== scope) accountRead = {
7071
+ scope,
7072
+ result: await local?.accountAccess?.(state, new AbortController().signal)
7073
+ };
7074
+ return accountRead.result;
7075
+ };
6820
7076
  for (let attempt = 0;; attempt++) {
6821
- const result = await commitResponse(input, source, rebase);
7077
+ const result = await commitResponse(input, source, rebase, readAccount);
6822
7078
  if (result.status !== "stale" || !rebase || attempt >= 4) return result;
6823
7079
  }
6824
7080
  }
6825
- async function commitResponse(input, source, rebase) {
7081
+ async function commitResponse(input, source, rebase, readAccount) {
6826
7082
  const current = await storage.current(input.journeyId);
6827
7083
  if (!current) throw new ApplicationError("UNKNOWN_ITEM");
6828
7084
  const expectedRevision = rebase ? current.revision : input.expectedRevision;
6829
- const accountAccess = current.revision === expectedRevision && input.action.kind === "scope-confirm" && current.scope.status !== "confirmed" && source === "conversation" && !input.decision ? await local?.accountAccess?.(current, new AbortController().signal) : void 0;
6830
- const state = current.revision === expectedRevision ? respondToJourney(current, input.action, source, now(), input.decision, input.preferenceScope, accountAccess) : current;
7085
+ const scopeRead = input.action.kind === "scope-confirm" && current.scope.status !== "confirmed" && source === "conversation" && !input.decision;
7086
+ const accountRead = current.revision === expectedRevision && (scopeRead || input.action.kind === "account-read") ? await readAccount(current) : void 0;
7087
+ const account = current.revision === expectedRevision && (input.action.kind === "consent" && input.action.allow && !current.consent || scopeRead) ? await local?.accountState?.() : void 0;
7088
+ const stop = scopeRead && accountReadStop(current, accountRead);
7089
+ if (stop && accountRead) {
7090
+ await storage.commit({
7091
+ expectedRevision,
7092
+ requestKey: randomUUID(),
7093
+ requestHash: digest$1(input),
7094
+ state: withAccountRead(current, accountRead, now()),
7095
+ event: {
7096
+ journeyId: current.journeyId,
7097
+ kind: "response",
7098
+ source,
7099
+ at: now()
7100
+ }
7101
+ });
7102
+ throw new ApplicationError(stop);
7103
+ }
7104
+ const state = current.revision === expectedRevision ? respondToJourney(current, input.action, source, now(), input.decision, input.preferenceScope, accountRead, account) : current;
6831
7105
  const groupIds = input.action.kind === "group" ? [input.action.groupId] : input.action.kind === "groups-hide" ? input.action.groupIds : [];
6832
7106
  const preferenceScopes = input.decision ? [] : state.groups.flatMap((group) => groupIds.includes(group.id) && group.preferenceScope ? [{
6833
7107
  groupId: group.id,
@@ -6838,6 +7112,12 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6838
7112
  requestKey: input.idempotencyKey,
6839
7113
  requestHash: digest$1(input),
6840
7114
  state,
7115
+ ...input.action.kind === "review-interrupted" || input.action.kind === "review-reader-closed" ? { closedReader: {
7116
+ packId: input.action.packId,
7117
+ workId: input.action.workId,
7118
+ inputRevision: input.action.inputRevision,
7119
+ batchIds: input.action.batchIds
7120
+ } } : {},
6841
7121
  event: {
6842
7122
  journeyId: state.journeyId,
6843
7123
  kind: "response",
@@ -6867,12 +7147,27 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6867
7147
  await setTimeout$1(Math.min(1e3, Math.max(0, deadline - Date.now())), void 0, { signal });
6868
7148
  }
6869
7149
  }
6870
- async function prepare(value, signal) {
7150
+ async function runPreparation(value, signal) {
6871
7151
  const input = nativeGetSchema.required().parse(value);
6872
7152
  if (!local) throw new ApplicationError("INVALID_TRANSITION");
6873
- await local.hide.recover(input.journeyId, signal ?? new AbortController().signal);
6874
- const state = await local.prepare(input.journeyId, signal);
6875
- return state ? present(state) : null;
7153
+ const permission = await storage.preparationPermission(input.journeyId);
7154
+ if (!permission) throw new ApplicationError("UNKNOWN_ITEM");
7155
+ if (permission.pendingHideId) await local.hide.recover(input.journeyId, signal ?? new AbortController().signal);
7156
+ return local.prepare(input.journeyId, signal);
7157
+ }
7158
+ async function prepare(value, signal) {
7159
+ const state = await runPreparation(value, signal);
7160
+ return state ? get({ journeyId: state.journeyId }) : null;
7161
+ }
7162
+ /** The owned CLI scan needs an exit receipt; the panel owns the full presentation read. */
7163
+ async function prepareReceipt(value, signal) {
7164
+ const state = await runPreparation(value, signal);
7165
+ return state && {
7166
+ journeyId: state.journeyId,
7167
+ revision: state.revision,
7168
+ phase: state.phase,
7169
+ readingComplete: state.readingComplete
7170
+ };
6876
7171
  }
6877
7172
  async function read(value) {
6878
7173
  return storage.readBatch(journeyBatchReadSchema.parse(value));
@@ -7168,8 +7463,10 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
7168
7463
  events,
7169
7464
  groupItems: (value) => readGroupItems(storage, value),
7170
7465
  /** ADR 0086: one pack of read units and the values found in them. */
7466
+ reopenEmpty: (value) => storage.reopenEmpty(emptyReviewCorrectionSchema.parse(value)),
7171
7467
  terms: async (value) => reviewTermsResultSchema.parse(await storage.recordTerms(reviewTermsInputSchema.parse(value))),
7172
7468
  prepare,
7469
+ prepareReceipt,
7173
7470
  read,
7174
7471
  guidance,
7175
7472
  inspect,
@@ -10813,7 +11110,7 @@ const bindingFields = {
10813
11110
  };
10814
11111
  const inspectionBindingSchema = z.union([z.object({
10815
11112
  ...bindingFields,
10816
- detectorVersion: z.literal(currentDetectorVersion),
11113
+ detectorVersion: z.enum([currentDetectorVersion, "static-catalog-11"]),
10817
11114
  dictionaryVersion: z.literal(entityDictionaryVersion)
10818
11115
  }).strict(), z.object({
10819
11116
  ...bindingFields,
@@ -13651,7 +13948,6 @@ z.object({
13651
13948
  }).strict();
13652
13949
  //#endregion
13653
13950
  //#region packages/application/src/journey/scan-equivalence.ts
13654
- /** State authority relevant to AI meaning; unrelated Work progress does not invalidate publication. */
13655
13951
  function scanBatchContextDigest(state, workId) {
13656
13952
  const work = state.works.find((work) => work.id === workId);
13657
13953
  if (!work) throw new ApplicationError("REVISION_CONFLICT");
@@ -13754,4 +14050,4 @@ function equivalenceDuplicates(batch) {
13754
14050
  return bindings.sort();
13755
14051
  }
13756
14052
  //#endregion
13757
- export { validateDetectionSummary as $, reviewRepairInputSchema as $n, isClassifying as $r, searchScopeDigest as $t, renewBeforeMs as A, addHideIndexTokens as An, displayedScopeDigest as Ar, obsoleteJourney as At, createApplication as B, confirmedHideKey as Bn, staticReviewDigest as Br, classificationPartition as Bt, rebindReviewOverlay as C, journeyEventSchema as Cn, validatePreparationFacts as Cr, sendActionSchema as Ct, staticallyHiddenFindings as D, renderedHideValue as Dn, journeyPreviewSchema as Dr, sendStartSchema as Dt, guardedFindingText as E, readConfirmedHideChange as En, journeyFindingsSchema as Er, sendRequestSchema as Et, readAccountAvatarUrl as F, bExceptionRanges as Fn, nativeEventsSchema as Fr, journeyGuidance as Ft, validateInspectionRequest as G, maskedHitsStale as Gn, agentSourcesSchema as Gr, reviewCompileSchema as Gt, inspectionStartSchema as H, confirmedHideTerms as Hn, staticSessionResourceSchema as Hr, applyClassification as Ht, storedCredentialSchema as I, bMaskedHits as In, nativeGetSchema as Ir, journeyBatchReadSchema as It, validateInspectionFence as J, workDecisionDigest as Jn, sendReceiptSchema as Jr, hideBeginSchema as Jt, inspectionQuerySchema as K, syncTermGroups as Kn, journeySendSchema as Kr, journeyContextQuerySchema as Kt, sourceIdentityEvidence as L, bTermMatcher as Ln, nativeRespondSchema as Lr, journeyReviewStorageQuerySchema as Lt, deviceAuthorizationSchema as M, maximumHideIndexTokens as Mn, applyCardAnswers as Mr, nativeAdapterVersions as Mt, deviceProfileScopeSchema as N, packHideIndex as Nn, assertJourneyPrivacyReady as Nr, attemptBudgetCodePoints as Nt, createAccount as O, settleConfirmedHides as On, journeyRenderInputSchema as Or, settleCorrections as Ot, profileUrlMatchesIdentity as P, bContainment as Pn, privacyDecisionDigest as Pr, validateAttemptAppend as Pt, validateInspectionContribution as Q, reviewCandidateResourceSchema as Qn, hasCompleteWorkDescription as Qr, journeySearchSaveSchema as Qt, withSourceEvidence as R, bWordBounded as Rn, mergePrivacyTerms as Rr, applyClassificationProposal as Rt, rebindReviewAlignment as S, journeyCommitSchema as Sn, preparationStartSchema as Sr, replaceSensitiveTermsSchema as St, reviewBatchReuse as T, confirmedHideDeltaSchema as Tn, journeyFindingImpactSchema as Tr, sendPrepareSchema as Tt, validateInspectionBatch as U, dictionaryHides as Un, agentSourcePathSegment as Ur, retireScanGeneration as Ut, inspectionCancelSchema as V, confirmedHideKinds as Vn, staticSessionId as Vr, classificationWorks as Vt, validateInspectionBatches as W, journeyConfirmedHides as Wn, agentSourceSchema as Wr, newJourneyWork as Wt, validateInspectionInputs as X, workCorrectionBinding as Xn, sendResolutionSchema as Xr, hidePublishSchema as Xt, validateInspectionHead as Y, workPrivacyDecisions as Yn, sendReplaceResultSchema as Yr, hideFinishSchema as Yt, validateInspectionQuery as Z, reviewCandidateInputSchema as Zn, sendSettingsSchema as Zr, journeySearchResultSchema as Zt, changeJourney as _, defaultScopedSettings as _i, sessionReviewResourceSchema as _n, journeyInventorySchema as _r, manualUploadStartSchema as _t, bCrossSegmentReviewSchema as a, withReadingProgress as ai, journeyPlaceSchema as an, scanAliasResourceSchema as ar, appendSensitiveTerms as at, isGenericPublicTerm as b, staticCatalogRef as bi, sessionUsefulnessResourceSchema as bn, preparationBeginSchema as br, maximumTermsFileBytes as bt, reconstructBFields as c, selectionBatchSchema as ci, reviewAttemptsQuerySchema as cn, scanSessionResourceSchema as cr, maximumAgentSetupSessions as ct, resolveGroupSplit as d, producerVersion as di, sessionReconcileResultSchema as dn, verifiedScanGitSeeds as dr, validateFailureLocation as dt, isIncludedWork as ei, journeyPolicyRespondSchema as en, reviewRepairResourceSchema as er, inspectionInputs as et, selectedValueMembers as f, stateSchema as fi, sessionReconciliationResourceSchema as fn, scanExclusionResourceSchema as fr, manualUploadActionSchema as ft, semanticGroupKey as g, effectiveTreatmentSchema as gi, sessionReviewQuerySchema as gn, journeyInventoryReadSchema as gr, manualUploadSchema as gt, hideScopeDigest as h, approvedTreatmentSchema as hi, sessionReviewInputSchema as hn, journeyInventoryQuerySchema as hr, manualUploadPrepareSchema as ht, validateBCrossSegmentReview as i, selectedWorks as ii, journeyIsolateSchema as in, planBRepair as ir, reviewInventory as it, accountProfileSchema as j, hideKeyId as jn, requestedScope as jr, metadataProcessingVersion as jt, renew as k, termHideAdditions as kn, journeyRenderSchema as kr, currentJourneyAdapters as kt, chooseCheapPlacement as l, selectionSchema as li, reviewAttemptsResultSchema as ln, scanGitSeedsId as lr, artifactFailureDetailSchema as lt, activeJourneyTerms as m, recommendParallelism as mi, sessionRereadResourceSchema as mn, resolveSourceSelection as mr, manualUploadChoiceSchema as mt, scanBatchEquivalenceDigest as n, isSelectedWork as ni, journeyResourceQuerySchema as nn, bReviewDigest as nr, settingsQuerySchema as nt, bRereadDigest as o, commandSchema as oi, sortingApplySchema as on, scanCanonicalResourceSchema as or, classifyAgentSetup as ot, splitSelectorValues as p, sourceChangePolicy as pi, sessionRereadInputSchema as pn, validateScanExclusion as pr, manualUploadBindingSchema as pt, validateInspectionClaim as q, retireWorkDescription as qn, sendArtifactSchema as qr, journeyContextSchema as qt, locateTermEvidence as r, isSkippedWork as ri, journeyResourceSchema as rn, mergeBRepair as rr, readWorkspaceOutcomes as rt, reconcileBSession as s, packageCommandSchema as si, reviewAttemptResourceSchema as sn, scanPreparationDigest as sr, defaultAgentSetupInclusion as st, scanBatchContextDigest as t, isReadingComplete as ti, projectPreparationProgress as tn, reviewTransportBytes as tr, settingsBatchSchema as tt, reviseGroupMembership as u, gitMetadataBudgetBytes as ui, sessionReconcileInputSchema as un, scanGitSeedsResource as ur, resolveFailureLocation as ut, projectJourneyInventory as v, freezeConfiguration as vi, sessionReviewStatusSchema as vn, journeyPreparationProgressSchema as vr, candidateSortAvailable as vt, rebindReviewSubmit as w, confirmedHideAdditions as wn, journeyClassificationSessionsSchema as wr, sendChoiceSchema as wt, bReviewScope as x, journeyCommitResultSchema as xn, preparationSettlementSchema as xr, normalizeSensitiveTerms as xt, preparationScope as y, selectFrozenConfiguration as yi, sessionUsefulnessInputSchema as yn, emptyPreparationFacts as yr, emptyTermsRevision as yt, agentSetupKey as z, confirmedHideGroupIds as zn, hasCurrentStaticReview as zr, classificationCommitSchema as zt };
14053
+ export { validateDetectionSummary as $, hideKeyId as $n, sendReplaceResultSchema as $r, journeyResourceSchema as $t, renewBeforeMs as A, scanGitSeedsResource as An, journeyPreviewSchema as Ar, obsoleteJourney as At, createApplication as B, journeySearchResultSchema as Bn, nativeRespondSchema as Br, classificationPartition as Bt, rebindReviewOverlay as C, selectFrozenConfiguration as Ci, mergeBRepair as Cn, preparationStartSchema as Cr, sendActionSchema as Ct, staticallyHiddenFindings as D, scanPreparationDigest as Dn, journeyClassificationSessionsSchema as Dr, sendStartSchema as Dt, guardedFindingText as E, scanCanonicalResourceSchema as En, validatePreparationFacts as Er, sendRequestSchema as Et, readAccountAvatarUrl as F, journeyInventoryQuerySchema as Fn, applyCardAnswers as Fr, journeyGuidance as Ft, validateInspectionRequest as G, journeyEventSchema as Gn, staticSessionId as Gr, journeyContextQuerySchema as Gt, inspectionStartSchema as H, searchScopeDigest as Hn, mergePrivacyTerms as Hr, retireScanGeneration as Ht, storedCredentialSchema as I, journeyInventoryReadSchema as In, assertJourneyPrivacyReady as Ir, journeyBatchReadSchema as It, validateInspectionFence as J, readConfirmedHideChange as Jn, agentSourceSchema as Jr, hideFinishSchema as Jt, inspectionQuerySchema as K, confirmedHideAdditions as Kn, staticSessionResourceSchema as Kr, journeyContextSchema as Kt, sourceIdentityEvidence as L, journeyInventorySchema as Ln, privacyDecisionDigest as Lr, journeyReviewStorageQuerySchema as Lt, deviceAuthorizationSchema as M, scanExclusionResourceSchema as Mn, journeyRenderSchema as Mr, nativeAdapterVersions as Mt, deviceProfileScopeSchema as N, validateScanExclusion as Nn, displayedScopeDigest as Nr, attemptBudgetCodePoints as Nt, createAccount as O, scanSessionResourceSchema as On, journeyFindingImpactSchema as Or, settleCorrections as Ot, profileUrlMatchesIdentity as P, resolveSourceSelection as Pn, requestedScope as Pr, validateAttemptAppend as Pt, validateInspectionContribution as Q, addHideIndexTokens as Qn, sendReceiptSchema as Qr, journeyResourceQuerySchema as Qt, withSourceEvidence as R, journeyPreparationProgressSchema as Rn, nativeEventsSchema as Rr, applyClassificationProposal as Rt, rebindReviewAlignment as S, freezeConfiguration as Si, bReviewDigest as Sn, preparationSettlementSchema as Sr, replaceSensitiveTermsSchema as St, reviewBatchReuse as T, scanAliasResourceSchema as Tn, preparationUpdateSchema as Tr, sendPrepareSchema as Tt, validateInspectionBatch as U, journeyCommitResultSchema as Un, hasCurrentStaticReview as Ur, newJourneyWork as Ut, inspectionCancelSchema as V, journeySearchSaveSchema as Vn, retryReviewActionSchema as Vr, classificationWorks as Vt, validateInspectionBatches as W, journeyCommitSchema as Wn, staticReviewDigest as Wr, reviewCompileSchema as Wt, validateInspectionInputs as X, settleConfirmedHides as Xn, journeySendSchema as Xr, journeyPolicyRespondSchema as Xt, validateInspectionHead as Y, renderedHideValue as Yn, agentSourcesSchema as Yr, hidePublishSchema as Yt, validateInspectionQuery as Z, termHideAdditions as Zn, sendArtifactSchema as Zr, projectPreparationProgress as Zt, changeJourney as _, sourceChangePolicy as _i, reviewCandidateInputSchema as _n, workPrivacyDecisions as _r, manualUploadStartSchema as _t, bCrossSegmentReviewSchema as a, isReadingComplete as ai, reviewAttemptsResultSchema as an, bTermMatcher as ar, appendSensitiveTerms as at, isGenericPublicTerm as b, effectiveTreatmentSchema as bi, reviewRepairResourceSchema as bn, preparationBeginSchema as br, maximumTermsFileBytes as bt, reconstructBFields as c, selectedWorks as ci, sessionReconciliationResourceSchema as cn, confirmedHideKey as cr, maximumAgentSetupSessions as ct, resolveGroupSplit as d, packageCommandSchema as di, sessionReviewInputSchema as dn, dictionaryHides as dr, validateFailureLocation as dt, sendResolutionSchema as ei, journeyIsolateSchema as en, maximumHideIndexTokens as er, inspectionInputs as et, selectedValueMembers as f, selectionBatchSchema as fi, sessionReviewQuerySchema as fn, journeyConfirmedHides as fr, manualUploadActionSchema as ft, semanticGroupKey as g, stateSchema as gi, sessionUsefulnessResourceSchema as gn, workDecisionDigest as gr, manualUploadSchema as gt, hideScopeDigest as h, producerVersion as hi, sessionUsefulnessInputSchema as hn, retireWorkDescription as hr, manualUploadPrepareSchema as ht, validateBCrossSegmentReview as i, isIncludedWork as ii, reviewAttemptsQuerySchema as in, bMaskedHits as ir, reviewInventory as it, accountProfileSchema as j, verifiedScanGitSeeds as jn, journeyRenderInputSchema as jr, metadataProcessingVersion as jt, renew as k, scanGitSeedsId as kn, journeyFindingsSchema as kr, currentJourneyAdapters as kt, chooseCheapPlacement as l, withReadingProgress as li, sessionRereadInputSchema as ln, confirmedHideKinds as lr, artifactFailureDetailSchema as lt, activeJourneyTerms as m, gitMetadataBudgetBytes as mi, sessionReviewStatusSchema as mn, syncTermGroups as mr, manualUploadChoiceSchema as mt, scanBatchEquivalenceDigest as n, hasCompleteWorkDescription as ni, sortingApplySchema as nn, bContainment as nr, settingsQuerySchema as nt, bRereadDigest as o, isSelectedWork as oi, sessionReconcileInputSchema as on, bWordBounded as or, classifyAgentSetup as ot, splitSelectorValues as p, selectionSchema as pi, sessionReviewResourceSchema as pn, maskedHitsStale as pr, manualUploadBindingSchema as pt, validateInspectionClaim as q, confirmedHideDeltaSchema as qn, agentSourcePathSegment as qr, hideBeginSchema as qt, locateTermEvidence as r, isClassifying as ri, reviewAttemptResourceSchema as rn, bExceptionRanges as rr, readWorkspaceOutcomes as rt, reconcileBSession as s, isSkippedWork as si, sessionReconcileResultSchema as sn, confirmedHideGroupIds as sr, defaultAgentSetupInclusion as st, scanBatchContextDigest as t, sendSettingsSchema as ti, journeyPlaceSchema as tn, packHideIndex as tr, settingsBatchSchema as tt, reviseGroupMembership as u, commandSchema as ui, sessionRereadResourceSchema as un, confirmedHideTerms as ur, resolveFailureLocation as ut, projectJourneyInventory as v, recommendParallelism as vi, reviewCandidateResourceSchema as vn, workCorrectionBinding as vr, candidateSortAvailable as vt, rebindReviewSubmit as w, staticCatalogRef as wi, planBRepair as wn, preparationStateSchema as wr, sendChoiceSchema as wt, bReviewScope as x, defaultScopedSettings as xi, reviewTransportBytes as xn, preparationPermissionSchema as xr, normalizeSensitiveTerms as xt, preparationScope as y, approvedTreatmentSchema as yi, reviewRepairInputSchema as yn, emptyPreparationFacts as yr, emptyTermsRevision as yt, agentSetupKey as z, applyClassification as zn, nativeGetSchema as zr, classificationCommitSchema as zt };