@gitroll/tacitt-cli 0.6.9 → 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 (57) hide show
  1. package/README.md +43 -4
  2. package/dist/{agent-setup-CODlN6zl.mjs → agent-setup-DdfF3zch.mjs} +2 -2
  3. package/dist/agent-setup-worker.mjs +5 -5
  4. package/dist/cli.mjs +35 -12
  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-Dj5Zs6UW.mjs → frame-Bq2zRh-i.mjs} +1 -1
  9. package/dist/{heap-Dt3-d27v.mjs → heap-CV8Eca8V.mjs} +217 -13
  10. package/dist/inspection-worker.mjs +4 -4
  11. package/dist/{job-BQXssW46.mjs → job-BKsifhyv.mjs} +38 -18
  12. package/dist/{locate-BWkblijS.mjs → locate-Cx52Zqxv.mjs} +204 -20
  13. package/dist/{model-CSx2dP_P.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-CfhcS4gd.mjs → query-AMZerUh8.mjs} +712 -215
  18. package/dist/{run-xlOaKCk8.mjs → run-CaPktPhH.mjs} +4 -4
  19. package/dist/{src-DzKuzGVH.mjs → src-D1iONla3.mjs} +2215 -1613
  20. package/dist/{src-V9GHLZQs.mjs → src-DlVbGB32.mjs} +74 -37
  21. package/dist/{src-BgsEnMsy.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/INDEX.md +6 -6
  28. package/skills/tacitt/SKILL.md +11 -11
  29. package/skills/tacitt/references/INDEX.md +3 -3
  30. package/skills/tacitt/references/b-method.md +2 -2
  31. package/skills/tacitt/references/bootstrap.md +2 -2
  32. package/skills/tacitt/references/classification.md +15 -5
  33. package/skills/tacitt/references/commands.md +37 -30
  34. package/skills/tacitt/references/journey.md +14 -6
  35. package/skills/tacitt/references/parallel-review.md +27 -33
  36. package/skills/tacitt/references/send.md +35 -9
  37. package/skills/tacitt/scripts/INDEX.md +37 -37
  38. package/skills/tacitt/scripts/answer.mjs +73 -15
  39. package/skills/tacitt/scripts/journey-classification-metadata.mjs +75 -26
  40. package/skills/tacitt/scripts/journey-classification-progress.mjs +1 -2
  41. package/skills/tacitt/scripts/journey-classification-validation.d.mts +13 -0
  42. package/skills/tacitt/scripts/journey-classification-validation.mjs +38 -0
  43. package/skills/tacitt/scripts/journey-classification.mjs +6 -1
  44. package/skills/tacitt/scripts/journey-consolidation.mjs +1 -0
  45. package/skills/tacitt/scripts/journey-scan-timing.d.mts +1 -0
  46. package/skills/tacitt/scripts/journey-scan-timing.mjs +19 -4
  47. package/skills/tacitt/scripts/journey-state.d.mts +15 -1
  48. package/skills/tacitt/scripts/journey-state.mjs +104 -5
  49. package/skills/tacitt/scripts/journey-tasks.d.mts +1 -0
  50. package/skills/tacitt/scripts/journey-tasks.mjs +13 -5
  51. package/skills/tacitt/scripts/journey.d.mts +46 -28
  52. package/skills/tacitt/scripts/journey.mjs +57 -8
  53. package/skills/tacitt/scripts/privacy-validator.mjs +178 -12
  54. package/skills/tacitt/scripts/review.d.mts +7 -6
  55. package/skills/tacitt/scripts/review.mjs +576 -147
  56. package/dist/web/assets/index-9UfCT_2C.js +0 -174
  57. 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-BWkblijS.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";
@@ -14,6 +14,13 @@ const processingPlanSchema = z.unknown().transform((value, context) => {
14
14
  return z.NEVER;
15
15
  }
16
16
  });
17
+ //#endregion
18
+ //#region packages/application/src/processing/settings/data.ts
19
+ /**
20
+ * Sessions in one frozen configuration. Packaging admits at most 60,000 source parts per run
21
+ * (the artifact's entry bound) and every selected session has a part, so this matches it.
22
+ */
23
+ const maximumConfigurationSessions = 6e4;
17
24
  var SettingsError = class extends Error {
18
25
  code;
19
26
  field;
@@ -433,7 +440,7 @@ const staticPresets = Object.freeze([
433
440
  /** Browser-safe fixed metadata and definitions; no recognizers, native sources or session state. */
434
441
  const staticCatalogRef = Object.freeze({
435
442
  id: "static-catalog",
436
- version: "11"
443
+ version: "12"
437
444
  });
438
445
  //#endregion
439
446
  //#region packages/application/src/processing/settings/catalog.ts
@@ -1214,12 +1221,30 @@ function isClassifying(state) {
1214
1221
  * not skipped. The Skill helpers mirror this rule in `journey-state.mjs`.
1215
1222
  */
1216
1223
  function isSkippedWork(work) {
1217
- 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;
1218
1225
  }
1219
1226
  /** A selected Work that is not skipped (ADR 0236). */
1220
1227
  function isIncludedWork(work) {
1221
1228
  return isSelectedWork(work) && !isSkippedWork(work);
1222
1229
  }
1230
+ /**
1231
+ * ADR 0274: Public needs a recorded choice: the person's panel click or named conversation
1232
+ * answer (origin `explicit`), or, under ADR 0275, the account proof that every root was already
1233
+ * Public (origin and channel `account`). A Public Work stored before `accessChoice` existed is
1234
+ * still pending. Mirrored by `phaseOf` in the Skill's `journey-state.mjs` and by the panel's
1235
+ * `WorkAccess`.
1236
+ */
1237
+ function hasPublicChoice(work) {
1238
+ if (work.access !== "public") return true;
1239
+ if (!work.accessConfirmed || !work.accessChoice) return false;
1240
+ return work.accessChoice.channel === "account" ? work.accessOrigin === "account" : work.accessOrigin === "explicit";
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
+ }
1223
1248
  function selectedWorks(state) {
1224
1249
  return state.works.filter(isSelectedWork);
1225
1250
  }
@@ -1235,9 +1260,13 @@ function hasCertifiedNoBatchReview(work, snapshotId) {
1235
1260
  const roots = proofs.flatMap((proof) => proof.rootSessionIds);
1236
1261
  return roots.length === work.rootSessionIds.length && new Set(roots).size === roots.length && work.rootSessionIds.every((id) => roots.includes(id));
1237
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
+ }
1238
1267
  /** Reading completes over included Works once the person has confirmed the scan scope. */
1239
1268
  function isReadingComplete(state) {
1240
- 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);
1241
1270
  }
1242
1271
  /** Recompute completion after a selection or reading change without leaving a paused/send phase. */
1243
1272
  function withReadingProgress(state) {
@@ -1466,6 +1495,26 @@ const sendItemSchema = z.object({
1466
1495
  title: z.string(),
1467
1496
  access
1468
1497
  }).strict().optional(),
1498
+ /** Safe preflight facts; local requested-access conflicts have no account owner. */
1499
+ accessConflict: z.object({
1500
+ sessionKey: journeyDigestSchema,
1501
+ requestedAccess: access,
1502
+ accountWorkId: journeyIdSchema.nullable(),
1503
+ accountAccess: z.enum([
1504
+ "public",
1505
+ "private",
1506
+ "removed",
1507
+ "mixed"
1508
+ ]).nullable(),
1509
+ rule: z.enum([
1510
+ "held-elsewhere",
1511
+ "destination-access",
1512
+ "requested-access",
1513
+ "content-access"
1514
+ ])
1515
+ }).strict().optional(),
1516
+ /** Distinct conflicting session keys across all rules and account holders. */
1517
+ accessConflictSessionCount: count$13.positive().optional(),
1469
1518
  /** Membership readback retained for visible conflict recovery. */
1470
1519
  membershipConflict: z.object({
1471
1520
  revision: count$13,
@@ -1721,6 +1770,16 @@ const journeyPropagateActionSchema = z.object({
1721
1770
  //#region packages/application/src/journey/scope/model.ts
1722
1771
  const count$11 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
1723
1772
  const safeText = z.string().trim().min(1).max(240);
1773
+ /**
1774
+ * One Work's stored account access (ADR 0274 D3(b), refined by ADR 0275): `public-all` when
1775
+ * every root key is held Public, `public` when the held keys are Public and other roots are new.
1776
+ */
1777
+ const accountWorkAccessSchema = z.enum([
1778
+ "private",
1779
+ "public",
1780
+ "public-all",
1781
+ "mixed"
1782
+ ]);
1724
1783
  const journeyScopeConfirmActionSchema = z.object({ kind: z.literal("scope-confirm") }).strict();
1725
1784
  const journeyWorkTypeActionSchema = z.object({
1726
1785
  kind: z.literal("work-type"),
@@ -1730,7 +1789,11 @@ const journeyWorkTypeActionSchema = z.object({
1730
1789
  const journeyWorksAccessActionSchema = z.object({
1731
1790
  kind: z.literal("works-access"),
1732
1791
  workIds: z.array(journeyIdSchema).min(1).max(1e4),
1733
- access: journeyAccessSchema
1792
+ access: journeyAccessSchema,
1793
+ accessChoice: z.object({
1794
+ channel: z.literal("conversation"),
1795
+ at: z.iso.datetime()
1796
+ }).strict().optional()
1734
1797
  }).strict();
1735
1798
  /** Static-only Work facts for proposing project types and scan scope; no source bodies. */
1736
1799
  const journeyCatalogQuerySchema = z.object({
@@ -1857,7 +1920,23 @@ const nativeStartSchema = z.object({
1857
1920
  period: sessionPeriodSchema.optional()
1858
1921
  }).strict();
1859
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
+ });
1860
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,
1861
1940
  journeySplitActionSchema,
1862
1941
  journeyMergeActionSchema,
1863
1942
  journeyPropagateActionSchema,
@@ -1870,6 +1949,7 @@ const journeyActionSchema = z.discriminatedUnion("kind", [
1870
1949
  kind: z.literal("consent"),
1871
1950
  allow: z.boolean()
1872
1951
  }).strict(),
1952
+ z.object({ kind: z.literal("account-read") }).strict(),
1873
1953
  z.object({ kind: z.literal("abandon") }).strict(),
1874
1954
  z.object({
1875
1955
  kind: z.literal("delegate"),
@@ -1930,7 +2010,11 @@ const journeyActionSchema = z.discriminatedUnion("kind", [
1930
2010
  z.object({
1931
2011
  kind: z.literal("work-access"),
1932
2012
  workId: journeyIdSchema,
1933
- access: journeyAccessSchema
2013
+ access: journeyAccessSchema,
2014
+ accessChoice: z.object({
2015
+ channel: z.literal("conversation"),
2016
+ at: z.iso.datetime()
2017
+ }).strict().optional()
1934
2018
  }).strict(),
1935
2019
  z.object({
1936
2020
  kind: z.literal("work-title"),
@@ -2047,15 +2131,24 @@ function privacyDecisionDigest(state) {
2047
2131
  return digest;
2048
2132
  }
2049
2133
  function assertJourneyScanned(state) {
2050
- 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");
2135
+ }
2136
+ /** Legacy model, delegate or unrecorded confirmation is not the person's Public choice. */
2137
+ function assertPublicChoices(state) {
2138
+ if (state.works.some((work) => isIncludedWork(work) && !hasPublicChoice(work))) throw new ApplicationError("SCOPE_NOT_READY");
2051
2139
  }
2052
2140
  /** Track B (ADR 0172): preview and packaging need every selected Work's static base. */
2053
2141
  function assertJourneyStaticReady(state) {
2054
2142
  if (state.works.some((work) => work.access !== "drop" && work.rootSessionIds.length > 0 && !isSkippedWork(work) && !hasCurrentStaticReview(state, work))) throw new ApplicationError("SCOPE_NOT_READY");
2055
2143
  }
2056
- /** Cards need only Track A, AI review and metadata; this final gate also needs Track B. */
2057
- function assertJourneyPrivacyReady(state, packaging = false) {
2144
+ /**
2145
+ * Cards need only Track A, AI review and metadata; this final gate also needs Track B.
2146
+ * `publicChoices` defaults to `packaging`: the Send click checks it before packaging, and the
2147
+ * follow-up of an already uploaded Send skips it because nothing more goes out (ADR 0274).
2148
+ */
2149
+ function assertJourneyPrivacyReady(state, packaging = false, { publicChoices = packaging } = {}) {
2058
2150
  assertJourneyScanned(state);
2151
+ if (publicChoices) assertPublicChoices(state);
2059
2152
  if (activePrivacyTerms(state).some((term) => term.disposition === "pending") || state.privacy.cards.length > 0 && (state.privacy.cards.some((card) => !card.answer) || state.privacy.confirmedAnswersRevision !== state.privacy.answersRevision)) throw new ApplicationError("CONFIRMATION_REQUIRED");
2060
2153
  assertJourneyStaticReady(state);
2061
2154
  if (packaging && state.privacy.packageApproval !== privacyDecisionDigest(state)) throw new ApplicationError("CONFIRMATION_REQUIRED");
@@ -2085,15 +2178,23 @@ function appendDecisionCards(state, input) {
2085
2178
  const open = new Set(activePrivacyTerms(state).filter((term) => term.disposition === "pending").map(privacyTermKey).filter((key) => !coveredBefore.has(key)));
2086
2179
  const extend = input.extend ?? [];
2087
2180
  const covered = [...input.cards.flatMap((card) => card.termKeys), ...extend.flatMap((item) => item.termKeys)];
2088
- if (!covered.length || state.privacy.cards.length + input.cards.length > 20 || new Set(input.cards.map((card) => card.id)).size !== input.cards.length || input.cards.some((card) => existing.has(card.id)) || new Set(extend.map((item) => item.cardId)).size !== extend.length || extend.some((item) => !existing.has(item.cardId)) || new Set(covered).size !== covered.length || covered.some((key) => !open.has(key))) throw new ApplicationError("INVALID_SELECTION");
2181
+ if (!covered.length) throw new ApplicationError("INVALID_SELECTION", "cards.append.empty");
2182
+ if (state.privacy.cards.length + input.cards.length > 20) throw new ApplicationError("INVALID_SELECTION", "cards.append.limit");
2183
+ if (new Set(input.cards.map((card) => card.id)).size !== input.cards.length) throw new ApplicationError("INVALID_SELECTION", "cards.append.duplicate-card");
2184
+ if (input.cards.some((card) => existing.has(card.id))) throw new ApplicationError("INVALID_SELECTION", "cards.append.existing-card");
2185
+ if (new Set(extend.map((item) => item.cardId)).size !== extend.length) throw new ApplicationError("INVALID_SELECTION", "cards.append.duplicate-extension");
2186
+ if (extend.some((item) => !existing.has(item.cardId))) throw new ApplicationError("INVALID_SELECTION", "cards.append.unknown-card");
2187
+ if (new Set(covered).size !== covered.length) throw new ApplicationError("INVALID_SELECTION", "cards.append.duplicate-term");
2188
+ if (covered.some((key) => !open.has(key))) throw new ApplicationError("INVALID_SELECTION", "cards.append.term-not-open");
2089
2189
  const terms = new Map(state.privacy.terms.map((term) => [privacyTermKey(term), term]));
2090
2190
  const ownValues = ownIdentityValues(state);
2091
2191
  for (const extension of extend) {
2092
2192
  const ownCard = !!state.privacy.cards.find((item) => item.id === extension.cardId)?.termKeys.some((key) => terms.get(key)?.type === "own-identity");
2093
- if (extension.termKeys.some((key) => {
2193
+ for (const key of extension.termKeys) {
2094
2194
  const term = terms.get(key);
2095
- return term?.type === "own-identity" ? !ownCard : term?.type === "people" && ownCard && !ownValues.has(identityValue(term.value));
2096
- })) throw new ApplicationError("INVALID_SELECTION");
2195
+ if (term?.type === "own-identity" && !ownCard) throw new ApplicationError("INVALID_SELECTION", "cards.append.own-identity-card");
2196
+ if (term?.type === "people" && ownCard && !ownValues.has(identityValue(term.value))) throw new ApplicationError("INVALID_SELECTION", "cards.append.other-person");
2197
+ }
2097
2198
  }
2098
2199
  const additions = new Map(extend.map((item) => [item.cardId, item.termKeys]));
2099
2200
  const inherited = new Map(extend.flatMap((item) => item.termKeys.map((key) => [key, item.cardId])));
@@ -2303,6 +2404,7 @@ function respondPrivacy(state, action, source) {
2303
2404
  };
2304
2405
  }
2305
2406
  if (action.kind === "package-confirm") {
2407
+ assertPublicChoices(state);
2306
2408
  assertJourneyPrivacyReady(state);
2307
2409
  return {
2308
2410
  ...state,
@@ -2356,7 +2458,7 @@ const journeyRenderSchema = z.object({
2356
2458
  /** One Work, or several in one call where each Work renders or fails on its own. */
2357
2459
  const journeyRenderInputSchema = z.union([journeyRenderSchema, z.object({
2358
2460
  journeyId: journeyIdSchema,
2359
- workIds: z.array(journeyIdSchema).min(1).max(256)
2461
+ workIds: z.array(journeyIdSchema).min(1).max(maximumConfigurationSessions)
2360
2462
  }).strict()]);
2361
2463
  const journeyPreviewSchema = journeyRenderSchema.extend({ cursor: z.object({
2362
2464
  sha256: journeyDigestSchema,
@@ -2531,6 +2633,45 @@ const preparationSettlementSchema = z.object({
2531
2633
  ...binding,
2532
2634
  facts: preparationFactsSchema
2533
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();
2534
2675
  function emptyPreparationFacts() {
2535
2676
  return {
2536
2677
  stage: "capture",
@@ -2557,1335 +2698,1727 @@ function validatePreparationFacts(start, facts) {
2557
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");
2558
2699
  }
2559
2700
  //#endregion
2560
- //#region packages/application/src/journey/inventory/schema.ts
2561
- const journeyInventoryQuerySchema = z.object({ journeyId: journeyIdSchema }).strict();
2562
- /** Trusted host binding, never accepted by the public command schema. */
2563
- const journeyInventoryReadSchema = journeyInventoryQuerySchema.extend({ expectedDestination: z.string().min(1).optional() }).strict();
2564
- const discoveryWarningCodeSchema = z.enum([
2565
- "UNSUPPORTED_SOURCE_ENCODING",
2566
- "SKIPPED_NONREGULAR_SOURCE",
2567
- "WORKFLOW_JOURNAL_EXCLUDED",
2568
- "AUTOMATIC_HELPER_EXCLUDED",
2569
- "UNRESOLVED_WORKERS_EXCLUDED",
2570
- "METADATA_LIMIT",
2571
- "METADATA_DEPTH_LIMIT",
2572
- "INCOMPLETE_DISCOVERY",
2573
- "DISCOVERY_ENTRY_LIMIT",
2574
- "DISCOVERY_DEPTH_LIMIT",
2575
- "BROKEN_SOURCE_LINK"
2576
- ]);
2577
- const evidence = z.object({
2578
- known: preparationCountSchema,
2579
- total: preparationCountSchema.nullable()
2580
- }).strict().refine((value) => value.total === null || value.total === value.known);
2581
- 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,
2582
2713
  workId: journeyIdSchema,
2583
- inputRevision: preparationCountSchema.min(1),
2584
- observation: z.enum([
2585
- "unavailable",
2586
- "incomplete",
2587
- "settled"
2588
- ]),
2589
- stage: preparationStageSchema.nullable(),
2590
- sourceParts: z.object({
2591
- unsupported: evidence,
2592
- resourceLimited: evidence,
2593
- excluded: evidence,
2594
- unclassified: preparationCountSchema
2595
- }).strict(),
2596
- emptyReviewRoots: evidence,
2597
- 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)
2598
2720
  }).strict();
2599
- const journeyInventorySchema = z.object({
2721
+ const reviewCorrectSchema = z.object({
2600
2722
  journeyId: journeyIdSchema,
2601
- revision: preparationCountSchema,
2602
- snapshotId: journeyDigestSchema.nullable(),
2603
- frozen: z.object({
2604
- retainedSessions: preparationCountSchema,
2605
- reviewRoots: preparationCountSchema,
2606
- identifiedMainRoots: preparationCountSchema,
2607
- unknownRoleRoots: preparationCountSchema,
2608
- attachedSubagents: preparationCountSchema,
2609
- unresolvedWorkerSessions: preparationCountSchema,
2610
- sourceParts: preparationCountSchema,
2611
- frozenSourceBytes: preparationCountSchema,
2612
- discoveryUnsupportedInputs: z.null(),
2613
- discoverySkippedInputs: z.null(),
2614
- discoverySkippedSourceBytes: z.null(),
2615
- warningOccurrences: z.array(z.object({
2616
- code: discoveryWarningCodeSchema,
2617
- occurrences: preparationCountSchema
2618
- }).strict())
2619
- }).strict().nullable(),
2620
- captures: z.object({
2621
- targetParts: preparationCountSchema,
2622
- capturedParts: preparationCountSchema,
2623
- capturedBytes: preparationCountSchema,
2624
- completeCapturedBytes: preparationCountSchema.nullable()
2625
- }).strict().nullable(),
2626
- workScope: z.object({
2627
- total: preparationCountSchema,
2628
- byStatus: z.object({
2629
- pending: preparationCountSchema,
2630
- reading: preparationCountSchema,
2631
- review: preparationCountSchema,
2632
- ready: preparationCountSchema,
2633
- blocked: preparationCountSchema
2634
- }).strict(),
2635
- byAccess: z.object({
2636
- public: preparationCountSchema,
2637
- private: preparationCountSchema,
2638
- drop: preparationCountSchema
2639
- }).strict(),
2640
- assignedReviewRoots: preparationCountSchema,
2641
- unassignedReviewRoots: preparationCountSchema.nullable(),
2642
- excludedAutomationRoots: preparationCountSchema.nullable(),
2643
- unassignedOtherRoots: preparationCountSchema.nullable()
2644
- }).strict(),
2645
- preparation: z.array(workPreparationObservationSchema)
2723
+ expectedRevision: z.number().int().nonnegative(),
2724
+ idempotencyKey: journeyIdSchema,
2725
+ report: correctionReportSchema
2646
2726
  }).strict();
2647
- /** Panel preparation counts over the selected Works: opaque Work IDs, parts and bytes only. */
2648
- const journeyPreparationProgressSchema = z.object({
2649
- currentWorkIds: z.array(journeyIdSchema),
2650
- targetParts: preparationCountSchema,
2651
- capturedParts: preparationCountSchema,
2652
- targetBytes: preparationCountSchema,
2653
- 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
2654
2753
  }).strict();
2655
2754
  //#endregion
2656
- //#region packages/application/src/source/resolve.ts
2657
- /** Queue-based ancestry resolution cannot leak deep descendants through a recursion cutoff. */
2658
- function resolveParts(inventory, route) {
2659
- const nodes = sourceGraph(inventory);
2660
- const families = reviewFamilies(inventory, nodes);
2661
- const resolved = /* @__PURE__ */ new Map();
2662
- const depths = /* @__PURE__ */ new Map();
2663
- const dependents = /* @__PURE__ */ new Map();
2664
- const remaining = /* @__PURE__ */ new Map();
2665
- const queue = [];
2666
- const finish = (node, included, unresolved, reason, depth = 0) => {
2667
- resolved.set(node.key, {
2668
- sessionId: node.session.id,
2669
- partId: node.part?.id ?? "",
2670
- included,
2671
- unresolved,
2672
- reason
2673
- });
2674
- depths.set(node.key, depth);
2675
- queue.push(node);
2676
- };
2677
- const pending = (node, reason) => finish(node, route === "original", true, reason);
2678
- for (const node of nodes) {
2679
- const { identity, reason } = node.observation;
2680
- if (families.unresolved.has(node.session.id)) finish(node, false, node.ancestry !== "missing", node.ancestry === "missing" ? "missing-parent" : "ancestry-unresolved");
2681
- 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");
2682
- else if (reason !== null) pending(node, reason);
2683
- else if (identity?.role === "guardian") finish(node, false, false, "guardian-excluded");
2684
- else if (identity?.role !== "worker") finish(node, true, false, null);
2685
- else if (node.ancestry === "missing") finish(node, false, false, "missing-parent");
2686
- else if (node.ancestry !== "parents") finish(node, false, true, "ancestry-unresolved");
2687
- else {
2688
- remaining.set(node.key, node.parents.length);
2689
- for (const parent of node.parents) {
2690
- const children = dependents.get(parent.key) ?? [];
2691
- children.push(node);
2692
- dependents.set(parent.key, children);
2693
- }
2694
- }
2695
- }
2696
- for (let cursor = 0; cursor < queue.length; cursor++) {
2697
- const parent = queue[cursor];
2698
- if (!parent) continue;
2699
- const value = resolved.get(parent.key);
2700
- for (const node of dependents.get(parent.key) ?? []) {
2701
- if (resolved.has(node.key)) continue;
2702
- const outstanding = (remaining.get(node.key) ?? 1) - 1;
2703
- remaining.set(node.key, outstanding);
2704
- const depth = (depths.get(parent.key) ?? 0) + 1;
2705
- if (value?.included && !value.unresolved && (route === "original" || depth <= 256)) {
2706
- finish(node, true, false, null, depth);
2707
- continue;
2708
- }
2709
- if (outstanding > 0) continue;
2710
- const results = node.parents.map((entry) => resolved.get(entry.key));
2711
- if (results.every((entry) => entry && [
2712
- "not-selected",
2713
- "workspace-not-selected",
2714
- "deselected-parent"
2715
- ].includes(entry.reason ?? ""))) finish(node, false, false, "deselected-parent");
2716
- else if (results.every((entry) => entry?.reason === "missing-parent" || entry?.reason === "deselected-parent")) finish(node, false, false, "missing-parent");
2717
- else finish(node, false, true, "ancestry-unresolved");
2718
- }
2719
- }
2720
- for (const node of nodes) if (!resolved.has(node.key)) finish(node, false, true, "ancestry-unresolved");
2721
- 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);
2722
2758
  }
2723
- //#endregion
2724
- //#region packages/application/src/source/index.ts
2725
- function resolveSourceSelection(inventory, route = "original") {
2726
- const parts = resolveParts(inventory, route);
2727
- const sessions = /* @__PURE__ */ new Map();
2728
- const families = reviewFamilies(inventory);
2729
- const includedWorkspaces = /* @__PURE__ */ new Set();
2730
- const counts = {
2731
- workspaces: 0,
2732
- sessions: 0,
2733
- parts: 0,
2734
- sourceBytes: 0
2735
- };
2736
- const coverage = {
2737
- includedSessions: 0,
2738
- excludedSessions: 0,
2739
- unresolvedSessions: 0,
2740
- unresolvedParts: 0
2741
- };
2742
- for (const session of inventory.sessions) {
2743
- const decisions = session.parts.flatMap((part) => {
2744
- const value = parts.get(sourceKey(session.id, part.id));
2745
- return value ? [{
2746
- part,
2747
- value
2748
- }] : [];
2749
- });
2750
- const included = decisions.filter(({ value }) => value.included);
2751
- const unresolved = decisions.filter(({ value }) => value.unresolved);
2752
- const excluded = decisions.filter(({ value }) => !value.included && !value.unresolved);
2753
- const selected = familySelected(inventory, families, session);
2754
- const missing = selected && session.parts.length === 0;
2755
- const reasons = [...new Set(decisions.flatMap(({ value }) => value.reason ? [value.reason] : []))].sort();
2756
- if (missing) reasons.push("unreadable-source");
2757
- const summary = Object.freeze({
2758
- status: unresolved.length || missing ? "unresolved" : included.length ? excluded.length ? "partial" : "included" : "excluded",
2759
- includedParts: included.length,
2760
- excludedParts: excluded.length,
2761
- unresolvedParts: unresolved.length,
2762
- sourceBytes: included.reduce((sum, { part }) => sum + part.snapshot.size, 0),
2763
- reasons: Object.freeze(reasons)
2764
- });
2765
- sessions.set(session.id, summary);
2766
- if (!selected) continue;
2767
- if (summary.includedParts) {
2768
- counts.sessions++;
2769
- counts.parts += summary.includedParts;
2770
- counts.sourceBytes += summary.sourceBytes;
2771
- includedWorkspaces.add(session.workspaceId);
2772
- coverage.includedSessions++;
2773
- }
2774
- if (summary.status === "excluded") coverage.excludedSessions++;
2775
- if (summary.status === "unresolved") coverage.unresolvedSessions++;
2776
- coverage.unresolvedParts += summary.unresolvedParts;
2777
- }
2778
- counts.workspaces = includedWorkspaces.size;
2779
- return Object.freeze({
2780
- counts: Object.freeze(counts),
2781
- coverage: Object.freeze(coverage),
2782
- sessions,
2783
- session: (sessionId) => {
2784
- const value = sessions.get(sessionId);
2785
- if (!value) throw new ApplicationError("UNKNOWN_ITEM");
2786
- return value;
2787
- },
2788
- part: (sessionId, partId) => parts.get(sourceKey(sessionId, partId))
2789
- });
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;
2790
2771
  }
2791
2772
  //#endregion
2792
- //#region packages/application/src/journey/scan-exclusion.ts
2793
- /** Manual source-policy proof; never an assertion that unknown content was scanned. */
2794
- const scanExclusionResourceSchema = z.object({
2795
- kind: z.literal("scan-exclusion"),
2796
- proofVersion: z.literal(1),
2797
- journeyId: journeyIdSchema,
2798
- workId: journeyIdSchema,
2799
- inputRevision: z.number().int().positive(),
2800
- snapshotId: journeyDigestSchema,
2801
- configurationDigest: journeyDigestSchema,
2802
- rootSessionId: z.string().min(1),
2803
- sessionId: z.string().min(1),
2804
- sourceId: z.string().min(1),
2805
- sourcePartId: z.string().min(1),
2806
- native: nativeSourceIdentitySchema,
2807
- adapterVersion: z.string().min(1),
2808
- reason: z.enum([
2809
- "guardian-excluded",
2810
- "missing-parent",
2811
- "deselected-parent",
2812
- "ancestry-unresolved",
2813
- "non-interactive-launch"
2814
- ]),
2815
- facts: z.object({
2816
- representation: z.enum(["jsonl", "jsonl.zst"]),
2817
- byteLength: z.number().int().nonnegative(),
2818
- sha256: journeyDigestSchema
2819
- }).strict()
2820
- }).strict();
2821
- /** Validate policy against frozen identity evidence, never a caller-asserted source role. */
2822
- function validateScanExclusion(resource, state, inventory, capture) {
2823
- const work = state.works.find((item) => item.id === resource.workId);
2824
- if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
2825
- if (!work || state.scope.status !== "confirmed" || work.access === "drop" || state.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
2826
- if (state.journeyId !== resource.journeyId || work.inputRevision !== resource.inputRevision || state.snapshotId !== resource.snapshotId || !work.rootSessionIds.includes(resource.rootSessionId)) throw new ApplicationError("REVISION_CONFLICT");
2827
- const families = reviewFamilies(inventory);
2828
- const part = (families.members.get(resource.rootSessionId)?.find((item) => item.id === resource.sessionId))?.parts.find((item) => item.id === resource.sourcePartId);
2829
- const evidence = capture.source.nativeEvidence;
2830
- const launch = resource.reason === "non-interactive-launch";
2831
- const ancestry = resource.reason === "ancestry-unresolved";
2832
- if (ancestry && resource.native.role !== "worker") throw new ApplicationError("CORRUPTED_STATE");
2833
- const prefixRoot = launch && resource.rootSessionId === resource.sessionId && resource.native.role === "main" && evidence?.recognition === "unknown" && evidence.completeness === "prefix" && JSON.stringify(evidence.identity) === JSON.stringify({
2834
- ...resource.native,
2835
- role: "unknown"
2836
- });
2837
- 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");
2838
- const ids = new Set(work.rootSessionIds.flatMap((id) => (families.members.get(id) ?? []).map((item) => item.id)));
2839
- const sessions = inventory.sessions.filter((item) => ids.has(item.id));
2840
- const workspaceIds = new Set(sessions.map((item) => item.workspaceId));
2841
- const decision = resolveSourceSelection({
2842
- ...inventory,
2843
- sessions,
2844
- workspaces: inventory.workspaces.filter((item) => workspaceIds.has(item.id))
2845
- }, "configured").part(resource.sessionId, resource.sourcePartId);
2846
- 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");
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));
2784
+ }
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
+ };
2847
2800
  }
2848
2801
  //#endregion
2849
- //#region packages/application/src/journey/scan-git-seeds.ts
2850
- /**
2851
- * Owner-only Git detector input fixed once per frozen snapshot and canonical root set.
2852
- * Later Git activity cannot change or invalidate a scan of the same frozen sources.
2853
- */
2854
- const scanGitSeedsResourceSchema = z.object({
2855
- kind: z.literal("git-seeds"),
2856
- journeyId: journeyIdSchema,
2857
- snapshotId: journeyDigestSchema,
2858
- roots: z.array(z.string()),
2859
- seeds: z.object({
2860
- names: z.array(z.string().min(1)),
2861
- snapshots: z.array(z.object({
2862
- id: z.string().min(1),
2863
- sha256: z.string()
2864
- }).strict())
2865
- }).strict(),
2866
- sha256: journeyDigestSchema
2867
- }).strict();
2868
- /** Storage key; equal root sets in any order share one receipt. */
2869
- const scanGitSeedsId = (snapshotId, roots) => `${snapshotId}:${textDigest(JSON.stringify([...new Set(roots)].sort()))}`;
2870
- function scanGitSeedsResource(journeyId, snapshotId, roots, seeds) {
2871
- const value = {
2872
- names: [...seeds.names],
2873
- 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"
2874
2847
  };
2875
2848
  return {
2876
- kind: "git-seeds",
2877
- journeyId,
2878
- snapshotId,
2879
- roots: [...new Set(roots)].sort(),
2880
- seeds: value,
2881
- 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"
2882
2854
  };
2883
2855
  }
2884
- /** A missing, tampered or foreign receipt is corrupted state, never a cue to reread live Git. */
2885
- function verifiedScanGitSeeds(resource, snapshotId, roots) {
2886
- if (resource.snapshotId !== snapshotId || scanGitSeedsId(resource.snapshotId, resource.roots) !== scanGitSeedsId(snapshotId, roots) || textDigest(JSON.stringify(resource.seeds)) !== resource.sha256) throw new ApplicationError("CORRUPTED_STATE");
2887
- return resource.seeds;
2888
- }
2889
- //#endregion
2890
- //#region packages/application/src/journey/scan-session.ts
2891
- const count$9 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
2892
- const scanFileSchema = z.object({
2893
- path: z.string().min(1),
2894
- identity: fileIdentitySchema,
2895
- sha256: journeyDigestSchema,
2896
- byteLength: count$9
2897
- }).strict();
2898
2856
  /**
2899
- * Track A source-bound prose (`file`) with built-in static protection spans; no delivery archive
2900
- * exists during scanning (ADR 0172). Obsolete version 3 inputs with detector evidence are refused.
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.
2901
2859
  */
2902
- const sessionScanInputSchema = z.object({
2903
- version: z.literal(4),
2904
- sessionId: z.string().min(1),
2905
- file: scanFileSchema,
2906
- sources: z.array(z.object({
2907
- sourceId: z.string().min(1),
2908
- sourcePartId: z.string().min(1),
2909
- native: nativeSourceIdentitySchema,
2910
- facts: z.object({
2911
- representation: z.enum(["jsonl", "jsonl.zst"]),
2912
- byteLength: count$9,
2913
- sha256: journeyDigestSchema
2914
- }).strict()
2915
- }).strict()),
2916
- recordCount: count$9,
2917
- fieldCount: count$9,
2918
- inputUtf16: count$9,
2919
- mainReviewVerified: z.literal(true),
2920
- emptyReviewVerified: z.boolean()
2921
- }).strict().refine((input) => new Set(input.sources.map((source) => source.sourceId)).size === input.sources.length);
2922
- const scanSessionResourceSchema = z.object({
2923
- kind: z.literal("scan-session"),
2924
- journeyId: journeyIdSchema,
2925
- workId: journeyIdSchema,
2926
- inputRevision: count$9.min(1),
2927
- snapshotId: journeyDigestSchema,
2928
- configurationDigest: journeyDigestSchema,
2929
- rootSessionId: z.string().min(1),
2930
- input: sessionScanInputSchema
2931
- }).strict();
2932
- /** Complete scan-set evidence for certified empty main input; never an output digest. */
2933
- function scanPreparationDigest(resources) {
2934
- return textDigest(JSON.stringify(resources.map((resource) => resource.kind === "scan-exclusion" ? resource : [
2935
- resource.input.sessionId,
2936
- resource.rootSessionId,
2937
- resource.configurationDigest,
2938
- resource.input.file.sha256,
2939
- resource.input.sources
2940
- ]).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))));
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;
2941
2877
  }
2942
- const scanBatchBinding = {
2943
- journeyId: journeyIdSchema,
2944
- workId: journeyIdSchema,
2945
- inputRevision: count$9.min(1),
2946
- snapshotId: journeyDigestSchema,
2947
- configurationDigest: journeyDigestSchema
2948
- };
2949
- const scanCanonicalResourceSchema = z.object({
2950
- ...scanBatchBinding,
2951
- kind: z.literal("scan-canonical"),
2952
- id: journeyDigestSchema,
2953
- batchId: journeyIdSchema,
2954
- batchDigest: journeyDigestSchema
2955
- }).strict();
2956
- const scanAliasResourceSchema = z.object({
2957
- ...scanBatchBinding,
2958
- kind: z.literal("scan-alias"),
2959
- id: journeyIdSchema,
2960
- canonicalBatchId: journeyIdSchema,
2961
- canonicalBatchDigest: journeyDigestSchema,
2962
- targetBatchId: journeyIdSchema,
2963
- targetBatchDigest: journeyDigestSchema,
2964
- equivalenceDigest: journeyDigestSchema
2965
- }).strict();
2966
2878
  //#endregion
2967
- //#region packages/application/src/journey/b/repair.ts
2968
- const bRepairPlanSchema = z.object({
2969
- unitId: journeyDigestSchema,
2970
- unitDigest: journeyDigestSchema,
2971
- priorDigest: journeyDigestSchema,
2972
- round: z.literal(1),
2973
- targetFields: z.array(bIdSchema).min(1).max(24),
2974
- remainingFields: z.array(bIdSchema)
2975
- }).strict();
2976
- const bReviewDigest = (submission) => bDigest(JSON.stringify(bReviewSubmissionSchema.parse(submission)));
2977
- const repairMeaning = (part) => JSON.stringify([part.category, part.classification]);
2978
- function planBRepair(unit, review, completedRepairs = 0) {
2979
- if (completedRepairs !== 0) throw new ApplicationError("INVALID_OPERATION");
2980
- const validated = validateBReview(unit, review.submission);
2981
- const needed = new Set(validated.issues.flatMap((issue) => issue.fieldId === null ? [] : [issue.fieldId]));
2982
- const targets = [...new Set(unit.fragments.filter((part) => part.core).map((part) => part.fieldId))].filter((fieldId) => needed.has(fieldId));
2983
- if (!targets.length) return null;
2984
- return bRepairPlanSchema.parse({
2985
- unitId: unit.id,
2986
- unitDigest: unit.digest,
2987
- priorDigest: bReviewDigest(review.submission),
2988
- round: 1,
2989
- targetFields: targets.slice(0, 24),
2990
- remainingFields: [...needed].filter((fieldId) => !targets.slice(0, 24).includes(fieldId))
2991
- });
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);
2889
+ /**
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).
2892
+ */
2893
+ function typeHides(state, type) {
2894
+ const group = typeGroup(state, type);
2895
+ return !termTypeInfo[type].asked || !group?.confirmed || group.choice !== "keep";
2992
2896
  }
2993
- function mergeBRepair(unit, previous, inputPlan, raw) {
2994
- const plan = bRepairPlanSchema.parse(inputPlan);
2995
- const expected = planBRepair(unit, previous);
2996
- if (JSON.stringify(plan) !== JSON.stringify(expected)) throw new ApplicationError("REVISION_CONFLICT");
2997
- const correction = bReviewSubmissionSchema.parse(raw);
2998
- const targeted = (fieldId) => plan.targetFields.includes(fieldId);
2999
- if ([
3000
- ...correction.terms.flatMap((term) => term.occurrences),
3001
- ...correction.retainedSourceGroups,
3002
- ...correction.representationLimits,
3003
- ...correction.uncertainOccurrences
3004
- ].some((part) => !targeted(part.fieldId))) throw new ApplicationError("INVALID_REVIEW_SPAN");
3005
- const repaired = validateBReview(unit, correction, plan.targetFields);
3006
- const prior = validateBReview(unit, previous.submission);
3007
- const correctedMeanings = /* @__PURE__ */ new Map();
3008
- for (const part of repaired.sensitive) {
3009
- const key = bAnchorKey(part);
3010
- const meanings = correctedMeanings.get(key) ?? /* @__PURE__ */ new Set();
3011
- meanings.add(repairMeaning(part));
3012
- correctedMeanings.set(key, meanings);
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
+ }
3013
2977
  }
3014
- const preservedKeys = /* @__PURE__ */ new Set();
3015
- const conflicts = [];
3016
- const preserved = prior.sensitive.filter((part) => targeted(part.fieldId)).flatMap((part) => {
3017
- const retentions = part.classification.class === "certain" ? [] : repaired.retained.filter((retained) => retained.fieldId === part.fieldId);
3018
- return subtractBRanges([part], retentions).map((range) => ({
3019
- fieldId: part.fieldId,
3020
- recordId: part.recordId,
3021
- fieldHash: part.fieldHash,
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;
3012
+ }
3013
+ //#endregion
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()];
3042
+ }
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));
3051
+ }
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 }) => ({
3022
3099
  ...range,
3023
- exactQuote: codePointSlice(part.exactQuote, range.start - part.start, range.end - part.start),
3024
- category: part.category,
3025
- classification: part.classification,
3026
- reason: "Prior sensitive obligation preserved during bounded repair."
3100
+ term: entity.value
3027
3101
  }));
3028
- }).filter((part) => {
3029
- const anchor = bAnchorKey(part), meaning = repairMeaning(part), corrected = correctedMeanings.get(anchor);
3030
- if (corrected && (corrected.size > 1 || !corrected.has(meaning))) conflicts.push(bIssue("CONFLICTING_PRIVACY_DECISIONS", part));
3031
- if (corrected?.has(meaning)) return false;
3032
- const key = JSON.stringify([anchor, meaning]);
3033
- if (preservedKeys.has(key)) return false;
3034
- preservedKeys.add(key);
3035
- return true;
3036
- });
3037
- const original = previous.submission;
3038
- const merged = {
3039
- ...original,
3040
- terms: [...original.terms.map((term) => ({
3041
- ...term,
3042
- occurrences: term.occurrences.filter((part) => !targeted(part.fieldId))
3043
- })).filter((term) => term.occurrences.length > 0), ...correction.terms],
3044
- retainedSourceGroups: [...original.retainedSourceGroups.filter((part) => !targeted(part.fieldId)), ...correction.retainedSourceGroups],
3045
- representationLimits: [
3046
- ...original.representationLimits.filter((part) => !targeted(part.fieldId)),
3047
- ...correction.representationLimits,
3048
- ...preserved
3049
- ],
3050
- uncertainOccurrences: [...original.uncertainOccurrences.filter((part) => !targeted(part.fieldId)), ...correction.uncertainOccurrences],
3051
- coverage: [...original.coverage.filter((part) => !targeted(part.fieldId)), ...correction.coverage],
3052
- sessionDecision: correction.sessionDecision
3053
3102
  };
3054
- const review = validateBReview(unit, merged);
3055
- review.issues = [...new Map([...review.issues, ...conflicts].map((issue) => [JSON.stringify(issue), issue])).values()];
3056
- return {
3057
- review,
3058
- receipt: {
3059
- ...plan,
3060
- resultDigest: bReviewDigest(merged),
3061
- priorObligationsPreserved: true,
3062
- 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(""));
3063
3159
  }
3160
+ run = [];
3161
+ edges = [];
3064
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;
3065
3178
  }
3066
- //#endregion
3067
- //#region packages/application/src/journey/review/limits.ts
3068
- /** Raw provider bytes stay bounded independently of JSON escaping and compiled findings. */
3069
- const reviewResponseBytes = 4194304;
3070
- /** Fits the escaped raw-response envelope and evidence plus compiled occurrence findings. */
3071
- const reviewTransportBytes = 33554432;
3072
- //#endregion
3073
- //#region packages/application/src/journey/review/candidate-schema.ts
3074
- const reviewIssueSchema = z.object({
3075
- code: z.string().regex(/^[A-Z_]{3,80}$/u),
3076
- fieldId: z.string().min(1).nullable(),
3077
- start: z.number().int().nonnegative().nullable(),
3078
- end: z.number().int().nonnegative().nullable()
3079
- }).strict();
3080
- const reviewCandidateInputSchema = z.object({
3081
- journeyId: journeyIdSchema,
3082
- batchId: journeyIdSchema,
3083
- batchDigest: journeyDigestSchema,
3084
- idempotencyKey: journeyIdSchema,
3085
- attemptId: journeyIdSchema,
3086
- response: z.string().min(1).refine((value) => Buffer.byteLength(value, "utf8") <= reviewResponseBytes),
3087
- repairId: journeyIdSchema.nullable(),
3088
- sessionReviewId: journeyIdSchema.nullable()
3089
- }).strict();
3090
- const reviewCandidateResourceSchema = reviewCandidateInputSchema.omit({ idempotencyKey: true }).extend({
3091
- kind: z.literal("b-candidate"),
3092
- id: journeyIdSchema,
3093
- requestHash: journeyDigestSchema,
3094
- at: z.iso.datetime(),
3095
- workId: journeyIdSchema,
3096
- inputRevision: z.number().int().positive(),
3097
- snapshotId: journeyDigestSchema,
3098
- responseDigest: journeyDigestSchema,
3099
- status: z.enum([
3100
- "valid",
3101
- "pending",
3102
- "invalid"
3103
- ]),
3104
- issues: z.array(reviewIssueSchema),
3105
- evidence: bReviewSubmissionSchema.nullable()
3106
- }).strict();
3107
- const reviewCandidateResultSchema = reviewCandidateResourceSchema.pick({
3108
- responseDigest: true,
3109
- status: true,
3110
- issues: true,
3111
- evidence: true
3112
- }).extend({ candidateId: journeyIdSchema }).strict();
3113
- const reviewRepairInputSchema = z.object({
3114
- journeyId: journeyIdSchema,
3115
- candidateId: journeyIdSchema,
3116
- idempotencyKey: journeyIdSchema
3117
- }).strict();
3118
- const reviewRepairResourceSchema = z.object({
3119
- kind: z.literal("b-repair"),
3120
- id: journeyIdSchema,
3121
- journeyId: journeyIdSchema,
3122
- workId: journeyIdSchema,
3123
- batchId: journeyIdSchema,
3124
- batchDigest: journeyDigestSchema,
3125
- inputRevision: z.number().int().positive(),
3126
- snapshotId: journeyDigestSchema,
3127
- candidateId: journeyIdSchema,
3128
- requestHash: journeyDigestSchema,
3129
- at: z.iso.datetime(),
3130
- plan: bRepairPlanSchema
3131
- }).strict();
3132
- //#endregion
3133
- //#region packages/application/src/journey/correction/model.ts
3134
- /** Original field-relative code-point coordinates, end-exclusive, exactly as B anchors. */
3135
- const correctionAnchorSchema = z.object({
3136
- fieldId: bIdSchema,
3137
- recordId: bIdSchema,
3138
- fieldHash: journeyDigestSchema,
3139
- start: bCountSchema,
3140
- end: bCountSchema,
3141
- quote: z.string().min(1).max(4096)
3142
- }).strict();
3143
- const correctionReportSchema = z.object({
3144
- id: journeyIdSchema,
3145
- workId: journeyIdSchema,
3146
- inputRevision: z.number().int().min(1),
3147
- batchId: journeyIdSchema,
3148
- groupId: journeyIdSchema,
3149
- kind: journeyKindSchema,
3150
- reason: z.string().trim().min(1).max(1e3),
3151
- anchors: z.array(correctionAnchorSchema).min(1).max(256)
3152
- }).strict();
3153
- const reviewCorrectSchema = z.object({
3154
- journeyId: journeyIdSchema,
3155
- expectedRevision: z.number().int().nonnegative(),
3156
- idempotencyKey: journeyIdSchema,
3157
- report: correctionReportSchema
3158
- }).strict();
3159
- /** Immutable private report receipt; the only place where correction quotes are retained. */
3160
- const correctionReportResourceSchema = z.object({
3161
- journeyId: journeyIdSchema,
3162
- kind: z.literal("correction-report"),
3163
- id: journeyIdSchema,
3164
- requestHash: journeyDigestSchema,
3165
- at: z.iso.datetime(),
3166
- snapshotId: journeyDigestSchema,
3167
- batchDigest: journeyDigestSchema,
3168
- decisionDigest: journeyDigestSchema,
3169
- report: correctionReportSchema
3170
- }).strict();
3171
- const correctionAuditSchema = z.object({
3172
- reportId: journeyIdSchema,
3173
- action: z.enum([
3174
- "report",
3175
- "hide",
3176
- "dismiss"
3177
- ]),
3178
- actor: z.enum([
3179
- "observer",
3180
- "human",
3181
- "delegate"
3182
- ]),
3183
- authorityId: journeyIdSchema.nullable(),
3184
- decisionDigest: journeyDigestSchema
3185
- }).strict();
3186
- //#endregion
3187
- //#region packages/application/src/journey/correction/binding.ts
3188
- function workCorrections(state, workId) {
3189
- 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;
3190
3190
  }
3191
- /** Binds rendered output and generated metadata to every correction disposition of the Work. */
3192
- function workCorrectionBinding(state, workId) {
3193
- const corrections = workCorrections(state, workId);
3194
- return corrections.length ? corrections.map(({ id, groupId, inputRevision, batchId, status, disposition }) => ({
3195
- id,
3196
- groupId,
3197
- inputRevision,
3198
- batchId,
3199
- status,
3200
- choice: disposition?.choice ?? null,
3201
- decisionDigest: disposition?.decisionDigest ?? null
3202
- })) : 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);
3203
3194
  }
3204
- //#endregion
3205
- //#region packages/application/src/journey/placement/decisions.ts
3206
- function workPrivacyDecisions(state, work) {
3207
- return state.groups.filter((group) => work.groupIds.includes(group.id)).map(({ id, key, kind, label, condition, choice, confirmed }) => ({
3208
- id,
3209
- key,
3210
- kind,
3211
- label,
3212
- condition,
3213
- choice,
3214
- confirmed
3215
- })).sort((a, b) => a.id.localeCompare(b.id));
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("");
3216
3201
  }
3217
- /** Corrections are additive under an unchanged Hide group, so they bind metadata review too. */
3218
- function workDecisionDigest(state, work) {
3219
- const decisions = workPrivacyDecisions(state, work);
3220
- const corrections = workCorrectionBinding(state, work.id);
3221
- return textDigest(JSON.stringify(corrections ? {
3222
- decisions,
3223
- corrections
3224
- } : decisions));
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;
3225
3215
  }
3226
- /** Retire generated text until it is reviewed; the processed project name keeps rows distinct. */
3227
- function retireWorkDescription(work) {
3216
+ //#endregion
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);
3228
3255
  return {
3229
- ...work,
3230
- suggestion: null
3256
+ term,
3257
+ kind,
3258
+ partial,
3259
+ rendered: confirmedHideKinds.map((item) => hideTokenDigest(key, renderedHideValue(item, term))),
3260
+ tokens: tokens && tokens.map((token) => hideTokenDigest(key, token))
3231
3261
  };
3232
3262
  }
3233
- //#endregion
3234
- //#region packages/application/src/journey/terms/groups.ts
3235
- const keyPrefix = "type:";
3236
- const termGroupKey = (type) => `${keyPrefix}${type}`;
3237
- /** RFC 9562 UUID version 5 of `name` in the `namespace` UUID. */
3238
- function uuidV5(namespace, name) {
3239
- const bytes = createHash("sha1").update(Buffer.from(namespace.replaceAll("-", ""), "hex")).update(name, "utf8").digest().subarray(0, 16);
3240
- bytes.writeUInt8(bytes.readUInt8(6) & 15 | 80, 6);
3241
- bytes.writeUInt8(bytes.readUInt8(8) & 63 | 128, 8);
3242
- const hex = bytes.toString("hex");
3243
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
3244
- }
3245
- /** Deterministic: every reader and writer of a journey names a type's group alike. */
3246
- const termGroupId = (journeyId, type) => uuidV5(journeyId, type);
3247
- /** The term type a group answers, or null for any other category. */
3248
- function termGroupType(group) {
3249
- if (!group.key.startsWith(keyPrefix)) return null;
3250
- const parsed = termTypeSchema.safeParse(group.key.slice(5));
3251
- return parsed.success ? parsed.data : null;
3252
- }
3253
- const typeGroup = (state, type) => state.groups.find((group) => termGroupType(group) === type);
3254
- function newTypeGroup(state, type, preference) {
3255
- const info = termTypeInfo[type];
3256
- const base = {
3257
- id: termGroupId(state.journeyId, type),
3258
- kind: info.kind,
3259
- key: termGroupKey(type),
3260
- label: info.label,
3261
- condition: info.condition,
3262
- count: 0,
3263
- preferenceScope: null,
3264
- representative: null
3265
- };
3266
- if (!info.asked) return {
3267
- ...base,
3268
- belief: .97,
3269
- choice: "hide",
3270
- confirmed: true,
3271
- origin: "policy"
3272
- };
3273
- if (state.questionsPaused) return {
3274
- ...base,
3275
- belief: .97,
3276
- choice: "hide",
3277
- confirmed: false,
3278
- origin: "paused"
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);
3272
+ return {
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)))
3279
3278
  };
3279
+ }
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)));
3280
3284
  return {
3281
- ...base,
3282
- belief: preference === null ? .5 : preference === "hide" ? .97 : .03,
3283
- choice: preference,
3284
- confirmed: false,
3285
- origin: preference === null ? "model" : "preference"
3285
+ scope: "additions",
3286
+ keyId: hideKeyId(key),
3287
+ values: [...unique.values()]
3286
3288
  };
3287
3289
  }
3288
3290
  /**
3289
- * One group per type with at least one dictionary value, `count` its distinct values. Existing
3290
- * 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.
3291
3293
  */
3292
- function syncTermGroups(state, counts, preference) {
3293
- const groups = state.groups.map((group) => {
3294
- const type = termGroupType(group);
3295
- return type === null ? group : {
3296
- ...group,
3297
- count: counts.get(type) ?? group.count
3298
- };
3299
- });
3300
- for (const type of termTypes) {
3301
- const count = counts.get(type) ?? 0;
3302
- if (count === 0 || groups.some((group) => termGroupType(group) === type)) continue;
3303
- groups.push({
3304
- ...newTypeGroup(state, type, preference(termGroupKey(type))),
3305
- count
3306
- });
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
+ }
3307
3321
  }
3308
- return groups;
3322
+ return {
3323
+ scope: "additions",
3324
+ keyId: hideKeyId(key),
3325
+ values: [...values.values()]
3326
+ };
3309
3327
  }
3310
- //#endregion
3311
- //#region packages/application/src/journey/review/confirmed-hides.ts
3312
- /** Kinds from lowest to highest mask priority. */
3313
- const confirmedHideKinds = [
3314
- "internal",
3315
- "identity",
3316
- "private",
3317
- "credentials"
3318
- ];
3319
- const kinds = confirmedHideKinds;
3320
- const rank$1 = (kind) => kinds.indexOf(kind);
3321
3328
  /**
3322
- * ADR 0086: a type's values are hidden unless its group is answered Keep; before an answer the
3323
- * default is Hide. Credentials and contact details are always hidden (D06).
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.
3333
+ */
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";
3341
+ }
3342
+ /**
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.
3324
3347
  */
3325
- function typeHides(state, type) {
3326
- const group = typeGroup(state, type);
3327
- return !termTypeInfo[type].asked || !group?.confirmed || group.choice !== "keep";
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
+ };
3328
3367
  }
3329
- /** B review (superseded by ADR 0086): IDs of the categories confirmed as Hide. */
3330
- const confirmedHideGroupIds = (state) => state.groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => group.id);
3331
- const overrideKey = (state) => (state.valueOverrides ?? []).map((item) => JSON.stringify([
3332
- item.type,
3333
- item.value,
3334
- item.choice
3335
- ])).toSorted();
3336
3368
  /**
3337
- * Identifies the hidden set (ADR 0079, 0086). The dictionary only grows and a type's group
3338
- * counts its distinct values, so each type's count and Hide answer, with the value overrides,
3339
- * name exactly one hidden set.
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.
3340
3372
  */
3341
- function confirmedHideKey(state) {
3342
- return createHash("sha256").update(JSON.stringify({
3343
- types: termTypes.map((type) => [
3344
- type,
3345
- typeGroup(state, type)?.count ?? 0,
3346
- typeHides(state, type)
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
+ })
3385
+ };
3386
+ }
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"
3347
3427
  ]),
3348
- overrides: overrideKey(state),
3349
- 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)))
3350
- })).digest("hex");
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
+ }));
3351
3570
  }
3352
- /** Whether two states hold the same value overrides. */
3353
- const sameValueOverrides = (left, right) => JSON.stringify(overrideKey(left)) === JSON.stringify(overrideKey(right));
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();
3609
+ //#endregion
3610
+ //#region packages/application/src/journey/scope/respond.ts
3354
3611
  /**
3355
- * A preparation's waived hits stay waived only while every category recorded for them is still a
3356
- * confirmed Hide; otherwise render may no longer mask them, so the session needs reconciliation.
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.
3357
3615
  */
3358
- function maskedHitsStale(maskedHits, hiding) {
3359
- const current = new Set(hiding);
3360
- return !!maskedHits?.some((hit) => hit.groupIds.some((id) => !current.has(id)));
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
+ };
3361
3628
  }
3362
3629
  /**
3363
- * ADR 0086 hidden set: values of types not answered Keep, minus Keep overrides, plus Hide
3364
- * overrides, plus every value of an always-hidden type. A value of several types takes the
3365
- * highest mask kind among the types hiding it.
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.
3366
3633
  */
3367
- function dictionaryHides(state, terms) {
3368
- const overrides = state.valueOverrides ?? [];
3369
- const kept = new Set(overrides.flatMap((item) => item.choice === "keep" ? [item.value] : []));
3370
- const hidden = /* @__PURE__ */ new Map();
3371
- const add = (value, type) => {
3372
- const prior = hidden.get(value);
3373
- const kind = termTypeInfo[type].kind;
3374
- hidden.set(value, {
3375
- kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
3376
- groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], termGroupId(state.journeyId, type)])].sort()
3377
- });
3378
- };
3379
- const all = [...terms];
3380
- const hiddenValues = new Set(state.privacy.terms.filter((item) => item.disposition === "hidden").map((item) => item.value));
3381
- for (const term of all) if (hiddenValues.has(term.value) && !kept.has(term.value)) add(term.value, term.type);
3382
- for (const item of overrides) if (item.choice === "hide") add(item.value, item.type);
3383
- for (const term of all) if (!termTypeInfo[term.type].asked) add(term.value, term.type);
3384
- return hidden;
3385
- }
3386
- /** The journey's current hidden set, applied to every selected Work (ADR 0086, D03). */
3387
- async function journeyConfirmedHides(reader, state) {
3388
- return dictionaryHides(state, [...await reader.journeyTerms({ journeyId: state.journeyId }), ...state.privacy.terms]);
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";
3389
3638
  }
3390
- function confirmedHideTerms(groups, records) {
3391
- const confirmed = new Map(groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => [group.id, group]));
3392
- const fields = /* @__PURE__ */ new Map();
3393
- for (const record of records) {
3394
- const assigned = new Map(record.assignments.map((item) => [item.findingId, item.groupId]));
3395
- const chunks = new Map(record.chunks.map((chunk) => [chunk.id, chunk]));
3396
- for (const finding of record.findings) {
3397
- const group = confirmed.get(assigned.get(finding.id) ?? "");
3398
- const chunk = chunks.get(finding.chunkId);
3399
- if (!group || !chunk || finding.class !== "gray") continue;
3400
- const key = JSON.stringify([chunk.fieldId, chunk.fieldHash]);
3401
- fields.set(key, [...fields.get(key) ?? [], {
3402
- start: chunk.offset + finding.start,
3403
- end: chunk.offset + finding.end,
3404
- text: chunk.text.slice(finding.start, finding.end),
3405
- split: [finding.start === 0, finding.end === chunk.text.length],
3406
- group
3407
- }]);
3408
- }
3409
- }
3410
- const terms = /* @__PURE__ */ new Map();
3411
- const add = (text, groupIds, kind) => {
3412
- for (const term of bLiteralTerms(text)) {
3413
- const prior = terms.get(term);
3414
- terms.set(term, {
3415
- kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
3416
- groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], ...groupIds])].sort()
3417
- });
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
3418
3650
  }
3419
3651
  };
3420
- for (const spans of fields.values()) {
3421
- let open;
3422
- for (const span of spans.toSorted((a, b) => a.start - b.start || b.end - a.end)) {
3423
- if (open && span.split[0] && span.start === open.end) {
3424
- open.text += span.text;
3425
- open.end = span.end;
3426
- open.groupIds.push(span.group.id);
3427
- } else {
3428
- if (open) add(open.text, open.groupIds, open.kind);
3429
- open = {
3430
- end: span.end,
3431
- text: span.text,
3432
- groupIds: [span.group.id],
3433
- kind: span.group.kind
3434
- };
3435
- }
3436
- if (!span.split[1]) {
3437
- add(open.text, open.groupIds, open.kind);
3438
- open = void 0;
3439
- }
3440
- }
3441
- if (open) add(open.text, open.groupIds, open.kind);
3442
- }
3443
- return terms;
3444
- }
3445
- //#endregion
3446
- //#region packages/application/src/journey/b/matches.ts
3447
- function bDictionaryHits(fields, literals, signal = new AbortController().signal) {
3448
- const matcher = createEntityMatcher([...new Set(literals.map((part) => part.term))].map((value) => ({
3449
- kind: "custom-term",
3450
- value
3451
- })));
3452
- const hits = [];
3453
- for (const field of fields) {
3454
- const index = createCodePointIndex(field.text);
3455
- const base = field.start ?? 0;
3456
- for (const match of matcher.find(field.text, signal, 1e5)) {
3457
- if (hits.length >= 1e6) throw new ApplicationError("PROCESSING_LIMIT");
3458
- const body = {
3459
- fieldId: field.id,
3460
- fieldHash: field.fieldHash,
3461
- recordId: field.recordId,
3462
- start: base + index.codePointOffset(match.range.start),
3463
- end: base + index.codePointOffset(match.range.end),
3464
- exactQuote: field.text.slice(match.range.start, match.range.end),
3465
- term: match.entity.value
3466
- };
3467
- hits.push({
3468
- ...body,
3469
- digest: bDigest(JSON.stringify(body))
3470
- });
3471
- }
3472
- }
3473
- return [...new Map(hits.map((hit) => [hit.digest, hit])).values()];
3474
- }
3475
- function unclassifiedBHits(hits, literals, negative) {
3476
- const issues = [];
3477
- const positiveFields = new Map([...Map.groupBy(literals, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
3478
- const negativeFields = new Map([...Map.groupBy(negative, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
3479
- for (const hit of hits) {
3480
- const positive = positiveFields.get(hit.fieldId)?.relation(hit).covers ?? false;
3481
- const negative = negativeFields.get(hit.fieldId)?.relation(hit);
3482
- if (!positive && !negative?.covers || positive && negative?.overlaps) issues.push(bIssue(positive ? "CONFLICTING_PRIVACY_DECISIONS" : "UNCLASSIFIED_MATCH", hit));
3483
- }
3484
- return issues;
3485
- }
3486
- 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);
3487
- /** A confirmed Hide value matches only where it does not run into a neighbouring word (ADR 0069). */
3488
- function bWordBounded(text, start, end) {
3489
- return !(bSpacedWord(scalarBefore(text, start)) && bSpacedWord(scalarAfter(text, start)) || bSpacedWord(scalarBefore(text, end)) && bSpacedWord(scalarAfter(text, end)));
3490
3652
  }
3491
3653
  /**
3492
- * Occurrences of a Work's explicit exceptions, matched like Hide everywhere: exact NFC values
3493
- * over complete graphemes. A confirmed Hide value inside one stays visible there.
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.
3494
3658
  */
3495
- function bExceptionRanges(values) {
3496
- const matcher = createEntityMatcher([...new Set([...values].map((value) => value.normalize("NFC")))].map((value) => ({
3497
- kind: "custom-term",
3498
- value
3499
- })));
3500
- 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);
3501
- }
3502
- /** Whether a range lies inside one of `ranges`: a prefix maximum of ends, O(log n) per query. */
3503
- function bContainment(ranges) {
3504
- const sorted = ranges.toSorted((a, b) => a.start - b.start);
3505
- const reach = [];
3506
- for (const [index, range] of sorted.entries()) reach.push(Math.max(range.end, reach[index - 1] ?? range.end));
3507
- return (range) => {
3508
- let low = 0, high = sorted.length;
3509
- while (low < high) {
3510
- const middle = low + high >> 1;
3511
- if ((sorted[middle]?.start ?? Infinity) <= range.start) low = middle + 1;
3512
- else high = middle;
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;
3513
3675
  }
3514
- return low > 0 && (reach[low - 1] ?? -1) >= range.end;
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
3515
3699
  };
3516
3700
  }
3517
- /**
3518
- * Word-bounded, exact, case-sensitive NFC occurrences of the terms, in UTF-16 coordinates,
3519
- * except where an explicit exception occurrence contains one.
3520
- */
3521
- function bTermMatcher(terms, exceptions = []) {
3522
- const matcher = createEntityMatcher([...new Set(terms)].map((value) => ({
3523
- kind: "custom-term",
3524
- value
3525
- })));
3526
- const excepted = bExceptionRanges(exceptions);
3527
- return (text, signal = new AbortController().signal) => {
3528
- const hits = matcher.find(text, signal, 1e5).filter(({ range }) => bWordBounded(text, range.start, range.end));
3529
- const kept = hits.length ? bContainment(excepted(text, signal)) : () => false;
3530
- return hits.filter(({ range }) => !kept(range)).map(({ range, entity }) => ({
3531
- ...range,
3532
- term: entity.value
3533
- }));
3534
- };
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
+ });
3535
3731
  }
3536
- /** Dictionary hits that a confirmed Hide value masks at render, so they need no classification. */
3537
- function bMaskedHits(fields, terms) {
3538
- const byId = new Map(fields.map((field) => [field.id, {
3539
- text: field.text,
3540
- index: createCodePointIndex(field.text),
3541
- base: field.start ?? 0
3542
- }]));
3543
- return (hit) => {
3544
- const field = byId.get(hit.fieldId);
3545
- return !!field && terms.has(hit.term) && bWordBounded(field.text, field.index.offset(hit.start - field.base), field.index.offset(hit.end - field.base));
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"
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
+ })
3546
3760
  };
3547
3761
  }
3548
- const segmenter = new Intl.Segmenter("und", { granularity: "grapheme" });
3549
- /** Beyond this many inner match edges in one word the index is not kept (retire instead). */
3550
- const maximumEdges = 16;
3551
- /** Beyond this many distinct pieces the index is not kept (retire instead). */
3552
- const maximumHideIndexTokens = 2e6;
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
+ });
3793
+ }
3553
3794
  /**
3554
- * Adds the word pieces of `text` that an occurrence of a confirmed Hide value can consist of.
3555
- * The matcher compares per-grapheme NFC text and checks word edges on the original characters
3556
- * at grapheme edges only, so a match may start or end inside a run of word characters there,
3557
- * or inside a grapheme. Every piece between such positions is added. Returns false when a run
3558
- * 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.
3559
3798
  */
3560
- function addHideIndexTokens(text, add) {
3561
- let complete = true;
3562
- let from = 0;
3563
- for (const { index } of text.matchAll(/[^\p{ASCII}]+/gu)) {
3564
- if (index < from) continue;
3565
- let start = index;
3566
- while (start > from && asciiWord(text.charCodeAt(start - 1))) start--;
3567
- let end = index + 1;
3568
- while (end < text.length && (text.charCodeAt(end) > 127 || asciiWord(text.charCodeAt(end)))) end++;
3569
- asciiTokens(text.slice(from, start), add);
3570
- complete = graphemeTokens(text.slice(start, end), add) && complete;
3571
- from = end;
3572
- }
3573
- asciiTokens(text.slice(from), add);
3574
- return complete;
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
+ });
3575
3827
  }
3576
- const asciiWord = (code) => code >= 48 && code <= 57 || code >= 65 && code <= 90 || code === 95 || code >= 97 && code <= 122;
3577
- function asciiTokens(text, add) {
3578
- for (const [token] of text.matchAll(/[A-Za-z0-9_]+/gu)) add(token);
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
+ })
3848
+ };
3579
3849
  }
3580
- function graphemeTokens(text, add) {
3581
- let complete = true, run = [], edges = [];
3582
- const flush = () => {
3583
- if (edges.length > maximumEdges) complete = false;
3584
- else {
3585
- const cuts = [
3586
- 0,
3587
- ...edges,
3588
- run.length
3589
- ];
3590
- 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(""));
3591
- }
3592
- run = [];
3593
- edges = [];
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
3964
+ });
3965
+ depths.set(node.key, depth);
3966
+ queue.push(node);
3594
3967
  };
3595
- let previous = "";
3596
- for (const { segment } of segmenter.segment(text)) {
3597
- const original = Array.from(segment);
3598
- Array.from(segment.normalize("NFC")).forEach((scalar, index) => {
3599
- if (!bSpacedWord(scalar)) {
3600
- if (run.length) flush();
3601
- return;
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);
3602
3984
  }
3603
- if (run.length && (index > 0 || !(bSpacedWord(previous) && bSpacedWord(original[0] ?? "")))) edges.push(run.length);
3604
- run.push(scalar);
3605
- });
3606
- previous = original.at(-1) ?? "";
3985
+ }
3607
3986
  }
3608
- if (run.length) flush();
3609
- return complete;
3610
- }
3611
- /** The words of an NFC value, or null when it has none and cannot be checked by words. */
3612
- function hideValueTokens(term) {
3613
- const tokens = [];
3614
- let run = "";
3615
- for (const scalar of term) if (bSpacedWord(scalar)) run += scalar;
3616
- else if (run) {
3617
- tokens.push(run);
3618
- 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
+ }
3619
4010
  }
3620
- if (run) tokens.push(run);
3621
- return tokens.length ? [...new Set(tokens)] : null;
3622
- }
3623
- /** A keyed, truncated digest; a collision can only retire an unaffected Work. */
3624
- function hideTokenDigest(key, token) {
3625
- return createHmac("sha256", key).update(token, "utf8").digest("base64url").slice(0, 11);
3626
- }
3627
- /** Identifies the key an index or delta was made with, without revealing it. */
3628
- const hideKeyId = (key) => hideTokenDigest(key, "\0key");
3629
- /** Sorted, distinct, fixed-width digests in one string. */
3630
- function packHideIndex(key, tokens) {
3631
- const secret = createSecretKey(key);
3632
- 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;
3633
4013
  }
3634
- /** One digest; an index is a run of them, checked by length and alphabet. */
3635
- const hideDigestPattern = /^[A-Za-z0-9_-]{11}$/u;
3636
- const isHideIndex = (value) => value.length % 11 === 0 && /^[A-Za-z0-9_-]*$/u.test(value);
3637
- function hideIndexHas(index, digest) {
3638
- let low = 0, high = index.length / 11 - 1;
3639
- while (low <= high) {
3640
- const middle = low + high >> 1;
3641
- const value = index.slice(middle * 11, (middle + 1) * 11);
3642
- if (value === digest) return true;
3643
- if (value < digest) low = middle + 1;
3644
- 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;
3645
4068
  }
3646
- 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
+ });
3647
4081
  }
3648
4082
  //#endregion
3649
- //#region packages/application/src/journey/review/retirement.ts
3650
- /**
3651
- * ADR 0079: the confirmed Hide values a commit adds, removes or recategorizes, with the kind
3652
- * each then has (null once removed; for additions, the lowest it can have). Terms exist only in
3653
- * memory for this commit. `rendered` holds the value's keyed digest for each kind, to compare
3654
- * with the values a Work was rendered with; `tokens` holds its words' keyed digests, or null
3655
- * when it has none. A partial value is a literal that may join a neighbour across a chunk edge,
3656
- * so its whole text, and every Work it may reach, is unknown.
3657
- */
3658
- const deltaValueSchema = z.object({
3659
- term: z.string().min(1),
3660
- kind: journeyKindSchema.nullable(),
3661
- partial: z.boolean(),
3662
- rendered: z.array(z.string().regex(hideDigestPattern)).length(confirmedHideKinds.length),
3663
- tokens: z.array(z.string().regex(hideDigestPattern)).min(1).nullable(),
3664
- /** The new findings an addition comes from; the store keeps it only if one joins a Hide. */
3665
- sources: z.array(z.object({
3666
- batchId: z.uuid(),
3667
- findingId: z.uuid()
3668
- }).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()
3669
4111
  }).strict();
3670
- /** `keyId` names the journey key the digests were made with; an index under another key is unusable. */
3671
- const keyId = z.string().regex(hideDigestPattern);
3672
- const confirmedHideDeltaSchema = z.discriminatedUnion("scope", [z.object({
3673
- scope: z.literal("complete"),
3674
- keyId,
3675
- from: z.string(),
3676
- to: z.string(),
3677
- values: z.array(deltaValueSchema)
3678
- }).strict(), z.object({
3679
- scope: z.literal("additions"),
3680
- keyId,
3681
- values: z.array(deltaValueSchema)
3682
- }).strict()]);
3683
- /** What a value was rendered as: its kind, in the digests a render keeps (ADR 0079). */
3684
- const renderedHideValue = (kind, term) => `${kind}\u0000${term}`;
3685
- function hideDeltaValue(key, term, kind, partial = false) {
3686
- const tokens = partial ? null : hideValueTokens(term);
3687
- return {
3688
- term,
3689
- kind,
3690
- partial,
3691
- rendered: confirmedHideKinds.map((item) => hideTokenDigest(key, renderedHideValue(item, term))),
3692
- tokens: tokens && tokens.map((token) => hideTokenDigest(key, token))
3693
- };
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");
3694
4138
  }
3695
- const kindOf = (kind) => journeyKindSchema.nullable().parse(kind ?? null);
3696
- const rank = (kind) => kind === null ? -1 : confirmedHideKinds.indexOf(kind);
4139
+ //#endregion
4140
+ //#region packages/application/src/journey/scan-git-seeds.ts
3697
4141
  /**
3698
- * Every value whose hidden kind differs between two states (ADR 0086): both are read against the
3699
- * 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.
3700
4144
  */
3701
- async function readConfirmedHideChange(reader, key, previous, next) {
3702
- const terms = await reader.journeyTerms({ journeyId: previous.journeyId });
3703
- const before = dictionaryHides(previous, terms), after = dictionaryHides(next, terms);
3704
- return {
3705
- scope: "complete",
3706
- keyId: hideKeyId(key),
3707
- from: confirmedHideKey(previous),
3708
- to: confirmedHideKey(next),
3709
- 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 }))
3710
4165
  };
3711
- }
3712
- /** Dictionary values a commit may add to the hidden set, each with its type's kind (ADR 0086). */
3713
- function termHideAdditions(key, values) {
3714
- const unique = /* @__PURE__ */ new Map();
3715
- for (const { term, kind } of values) unique.set(JSON.stringify([term, kind]), hideDeltaValue(key, term, kindOf(kind)));
3716
4166
  return {
3717
- scope: "additions",
3718
- keyId: hideKeyId(key),
3719
- 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))
3720
4173
  };
3721
4174
  }
3722
- /**
3723
- * The values new Gray findings can add. A finding at a chunk edge may join a neighbouring
3724
- * literal of the same field, so its words are not checked.
3725
- */
3726
- function confirmedHideAdditions(key, records) {
3727
- const values = /* @__PURE__ */ new Map();
3728
- for (const record of records) {
3729
- const chunks = new Map(record.chunks.map((chunk) => [chunk.id, chunk]));
3730
- for (const finding of record.findings) {
3731
- const chunk = chunks.get(finding.chunkId);
3732
- if (!chunk || finding.class !== "gray") continue;
3733
- const whole = chunk.offset === 0 && chunk.textHash === chunk.fieldHash;
3734
- const joins = finding.start === 0 && chunk.offset > 0 || finding.end === chunk.text.length && !whole;
3735
- const source = {
3736
- batchId: record.batchId,
3737
- findingId: finding.id
3738
- };
3739
- for (const term of bLiteralTerms(chunk.text.slice(finding.start, finding.end))) {
3740
- const id = JSON.stringify([
3741
- term,
3742
- finding.kind,
3743
- joins
3744
- ]);
3745
- const value = values.get(id);
3746
- if (value) value.sources.push(source);
3747
- else values.set(id, {
3748
- ...hideDeltaValue(key, term, finding.kind, joins),
3749
- sources: [source]
3750
- });
3751
- }
3752
- }
3753
- }
3754
- return {
3755
- scope: "additions",
3756
- keyId: hideKeyId(key),
3757
- values: [...values.values()]
3758
- };
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;
3759
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();
3760
4189
  /**
3761
- * How hidden values moved when the value overrides and type answers stay the same: not at all,
3762
- * only added (new dictionary values of hiding types), only removed, or otherwise. A type's
3763
- * hidden values are its distinct values while it hides; an answer that changes whether a type
3764
- * 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.
3765
4192
  */
3766
- function memberChange(previous, next) {
3767
- if (!sameValueOverrides(previous, next)) return "other";
3768
- if (termTypes.some((type) => typeGroup(previous, type) !== void 0 && typeHides(previous, type) !== typeHides(next, type))) return "other";
3769
- const hidden = (state, type) => typeHides(state, type) ? typeGroup(state, type)?.count ?? 0 : 0;
3770
- const counts = termTypes.map((type) => hidden(next, type) - hidden(previous, type));
3771
- if (counts.every((count) => count === 0)) return "same";
3772
- 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)))));
3773
4232
  }
3774
- /**
3775
- * Whether a Work's output may change with the delta (Work metadata is never masked, so a
3776
- * 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
3777
- * members were only removed: no value is then added, so nothing can be under-hidden, and an
3778
- * output may keep masking a value that left its category until it is rendered again.
3779
- */
3780
- function impact(previous, next, delta, outputIndex) {
3781
- const change = memberChange(previous, next);
3782
- const covered = delta && (delta.scope === "complete" ? delta.from === confirmedHideKey(previous) && delta.to === confirmedHideKey(next) : change === "additions" || change === "same");
3783
- if (!delta || !covered) return change === "removals" || change === "same" ? () => false : () => true;
3784
- const unchanged = (index, value) => {
3785
- const at = value.rendered.findIndex((digest) => hideIndexHas(index.values, digest));
3786
- const was = at < 0 ? null : confirmedHideKinds[at] ?? null;
3787
- return delta.scope === "additions" ? rank(was) >= rank(value.kind) : was === value.kind;
3788
- };
3789
- const reaches = (index, value) => {
3790
- const digests = index.digests;
3791
- return !value.tokens || !digests || value.tokens.every((token) => hideIndexHas(digests, token));
3792
- };
3793
- return (work) => {
3794
- if (!work.artifact) return false;
3795
- const found = outputIndex(work);
3796
- const index = found?.keyId === delta.keyId ? found : null;
3797
- return !index || delta.values.some((value) => value.partial || !unchanged(index, value) && reaches(index, value));
3798
- };
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
+ });
3799
4283
  }
3800
- /**
3801
- * Applied with every Journey commit. When confirmed Hide categories or their members change,
3802
- * a Work whose output or generated description may change is retired until both are produced
3803
- * again (ADR 0069); ADR 0079 limits this to Works the changed values can reach.
3804
- */
3805
- function settleConfirmedHides(previous, next, delta, outputIndex = () => null) {
3806
- if (!previous || confirmedHideKey(previous) === confirmedHideKey(next)) return next;
3807
- const before = new Map(previous.works.map((work) => [work.id, work.revision]));
3808
- 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()];
3809
4347
  return {
3810
- ...next,
3811
- works: next.works.map((work) => work.artifact === null && work.revision > (before.get(work.id) ?? work.revision) || !affected(work) ? work : {
3812
- ...retireWorkDescription(work),
3813
- artifact: null,
3814
- revision: work.revision + 1,
3815
- status: work.status === "ready" ? "review" : work.status
3816
- })
4348
+ review,
4349
+ receipt: {
4350
+ ...plan,
4351
+ resultDigest: bReviewDigest(merged),
4352
+ priorObligationsPreserved: true,
4353
+ unresolvedIssues: review.issues
4354
+ }
3817
4355
  };
3818
4356
  }
3819
4357
  //#endregion
3820
- //#region packages/application/src/journey/storage.ts
3821
- const journeyEventSchema = z.object({
3822
- 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({
3823
4372
  journeyId: journeyIdSchema,
3824
- revision: z.number().int().nonnegative(),
3825
- kind: z.enum([
3826
- "started",
3827
- "response",
3828
- "snapshot",
3829
- "batch",
3830
- "output",
3831
- "send",
3832
- "search"
3833
- ]),
3834
- source: z.enum([
3835
- "conversation",
3836
- "panel",
3837
- "system"
3838
- ]),
3839
- at: z.iso.datetime(),
3840
- alignment: reviewAlignmentAuditSchema.optional(),
3841
- preferenceScopes: z.array(z.object({
3842
- groupId: journeyIdSchema,
3843
- preferenceScope: journeyPreferenceScopeSchema
3844
- }).strict()).min(1).max(1e4).optional(),
3845
- correction: correctionAuditSchema.optional(),
3846
- decision: delegatedDecisionSchema.extend({
3847
- action: z.enum([
3848
- "group",
3849
- "groups-hide",
3850
- "work-access",
3851
- "instruction",
3852
- "correction",
3853
- "works-access",
3854
- "work-type",
3855
- "scope-confirm",
3856
- "card-answer",
3857
- "cards-confirm"
3858
- ]),
3859
- targetIds: z.array(journeyIdSchema).min(1),
3860
- choice: z.string().min(1).max(16)
3861
- }).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()
3862
4380
  }).strict();
3863
- const journeyCommitSchema = z.object({
3864
- expectedRevision: z.number().int().nonnegative().nullable(),
3865
- requestKey: journeyIdSchema,
4381
+ const reviewCandidateResourceSchema = reviewCandidateInputSchema.omit({ idempotencyKey: true }).extend({
4382
+ kind: z.literal("b-candidate"),
4383
+ id: journeyIdSchema,
3866
4384
  requestHash: journeyDigestSchema,
3867
- state: nativeJourneySchema,
3868
- event: journeyEventSchema.omit({
3869
- sequence: true,
3870
- revision: true
3871
- }),
3872
- /** Inserted in the same transaction as this state; never through the generic resource save. */
3873
- correctionReport: correctionReportResourceSchema.optional(),
3874
- /** Set by the store for this commit only and never persisted (ADR 0079). */
3875
- confirmedHideDelta: confirmedHideDeltaSchema.optional(),
3876
- /** Dictionary values a Hide override adds (ADR 0086), inserted with this state. */
3877
- terms: z.array(z.object({
3878
- type: termTypeSchema,
3879
- value: z.string().min(1)
3880
- }).strict()).max(2e4).optional()
3881
- }).strict();
3882
- const journeyCommitResultSchema = z.object({
4385
+ at: z.iso.datetime(),
4386
+ workId: journeyIdSchema,
4387
+ inputRevision: z.number().int().positive(),
4388
+ snapshotId: journeyDigestSchema,
4389
+ responseDigest: journeyDigestSchema,
3883
4390
  status: z.enum([
3884
- "applied",
3885
- "replayed",
3886
- "stale"
4391
+ "valid",
4392
+ "pending",
4393
+ "invalid"
3887
4394
  ]),
3888
- 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
3889
4422
  }).strict();
3890
4423
  //#endregion
3891
4424
  //#region packages/application/src/journey/review/usefulness-schema.ts
@@ -3957,7 +4490,7 @@ const reviewProgressSchema = z.array(z.object({
3957
4490
  }).strict());
3958
4491
  //#endregion
3959
4492
  //#region packages/application/src/journey/review/session-schema.ts
3960
- const count$8 = z.number().int().nonnegative();
4493
+ const count$7 = z.number().int().nonnegative();
3961
4494
  const sessionReviewInputSchema = z.object({
3962
4495
  journeyId: journeyIdSchema,
3963
4496
  workId: journeyIdSchema,
@@ -3978,7 +4511,7 @@ const sessionReviewResourceSchema = z.object({
3978
4511
  workId: journeyIdSchema,
3979
4512
  sessionId: z.string().min(1).max(128),
3980
4513
  snapshotId: journeyDigestSchema,
3981
- inputRevision: count$8.positive(),
4514
+ inputRevision: count$7.positive(),
3982
4515
  requestHash: journeyDigestSchema,
3983
4516
  bindingDigest: journeyDigestSchema,
3984
4517
  at: z.iso.datetime(),
@@ -3992,8 +4525,8 @@ const sessionReviewResourceSchema = z.object({
3992
4525
  issues: z.array(reviewIssueSchema),
3993
4526
  requiresSemantic: z.boolean(),
3994
4527
  usefulness: sessionUsefulnessSchema,
3995
- totalCodePoints: count$8,
3996
- reviewedCodePoints: count$8
4528
+ totalCodePoints: count$7,
4529
+ reviewedCodePoints: count$7
3997
4530
  }).strict();
3998
4531
  const sessionRereadInputSchema = z.object({
3999
4532
  journeyId: journeyIdSchema,
@@ -4002,8 +4535,8 @@ const sessionRereadInputSchema = z.object({
4002
4535
  anchors: z.array(z.object({
4003
4536
  fieldId: z.string().min(1),
4004
4537
  fieldHash: journeyDigestSchema,
4005
- start: count$8,
4006
- end: count$8
4538
+ start: count$7,
4539
+ end: count$7
4007
4540
  }).strict()).min(1).max(128)
4008
4541
  }).strict();
4009
4542
  const sessionRereadResourceSchema = z.object({
@@ -4012,7 +4545,7 @@ const sessionRereadResourceSchema = z.object({
4012
4545
  journeyId: journeyIdSchema,
4013
4546
  workId: journeyIdSchema,
4014
4547
  sessionId: z.string().min(1).max(128),
4015
- inputRevision: count$8.positive(),
4548
+ inputRevision: count$7.positive(),
4016
4549
  snapshotId: journeyDigestSchema,
4017
4550
  sessionReviewId: journeyIdSchema,
4018
4551
  bindingDigest: journeyDigestSchema,
@@ -4030,8 +4563,8 @@ const sessionReconcileInputSchema = z.object({
4030
4563
  alignment: reviewAlignmentSchema
4031
4564
  }).strict();
4032
4565
  const retainedReviewAnchorSchema = bAnchorSchema.extend({
4033
- utf16Start: count$8,
4034
- utf16End: count$8,
4566
+ utf16Start: count$7,
4567
+ utf16End: count$7,
4035
4568
  owned: z.boolean()
4036
4569
  }).strict();
4037
4570
  const sessionOverlaySchema = z.object({
@@ -4045,7 +4578,7 @@ const sessionReconciliationResourceSchema = z.object({
4045
4578
  journeyId: journeyIdSchema,
4046
4579
  workId: journeyIdSchema,
4047
4580
  sessionId: z.string().min(1).max(128),
4048
- inputRevision: count$8.positive(),
4581
+ inputRevision: count$7.positive(),
4049
4582
  snapshotId: journeyDigestSchema,
4050
4583
  sessionReviewId: journeyIdSchema,
4051
4584
  bindingDigest: journeyDigestSchema,
@@ -4073,8 +4606,8 @@ const sessionReviewQuerySchema = z.object({
4073
4606
  workId: journeyIdSchema.optional(),
4074
4607
  sessionId: z.string().min(1).max(128).optional(),
4075
4608
  sessionReviewId: journeyIdSchema.optional(),
4076
- afterHit: count$8.default(0),
4077
- limit: count$8.positive().max(500).default(100)
4609
+ afterHit: count$7.default(0),
4610
+ limit: count$7.positive().max(500).default(100)
4078
4611
  }).strict();
4079
4612
  const sessionReviewStatusSchema = z.object({
4080
4613
  complete: z.boolean(),
@@ -4090,8 +4623,8 @@ const sessionReviewStatusSchema = z.object({
4090
4623
  requiresHumanDecision: z.boolean(),
4091
4624
  requiresSemantic: z.boolean(),
4092
4625
  issues: z.array(reviewIssueSchema),
4093
- totalCodePoints: count$8,
4094
- reviewedCodePoints: count$8,
4626
+ totalCodePoints: count$7,
4627
+ reviewedCodePoints: count$7,
4095
4628
  reconciliationId: journeyIdSchema.nullable(),
4096
4629
  /** ADR 0086: a read unit of the session suggests excluding it; advice, never a gate. */
4097
4630
  suggestExclude: z.boolean(),
@@ -4099,8 +4632,8 @@ const sessionReviewStatusSchema = z.object({
4099
4632
  reason: z.string().min(1).max(2e3).nullable()
4100
4633
  }).strict()),
4101
4634
  hits: z.array(sessionReviewHitSchema),
4102
- nextHit: count$8.nullable(),
4103
- totalHits: count$8
4635
+ nextHit: count$7.nullable(),
4636
+ totalHits: count$7
4104
4637
  }).strict();
4105
4638
  const sessionReviewResultSchema = z.object({
4106
4639
  preparation: sessionReviewResourceSchema,
@@ -4184,13 +4717,13 @@ const metadataFieldsSchema = z.object({
4184
4717
  }).strict();
4185
4718
  //#endregion
4186
4719
  //#region packages/application/src/journey/placement/routing-schema.ts
4187
- 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);
4188
4721
  const bucket = z.object({
4189
4722
  complete: z.boolean(),
4190
- total: count$7,
4723
+ total: count$6,
4191
4724
  repositories: z.array(z.object({
4192
4725
  id: z.string().min(1),
4193
- count: count$7.min(1)
4726
+ count: count$6.min(1)
4194
4727
  }).strict())
4195
4728
  }).strict();
4196
4729
  const placementSignalsSchema = z.object({
@@ -4208,8 +4741,8 @@ const cheapPlacementSchema = z.object({
4208
4741
  "name"
4209
4742
  ]),
4210
4743
  confidence: z.enum(["high", "medium"]),
4211
- matched: count$7.min(1),
4212
- total: count$7.min(1),
4744
+ matched: count$6.min(1),
4745
+ total: count$6.min(1),
4213
4746
  otherRepositoryIds: z.array(z.string())
4214
4747
  }).strict();
4215
4748
  const sortingResourceSchema = z.object({
@@ -4230,26 +4763,26 @@ const sortingResourceSchema = z.object({
4230
4763
  const sortingApplySchema = z.object({
4231
4764
  journeyId: z.uuid(),
4232
4765
  sessionId: z.string().min(1).max(128),
4233
- expectedRevision: count$7,
4766
+ expectedRevision: count$6,
4234
4767
  idempotencyKey: z.uuid()
4235
4768
  }).strict();
4236
4769
  /** Preparation moves identified failing roots of one pending Work into its blocked Work. */
4237
4770
  const journeyIsolateSchema = z.object({
4238
4771
  journeyId: z.uuid(),
4239
4772
  workId: z.uuid(),
4240
- inputRevision: count$7.min(1),
4773
+ inputRevision: count$6.min(1),
4241
4774
  roots: z.array(z.object({
4242
4775
  sessionId: z.string().min(1).max(128),
4243
4776
  code: z.string().regex(/^[A-Z_]{3,80}$/u)
4244
4777
  }).strict()).min(1).max(1e5),
4245
- expectedRevision: count$7,
4778
+ expectedRevision: count$6,
4246
4779
  idempotencyKey: z.uuid()
4247
4780
  }).strict();
4248
4781
  const placement = z.object({
4249
4782
  workId: z.uuid(),
4250
4783
  sessionId: z.string().min(1).max(128),
4251
- inputRevision: count$7.min(1),
4252
- throughEvent: count$7,
4784
+ inputRevision: count$6.min(1),
4785
+ throughEvent: count$6,
4253
4786
  contextDigest: z.string().regex(/^[a-f0-9]{64}$/u),
4254
4787
  decision: z.enum([
4255
4788
  "join",
@@ -4265,13 +4798,13 @@ const placement = z.object({
4265
4798
  }).strict();
4266
4799
  const journeyPlaceSchema = z.object({
4267
4800
  journeyId: z.uuid(),
4268
- expectedRevision: count$7,
4801
+ expectedRevision: count$6,
4269
4802
  idempotencyKey: z.uuid(),
4270
4803
  placements: z.array(placement).min(1).max(1e3)
4271
4804
  }).strict();
4272
4805
  //#endregion
4273
4806
  //#region packages/application/src/journey/scope/progress.ts
4274
- 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);
4275
4808
  const ids = z.array(z.string().min(1).max(128)).max(1e5).refine((values) => new Set(values).size === values.length);
4276
4809
  const journeyClassificationProgressSchema = z.object({
4277
4810
  journeyId: journeyIdSchema,
@@ -4279,8 +4812,8 @@ const journeyClassificationProgressSchema = z.object({
4279
4812
  id: z.literal("current"),
4280
4813
  snapshotId: journeyDigestSchema,
4281
4814
  inputFingerprint: journeyDigestSchema,
4282
- baselineRevision: count$6,
4283
- proposalRevision: count$6.nullable(),
4815
+ baselineRevision: count$5,
4816
+ proposalRevision: count$5.nullable(),
4284
4817
  stage: z.enum(["classification", "proposal"]),
4285
4818
  status: z.enum([
4286
4819
  "running",
@@ -4290,7 +4823,7 @@ const journeyClassificationProgressSchema = z.object({
4290
4823
  ]),
4291
4824
  startedAt: z.iso.datetime(),
4292
4825
  updatedAt: z.iso.datetime(),
4293
- elapsedMs: count$6,
4826
+ elapsedMs: count$5,
4294
4827
  rootIds: ids,
4295
4828
  completedRootIds: ids,
4296
4829
  groupedRootIds: ids,
@@ -4325,9 +4858,23 @@ const journeyCaptureSchema = z.object({
4325
4858
  }).strict();
4326
4859
  const base = { journeyId: journeyIdSchema };
4327
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(),
4328
4868
  scanSessionResourceSchema,
4329
4869
  scanCanonicalResourceSchema,
4330
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(),
4331
4878
  scanExclusionResourceSchema,
4332
4879
  scanGitSeedsResourceSchema,
4333
4880
  staticSessionResourceSchema,
@@ -4540,124 +5087,18 @@ function projectPreparationProgress(state, plan, captures, starts, settlements)
4540
5087
  }).map((work) => work.id),
4541
5088
  targetParts: sizes.size,
4542
5089
  capturedParts: captured.length,
4543
- targetBytes: [...sizes.values()].reduce((sum, size) => sum + size, 0),
4544
- capturedBytes: captured.reduce((sum, resource) => sum + resource.capture.facts.byteLength, 0)
4545
- };
4546
- }
4547
- //#endregion
4548
- //#region packages/application/src/journey/policy/port.ts
4549
- const journeyPolicyRespondSchema = z.object({
4550
- request: nativeRespondSchema,
4551
- source: z.enum(["conversation", "panel"]),
4552
- now: z.iso.datetime()
4553
- }).strict();
4554
- //#endregion
4555
- //#region packages/application/src/journey/search.ts
4556
- const count$5 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
4557
- const term = z.string().min(1).max(1024).refine((value) => Buffer.byteLength(value) <= 1024 && !/[\0\r\n]/u.test(value));
4558
- const journeySearchSchema = z.object({
4559
- journeyId: journeyIdSchema,
4560
- terms: z.array(z.object({
4561
- id: z.string().min(1).max(64),
4562
- text: term,
4563
- variants: z.array(term).max(7).default([])
4564
- }).strict()).min(1).max(16)
4565
- }).strict();
4566
- const journeySearchResultSchema = z.object({
4567
- binding: z.object({
4568
- journeyId: journeyIdSchema,
4569
- snapshotId: journeyDigestSchema,
4570
- revision: count$5,
4571
- scopeDigest: journeyDigestSchema,
4572
- outputs: z.array(z.object({
4573
- workId: journeyIdSchema,
4574
- outputRevision: count$5,
4575
- sha256: journeyDigestSchema
4576
- }).strict())
4577
- }).strict(),
4578
- status: z.enum(["complete", "partial"]),
4579
- scope: z.object({
4580
- retainedText: z.literal(true),
4581
- metadata: z.literal(true),
4582
- setupText: z.literal(true),
4583
- attachments: z.literal(false),
4584
- images: z.literal(false),
4585
- originals: z.literal("frozen-native-records-and-discovery-metadata"),
4586
- matching: z.literal("case-sensitive-literal-leftmost-longest")
4587
- }).strict(),
4588
- terms: z.array(z.object({
4589
- id: z.string(),
4590
- tried: z.array(z.string()),
4591
- sentMatches: count$5.nullable(),
4592
- originalMatches: count$5.nullable(),
4593
- sentSessions: count$5.nullable(),
4594
- originalSessions: count$5.nullable()
4595
- }).strict()),
4596
- hits: z.array(z.object({
4597
- termId: z.string(),
4598
- basis: z.enum(["sent", "original"]),
4599
- workId: journeyIdSchema,
4600
- locator: z.object({
4601
- kind: z.enum([
4602
- "retained-text",
4603
- "metadata",
4604
- "setup-text"
4605
- ]),
4606
- entryPath: z.string().optional(),
4607
- archiveSessionId: z.string().optional(),
4608
- sessionId: z.string().optional(),
4609
- sourcePartId: z.string().optional(),
4610
- recordIndex: count$5,
4611
- fieldId: z.string(),
4612
- line: count$5
4613
- }).strict(),
4614
- occurrences: count$5,
4615
- modelContext: z.string().max(4096).optional(),
4616
- match: z.object({
4617
- variantIndex: count$5.max(7),
4618
- start: count$5,
4619
- end: count$5
4620
- }).strict().optional()
4621
- }).strict()),
4622
- failures: z.array(z.object({
4623
- workId: journeyIdSchema,
4624
- basis: z.enum(["sent", "original"]),
4625
- code: z.enum(["SEARCH_INPUT_UNAVAILABLE", "SEARCH_ENGINE_FAILED"])
4626
- }).strict()),
4627
- truncated: z.boolean()
4628
- }).strict().refine((result) => result.hits.every((hit) => {
4629
- if (hit.basis === "original") return hit.modelContext === void 0 && hit.match === void 0;
4630
- if (!hit.match || hit.modelContext === void 0) return false;
4631
- const { start, end, variantIndex } = hit.match;
4632
- const variant = result.terms.find((term) => term.id === hit.termId)?.tried[variantIndex];
4633
- if (!variant || end <= start || end > hit.modelContext.length || hit.modelContext.slice(start, end) !== variant) return false;
4634
- return [start, end].every((offset) => {
4635
- const before = hit.modelContext?.charCodeAt(offset - 1) ?? 0;
4636
- const after = hit.modelContext?.charCodeAt(offset) ?? 0;
4637
- return !(before >= 55296 && before <= 56319 && after >= 56320 && after <= 57343);
4638
- });
4639
- }));
4640
- const journeySearchSaveSchema = z.object({
4641
- journeyId: journeyIdSchema,
4642
- expectedRevision: count$5,
4643
- idempotencyKey: journeyIdSchema,
4644
- result: journeySearchResultSchema
4645
- }).strict();
4646
- function searchScopeDigest(state) {
4647
- return textDigest(JSON.stringify({
4648
- snapshot: state.snapshotId,
4649
- configurationId: state.configurationId,
4650
- pendingHideId: state.pendingHideId,
4651
- works: state.works.map((work) => ({
4652
- id: work.id,
4653
- revision: work.revision,
4654
- inputRevision: work.inputRevision,
4655
- access: work.access,
4656
- artifact: work.artifact?.sha256 ?? null
4657
- }))
4658
- }));
5090
+ targetBytes: [...sizes.values()].reduce((sum, size) => sum + size, 0),
5091
+ capturedBytes: captured.reduce((sum, resource) => sum + resource.capture.facts.byteLength, 0)
5092
+ };
4659
5093
  }
4660
5094
  //#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
4661
5102
  //#region packages/application/src/journey/hide/port.ts
4662
5103
  const hidePublishSchema = z.object({
4663
5104
  expectedRevision: z.number().int().nonnegative(),
@@ -4815,7 +5256,7 @@ function retireScanGeneration(state, workId) {
4815
5256
  }
4816
5257
  /** The same Work with a new scan generation: it is read again from its first session. */
4817
5258
  function resetWorkScan(work) {
4818
- 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;
4819
5260
  return {
4820
5261
  ...retained,
4821
5262
  revision: work.revision + 1,
@@ -4851,6 +5292,11 @@ const classificationCommitSchema = z.object({
4851
5292
  /** `classificationPartition` of the state the proposal was prepared against. */
4852
5293
  partition: journeyDigestSchema,
4853
5294
  late: z.boolean(),
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(),
4854
5300
  items: z.array(journeyClassificationSubmitSchema.shape.items.element.extend({
4855
5301
  workId: journeyIdSchema,
4856
5302
  workspaceIds: workspaces
@@ -4874,13 +5320,22 @@ function classificationPartition(state) {
4874
5320
  work.rootSessionIds.toSorted()
4875
5321
  ]))).digest("hex");
4876
5322
  }
5323
+ /**
5324
+ * Two placed Works one late item may join: the same access, origin, confirmation, recorded
5325
+ * Public choice and account access conflict, as local-host isolation.ts compares. The Skill's `validateLatePlacedWorks` mirrors this rule before commit.
5326
+ */
5327
+ function sameAccessRecord(work, other) {
5328
+ return work.access === other?.access && work.accessOrigin === other.accessOrigin && work.accessConfirmed === other.accessConfirmed && work.accessChoice?.channel === other.accessChoice?.channel && work.accessChoice?.at === other.accessChoice?.at && work.accountAccessConflict === other.accountAccessConflict;
5329
+ }
4877
5330
  /** The proposal's Works built from the given state's current Works (E2). */
4878
5331
  function classificationWorks(state, plan) {
4879
5332
  const late = plan.late;
4880
5333
  const works = [];
4881
5334
  for (const item of plan.items) {
4882
5335
  const existing = state.works.find((work) => work.placement !== "classifying" && work.rootSessionIds.length === item.rootSessionIds.length && work.rootSessionIds.every((root) => item.rootSessionIds.includes(root)));
4883
- const extended = late ? state.works.find((work) => work.placement === "placed" && work.rootSessionIds.every((root) => item.rootSessionIds.includes(root))) : void 0;
5336
+ const owners = late ? state.works.filter((work) => work.placement === "placed" && work.rootSessionIds.some((root) => item.rootSessionIds.includes(root))) : [];
5337
+ if (owners.some((work) => !sameAccessRecord(work, owners[0]))) throw new ApplicationError("INVALID_SELECTION", "classification.late.several-works");
5338
+ const extended = owners.find((work) => work.rootSessionIds.every((root) => item.rootSessionIds.includes(root)));
4884
5339
  if (late && existing) {
4885
5340
  works.push(existing);
4886
5341
  continue;
@@ -4891,6 +5346,9 @@ function classificationWorks(state, plan) {
4891
5346
  sourceWorkspaceIds: [.../* @__PURE__ */ new Set([...extended.sourceWorkspaceIds, ...item.workspaceIds])]
4892
5347
  } : newJourneyWork(item.workId, item.projectName, item.rootSessionIds, item.workspaceIds, "placed"));
4893
5348
  const { classificationMetadata: _display, ...original } = base;
5349
+ const account = plan.accountAccess?.[base.id];
5350
+ const inherit = late && account && base.accessOrigin !== "explicit";
5351
+ const unproven = extended && base.accessChoice?.channel === "account" && account !== "public-all";
4894
5352
  works.push({
4895
5353
  ...original,
4896
5354
  projectName: item.projectName,
@@ -4905,8 +5363,13 @@ function classificationWorks(state, plan) {
4905
5363
  ...late && !extended ? {
4906
5364
  access: item.recommendation.access,
4907
5365
  accessOrigin: "model",
4908
- accessConfirmed: true
5366
+ accessConfirmed: item.recommendation.access !== "public"
5367
+ } : {},
5368
+ ...unproven ? {
5369
+ accessConfirmed: false,
5370
+ accessChoice: void 0
4909
5371
  } : {},
5372
+ ...inherit && account ? accountSeededAccess(account, plan.at) : {},
4910
5373
  classificationRecommendation: item.recommendation,
4911
5374
  suggestion: null,
4912
5375
  artifact: null,
@@ -4977,7 +5440,12 @@ function regroupTerms(terms, works, changedMetadata, chunkSessions) {
4977
5440
  function applyClassificationProposal(state, plan) {
4978
5441
  const classifying = isClassifying(state);
4979
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;
4980
- 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);
4981
5449
  const retained = new Set(works.map((work) => work.id));
4982
5450
  const regrouped = !plan.late && (state.works.some((work) => !retained.has(work.id)) || changedMetadata.size > 0);
4983
5451
  const cardTerms = new Set(state.privacy.cards.flatMap((card) => card.termKeys));
@@ -5008,6 +5476,10 @@ function applyClassificationProposal(state, plan) {
5008
5476
  confirmedAnswersRevision: null
5009
5477
  } : {}
5010
5478
  },
5479
+ ...currentPlan.accountRead ? { accountRead: {
5480
+ status: currentPlan.accountRead,
5481
+ at: plan.at
5482
+ } } : {},
5011
5483
  readingComplete: !plan.pending && works.filter((work) => work.access !== "drop").every((work) => work.totalBatches > 0 && work.completedBatches === work.totalBatches),
5012
5484
  phase: "processing"
5013
5485
  };
@@ -5073,44 +5545,6 @@ z.object({
5073
5545
  groupId: journeyIdSchema
5074
5546
  }).strict())
5075
5547
  }).strict().nullable();
5076
- z.object({
5077
- journeyId: journeyIdSchema.optional(),
5078
- workIds: z.array(journeyIdSchema).min(1).max(1e3).optional()
5079
- }).strict();
5080
- const categories = z.object({
5081
- identity: z.number().int().nonnegative(),
5082
- credentials: z.number().int().nonnegative(),
5083
- internal: z.number().int().nonnegative(),
5084
- private: z.number().int().nonnegative()
5085
- }).strict();
5086
- z.object({
5087
- journeyId: journeyIdSchema,
5088
- nonAdditiveMechanismCounts: z.literal(true),
5089
- total: z.number().int().nonnegative(),
5090
- categories,
5091
- byMechanism: z.object({
5092
- staticNodes: z.record(z.string(), z.number().int().nonnegative()),
5093
- semantic: z.number().int().nonnegative(),
5094
- correction: z.number().int().nonnegative()
5095
- }).strict(),
5096
- works: z.array(z.object({
5097
- workId: journeyIdSchema,
5098
- revision: z.number().int().positive(),
5099
- inputRevision: z.number().int().positive(),
5100
- primarySha256: z.string().length(64),
5101
- companionSha256: z.string().length(64),
5102
- ledgerSha256: z.string().length(64),
5103
- decisionDigest: z.string().length(64),
5104
- reviewInputDigest: z.string().length(64),
5105
- total: z.number().int().nonnegative(),
5106
- categories,
5107
- byMechanism: z.object({
5108
- staticNodes: z.record(z.string(), z.number().int().nonnegative()),
5109
- semantic: z.number().int().nonnegative(),
5110
- correction: z.number().int().nonnegative()
5111
- }).strict()
5112
- }).strict())
5113
- }).strict();
5114
5548
  //#endregion
5115
5549
  //#region packages/application/src/journey/delegation.ts
5116
5550
  function delegatedDecisionScope(action) {
@@ -5163,6 +5597,11 @@ function delegatedDecisionTargets(action, journeyId) {
5163
5597
  targetIds: [journeyId],
5164
5598
  choice: String(action.answersRevision)
5165
5599
  };
5600
+ if (action.kind === "account-read") return {
5601
+ action: action.kind,
5602
+ targetIds: [journeyId],
5603
+ choice: "retry"
5604
+ };
5166
5605
  return action.kind === "scope-confirm" ? {
5167
5606
  action: action.kind,
5168
5607
  targetIds: [journeyId],
@@ -5179,7 +5618,7 @@ function assertDelegatedDecision(state, action, decision, source) {
5179
5618
  }
5180
5619
  if (action.kind === "cards-confirm") return;
5181
5620
  if (action.kind === "work-access" || action.kind === "works-access") {
5182
- if (action.access === "drop") throw new ApplicationError("CONFIRMATION_REQUIRED");
5621
+ if (action.access === "drop" || action.access === "public") throw new ApplicationError("CONFIRMATION_REQUIRED");
5183
5622
  const ids = action.kind === "work-access" ? [action.workId] : action.workIds;
5184
5623
  if (state.works.some((work) => ids.includes(work.id) && work.accessOrigin === "explicit" && work.access !== action.access)) throw new ApplicationError("CONFIRMATION_REQUIRED");
5185
5624
  return;
@@ -5194,7 +5633,7 @@ function assertDelegatedDecision(state, action, decision, source) {
5194
5633
  if (work?.projectType?.origin === "explicit" && work.projectType.type !== action.type) throw new ApplicationError("CONFIRMATION_REQUIRED");
5195
5634
  return;
5196
5635
  }
5197
- if (action.kind === "scope-confirm") return;
5636
+ if (action.kind === "scope-confirm" || action.kind === "account-read") return;
5198
5637
  if (action.kind !== "group" && action.kind !== "groups-hide") throw new ApplicationError("CONFIRMATION_REQUIRED");
5199
5638
  const ids = action.kind === "group" ? [action.groupId] : action.groupIds;
5200
5639
  const choice = action.kind === "group" ? action.choice : "hide";
@@ -5259,135 +5698,6 @@ function propagateGroups(state, action) {
5259
5698
  };
5260
5699
  }
5261
5700
  //#endregion
5262
- //#region packages/application/src/journey/scope/respond.ts
5263
- /** Classification confirmation fixes visibility and scan membership for this journey. */
5264
- function confirmScope(state, now, decision) {
5265
- if (state.scope.status === "confirmed") return state;
5266
- 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");
5267
- return withReadingProgress({
5268
- ...state,
5269
- scope: {
5270
- status: "confirmed",
5271
- actor: decision ? "delegate" : "human",
5272
- at: now
5273
- },
5274
- works: state.works.map((work) => work.placement === "classifying" ? work : work.accessConfirmed || !work.classificationRecommendation ? {
5275
- ...work,
5276
- accessConfirmed: true
5277
- } : {
5278
- ...work,
5279
- access: work.classificationRecommendation.access,
5280
- accessOrigin: "model",
5281
- accessConfirmed: true
5282
- })
5283
- });
5284
- }
5285
- function setWorkType(state, workId, type, decision) {
5286
- if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
5287
- const projectType = decision ? {
5288
- type,
5289
- reason: "Chosen by your delegate for this journey.",
5290
- origin: "delegate"
5291
- } : {
5292
- type,
5293
- reason: "Chosen by you.",
5294
- origin: "explicit"
5295
- };
5296
- return {
5297
- ...state,
5298
- scope: {
5299
- status: "pending",
5300
- actor: null,
5301
- at: null
5302
- },
5303
- privacy: {
5304
- ...state.privacy,
5305
- cards: [],
5306
- confirmedAnswersRevision: null,
5307
- packageApproval: null
5308
- },
5309
- works: state.works.map((work) => work.id !== workId || decision && work.projectType?.origin === "explicit" ? work : {
5310
- ...work,
5311
- projectType
5312
- })
5313
- };
5314
- }
5315
- /** One rule for single and bulk choices; selection changes recompute reading completion. */
5316
- function setWorksAccess(state, workIds, access, decision) {
5317
- if (new Set(workIds).size !== workIds.length) throw new ApplicationError("INVALID_SELECTION");
5318
- if (workIds.some((id) => !state.works.some((work) => work.id === id))) throw new ApplicationError("UNKNOWN_WORKSPACE");
5319
- if (state.scope.status === "confirmed") return changeConfirmedAccess(state, workIds, access, decision);
5320
- return withReadingProgress({
5321
- ...state,
5322
- scope: {
5323
- status: "pending",
5324
- actor: null,
5325
- at: null
5326
- },
5327
- privacy: {
5328
- ...state.privacy,
5329
- cards: [],
5330
- confirmedAnswersRevision: null,
5331
- packageApproval: null
5332
- },
5333
- works: state.works.map((work) => !workIds.includes(work.id) ? work : {
5334
- ...work,
5335
- revision: work.revision + 1,
5336
- artifact: null,
5337
- status: work.status === "ready" ? "review" : work.status,
5338
- access,
5339
- accessConfirmed: true,
5340
- accessOrigin: decision && work.accessOrigin !== "explicit" ? "delegate" : "explicit"
5341
- })
5342
- });
5343
- }
5344
- /**
5345
- * After confirmation the person, never a delegate, may still change any Work's visibility
5346
- * (D2, superseding ADR 0162's lock and ADR 0188's blocked-only Drop). Drop stops that Work's
5347
- * scan; a Work moved out of Drop is read again; reading progress is recomputed.
5348
- */
5349
- function changeConfirmedAccess(state, workIds, access, decision) {
5350
- if (decision || state.phase === "sending" || state.phase === "completed" || state.works.some((work) => workIds.includes(work.id) && work.access === access)) throw new ApplicationError("INVALID_TRANSITION");
5351
- return withReadingProgress({
5352
- ...state,
5353
- privacy: {
5354
- ...state.privacy,
5355
- packageApproval: null
5356
- },
5357
- works: state.works.map((work) => workIds.includes(work.id) ? {
5358
- ...work,
5359
- revision: work.revision + 1,
5360
- artifact: null,
5361
- status: work.status === "ready" ? "review" : work.status,
5362
- access,
5363
- accessConfirmed: true,
5364
- accessOrigin: "explicit"
5365
- } : work)
5366
- });
5367
- }
5368
- /** Model suggestions fill or refresh model types only; a person's or delegate's type stays. */
5369
- function applyClassification(state, items) {
5370
- if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
5371
- const byWork = new Map(items.map((item) => [item.workId, item]));
5372
- if (byWork.size !== items.length) throw new ApplicationError("INVALID_SELECTION");
5373
- if (items.some((item) => !state.works.some((work) => work.id === item.workId))) throw new ApplicationError("UNKNOWN_WORKSPACE");
5374
- return {
5375
- ...state,
5376
- works: state.works.map((work) => {
5377
- const item = byWork.get(work.id);
5378
- if (!item || work.projectType && work.projectType.origin !== "model") return work;
5379
- return {
5380
- ...work,
5381
- projectType: {
5382
- type: item.type,
5383
- reason: item.reason,
5384
- origin: "model"
5385
- }
5386
- };
5387
- })
5388
- };
5389
- }
5390
- //#endregion
5391
5701
  //#region packages/application/src/journey/respond.ts
5392
5702
  /** A changed privacy instruction requires a new read of the same frozen roots. */
5393
5703
  function invalidateAnalysisIntent(state) {
@@ -5396,7 +5706,7 @@ function invalidateAnalysisIntent(state) {
5396
5706
  ...state,
5397
5707
  works: state.works.map((work) => {
5398
5708
  if (!work.rootSessionIds.length) return work;
5399
- const { emptyReview: _empty, excludedReview: _excluded, ...retired } = retireWorkDescription(work);
5709
+ const { reviewInterruptions: _reviewInterruptions, emptyReview: _empty, excludedReview: _excluded, ...retired } = retireWorkDescription(work);
5400
5710
  return {
5401
5711
  ...retired,
5402
5712
  title: work.explicitTitleId ? work.title : retired.title,
@@ -5411,7 +5721,7 @@ function invalidateAnalysisIntent(state) {
5411
5721
  })
5412
5722
  });
5413
5723
  }
5414
- function respondToJourney(state, action, source, now, decision, preferenceScope) {
5724
+ function respondToJourney(state, action, source, now, decision, preferenceScope, accountRead, account) {
5415
5725
  if (preferenceScope && (decision || !["group", "groups-hide"].includes(action.kind))) throw new ApplicationError("INVALID_ARGUMENTS");
5416
5726
  if (state.abandonedAt) throw new ApplicationError("INVALID_TRANSITION");
5417
5727
  if (action.kind === "abandon") {
@@ -5465,12 +5775,14 @@ function respondToJourney(state, action, source, now, decision, preferenceScope)
5465
5775
  ...state,
5466
5776
  phase: "welcome"
5467
5777
  };
5778
+ if (account?.configured && !account.subject) throw new ApplicationError("JOURNEY_SIGN_IN_REQUIRED");
5468
5779
  return {
5469
5780
  ...state,
5470
5781
  consent: {
5471
5782
  provider: state.provider,
5472
5783
  source,
5473
- allowedAt: now
5784
+ allowedAt: now,
5785
+ ...account?.subject ? { subject: account.subject } : {}
5474
5786
  },
5475
5787
  phase: "discovering"
5476
5788
  };
@@ -5525,10 +5837,21 @@ function respondToJourney(state, action, source, now, decision, preferenceScope)
5525
5837
  };
5526
5838
  if (action.kind === "scope-confirm") {
5527
5839
  if (source !== "conversation" || decision) throw new ApplicationError("CONFIRMATION_REQUIRED");
5528
- return confirmScope(state, now);
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);
5529
5848
  }
5530
5849
  if (action.kind === "card-answer" || action.kind === "cards-confirm" || action.kind === "package-confirm") throw new ApplicationError("INVALID_TRANSITION");
5531
- if (action.kind === "works-access") return setWorksAccess(state, action.workIds, action.access, decision);
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");
5851
+ if (action.kind === "works-access") return setWorksAccess(state, action.workIds, action.access, decision, source === "panel" ? {
5852
+ channel: source,
5853
+ at: now
5854
+ } : action.accessChoice);
5532
5855
  if (action.kind === "group" || action.kind === "groups-hide") {
5533
5856
  const ids = action.kind === "group" ? [action.groupId] : action.groupIds;
5534
5857
  if (new Set(ids).size !== ids.length || ids.some((id) => !state.groups.some((group) => group.id === id))) throw new ApplicationError("UNKNOWN_ITEM");
@@ -5542,6 +5865,48 @@ function respondToJourney(state, action, source, now, decision, preferenceScope)
5542
5865
  const workId = action.workId;
5543
5866
  if (!state.works.some((work) => work.id === workId)) throw new ApplicationError("UNKNOWN_WORKSPACE");
5544
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
+ }
5545
5910
  if (action.kind === "retry-work") {
5546
5911
  if (state.works.find((item) => item.id === workId)?.status !== "blocked" || state.phase === "sending" || state.phase === "completed") throw new ApplicationError("INVALID_TRANSITION");
5547
5912
  return withReadingProgress({
@@ -5559,7 +5924,10 @@ function respondToJourney(state, action, source, now, decision, preferenceScope)
5559
5924
  })
5560
5925
  });
5561
5926
  }
5562
- return setWorksAccess(state, [workId], action.access, decision);
5927
+ return setWorksAccess(state, [workId], action.access, decision, source === "panel" ? {
5928
+ channel: source,
5929
+ at: now
5930
+ } : action.accessChoice);
5563
5931
  }
5564
5932
  //#endregion
5565
5933
  //#region packages/application/src/journey/questions.ts
@@ -6290,10 +6658,19 @@ function digest$1(value) {
6290
6658
  * quotes stay out. The agent reads override values from `journey.get` and the guidance.
6291
6659
  */
6292
6660
  function journeyView(state, now) {
6293
- const { exceptions, instructions, corrections, valueOverrides, ...safe } = state;
6661
+ const { exceptions, instructions, corrections, valueOverrides, consent, accountRead, ...safe } = state;
6294
6662
  const terms = activePrivacyTerms(state);
6295
6663
  return {
6296
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
+ } } : {},
6297
6674
  displayedScopeDigest: displayedScopeDigest(state),
6298
6675
  privacy: {
6299
6676
  ...state.privacy,
@@ -6302,10 +6679,12 @@ function journeyView(state, now) {
6302
6679
  works: state.works.map((work) => {
6303
6680
  const localTitle = work.suggestion || work.explicitTitleId ? work.title : work.classificationMetadata?.title ?? work.title;
6304
6681
  const imagesOmitted = hasCurrentStaticReview(state, work) ? work.staticReview?.imagesOmitted ?? 0 : 0;
6682
+ const source = publicSource(work);
6305
6683
  if (!work.classificationMetadata) return {
6306
6684
  ...work,
6307
6685
  localTitle,
6308
- imagesOmitted
6686
+ imagesOmitted,
6687
+ publicSource: source
6309
6688
  };
6310
6689
  const metadata = work.classificationMetadata;
6311
6690
  const displayed = {
@@ -6319,6 +6698,7 @@ function journeyView(state, now) {
6319
6698
  ...displayed,
6320
6699
  localTitle,
6321
6700
  imagesOmitted,
6701
+ publicSource: source,
6322
6702
  projectType: work.projectType ? {
6323
6703
  ...work.projectType,
6324
6704
  reason: work.projectType.origin === "model" ? displayed.reason : work.projectType.reason
@@ -6351,6 +6731,59 @@ function journeyView(state, now) {
6351
6731
  }
6352
6732
  /** Works rendered at once by one several-Work call. */
6353
6733
  const renderConcurrency = 4;
6734
+ const inFlightRenders = /* @__PURE__ */ new WeakMap();
6735
+ const renderScopeTails = /* @__PURE__ */ new WeakMap();
6736
+ function waitForRender(pending, signal) {
6737
+ signal.throwIfAborted();
6738
+ return new Promise((resolve, reject) => {
6739
+ const abort = () => reject(signal.reason);
6740
+ signal.addEventListener("abort", abort, { once: true });
6741
+ pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
6742
+ });
6743
+ }
6744
+ async function acquireRenderScope(port, signal) {
6745
+ const previous = renderScopeTails.get(port);
6746
+ const gate = Promise.withResolvers();
6747
+ const tail = (previous ?? Promise.resolve()).then(() => gate.promise);
6748
+ renderScopeTails.set(port, tail);
6749
+ const release = () => {
6750
+ gate.resolve();
6751
+ if (renderScopeTails.get(port) === tail) renderScopeTails.delete(port);
6752
+ };
6753
+ try {
6754
+ await waitForRender(previous ?? Promise.resolve(), signal);
6755
+ return {
6756
+ release,
6757
+ waited: previous !== void 0
6758
+ };
6759
+ } catch (error) {
6760
+ tail.then(() => {
6761
+ if (renderScopeTails.get(port) === tail) renderScopeTails.delete(port);
6762
+ });
6763
+ gate.resolve();
6764
+ throw error;
6765
+ }
6766
+ }
6767
+ async function joinRender(port, journeyId, workId, run, signal, retryConflict = true) {
6768
+ let pending = inFlightRenders.get(port);
6769
+ if (!pending) {
6770
+ pending = /* @__PURE__ */ new Map();
6771
+ inFlightRenders.set(port, pending);
6772
+ }
6773
+ const key = `${journeyId}:${workId}`;
6774
+ const existing = pending.get(key);
6775
+ if (existing) try {
6776
+ return await waitForRender(existing, signal);
6777
+ } catch (error) {
6778
+ signal.throwIfAborted();
6779
+ if (retryConflict && safeCode(error) === "REVISION_CONFLICT") return joinRender(port, journeyId, workId, run, signal, false);
6780
+ throw error;
6781
+ }
6782
+ const entries = pending;
6783
+ const result = Promise.resolve().then(run).finally(() => entries.delete(key));
6784
+ entries.set(key, result);
6785
+ return result;
6786
+ }
6354
6787
  function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).toISOString(), local) {
6355
6788
  /** Callers present after their write is durable, so a concurrent commit must not turn it into an error. */
6356
6789
  async function present(committed) {
@@ -6384,12 +6817,14 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6384
6817
  status: state.phase === "paused" && classificationProgress.status === "running" ? "paused" : classificationProgress.status
6385
6818
  } : null;
6386
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;
6387
6821
  return {
6388
6822
  ...view,
6389
6823
  ...reads,
6390
6824
  packaging,
6391
6825
  preparationProgress,
6392
- classificationProgress: liveClassificationProgress
6826
+ classificationProgress: liveClassificationProgress,
6827
+ accountReadCurrent: state.accountRead?.status === "read" && typeof subject === "string" && state.accountRead.subject === subject
6393
6828
  };
6394
6829
  }
6395
6830
  }
@@ -6623,18 +7058,50 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6623
7058
  "works-access",
6624
7059
  "card-answer",
6625
7060
  "profile-address",
6626
- "profile-links"
7061
+ "profile-links",
7062
+ "account-read"
6627
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
+ };
6628
7076
  for (let attempt = 0;; attempt++) {
6629
- const result = await commitResponse(input, source, rebase);
7077
+ const result = await commitResponse(input, source, rebase, readAccount);
6630
7078
  if (result.status !== "stale" || !rebase || attempt >= 4) return result;
6631
7079
  }
6632
7080
  }
6633
- async function commitResponse(input, source, rebase) {
7081
+ async function commitResponse(input, source, rebase, readAccount) {
6634
7082
  const current = await storage.current(input.journeyId);
6635
7083
  if (!current) throw new ApplicationError("UNKNOWN_ITEM");
6636
7084
  const expectedRevision = rebase ? current.revision : input.expectedRevision;
6637
- const state = current.revision === expectedRevision ? respondToJourney(current, input.action, source, now(), input.decision, input.preferenceScope) : 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;
6638
7105
  const groupIds = input.action.kind === "group" ? [input.action.groupId] : input.action.kind === "groups-hide" ? input.action.groupIds : [];
6639
7106
  const preferenceScopes = input.decision ? [] : state.groups.flatMap((group) => groupIds.includes(group.id) && group.preferenceScope ? [{
6640
7107
  groupId: group.id,
@@ -6645,6 +7112,12 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6645
7112
  requestKey: input.idempotencyKey,
6646
7113
  requestHash: digest$1(input),
6647
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
+ } } : {},
6648
7121
  event: {
6649
7122
  journeyId: state.journeyId,
6650
7123
  kind: "response",
@@ -6674,12 +7147,27 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6674
7147
  await setTimeout$1(Math.min(1e3, Math.max(0, deadline - Date.now())), void 0, { signal });
6675
7148
  }
6676
7149
  }
6677
- async function prepare(value, signal) {
7150
+ async function runPreparation(value, signal) {
6678
7151
  const input = nativeGetSchema.required().parse(value);
6679
7152
  if (!local) throw new ApplicationError("INVALID_TRANSITION");
6680
- await local.hide.recover(input.journeyId, signal ?? new AbortController().signal);
6681
- const state = await local.prepare(input.journeyId, signal);
6682
- 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
+ };
6683
7171
  }
6684
7172
  async function read(value) {
6685
7173
  return storage.readBatch(journeyBatchReadSchema.parse(value));
@@ -6750,7 +7238,7 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6750
7238
  const release = await port.renderActivity?.begin(input.journeyId, workIds);
6751
7239
  let result;
6752
7240
  try {
6753
- result = "workId" in input ? { state: await port.render(input.journeyId, input.workId, signal) } : {
7241
+ result = "workId" in input ? { state: await joinRender(port, input.journeyId, input.workId, () => port.render(input.journeyId, input.workId, signal), signal) } : {
6754
7242
  renders: await renderSeveral(port, input.journeyId, workIds, signal),
6755
7243
  state: await currentState(input.journeyId)
6756
7244
  };
@@ -6770,35 +7258,51 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6770
7258
  }
6771
7259
  async function renderSeveral(port, journeyId, workIds, signal) {
6772
7260
  const renders = [];
6773
- let next = 0;
6774
- const scope = await port.renderScope?.(journeyId, workIds.length, signal);
6775
- const renderWork = scope ? (workId) => scope.render(workId) : (workId) => port.render(journeyId, workId, signal);
7261
+ const lease = await acquireRenderScope(port, signal);
6776
7262
  try {
6777
- const failure = (await Promise.allSettled(Array.from({ length: Math.min(scope?.concurrency ?? renderConcurrency, workIds.length) }, async () => {
6778
- for (let workId = workIds[next++]; workId !== void 0; workId = workIds[next++]) {
6779
- try {
6780
- await renderWork(workId);
6781
- renders.push({
6782
- workId,
6783
- status: "rendered"
6784
- });
6785
- } catch (error) {
6786
- signal.throwIfAborted();
6787
- port.renderFailed?.(workId, error);
6788
- renders.push({
6789
- workId,
6790
- status: "failed",
6791
- code: safeCode(error)
6792
- });
7263
+ const current = lease.waited ? await currentState(journeyId) : void 0;
7264
+ const completed = new Set(current?.works.filter((work) => work.artifact?.revision === work.revision && work.status === "ready" && !current.pendingHideId && current.phase !== "paused").map((work) => work.id));
7265
+ const remaining = workIds.filter((workId) => {
7266
+ if (!completed.has(workId)) return true;
7267
+ renders.push({
7268
+ workId,
7269
+ status: "rendered"
7270
+ });
7271
+ return false;
7272
+ });
7273
+ let next = 0;
7274
+ const scope = remaining.length ? await port.renderScope?.(journeyId, remaining.length, signal) : void 0;
7275
+ const renderWork = scope ? (workId) => scope.render(workId) : (workId) => port.render(journeyId, workId, signal);
7276
+ try {
7277
+ const failure = (await Promise.allSettled(Array.from({ length: Math.min(scope?.concurrency ?? renderConcurrency, remaining.length) }, async () => {
7278
+ for (let workId = remaining[next++]; workId !== void 0; workId = remaining[next++]) {
7279
+ try {
7280
+ await joinRender(port, journeyId, workId, () => renderWork(workId), signal);
7281
+ renders.push({
7282
+ workId,
7283
+ status: "rendered"
7284
+ });
7285
+ } catch (error) {
7286
+ signal.throwIfAborted();
7287
+ port.renderFailed?.(workId, error);
7288
+ renders.push({
7289
+ workId,
7290
+ status: "failed",
7291
+ code: safeCode(error)
7292
+ });
7293
+ }
7294
+ await new Promise((resolve) => setImmediate(resolve));
6793
7295
  }
6794
- await new Promise((resolve) => setImmediate(resolve));
6795
- }
6796
- }))).find((item) => item.status === "rejected");
6797
- if (failure) throw failure.reason;
7296
+ }))).find((item) => item.status === "rejected");
7297
+ if (failure) throw failure.reason;
7298
+ } finally {
7299
+ await scope?.close();
7300
+ }
7301
+ const order = new Map(workIds.map((workId, index) => [workId, index]));
7302
+ return renders.toSorted((a, b) => (order.get(a.workId) ?? 0) - (order.get(b.workId) ?? 0));
6798
7303
  } finally {
6799
- await scope?.close();
7304
+ lease.release();
6800
7305
  }
6801
- return renders.toSorted((a, b) => workIds.indexOf(a.workId) - workIds.indexOf(b.workId));
6802
7306
  }
6803
7307
  async function findings(value, signal = new AbortController().signal) {
6804
7308
  if (!local) throw new ApplicationError("INVALID_TRANSITION");
@@ -6888,7 +7392,32 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6888
7392
  const current = await storage.current(input.journeyId);
6889
7393
  if (!current) throw new ApplicationError("UNKNOWN_ITEM");
6890
7394
  const expectedRevision = rebase ? current.revision : input.expectedRevision;
6891
- const state = current.revision !== expectedRevision ? current : "resolutions" in input ? resolvePrivacyTerms(current, input) : publishDecisionCards(current, input);
7395
+ let state;
7396
+ try {
7397
+ state = current.revision !== expectedRevision ? current : "resolutions" in input ? resolvePrivacyTerms(current, input) : publishDecisionCards(current, input);
7398
+ } catch (error) {
7399
+ if (!rebase || current.revision <= input.expectedRevision || !(error instanceof ApplicationError) || error.code !== "INVALID_SELECTION") throw error;
7400
+ const receipt = await storage.commit({
7401
+ expectedRevision: input.expectedRevision,
7402
+ requestKey: input.idempotencyKey,
7403
+ requestHash: digest$1(input),
7404
+ state: current,
7405
+ event: {
7406
+ journeyId: current.journeyId,
7407
+ kind: "response",
7408
+ source: "conversation",
7409
+ at: now()
7410
+ }
7411
+ }).catch((probeError) => {
7412
+ if (probeError instanceof ApplicationError && probeError.code === "OPERATION_CONFLICT") throw error;
7413
+ throw probeError;
7414
+ });
7415
+ if (receipt.status === "replayed") return {
7416
+ ...receipt,
7417
+ state: await present(receipt.state)
7418
+ };
7419
+ throw error;
7420
+ }
6892
7421
  const result = await storage.commit({
6893
7422
  expectedRevision,
6894
7423
  requestKey: input.idempotencyKey,
@@ -6934,8 +7463,10 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6934
7463
  events,
6935
7464
  groupItems: (value) => readGroupItems(storage, value),
6936
7465
  /** ADR 0086: one pack of read units and the values found in them. */
7466
+ reopenEmpty: (value) => storage.reopenEmpty(emptyReviewCorrectionSchema.parse(value)),
6937
7467
  terms: async (value) => reviewTermsResultSchema.parse(await storage.recordTerms(reviewTermsInputSchema.parse(value))),
6938
7468
  prepare,
7469
+ prepareReceipt,
6939
7470
  read,
6940
7471
  guidance,
6941
7472
  inspect,
@@ -10579,7 +11110,7 @@ const bindingFields = {
10579
11110
  };
10580
11111
  const inspectionBindingSchema = z.union([z.object({
10581
11112
  ...bindingFields,
10582
- detectorVersion: z.literal(currentDetectorVersion),
11113
+ detectorVersion: z.enum([currentDetectorVersion, "static-catalog-11"]),
10583
11114
  dictionaryVersion: z.literal(entityDictionaryVersion)
10584
11115
  }).strict(), z.object({
10585
11116
  ...bindingFields,
@@ -11749,6 +12280,48 @@ const storedCredentialSchema = accountProfileSchema.extend({
11749
12280
  expiresAt: z.string().min(1).max(64)
11750
12281
  }).refine((value) => profileUrlMatchesIdentity(value.profileUrl, value.identityUrl));
11751
12282
  //#endregion
12283
+ //#region packages/application/src/account/renew.ts
12284
+ const renewBeforeMs = 12e4;
12285
+ const renewTimeoutMs = 3e4;
12286
+ const renewals = /* @__PURE__ */ new WeakMap();
12287
+ function renew(account, credential) {
12288
+ function configured(value) {
12289
+ return value?.identityUrl === account.configuration.identityUrl && value.clientId === account.configuration.clientId ? value : null;
12290
+ }
12291
+ const running = renewals.get(account);
12292
+ if (running) return running;
12293
+ const renewing = (async () => {
12294
+ try {
12295
+ const stored = configured((await account.credentials.update((state) => state)).credential);
12296
+ if (!stored || stored.refreshToken !== credential.refreshToken) return stored;
12297
+ const token = await account.identity.refresh(credential.refreshToken, AbortSignal.timeout(renewTimeoutMs));
12298
+ if (!token) return configured((await account.credentials.update((state) => {
12299
+ if (state.credential?.refreshToken === credential.refreshToken) {
12300
+ state.credential = null;
12301
+ state.connection = null;
12302
+ state.lastError = "SIGN_IN_EXPIRED";
12303
+ }
12304
+ return state;
12305
+ })).credential);
12306
+ return configured((await account.credentials.update((value) => {
12307
+ if (value.credential?.refreshToken === credential.refreshToken) value.credential = {
12308
+ ...value.credential,
12309
+ accessToken: token.accessToken,
12310
+ refreshToken: token.refreshToken ?? credential.refreshToken,
12311
+ scope: token.scope,
12312
+ issuedAt: account.now(),
12313
+ expiresAt: token.expiresAt
12314
+ };
12315
+ return value;
12316
+ })).credential);
12317
+ } finally {
12318
+ renewals.delete(account);
12319
+ }
12320
+ })();
12321
+ renewals.set(account, renewing);
12322
+ return renewing;
12323
+ }
12324
+ //#endregion
11752
12325
  //#region packages/application/src/account/poll-interval.ts
11753
12326
  /** RFC 8628 section 3.5: each `slow_down` lengthens the interval by five seconds. */
11754
12327
  const slowDownStepMs = 5e3;
@@ -12037,11 +12610,41 @@ function createSharedAccount(ports) {
12037
12610
  return stop(true);
12038
12611
  },
12039
12612
  async verify() {
12040
- const saved = (await read()).credential;
12613
+ let saved = (await read()).credential;
12041
12614
  if (!saved || !belongsToConfiguration(saved)) throw new ApplicationError("ACCOUNT_SIGNED_OUT");
12042
- const result = await ports.connection.check(saved.accessToken, shutdown.signal);
12615
+ let result;
12616
+ let renewing = false;
12617
+ try {
12618
+ const left = Date.parse(saved.expiresAt) - Date.parse(ports.now());
12619
+ if (saved.refreshToken && (!Number.isFinite(left) || left <= 12e4)) {
12620
+ renewing = true;
12621
+ saved = await renew(ports, {
12622
+ ...saved,
12623
+ refreshToken: saved.refreshToken
12624
+ });
12625
+ renewing = false;
12626
+ if (!saved) return view(await read());
12627
+ }
12628
+ result = await ports.connection.check(saved.accessToken, shutdown.signal);
12629
+ if (result === "rejected" && saved.refreshToken) {
12630
+ renewing = true;
12631
+ saved = await renew(ports, {
12632
+ ...saved,
12633
+ refreshToken: saved.refreshToken
12634
+ });
12635
+ renewing = false;
12636
+ if (!saved) return view(await read());
12637
+ result = await ports.connection.check(saved.accessToken, shutdown.signal);
12638
+ }
12639
+ } catch (error) {
12640
+ if (safeCode(error) !== "IDENTITY_UNAVAILABLE" && !(renewing && saved && !expired(saved.expiresAt, ports.now()))) throw error;
12641
+ const detail = typeof error === "object" && error !== null && "detail" in error ? ` (${String(error.detail)})` : "";
12642
+ console.error(`Account verification failed: ${safeCode(error)}${detail}`);
12643
+ result = "unavailable";
12644
+ }
12645
+ const checked = saved;
12043
12646
  return view(await update((state) => {
12044
- if (sameCredential(state.credential, saved)) {
12647
+ if (checked && sameCredential(state.credential, checked)) {
12045
12648
  if (result === "rejected") {
12046
12649
  state.credential = null;
12047
12650
  state.connection = null;
@@ -13345,7 +13948,6 @@ z.object({
13345
13948
  }).strict();
13346
13949
  //#endregion
13347
13950
  //#region packages/application/src/journey/scan-equivalence.ts
13348
- /** State authority relevant to AI meaning; unrelated Work progress does not invalidate publication. */
13349
13951
  function scanBatchContextDigest(state, workId) {
13350
13952
  const work = state.works.find((work) => work.id === workId);
13351
13953
  if (!work) throw new ApplicationError("REVISION_CONFLICT");
@@ -13448,4 +14050,4 @@ function equivalenceDuplicates(batch) {
13448
14050
  return bindings.sort();
13449
14051
  }
13450
14052
  //#endregion
13451
- export { settingsBatchSchema as $, reviewTransportBytes as $n, isReadingComplete as $r, projectPreparationProgress as $t, deviceAuthorizationSchema as A, maximumHideIndexTokens as An, applyCardAnswers as Ar, nativeAdapterVersions as At, inspectionStartSchema as B, confirmedHideTerms as Bn, staticSessionResourceSchema as Br, classificationWorks as Bt, rebindReviewOverlay as C, confirmedHideDeltaSchema as Cn, journeyFindingImpactSchema as Cr, sendPrepareSchema as Ct, staticallyHiddenFindings as D, termHideAdditions as Dn, journeyRenderSchema as Dr, currentJourneyAdapters as Dt, guardedFindingText as E, settleConfirmedHides as En, journeyRenderInputSchema as Er, settleCorrections as Et, sourceIdentityEvidence as F, bTermMatcher as Fn, nativeRespondSchema as Fr, journeyBatchReadSchema as Ft, validateInspectionClaim as G, retireWorkDescription as Gn, sendArtifactSchema as Gr, journeyContextSchema as Gt, validateInspectionBatches as H, journeyConfirmedHides as Hn, agentSourceSchema as Hr, newJourneyWork as Ht, withSourceEvidence as I, bWordBounded as In, mergePrivacyTerms as Ir, journeyReviewStorageQuerySchema as It, validateInspectionInputs as J, workCorrectionBinding as Jn, sendResolutionSchema as Jr, hidePublishSchema as Jt, validateInspectionFence as K, workDecisionDigest as Kn, sendReceiptSchema as Kr, hideBeginSchema as Kt, agentSetupKey as L, confirmedHideGroupIds as Ln, hasCurrentStaticReview as Lr, applyClassificationProposal as Lt, profileUrlMatchesIdentity as M, bContainment as Mn, privacyDecisionDigest as Mr, validateAttemptAppend as Mt, readAccountAvatarUrl as N, bExceptionRanges as Nn, nativeEventsSchema as Nr, journeyGuidance as Nt, createAccount as O, addHideIndexTokens as On, displayedScopeDigest as Or, obsoleteJourney as Ot, storedCredentialSchema as P, bMaskedHits as Pn, nativeGetSchema as Pr, applyClassification as Pt, inspectionInputs as Q, reviewRepairResourceSchema as Qn, isIncludedWork as Qr, journeyPolicyRespondSchema as Qt, createApplication as R, confirmedHideKey as Rn, staticReviewDigest as Rr, classificationCommitSchema as Rt, rebindReviewAlignment as S, confirmedHideAdditions as Sn, journeyClassificationSessionsSchema as Sr, sendChoiceSchema as St, reviewBatchReuse as T, renderedHideValue as Tn, journeyPreviewSchema as Tr, sendStartSchema as Tt, validateInspectionRequest as U, maskedHitsStale as Un, agentSourcesSchema as Ur, reviewCompileSchema as Ut, validateInspectionBatch as V, dictionaryHides as Vn, agentSourcePathSegment as Vr, retireScanGeneration as Vt, inspectionQuerySchema as W, syncTermGroups as Wn, journeySendSchema as Wr, journeyContextQuerySchema as Wt, validateInspectionContribution as X, reviewCandidateResourceSchema as Xn, hasCompleteWorkDescription as Xr, journeySearchSaveSchema as Xt, validateInspectionQuery as Y, reviewCandidateInputSchema as Yn, sendSettingsSchema as Yr, journeySearchResultSchema as Yt, validateDetectionSummary as Z, reviewRepairInputSchema as Zn, isClassifying as Zr, searchScopeDigest as Zt, changeJourney as _, selectFrozenConfiguration as _i, sessionUsefulnessInputSchema as _n, emptyPreparationFacts as _r, emptyTermsRevision as _t, bCrossSegmentReviewSchema as a, packageCommandSchema as ai, reviewAttemptResourceSchema as an, scanPreparationDigest as ar, defaultAgentSetupInclusion as at, isGenericPublicTerm as b, journeyCommitSchema as bn, preparationStartSchema as br, replaceSensitiveTermsSchema as bt, reconstructBFields as c, gitMetadataBudgetBytes as ci, sessionReconcileInputSchema as cn, scanGitSeedsResource as cr, resolveFailureLocation as ct, resolveGroupSplit as d, sourceChangePolicy as di, sessionRereadInputSchema as dn, validateScanExclusion as dr, manualUploadBindingSchema as dt, isSelectedWork as ei, journeyResourceQuerySchema as en, bReviewDigest as er, settingsQuerySchema as et, selectedValueMembers as f, recommendParallelism as fi, sessionRereadResourceSchema as fn, resolveSourceSelection as fr, manualUploadChoiceSchema as ft, semanticGroupKey as g, freezeConfiguration as gi, sessionReviewStatusSchema as gn, journeyPreparationProgressSchema as gr, candidateSortAvailable as gt, hideScopeDigest as h, defaultScopedSettings as hi, sessionReviewResourceSchema as hn, journeyInventorySchema as hr, manualUploadStartSchema as ht, validateBCrossSegmentReview as i, commandSchema as ii, sortingApplySchema as in, scanCanonicalResourceSchema as ir, classifyAgentSetup as it, deviceProfileScopeSchema as j, packHideIndex as jn, assertJourneyPrivacyReady as jr, attemptBudgetCodePoints as jt, accountProfileSchema as k, hideKeyId as kn, requestedScope as kr, metadataProcessingVersion as kt, chooseCheapPlacement as l, producerVersion as li, sessionReconcileResultSchema as ln, verifiedScanGitSeeds as lr, validateFailureLocation as lt, activeJourneyTerms as m, effectiveTreatmentSchema as mi, sessionReviewQuerySchema as mn, journeyInventoryReadSchema as mr, manualUploadSchema as mt, scanBatchEquivalenceDigest as n, selectedWorks as ni, journeyIsolateSchema as nn, planBRepair as nr, reviewInventory as nt, bRereadDigest as o, selectionBatchSchema as oi, reviewAttemptsQuerySchema as on, scanSessionResourceSchema as or, maximumAgentSetupSessions as ot, splitSelectorValues as p, approvedTreatmentSchema as pi, sessionReviewInputSchema as pn, journeyInventoryQuerySchema as pr, manualUploadPrepareSchema as pt, validateInspectionHead as q, workPrivacyDecisions as qn, sendReplaceResultSchema as qr, hideFinishSchema as qt, locateTermEvidence as r, withReadingProgress as ri, journeyPlaceSchema as rn, scanAliasResourceSchema as rr, appendSensitiveTerms as rt, reconcileBSession as s, selectionSchema as si, reviewAttemptsResultSchema as sn, scanGitSeedsId as sr, artifactFailureDetailSchema as st, scanBatchContextDigest as t, isSkippedWork as ti, journeyResourceSchema as tn, mergeBRepair as tr, readWorkspaceOutcomes as tt, reviseGroupMembership as u, stateSchema as ui, sessionReconciliationResourceSchema as un, scanExclusionResourceSchema as ur, manualUploadActionSchema as ut, projectJourneyInventory as v, staticCatalogRef as vi, sessionUsefulnessResourceSchema as vn, preparationBeginSchema as vr, maximumTermsFileBytes as vt, rebindReviewSubmit as w, readConfirmedHideChange as wn, journeyFindingsSchema as wr, sendRequestSchema as wt, bReviewScope as x, journeyEventSchema as xn, validatePreparationFacts as xr, sendActionSchema as xt, preparationScope as y, journeyCommitResultSchema as yn, preparationSettlementSchema as yr, normalizeSensitiveTerms as yt, inspectionCancelSchema as z, confirmedHideKinds as zn, staticSessionId as zr, classificationPartition 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 };