@gitroll/tacitt-cli 0.6.8 → 0.6.10

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 (46) hide show
  1. package/README.md +1 -1
  2. package/dist/{agent-setup-YlFIJz-h.mjs → agent-setup-LPd8WEsN.mjs} +2 -2
  3. package/dist/agent-setup-worker.mjs +5 -5
  4. package/dist/cli.mjs +19 -10
  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 +5 -5
  8. package/dist/{frame-Dj5Zs6UW.mjs → frame-ByT1xayJ.mjs} +1 -1
  9. package/dist/{heap-DsE74_-n.mjs → heap-CM298Yu0.mjs} +8 -6
  10. package/dist/inspection-worker.mjs +4 -4
  11. package/dist/{job-CeSf9UCC.mjs → job-BHIwElNF.mjs} +5 -5
  12. package/dist/{locate-BWkblijS.mjs → locate-Cwv6WH8n.mjs} +25 -2
  13. package/dist/{model-f_QnDyyb.mjs → model-zvn5GUJ9.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 +1 -1
  17. package/dist/{query-CaBXQlpF.mjs → query-By41gaKm.mjs} +585 -250
  18. package/dist/{run-RA_8zZsL.mjs → run-ZQx7p4Rw.mjs} +4 -4
  19. package/dist/{src-p_uJNjV3.mjs → src-3jvMvSQn.mjs} +3 -3
  20. package/dist/{src-d7n58sbH.mjs → src-BqtdFVq7.mjs} +683 -341
  21. package/dist/{src-BBjnljUs.mjs → src-D_DfY--2.mjs} +4 -4
  22. package/dist/store-database-worker.mjs +1 -1
  23. package/dist/web/assets/index-B14e6_-z.js +174 -0
  24. package/dist/web/index.html +1 -1
  25. package/package.json +1 -1
  26. package/skills/tacitt/INDEX.md +6 -6
  27. package/skills/tacitt/SKILL.md +4 -4
  28. package/skills/tacitt/references/INDEX.md +3 -3
  29. package/skills/tacitt/references/classification.md +12 -2
  30. package/skills/tacitt/references/commands.md +60 -13
  31. package/skills/tacitt/references/journey.md +10 -2
  32. package/skills/tacitt/references/parallel-review.md +21 -0
  33. package/skills/tacitt/references/send.md +29 -8
  34. package/skills/tacitt/scripts/INDEX.md +39 -36
  35. package/skills/tacitt/scripts/journey-classification-validation.d.mts +13 -0
  36. package/skills/tacitt/scripts/journey-classification-validation.mjs +38 -0
  37. package/skills/tacitt/scripts/journey-scan-timing.d.mts +15 -0
  38. package/skills/tacitt/scripts/journey-scan-timing.mjs +153 -1
  39. package/skills/tacitt/scripts/journey-state.d.mts +6 -0
  40. package/skills/tacitt/scripts/journey-state.mjs +24 -1
  41. package/skills/tacitt/scripts/journey.d.mts +5 -0
  42. package/skills/tacitt/scripts/journey.mjs +48 -3
  43. package/skills/tacitt/scripts/privacy-validator.mjs +11 -1
  44. package/skills/tacitt/scripts/review.mjs +294 -38
  45. package/LICENSE +0 -11
  46. package/dist/web/assets/index-B6kJ6XXF.js +0 -174
@@ -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 bCountSchema, $n as detectStatic, A as bDuplicateMirror, B as bEvidenceReader, Bt as sessionPeriodSchema, C as validateBUnit, Cn as inspectionSemanticVersions, Ct as cardsSubmitSchema, D as uncoveredBObligations, Dn as createStaticProcessor, Dt as privacyResolveSchema, E as bLiterals, Et as packageConfirmActionSchema, Fn as entityDictionaryVersion, Ft as termTypeInfo, G as subtractBRanges, Gn as validateProfile, H as codePointLength, Ht as journeyProfileAddressActionSchema, I as bIssue, It as termTypeSchema, J as bRetainedGroupSchema, Jn as validatePlan, Jt as respondProfileLinks, Kn as orderProfile, L as bOverlaps, Lt as termTypes, M as bLegacyBindings, Mt as reviewTermsResultSchema, N as bUnitFragments, Nt as termAssessmentSchema, O as indexBRanges, On as staticDefaults, Ot as privacyTermKey, P as bAdoptedObligations, Pn as cleanupImplementationVersion, Pt as termOverrideValueSchema, Q as bAnchorSchema, Qt as safeCode, R as unownedBObligations, Rt as deselectOutsidePeriod, S as validateBFields, Sn as currentDetectorVersion, St as cardsConfirmActionSchema, T as bLiteralTerms, Tn as gapReasons, Tt as emptyJourneyPrivacy, U as codePointSlice, Ut as journeyProfileLinksActionSchema, V as bDigest, Vt as emptyJourneyProfile, W as createCodePointIndex, X as bSessionDecisionSchema, Xt as ApplicationError, Y as bReviewSubmissionSchema, Yn as copyData, Yt as classificationTitleMaskSchema, Z as bTermSchema, Zt as UnfinishedJourneyError, _ as semanticFindingSchema, _t as hideConfirmSchema, a as reviewChunkSchema, an as observeSource, at as journeyExceptionSchema, b as validateBReview, bt as journeyConfigurationSchema, c as textDigest, cn as inventorySchema, cr as scalarBefore, ct as journeyPreferenceScopeSchema, dt as nativeJourneySchema, en as familyNeedsReview, et as bIdSchema, f as reviewAlignmentAuditSchema, fn as nativeSourceIdentitySchema, ft as journeyGuidanceSchema, g as validateReviewAlignment, h as reviewFindingsDigest, ht as journeyDescribeSchema, i as reviewBatchSchema, in as sourceKey, ir as createEntityMatcher, it as journeyDigestSchema, jt as reviewTermsInputSchema, kn as staticOptions, l as validateReview, lr as defaultDetectionLimits, lt as journeyProjectTypeSchema, m as reviewAlignmentSchema, mt as delegatedDecisionSchema, n as compileBFindings, nn as reviewFamilies, nt as journeyAccessSchema, o as reviewDigest, on as sourceSnapshotId, ot as journeyIdSchema, p as reviewAlignmentInputSchema, pt as journeyInstructionInputSchema, q as bRepresentationSchema, r as reviewBatchChunks, rn as sourceGraph, rt as journeyChoiceSchema, s as reviewSubmitSchema, sn as fileIdentitySchema, sr as scalarAfter, st as journeyKindSchema, tn as familySelected, tr as staticCategories, tt as bMethod, u as findingReferenceKey, un as sourcePartSchema, ut as journeyWorkTypeSchema, vt as hidePlanInputSchema, w as bLiteralAdmitted, x as createBAnalysisUnits, xt as cardAnswerActionSchema, y as validateBClassification, yt as hidePlanResourceSchema, z as bAnchorKey } from "./locate-Cwv6WH8n.mjs";
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;
@@ -1192,152 +1199,6 @@ function requireSelecting(run) {
1192
1199
  if (run.phase !== "selecting") throw new ApplicationError("INVALID_TRANSITION");
1193
1200
  }
1194
1201
  //#endregion
1195
- //#region packages/application/src/journey/preview.ts
1196
- const journeyRenderSchema = z.object({
1197
- journeyId: journeyIdSchema,
1198
- workId: journeyIdSchema
1199
- }).strict();
1200
- /** One Work, or several in one call where each Work renders or fails on its own. */
1201
- const journeyRenderInputSchema = z.union([journeyRenderSchema, z.object({
1202
- journeyId: journeyIdSchema,
1203
- workIds: z.array(journeyIdSchema).min(1).max(256)
1204
- }).strict()]);
1205
- const journeyPreviewSchema = journeyRenderSchema.extend({ cursor: z.object({
1206
- sha256: journeyDigestSchema,
1207
- after: z.number().int().min(-1)
1208
- }).strict().optional() }).strict();
1209
- z.object({
1210
- workId: journeyIdSchema,
1211
- revision: z.number().int().nonnegative(),
1212
- sha256: journeyDigestSchema,
1213
- title: z.string(),
1214
- rows: z.array(z.object({
1215
- ordinal: z.number().int().nonnegative(),
1216
- sessionId: z.string(),
1217
- role: z.enum(["user", "assistant"]),
1218
- text: z.string()
1219
- }).strict()),
1220
- cursor: z.object({
1221
- sha256: journeyDigestSchema,
1222
- after: z.number().int().min(-1)
1223
- }).strict().nullable()
1224
- }).strict();
1225
- //#endregion
1226
- //#region packages/application/src/journey/privacy/findings.ts
1227
- /** UTF-16 source coordinates; contexts are local private evidence, never journey state. */
1228
- const journeyFindingsSchema = journeyPreviewSchema;
1229
- const journeyFindingSchema = z.object({
1230
- ordinal: z.number().int().nonnegative(),
1231
- termKey: z.string(),
1232
- value: z.string(),
1233
- type: termTypeSchema,
1234
- disposition: z.enum(["hidden", "pending"]),
1235
- reason: z.string(),
1236
- entryPath: z.string(),
1237
- basis: z.enum([
1238
- "static-original",
1239
- "static-base",
1240
- "classification-metadata",
1241
- "scan-source"
1242
- ]),
1243
- source: z.discriminatedUnion("kind", [
1244
- z.object({
1245
- kind: z.literal("session"),
1246
- sessionId: z.string(),
1247
- sourcePartId: z.string().nullable(),
1248
- recordIndex: z.number().int().nonnegative(),
1249
- pointer: z.string()
1250
- }).strict(),
1251
- z.object({
1252
- kind: z.literal("metadata"),
1253
- field: z.string()
1254
- }).strict(),
1255
- z.object({
1256
- kind: z.literal("setup"),
1257
- pointer: z.string()
1258
- }).strict()
1259
- ]),
1260
- start: z.number().int().nonnegative(),
1261
- end: z.number().int().nonnegative(),
1262
- context: z.object({
1263
- text: z.string(),
1264
- start: z.number().int().nonnegative(),
1265
- matchStart: z.number().int().nonnegative(),
1266
- matchEnd: z.number().int().nonnegative()
1267
- }).strict()
1268
- }).strict();
1269
- z.object({
1270
- workId: z.uuid(),
1271
- countScope: z.literal("source-evidence"),
1272
- revision: z.number().int().nonnegative(),
1273
- sha256: z.string(),
1274
- rows: z.array(journeyFindingSchema).max(32),
1275
- cursor: journeyPreviewSchema.shape.cursor.unwrap().nullable()
1276
- }).strict();
1277
- const journeyFindingImpactSchema = z.object({
1278
- journeyId: z.uuid(),
1279
- termKeys: z.array(z.string()).min(1).max(2e4),
1280
- cursor: journeyPreviewSchema.shape.cursor
1281
- }).strict();
1282
- //#endregion
1283
- //#region packages/application/src/journey/privacy/record.ts
1284
- /** Merge independently reviewed occurrences into one journey-wide decision. */
1285
- function mergePrivacyTerms(state, incoming) {
1286
- const terms = new Map(state.privacy.terms.map((term) => [privacyTermKey(term), term]));
1287
- const overrides = /* @__PURE__ */ new Map();
1288
- for (const item of state.valueOverrides ?? []) {
1289
- const value = item.value.normalize("NFC");
1290
- if (!overrides.has(value)) overrides.set(value, item);
1291
- }
1292
- for (const term of incoming) {
1293
- const key = privacyTermKey(term), prior = terms.get(key);
1294
- const override = overrides.get(term.value.normalize("NFC"));
1295
- const preference = state.groups.find((group) => group.key === `type:${term.type}` && group.origin === "preference");
1296
- const disposition = !termTypeInfo[term.type].asked ? "hidden" : override ? override.choice === "hide" ? "hidden" : "keep" : preference?.choice ? preference.choice === "hide" ? "hidden" : "keep" : prior?.disposition === "hidden" || term.disposition === "hidden" ? "hidden" : prior && prior.disposition !== term.disposition ? "pending" : term.disposition;
1297
- const sources = [...new Map([...prior?.sources ?? [], ...term.sources].map((source) => [JSON.stringify(source.kind === "session" ? [
1298
- source.workId,
1299
- source.batchId,
1300
- source.chunkId
1301
- ] : [source.workId, source.field]), source])).values()];
1302
- terms.set(key, {
1303
- ...term,
1304
- disposition,
1305
- sources,
1306
- reason: prior && prior.disposition !== term.disposition ? disposition === "pending" ? "Conflicting scan judgments require a contextual decision." : disposition === "hidden" ? "A Hidden judgment for this keyword takes precedence." : term.reason : term.reason
1307
- });
1308
- }
1309
- const values = Map.groupBy([...terms.values()], (term) => term.value.normalize("NFC"));
1310
- for (const same of values.values()) if (same.some((term) => !termTypeInfo[term.type].asked)) for (const term of same) terms.set(privacyTermKey(term), {
1311
- ...term,
1312
- disposition: "hidden"
1313
- });
1314
- else if (overrides.has(same[0]?.value.normalize("NFC") ?? "")) {
1315
- const override = overrides.get(same[0]?.value.normalize("NFC") ?? "");
1316
- for (const term of same) terms.set(privacyTermKey(term), {
1317
- ...term,
1318
- disposition: override?.choice === "hide" ? "hidden" : "keep"
1319
- });
1320
- } else if (same.some((term) => term.disposition === "hidden")) for (const term of same) terms.set(privacyTermKey(term), {
1321
- ...term,
1322
- disposition: "hidden",
1323
- reason: term.disposition === "hidden" ? term.reason : "A Hidden judgment for this NFC keyword takes precedence across term types."
1324
- });
1325
- else if (new Set(same.map((term) => term.disposition)).size > 1) for (const term of same) terms.set(privacyTermKey(term), {
1326
- ...term,
1327
- disposition: "pending",
1328
- reason: "Conflicting scan judgments require a contextual decision."
1329
- });
1330
- return {
1331
- ...state.privacy,
1332
- terms: [...terms.values()],
1333
- packageApproval: null
1334
- };
1335
- }
1336
- /** A journey-wide literal can affect selected Works even when its reporting Work was Dropped. */
1337
- function activePrivacyTerms(state) {
1338
- return state.works.some((work) => work.access !== "drop" && work.rootSessionIds.length > 0) ? state.privacy.terms : [];
1339
- }
1340
- //#endregion
1341
1202
  //#region packages/application/src/journey/placement/readiness.ts
1342
1203
  function hasCompleteWorkDescription(work) {
1343
1204
  return work.placement === "placed" && !!work.description.trim() && work.classificationRecommendation !== null;
@@ -1366,6 +1227,18 @@ function isSkippedWork(work) {
1366
1227
  function isIncludedWork(work) {
1367
1228
  return isSelectedWork(work) && !isSkippedWork(work);
1368
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
+ }
1369
1242
  function selectedWorks(state) {
1370
1243
  return state.works.filter(isSelectedWork);
1371
1244
  }
@@ -1612,6 +1485,26 @@ const sendItemSchema = z.object({
1612
1485
  title: z.string(),
1613
1486
  access
1614
1487
  }).strict().optional(),
1488
+ /** Safe preflight facts; local requested-access conflicts have no account owner. */
1489
+ accessConflict: z.object({
1490
+ sessionKey: journeyDigestSchema,
1491
+ requestedAccess: access,
1492
+ accountWorkId: journeyIdSchema.nullable(),
1493
+ accountAccess: z.enum([
1494
+ "public",
1495
+ "private",
1496
+ "removed",
1497
+ "mixed"
1498
+ ]).nullable(),
1499
+ rule: z.enum([
1500
+ "held-elsewhere",
1501
+ "destination-access",
1502
+ "requested-access",
1503
+ "content-access"
1504
+ ])
1505
+ }).strict().optional(),
1506
+ /** Distinct conflicting session keys across all rules and account holders. */
1507
+ accessConflictSessionCount: count$13.positive().optional(),
1615
1508
  /** Membership readback retained for visible conflict recovery. */
1616
1509
  membershipConflict: z.object({
1617
1510
  revision: count$13,
@@ -1754,6 +1647,64 @@ function hasCurrentStaticReview(state, work) {
1754
1647
  return work.staticReview !== void 0 && work.staticReview.snapshotId === state.snapshotId && work.staticReview.inputRevision === work.inputRevision;
1755
1648
  }
1756
1649
  //#endregion
1650
+ //#region packages/application/src/journey/privacy/record.ts
1651
+ /** Merge independently reviewed occurrences into one journey-wide decision. */
1652
+ function mergePrivacyTerms(state, incoming) {
1653
+ const terms = new Map(state.privacy.terms.map((term) => [privacyTermKey(term), term]));
1654
+ const overrides = /* @__PURE__ */ new Map();
1655
+ for (const item of state.valueOverrides ?? []) {
1656
+ const value = item.value.normalize("NFC");
1657
+ if (!overrides.has(value)) overrides.set(value, item);
1658
+ }
1659
+ for (const term of incoming) {
1660
+ const key = privacyTermKey(term), prior = terms.get(key);
1661
+ const override = overrides.get(term.value.normalize("NFC"));
1662
+ const preference = state.groups.find((group) => group.key === `type:${term.type}` && group.origin === "preference");
1663
+ const disposition = !termTypeInfo[term.type].asked ? "hidden" : override ? override.choice === "hide" ? "hidden" : "keep" : preference?.choice ? preference.choice === "hide" ? "hidden" : "keep" : prior?.disposition === "hidden" || term.disposition === "hidden" ? "hidden" : prior && prior.disposition !== term.disposition ? "pending" : term.disposition;
1664
+ const sources = [...new Map([...prior?.sources ?? [], ...term.sources].map((source) => [JSON.stringify(source.kind === "session" ? [
1665
+ source.workId,
1666
+ source.batchId,
1667
+ source.chunkId
1668
+ ] : [source.workId, source.field]), source])).values()];
1669
+ terms.set(key, {
1670
+ ...term,
1671
+ disposition,
1672
+ sources,
1673
+ reason: prior && prior.disposition !== term.disposition ? disposition === "pending" ? "Conflicting scan judgments require a contextual decision." : disposition === "hidden" ? "A Hidden judgment for this keyword takes precedence." : term.reason : term.reason
1674
+ });
1675
+ }
1676
+ const values = Map.groupBy([...terms.values()], (term) => term.value.normalize("NFC"));
1677
+ for (const same of values.values()) if (same.some((term) => !termTypeInfo[term.type].asked)) for (const term of same) terms.set(privacyTermKey(term), {
1678
+ ...term,
1679
+ disposition: "hidden"
1680
+ });
1681
+ else if (overrides.has(same[0]?.value.normalize("NFC") ?? "")) {
1682
+ const override = overrides.get(same[0]?.value.normalize("NFC") ?? "");
1683
+ for (const term of same) terms.set(privacyTermKey(term), {
1684
+ ...term,
1685
+ disposition: override?.choice === "hide" ? "hidden" : "keep"
1686
+ });
1687
+ } else if (same.some((term) => term.disposition === "hidden")) for (const term of same) terms.set(privacyTermKey(term), {
1688
+ ...term,
1689
+ disposition: "hidden",
1690
+ reason: term.disposition === "hidden" ? term.reason : "A Hidden judgment for this NFC keyword takes precedence across term types."
1691
+ });
1692
+ else if (new Set(same.map((term) => term.disposition)).size > 1) for (const term of same) terms.set(privacyTermKey(term), {
1693
+ ...term,
1694
+ disposition: "pending",
1695
+ reason: "Conflicting scan judgments require a contextual decision."
1696
+ });
1697
+ return {
1698
+ ...state.privacy,
1699
+ terms: [...terms.values()],
1700
+ packageApproval: null
1701
+ };
1702
+ }
1703
+ /** A journey-wide literal can affect selected Works even when its reporting Work was Dropped. */
1704
+ function activePrivacyTerms(state) {
1705
+ return state.works.some((work) => work.access !== "drop" && work.rootSessionIds.length > 0) ? state.privacy.terms : [];
1706
+ }
1707
+ //#endregion
1757
1708
  //#region packages/application/src/journey/policy/commands.ts
1758
1709
  const journeyFindingReferenceSchema = z.object({
1759
1710
  batchId: journeyIdSchema,
@@ -1809,6 +1760,16 @@ const journeyPropagateActionSchema = z.object({
1809
1760
  //#region packages/application/src/journey/scope/model.ts
1810
1761
  const count$11 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
1811
1762
  const safeText = z.string().trim().min(1).max(240);
1763
+ /**
1764
+ * One Work's stored account access (ADR 0274 D3(b), refined by ADR 0275): `public-all` when
1765
+ * every root key is held Public, `public` when the held keys are Public and other roots are new.
1766
+ */
1767
+ const accountWorkAccessSchema = z.enum([
1768
+ "private",
1769
+ "public",
1770
+ "public-all",
1771
+ "mixed"
1772
+ ]);
1812
1773
  const journeyScopeConfirmActionSchema = z.object({ kind: z.literal("scope-confirm") }).strict();
1813
1774
  const journeyWorkTypeActionSchema = z.object({
1814
1775
  kind: z.literal("work-type"),
@@ -1818,7 +1779,11 @@ const journeyWorkTypeActionSchema = z.object({
1818
1779
  const journeyWorksAccessActionSchema = z.object({
1819
1780
  kind: z.literal("works-access"),
1820
1781
  workIds: z.array(journeyIdSchema).min(1).max(1e4),
1821
- access: journeyAccessSchema
1782
+ access: journeyAccessSchema,
1783
+ accessChoice: z.object({
1784
+ channel: z.literal("conversation"),
1785
+ at: z.iso.datetime()
1786
+ }).strict().optional()
1822
1787
  }).strict();
1823
1788
  /** Static-only Work facts for proposing project types and scan scope; no source bodies. */
1824
1789
  const journeyCatalogQuerySchema = z.object({
@@ -2018,7 +1983,11 @@ const journeyActionSchema = z.discriminatedUnion("kind", [
2018
1983
  z.object({
2019
1984
  kind: z.literal("work-access"),
2020
1985
  workId: journeyIdSchema,
2021
- access: journeyAccessSchema
1986
+ access: journeyAccessSchema,
1987
+ accessChoice: z.object({
1988
+ channel: z.literal("conversation"),
1989
+ at: z.iso.datetime()
1990
+ }).strict().optional()
2022
1991
  }).strict(),
2023
1992
  z.object({
2024
1993
  kind: z.literal("work-title"),
@@ -2137,13 +2106,22 @@ function privacyDecisionDigest(state) {
2137
2106
  function assertJourneyScanned(state) {
2138
2107
  if (!state.consent || state.scope.status !== "confirmed" || !state.readingComplete || state.works.some((work) => isIncludedWork(work) && (!work.classificationMetadata || work.totalBatches === 0 && !hasCertifiedNoBatchReview(work, state.snapshotId) || work.completedBatches !== work.totalBatches || work.status === "blocked"))) throw new ApplicationError("SCOPE_NOT_READY");
2139
2108
  }
2109
+ /** Legacy model, delegate or unrecorded confirmation is not the person's Public choice. */
2110
+ function assertPublicChoices(state) {
2111
+ if (state.works.some((work) => isIncludedWork(work) && !hasPublicChoice(work))) throw new ApplicationError("SCOPE_NOT_READY");
2112
+ }
2140
2113
  /** Track B (ADR 0172): preview and packaging need every selected Work's static base. */
2141
2114
  function assertJourneyStaticReady(state) {
2142
2115
  if (state.works.some((work) => work.access !== "drop" && work.rootSessionIds.length > 0 && !isSkippedWork(work) && !hasCurrentStaticReview(state, work))) throw new ApplicationError("SCOPE_NOT_READY");
2143
2116
  }
2144
- /** Cards need only Track A, AI review and metadata; this final gate also needs Track B. */
2145
- function assertJourneyPrivacyReady(state, packaging = false) {
2117
+ /**
2118
+ * Cards need only Track A, AI review and metadata; this final gate also needs Track B.
2119
+ * `publicChoices` defaults to `packaging`: the Send click checks it before packaging, and the
2120
+ * follow-up of an already uploaded Send skips it because nothing more goes out (ADR 0274).
2121
+ */
2122
+ function assertJourneyPrivacyReady(state, packaging = false, { publicChoices = packaging } = {}) {
2146
2123
  assertJourneyScanned(state);
2124
+ if (publicChoices) assertPublicChoices(state);
2147
2125
  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");
2148
2126
  assertJourneyStaticReady(state);
2149
2127
  if (packaging && state.privacy.packageApproval !== privacyDecisionDigest(state)) throw new ApplicationError("CONFIRMATION_REQUIRED");
@@ -2173,15 +2151,23 @@ function appendDecisionCards(state, input) {
2173
2151
  const open = new Set(activePrivacyTerms(state).filter((term) => term.disposition === "pending").map(privacyTermKey).filter((key) => !coveredBefore.has(key)));
2174
2152
  const extend = input.extend ?? [];
2175
2153
  const covered = [...input.cards.flatMap((card) => card.termKeys), ...extend.flatMap((item) => item.termKeys)];
2176
- 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");
2154
+ if (!covered.length) throw new ApplicationError("INVALID_SELECTION", "cards.append.empty");
2155
+ if (state.privacy.cards.length + input.cards.length > 20) throw new ApplicationError("INVALID_SELECTION", "cards.append.limit");
2156
+ if (new Set(input.cards.map((card) => card.id)).size !== input.cards.length) throw new ApplicationError("INVALID_SELECTION", "cards.append.duplicate-card");
2157
+ if (input.cards.some((card) => existing.has(card.id))) throw new ApplicationError("INVALID_SELECTION", "cards.append.existing-card");
2158
+ if (new Set(extend.map((item) => item.cardId)).size !== extend.length) throw new ApplicationError("INVALID_SELECTION", "cards.append.duplicate-extension");
2159
+ if (extend.some((item) => !existing.has(item.cardId))) throw new ApplicationError("INVALID_SELECTION", "cards.append.unknown-card");
2160
+ if (new Set(covered).size !== covered.length) throw new ApplicationError("INVALID_SELECTION", "cards.append.duplicate-term");
2161
+ if (covered.some((key) => !open.has(key))) throw new ApplicationError("INVALID_SELECTION", "cards.append.term-not-open");
2177
2162
  const terms = new Map(state.privacy.terms.map((term) => [privacyTermKey(term), term]));
2178
2163
  const ownValues = ownIdentityValues(state);
2179
2164
  for (const extension of extend) {
2180
2165
  const ownCard = !!state.privacy.cards.find((item) => item.id === extension.cardId)?.termKeys.some((key) => terms.get(key)?.type === "own-identity");
2181
- if (extension.termKeys.some((key) => {
2166
+ for (const key of extension.termKeys) {
2182
2167
  const term = terms.get(key);
2183
- return term?.type === "own-identity" ? !ownCard : term?.type === "people" && ownCard && !ownValues.has(identityValue(term.value));
2184
- })) throw new ApplicationError("INVALID_SELECTION");
2168
+ if (term?.type === "own-identity" && !ownCard) throw new ApplicationError("INVALID_SELECTION", "cards.append.own-identity-card");
2169
+ if (term?.type === "people" && ownCard && !ownValues.has(identityValue(term.value))) throw new ApplicationError("INVALID_SELECTION", "cards.append.other-person");
2170
+ }
2185
2171
  }
2186
2172
  const additions = new Map(extend.map((item) => [item.cardId, item.termKeys]));
2187
2173
  const inherited = new Map(extend.flatMap((item) => item.termKeys.map((key) => [key, item.cardId])));
@@ -2391,6 +2377,7 @@ function respondPrivacy(state, action, source) {
2391
2377
  };
2392
2378
  }
2393
2379
  if (action.kind === "package-confirm") {
2380
+ assertPublicChoices(state);
2394
2381
  assertJourneyPrivacyReady(state);
2395
2382
  return {
2396
2383
  ...state,
@@ -2403,6 +2390,127 @@ function respondPrivacy(state, action, source) {
2403
2390
  return null;
2404
2391
  }
2405
2392
  //#endregion
2393
+ //#region packages/application/src/journey/send/displayed-scope.ts
2394
+ function hash(value) {
2395
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
2396
+ }
2397
+ /** Artifact-independent authorization: packaging progress cannot expand this scope. */
2398
+ function requestedScope(journey) {
2399
+ return hash({
2400
+ privacy: privacyDecisionDigest(journey),
2401
+ configurationId: journey.configurationId,
2402
+ confirmedHideRevision: journey.confirmedHideRevision ?? 0,
2403
+ consent: journey.consent,
2404
+ profile: journey.profile,
2405
+ works: journey.works.map((work) => ({
2406
+ id: work.id,
2407
+ inputRevision: work.inputRevision,
2408
+ access: work.access,
2409
+ accessConfirmed: work.accessConfirmed,
2410
+ included: isIncludedWork(work),
2411
+ placement: work.placement,
2412
+ suggestion: work.suggestion
2413
+ }))
2414
+ });
2415
+ }
2416
+ /** Host-issued review fence; clients echo it without hashing private decision data. */
2417
+ function displayedScopeDigest(journey) {
2418
+ return hash({
2419
+ scope: requestedScope(journey),
2420
+ phase: journey.phase === "review" ? "ready" : journey.phase,
2421
+ pendingHideId: journey.pendingHideId,
2422
+ abandonedAt: journey.abandonedAt
2423
+ });
2424
+ }
2425
+ //#endregion
2426
+ //#region packages/application/src/journey/preview.ts
2427
+ const journeyRenderSchema = z.object({
2428
+ journeyId: journeyIdSchema,
2429
+ workId: journeyIdSchema
2430
+ }).strict();
2431
+ /** One Work, or several in one call where each Work renders or fails on its own. */
2432
+ const journeyRenderInputSchema = z.union([journeyRenderSchema, z.object({
2433
+ journeyId: journeyIdSchema,
2434
+ workIds: z.array(journeyIdSchema).min(1).max(maximumConfigurationSessions)
2435
+ }).strict()]);
2436
+ const journeyPreviewSchema = journeyRenderSchema.extend({ cursor: z.object({
2437
+ sha256: journeyDigestSchema,
2438
+ after: z.number().int().min(-1)
2439
+ }).strict().optional() }).strict();
2440
+ z.object({
2441
+ workId: journeyIdSchema,
2442
+ revision: z.number().int().nonnegative(),
2443
+ sha256: journeyDigestSchema,
2444
+ title: z.string(),
2445
+ rows: z.array(z.object({
2446
+ ordinal: z.number().int().nonnegative(),
2447
+ sessionId: z.string(),
2448
+ role: z.enum(["user", "assistant"]),
2449
+ text: z.string()
2450
+ }).strict()),
2451
+ cursor: z.object({
2452
+ sha256: journeyDigestSchema,
2453
+ after: z.number().int().min(-1)
2454
+ }).strict().nullable()
2455
+ }).strict();
2456
+ //#endregion
2457
+ //#region packages/application/src/journey/privacy/findings.ts
2458
+ /** UTF-16 source coordinates; contexts are local private evidence, never journey state. */
2459
+ const journeyFindingsSchema = journeyPreviewSchema;
2460
+ const journeyFindingSchema = z.object({
2461
+ ordinal: z.number().int().nonnegative(),
2462
+ termKey: z.string(),
2463
+ value: z.string(),
2464
+ type: termTypeSchema,
2465
+ disposition: z.enum(["hidden", "pending"]),
2466
+ reason: z.string(),
2467
+ entryPath: z.string(),
2468
+ basis: z.enum([
2469
+ "static-original",
2470
+ "static-base",
2471
+ "classification-metadata",
2472
+ "scan-source"
2473
+ ]),
2474
+ source: z.discriminatedUnion("kind", [
2475
+ z.object({
2476
+ kind: z.literal("session"),
2477
+ sessionId: z.string(),
2478
+ sourcePartId: z.string().nullable(),
2479
+ recordIndex: z.number().int().nonnegative(),
2480
+ pointer: z.string()
2481
+ }).strict(),
2482
+ z.object({
2483
+ kind: z.literal("metadata"),
2484
+ field: z.string()
2485
+ }).strict(),
2486
+ z.object({
2487
+ kind: z.literal("setup"),
2488
+ pointer: z.string()
2489
+ }).strict()
2490
+ ]),
2491
+ start: z.number().int().nonnegative(),
2492
+ end: z.number().int().nonnegative(),
2493
+ context: z.object({
2494
+ text: z.string(),
2495
+ start: z.number().int().nonnegative(),
2496
+ matchStart: z.number().int().nonnegative(),
2497
+ matchEnd: z.number().int().nonnegative()
2498
+ }).strict()
2499
+ }).strict();
2500
+ z.object({
2501
+ workId: z.uuid(),
2502
+ countScope: z.literal("source-evidence"),
2503
+ revision: z.number().int().nonnegative(),
2504
+ sha256: z.string(),
2505
+ rows: z.array(journeyFindingSchema).max(32),
2506
+ cursor: journeyPreviewSchema.shape.cursor.unwrap().nullable()
2507
+ }).strict();
2508
+ const journeyFindingImpactSchema = z.object({
2509
+ journeyId: z.uuid(),
2510
+ termKeys: z.array(z.string()).min(1).max(2e4),
2511
+ cursor: journeyPreviewSchema.shape.cursor
2512
+ }).strict();
2513
+ //#endregion
2406
2514
  //#region packages/application/src/journey/scope/classification.ts
2407
2515
  const count$10 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
2408
2516
  const classificationCursorSchema = z.object({
@@ -4768,32 +4876,200 @@ function retireScanGeneration(state, workId) {
4768
4876
  });
4769
4877
  return {
4770
4878
  ...state,
4771
- readingComplete: false,
4772
- phase: state.phase === "paused" ? "paused" : "processing",
4879
+ readingComplete: false,
4880
+ phase: state.phase === "paused" ? "paused" : "processing",
4881
+ privacy: {
4882
+ ...state.privacy,
4883
+ terms,
4884
+ cards: state.privacy.cards.filter((card) => card.termKeys.every((key) => !changedKeys.has(key))),
4885
+ confirmedAnswersRevision: null,
4886
+ packageApproval: null
4887
+ },
4888
+ works: state.works.map((work) => work.id === workId ? resetWorkScan(work) : work)
4889
+ };
4890
+ }
4891
+ /** The same Work with a new scan generation: it is read again from its first session. */
4892
+ function resetWorkScan(work) {
4893
+ const { emptyReview: _empty, excludedReview: _excluded, staticReview: _static, metadataReviewedDigest: _metadata, reviewReuseFrom: _reuse, renderFailure: _failure, ...retained } = work;
4894
+ return {
4895
+ ...retained,
4896
+ revision: work.revision + 1,
4897
+ inputRevision: work.inputRevision + 1,
4898
+ status: "pending",
4899
+ suggestion: null,
4900
+ totalBatches: 0,
4901
+ completedBatches: 0,
4902
+ groupIds: [],
4903
+ blockedCodes: [],
4904
+ artifact: null
4905
+ };
4906
+ }
4907
+ //#endregion
4908
+ //#region packages/application/src/journey/scope/respond.ts
4909
+ /**
4910
+ * Account-seeded access (ADR 0274 D3(b)). With `held`, the time the account inventory was read,
4911
+ * a Work whose every root is held Public is confirmed Public with the account as its recorded
4912
+ * choice (ADR 0275); without it, Public stays a suggestion.
4913
+ */
4914
+ function accountSeededAccess(account, held) {
4915
+ const accessChoice = account === "public-all" && held !== void 0 ? {
4916
+ channel: "account",
4917
+ at: held
4918
+ } : void 0;
4919
+ return {
4920
+ access: account === "public" || account === "public-all" ? "public" : "private",
4921
+ accessOrigin: "account",
4922
+ accessConfirmed: account === "private" || accessChoice !== void 0,
4923
+ accountAccessConflict: account === "mixed",
4924
+ accessChoice
4925
+ };
4926
+ }
4927
+ /** Classification confirmation initializes visibility and scan membership for this journey. */
4928
+ function confirmScope(state, now, decision, accountAccess) {
4929
+ if (state.scope.status === "confirmed") return state;
4930
+ if (state.classificationRevision === null || !state.works.length || state.works.some((work) => work.placement !== "placed" && work.placement !== "classifying" || work.placement === "placed" && !(work.status === "blocked" && work.access === "drop" && work.accessConfirmed) && (!work.description.trim() || !work.projectType))) throw new ApplicationError("SCOPE_NOT_READY");
4931
+ return withReadingProgress({
4932
+ ...state,
4933
+ scope: {
4934
+ status: "confirmed",
4935
+ actor: decision ? "delegate" : "human",
4936
+ at: now
4937
+ },
4938
+ works: state.works.map((work) => {
4939
+ if (work.placement === "classifying") return work;
4940
+ if (work.accessOrigin === "explicit" && work.accessConfirmed) return work;
4941
+ const account = accountAccess?.get(work.id);
4942
+ if (account) return {
4943
+ ...work,
4944
+ ...accountSeededAccess(account, now)
4945
+ };
4946
+ if (work.accessOrigin === "account") return work;
4947
+ const access = work.accessConfirmed ? work.access : work.classificationRecommendation?.access ?? work.access;
4948
+ return {
4949
+ ...work,
4950
+ access,
4951
+ accessOrigin: work.accessConfirmed || !work.classificationRecommendation ? work.accessOrigin : "model",
4952
+ accessConfirmed: access !== "public"
4953
+ };
4954
+ })
4955
+ });
4956
+ }
4957
+ function setWorkType(state, workId, type, decision) {
4958
+ if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
4959
+ const projectType = decision ? {
4960
+ type,
4961
+ reason: "Chosen by your delegate for this journey.",
4962
+ origin: "delegate"
4963
+ } : {
4964
+ type,
4965
+ reason: "Chosen by you.",
4966
+ origin: "explicit"
4967
+ };
4968
+ return {
4969
+ ...state,
4970
+ scope: {
4971
+ status: "pending",
4972
+ actor: null,
4973
+ at: null
4974
+ },
4975
+ privacy: {
4976
+ ...state.privacy,
4977
+ cards: [],
4978
+ confirmedAnswersRevision: null,
4979
+ packageApproval: null
4980
+ },
4981
+ works: state.works.map((work) => work.id !== workId || decision && work.projectType?.origin === "explicit" ? work : {
4982
+ ...work,
4983
+ projectType
4984
+ })
4985
+ };
4986
+ }
4987
+ /** One rule for single and bulk choices; selection changes recompute reading completion. */
4988
+ function setWorksAccess(state, workIds, access, decision, accessChoice) {
4989
+ if (decision && access === "public") throw new ApplicationError("CONFIRMATION_REQUIRED");
4990
+ if (new Set(workIds).size !== workIds.length) throw new ApplicationError("INVALID_SELECTION");
4991
+ if (workIds.some((id) => !state.works.some((work) => work.id === id))) throw new ApplicationError("UNKNOWN_WORKSPACE");
4992
+ if (state.scope.status === "confirmed") return changeConfirmedAccess(state, workIds, access, decision, accessChoice);
4993
+ return withReadingProgress({
4994
+ ...state,
4995
+ scope: {
4996
+ status: "pending",
4997
+ actor: null,
4998
+ at: null
4999
+ },
4773
5000
  privacy: {
4774
5001
  ...state.privacy,
4775
- terms,
4776
- cards: state.privacy.cards.filter((card) => card.termKeys.every((key) => !changedKeys.has(key))),
5002
+ cards: [],
4777
5003
  confirmedAnswersRevision: null,
4778
5004
  packageApproval: null
4779
5005
  },
4780
- works: state.works.map((work) => work.id === workId ? resetWorkScan(work) : work)
5006
+ works: state.works.map((work) => !workIds.includes(work.id) ? work : {
5007
+ ...work,
5008
+ revision: work.revision + 1,
5009
+ artifact: null,
5010
+ status: work.status === "ready" ? "review" : work.status,
5011
+ access,
5012
+ accessConfirmed: true,
5013
+ accountAccessConflict: false,
5014
+ accessChoice: access === "public" ? accessChoice : void 0,
5015
+ accessOrigin: decision && work.accessOrigin !== "explicit" ? "delegate" : "explicit"
5016
+ })
5017
+ });
5018
+ }
5019
+ /**
5020
+ * After confirmation the person, never a delegate, may still change any Work's visibility
5021
+ * (D2, superseding ADR 0162's lock and ADR 0188's blocked-only Drop). Drop stops that Work's
5022
+ * scan; a Work moved out of Drop is read again; reading progress is recomputed.
5023
+ */
5024
+ function changeConfirmedAccess(state, workIds, access, decision, accessChoice) {
5025
+ if (decision || state.phase === "sending" || state.phase === "completed" || state.works.some((work) => workIds.includes(work.id) && work.access === access && work.accessConfirmed && hasPublicChoice(work))) throw new ApplicationError("INVALID_TRANSITION");
5026
+ if (access === "public" && state.works.every((work) => !workIds.includes(work.id) || work.access === "public" && work.accessConfirmed)) return {
5027
+ ...state,
5028
+ works: state.works.map((work) => workIds.includes(work.id) && accessChoice ? {
5029
+ ...work,
5030
+ accessOrigin: "explicit",
5031
+ accessChoice
5032
+ } : work)
4781
5033
  };
5034
+ return withReadingProgress({
5035
+ ...state,
5036
+ privacy: {
5037
+ ...state.privacy,
5038
+ packageApproval: null
5039
+ },
5040
+ works: state.works.map((work) => workIds.includes(work.id) ? {
5041
+ ...work,
5042
+ revision: work.revision + 1,
5043
+ artifact: null,
5044
+ status: work.status === "ready" ? "review" : work.status,
5045
+ access,
5046
+ accessConfirmed: true,
5047
+ accountAccessConflict: false,
5048
+ accessChoice: access === "public" ? accessChoice : void 0,
5049
+ accessOrigin: "explicit"
5050
+ } : work)
5051
+ });
4782
5052
  }
4783
- /** The same Work with a new scan generation: it is read again from its first session. */
4784
- function resetWorkScan(work) {
4785
- const { emptyReview: _empty, excludedReview: _excluded, staticReview: _static, metadataReviewedDigest: _metadata, reviewReuseFrom: _reuse, renderFailure: _failure, ...retained } = work;
5053
+ /** Model suggestions fill or refresh model types only; a person's or delegate's type stays. */
5054
+ function applyClassification(state, items) {
5055
+ if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
5056
+ const byWork = new Map(items.map((item) => [item.workId, item]));
5057
+ if (byWork.size !== items.length) throw new ApplicationError("INVALID_SELECTION");
5058
+ if (items.some((item) => !state.works.some((work) => work.id === item.workId))) throw new ApplicationError("UNKNOWN_WORKSPACE");
4786
5059
  return {
4787
- ...retained,
4788
- revision: work.revision + 1,
4789
- inputRevision: work.inputRevision + 1,
4790
- status: "pending",
4791
- suggestion: null,
4792
- totalBatches: 0,
4793
- completedBatches: 0,
4794
- groupIds: [],
4795
- blockedCodes: [],
4796
- artifact: null
5060
+ ...state,
5061
+ works: state.works.map((work) => {
5062
+ const item = byWork.get(work.id);
5063
+ if (!item || work.projectType && work.projectType.origin !== "model") return work;
5064
+ return {
5065
+ ...work,
5066
+ projectType: {
5067
+ type: item.type,
5068
+ reason: item.reason,
5069
+ origin: "model"
5070
+ }
5071
+ };
5072
+ })
4797
5073
  };
4798
5074
  }
4799
5075
  //#endregion
@@ -4818,6 +5094,7 @@ const classificationCommitSchema = z.object({
4818
5094
  /** `classificationPartition` of the state the proposal was prepared against. */
4819
5095
  partition: journeyDigestSchema,
4820
5096
  late: z.boolean(),
5097
+ accountAccess: z.record(z.string(), accountWorkAccessSchema).optional(),
4821
5098
  items: z.array(journeyClassificationSubmitSchema.shape.items.element.extend({
4822
5099
  workId: journeyIdSchema,
4823
5100
  workspaceIds: workspaces
@@ -4841,13 +5118,22 @@ function classificationPartition(state) {
4841
5118
  work.rootSessionIds.toSorted()
4842
5119
  ]))).digest("hex");
4843
5120
  }
5121
+ /**
5122
+ * Two placed Works one late item may join: the same access, origin, confirmation, recorded
5123
+ * Public choice and account access conflict, as local-host isolation.ts compares. The Skill's `validateLatePlacedWorks` mirrors this rule before commit.
5124
+ */
5125
+ function sameAccessRecord(work, other) {
5126
+ 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;
5127
+ }
4844
5128
  /** The proposal's Works built from the given state's current Works (E2). */
4845
5129
  function classificationWorks(state, plan) {
4846
5130
  const late = plan.late;
4847
5131
  const works = [];
4848
5132
  for (const item of plan.items) {
4849
5133
  const existing = state.works.find((work) => work.placement !== "classifying" && work.rootSessionIds.length === item.rootSessionIds.length && work.rootSessionIds.every((root) => item.rootSessionIds.includes(root)));
4850
- const extended = late ? state.works.find((work) => work.placement === "placed" && work.rootSessionIds.every((root) => item.rootSessionIds.includes(root))) : void 0;
5134
+ const owners = late ? state.works.filter((work) => work.placement === "placed" && work.rootSessionIds.some((root) => item.rootSessionIds.includes(root))) : [];
5135
+ if (owners.some((work) => !sameAccessRecord(work, owners[0]))) throw new ApplicationError("INVALID_SELECTION", "classification.late.several-works");
5136
+ const extended = owners.find((work) => work.rootSessionIds.every((root) => item.rootSessionIds.includes(root)));
4851
5137
  if (late && existing) {
4852
5138
  works.push(existing);
4853
5139
  continue;
@@ -4858,6 +5144,9 @@ function classificationWorks(state, plan) {
4858
5144
  sourceWorkspaceIds: [.../* @__PURE__ */ new Set([...extended.sourceWorkspaceIds, ...item.workspaceIds])]
4859
5145
  } : newJourneyWork(item.workId, item.projectName, item.rootSessionIds, item.workspaceIds, "placed"));
4860
5146
  const { classificationMetadata: _display, ...original } = base;
5147
+ const account = plan.accountAccess?.[base.id];
5148
+ const inherit = late && account && base.accessOrigin !== "explicit";
5149
+ const unproven = extended && base.accessChoice?.channel === "account" && account !== "public-all";
4861
5150
  works.push({
4862
5151
  ...original,
4863
5152
  projectName: item.projectName,
@@ -4872,8 +5161,13 @@ function classificationWorks(state, plan) {
4872
5161
  ...late && !extended ? {
4873
5162
  access: item.recommendation.access,
4874
5163
  accessOrigin: "model",
4875
- accessConfirmed: true
5164
+ accessConfirmed: item.recommendation.access !== "public"
5165
+ } : {},
5166
+ ...unproven ? {
5167
+ accessConfirmed: false,
5168
+ accessChoice: void 0
4876
5169
  } : {},
5170
+ ...inherit && account ? accountSeededAccess(account, plan.at) : {},
4877
5171
  classificationRecommendation: item.recommendation,
4878
5172
  suggestion: null,
4879
5173
  artifact: null,
@@ -5146,7 +5440,7 @@ function assertDelegatedDecision(state, action, decision, source) {
5146
5440
  }
5147
5441
  if (action.kind === "cards-confirm") return;
5148
5442
  if (action.kind === "work-access" || action.kind === "works-access") {
5149
- if (action.access === "drop") throw new ApplicationError("CONFIRMATION_REQUIRED");
5443
+ if (action.access === "drop" || action.access === "public") throw new ApplicationError("CONFIRMATION_REQUIRED");
5150
5444
  const ids = action.kind === "work-access" ? [action.workId] : action.workIds;
5151
5445
  if (state.works.some((work) => ids.includes(work.id) && work.accessOrigin === "explicit" && work.access !== action.access)) throw new ApplicationError("CONFIRMATION_REQUIRED");
5152
5446
  return;
@@ -5226,135 +5520,6 @@ function propagateGroups(state, action) {
5226
5520
  };
5227
5521
  }
5228
5522
  //#endregion
5229
- //#region packages/application/src/journey/scope/respond.ts
5230
- /** Classification confirmation fixes visibility and scan membership for this journey. */
5231
- function confirmScope(state, now, decision) {
5232
- if (state.scope.status === "confirmed") return state;
5233
- 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");
5234
- return withReadingProgress({
5235
- ...state,
5236
- scope: {
5237
- status: "confirmed",
5238
- actor: decision ? "delegate" : "human",
5239
- at: now
5240
- },
5241
- works: state.works.map((work) => work.placement === "classifying" ? work : work.accessConfirmed || !work.classificationRecommendation ? {
5242
- ...work,
5243
- accessConfirmed: true
5244
- } : {
5245
- ...work,
5246
- access: work.classificationRecommendation.access,
5247
- accessOrigin: "model",
5248
- accessConfirmed: true
5249
- })
5250
- });
5251
- }
5252
- function setWorkType(state, workId, type, decision) {
5253
- if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
5254
- const projectType = decision ? {
5255
- type,
5256
- reason: "Chosen by your delegate for this journey.",
5257
- origin: "delegate"
5258
- } : {
5259
- type,
5260
- reason: "Chosen by you.",
5261
- origin: "explicit"
5262
- };
5263
- return {
5264
- ...state,
5265
- scope: {
5266
- status: "pending",
5267
- actor: null,
5268
- at: null
5269
- },
5270
- privacy: {
5271
- ...state.privacy,
5272
- cards: [],
5273
- confirmedAnswersRevision: null,
5274
- packageApproval: null
5275
- },
5276
- works: state.works.map((work) => work.id !== workId || decision && work.projectType?.origin === "explicit" ? work : {
5277
- ...work,
5278
- projectType
5279
- })
5280
- };
5281
- }
5282
- /** One rule for single and bulk choices; selection changes recompute reading completion. */
5283
- function setWorksAccess(state, workIds, access, decision) {
5284
- if (new Set(workIds).size !== workIds.length) throw new ApplicationError("INVALID_SELECTION");
5285
- if (workIds.some((id) => !state.works.some((work) => work.id === id))) throw new ApplicationError("UNKNOWN_WORKSPACE");
5286
- if (state.scope.status === "confirmed") return changeConfirmedAccess(state, workIds, access, decision);
5287
- return withReadingProgress({
5288
- ...state,
5289
- scope: {
5290
- status: "pending",
5291
- actor: null,
5292
- at: null
5293
- },
5294
- privacy: {
5295
- ...state.privacy,
5296
- cards: [],
5297
- confirmedAnswersRevision: null,
5298
- packageApproval: null
5299
- },
5300
- works: state.works.map((work) => !workIds.includes(work.id) ? work : {
5301
- ...work,
5302
- revision: work.revision + 1,
5303
- artifact: null,
5304
- status: work.status === "ready" ? "review" : work.status,
5305
- access,
5306
- accessConfirmed: true,
5307
- accessOrigin: decision && work.accessOrigin !== "explicit" ? "delegate" : "explicit"
5308
- })
5309
- });
5310
- }
5311
- /**
5312
- * After confirmation the person, never a delegate, may still change any Work's visibility
5313
- * (D2, superseding ADR 0162's lock and ADR 0188's blocked-only Drop). Drop stops that Work's
5314
- * scan; a Work moved out of Drop is read again; reading progress is recomputed.
5315
- */
5316
- function changeConfirmedAccess(state, workIds, access, decision) {
5317
- if (decision || state.phase === "sending" || state.phase === "completed" || state.works.some((work) => workIds.includes(work.id) && work.access === access)) throw new ApplicationError("INVALID_TRANSITION");
5318
- return withReadingProgress({
5319
- ...state,
5320
- privacy: {
5321
- ...state.privacy,
5322
- packageApproval: null
5323
- },
5324
- works: state.works.map((work) => workIds.includes(work.id) ? {
5325
- ...work,
5326
- revision: work.revision + 1,
5327
- artifact: null,
5328
- status: work.status === "ready" ? "review" : work.status,
5329
- access,
5330
- accessConfirmed: true,
5331
- accessOrigin: "explicit"
5332
- } : work)
5333
- });
5334
- }
5335
- /** Model suggestions fill or refresh model types only; a person's or delegate's type stays. */
5336
- function applyClassification(state, items) {
5337
- if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
5338
- const byWork = new Map(items.map((item) => [item.workId, item]));
5339
- if (byWork.size !== items.length) throw new ApplicationError("INVALID_SELECTION");
5340
- if (items.some((item) => !state.works.some((work) => work.id === item.workId))) throw new ApplicationError("UNKNOWN_WORKSPACE");
5341
- return {
5342
- ...state,
5343
- works: state.works.map((work) => {
5344
- const item = byWork.get(work.id);
5345
- if (!item || work.projectType && work.projectType.origin !== "model") return work;
5346
- return {
5347
- ...work,
5348
- projectType: {
5349
- type: item.type,
5350
- reason: item.reason,
5351
- origin: "model"
5352
- }
5353
- };
5354
- })
5355
- };
5356
- }
5357
- //#endregion
5358
5523
  //#region packages/application/src/journey/respond.ts
5359
5524
  /** A changed privacy instruction requires a new read of the same frozen roots. */
5360
5525
  function invalidateAnalysisIntent(state) {
@@ -5378,7 +5543,7 @@ function invalidateAnalysisIntent(state) {
5378
5543
  })
5379
5544
  });
5380
5545
  }
5381
- function respondToJourney(state, action, source, now, decision, preferenceScope) {
5546
+ function respondToJourney(state, action, source, now, decision, preferenceScope, accountAccess) {
5382
5547
  if (preferenceScope && (decision || !["group", "groups-hide"].includes(action.kind))) throw new ApplicationError("INVALID_ARGUMENTS");
5383
5548
  if (state.abandonedAt) throw new ApplicationError("INVALID_TRANSITION");
5384
5549
  if (action.kind === "abandon") {
@@ -5492,10 +5657,14 @@ function respondToJourney(state, action, source, now, decision, preferenceScope)
5492
5657
  };
5493
5658
  if (action.kind === "scope-confirm") {
5494
5659
  if (source !== "conversation" || decision) throw new ApplicationError("CONFIRMATION_REQUIRED");
5495
- return confirmScope(state, now);
5660
+ return confirmScope(state, now, void 0, accountAccess);
5496
5661
  }
5497
5662
  if (action.kind === "card-answer" || action.kind === "cards-confirm" || action.kind === "package-confirm") throw new ApplicationError("INVALID_TRANSITION");
5498
- if (action.kind === "works-access") return setWorksAccess(state, action.workIds, action.access, decision);
5663
+ 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");
5664
+ if (action.kind === "works-access") return setWorksAccess(state, action.workIds, action.access, decision, source === "panel" ? {
5665
+ channel: source,
5666
+ at: now
5667
+ } : action.accessChoice);
5499
5668
  if (action.kind === "group" || action.kind === "groups-hide") {
5500
5669
  const ids = action.kind === "group" ? [action.groupId] : action.groupIds;
5501
5670
  if (new Set(ids).size !== ids.length || ids.some((id) => !state.groups.some((group) => group.id === id))) throw new ApplicationError("UNKNOWN_ITEM");
@@ -5526,7 +5695,10 @@ function respondToJourney(state, action, source, now, decision, preferenceScope)
5526
5695
  })
5527
5696
  });
5528
5697
  }
5529
- return setWorksAccess(state, [workId], action.access, decision);
5698
+ return setWorksAccess(state, [workId], action.access, decision, source === "panel" ? {
5699
+ channel: source,
5700
+ at: now
5701
+ } : action.accessChoice);
5530
5702
  }
5531
5703
  //#endregion
5532
5704
  //#region packages/application/src/journey/questions.ts
@@ -6261,6 +6433,7 @@ function journeyView(state, now) {
6261
6433
  const terms = activePrivacyTerms(state);
6262
6434
  return {
6263
6435
  ...safe,
6436
+ displayedScopeDigest: displayedScopeDigest(state),
6264
6437
  privacy: {
6265
6438
  ...state.privacy,
6266
6439
  terms
@@ -6317,6 +6490,59 @@ function journeyView(state, now) {
6317
6490
  }
6318
6491
  /** Works rendered at once by one several-Work call. */
6319
6492
  const renderConcurrency = 4;
6493
+ const inFlightRenders = /* @__PURE__ */ new WeakMap();
6494
+ const renderScopeTails = /* @__PURE__ */ new WeakMap();
6495
+ function waitForRender(pending, signal) {
6496
+ signal.throwIfAborted();
6497
+ return new Promise((resolve, reject) => {
6498
+ const abort = () => reject(signal.reason);
6499
+ signal.addEventListener("abort", abort, { once: true });
6500
+ pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
6501
+ });
6502
+ }
6503
+ async function acquireRenderScope(port, signal) {
6504
+ const previous = renderScopeTails.get(port);
6505
+ const gate = Promise.withResolvers();
6506
+ const tail = (previous ?? Promise.resolve()).then(() => gate.promise);
6507
+ renderScopeTails.set(port, tail);
6508
+ const release = () => {
6509
+ gate.resolve();
6510
+ if (renderScopeTails.get(port) === tail) renderScopeTails.delete(port);
6511
+ };
6512
+ try {
6513
+ await waitForRender(previous ?? Promise.resolve(), signal);
6514
+ return {
6515
+ release,
6516
+ waited: previous !== void 0
6517
+ };
6518
+ } catch (error) {
6519
+ tail.then(() => {
6520
+ if (renderScopeTails.get(port) === tail) renderScopeTails.delete(port);
6521
+ });
6522
+ gate.resolve();
6523
+ throw error;
6524
+ }
6525
+ }
6526
+ async function joinRender(port, journeyId, workId, run, signal, retryConflict = true) {
6527
+ let pending = inFlightRenders.get(port);
6528
+ if (!pending) {
6529
+ pending = /* @__PURE__ */ new Map();
6530
+ inFlightRenders.set(port, pending);
6531
+ }
6532
+ const key = `${journeyId}:${workId}`;
6533
+ const existing = pending.get(key);
6534
+ if (existing) try {
6535
+ return await waitForRender(existing, signal);
6536
+ } catch (error) {
6537
+ signal.throwIfAborted();
6538
+ if (retryConflict && safeCode(error) === "REVISION_CONFLICT") return joinRender(port, journeyId, workId, run, signal, false);
6539
+ throw error;
6540
+ }
6541
+ const entries = pending;
6542
+ const result = Promise.resolve().then(run).finally(() => entries.delete(key));
6543
+ entries.set(key, result);
6544
+ return result;
6545
+ }
6320
6546
  function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).toISOString(), local) {
6321
6547
  /** Callers present after their write is durable, so a concurrent commit must not turn it into an error. */
6322
6548
  async function present(committed) {
@@ -6600,7 +6826,8 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6600
6826
  const current = await storage.current(input.journeyId);
6601
6827
  if (!current) throw new ApplicationError("UNKNOWN_ITEM");
6602
6828
  const expectedRevision = rebase ? current.revision : input.expectedRevision;
6603
- const state = current.revision === expectedRevision ? respondToJourney(current, input.action, source, now(), input.decision, input.preferenceScope) : current;
6829
+ const accountAccess = current.revision === expectedRevision && input.action.kind === "scope-confirm" && current.scope.status !== "confirmed" && source === "conversation" && !input.decision ? await local?.accountAccess?.(current, new AbortController().signal) : void 0;
6830
+ const state = current.revision === expectedRevision ? respondToJourney(current, input.action, source, now(), input.decision, input.preferenceScope, accountAccess) : current;
6604
6831
  const groupIds = input.action.kind === "group" ? [input.action.groupId] : input.action.kind === "groups-hide" ? input.action.groupIds : [];
6605
6832
  const preferenceScopes = input.decision ? [] : state.groups.flatMap((group) => groupIds.includes(group.id) && group.preferenceScope ? [{
6606
6833
  groupId: group.id,
@@ -6716,7 +6943,7 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6716
6943
  const release = await port.renderActivity?.begin(input.journeyId, workIds);
6717
6944
  let result;
6718
6945
  try {
6719
- result = "workId" in input ? { state: await port.render(input.journeyId, input.workId, signal) } : {
6946
+ result = "workId" in input ? { state: await joinRender(port, input.journeyId, input.workId, () => port.render(input.journeyId, input.workId, signal), signal) } : {
6720
6947
  renders: await renderSeveral(port, input.journeyId, workIds, signal),
6721
6948
  state: await currentState(input.journeyId)
6722
6949
  };
@@ -6736,35 +6963,51 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6736
6963
  }
6737
6964
  async function renderSeveral(port, journeyId, workIds, signal) {
6738
6965
  const renders = [];
6739
- let next = 0;
6740
- const scope = await port.renderScope?.(journeyId, workIds.length, signal);
6741
- const renderWork = scope ? (workId) => scope.render(workId) : (workId) => port.render(journeyId, workId, signal);
6966
+ const lease = await acquireRenderScope(port, signal);
6742
6967
  try {
6743
- const failure = (await Promise.allSettled(Array.from({ length: Math.min(scope?.concurrency ?? renderConcurrency, workIds.length) }, async () => {
6744
- for (let workId = workIds[next++]; workId !== void 0; workId = workIds[next++]) {
6745
- try {
6746
- await renderWork(workId);
6747
- renders.push({
6748
- workId,
6749
- status: "rendered"
6750
- });
6751
- } catch (error) {
6752
- signal.throwIfAborted();
6753
- port.renderFailed?.(workId, error);
6754
- renders.push({
6755
- workId,
6756
- status: "failed",
6757
- code: safeCode(error)
6758
- });
6968
+ const current = lease.waited ? await currentState(journeyId) : void 0;
6969
+ 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));
6970
+ const remaining = workIds.filter((workId) => {
6971
+ if (!completed.has(workId)) return true;
6972
+ renders.push({
6973
+ workId,
6974
+ status: "rendered"
6975
+ });
6976
+ return false;
6977
+ });
6978
+ let next = 0;
6979
+ const scope = remaining.length ? await port.renderScope?.(journeyId, remaining.length, signal) : void 0;
6980
+ const renderWork = scope ? (workId) => scope.render(workId) : (workId) => port.render(journeyId, workId, signal);
6981
+ try {
6982
+ const failure = (await Promise.allSettled(Array.from({ length: Math.min(scope?.concurrency ?? renderConcurrency, remaining.length) }, async () => {
6983
+ for (let workId = remaining[next++]; workId !== void 0; workId = remaining[next++]) {
6984
+ try {
6985
+ await joinRender(port, journeyId, workId, () => renderWork(workId), signal);
6986
+ renders.push({
6987
+ workId,
6988
+ status: "rendered"
6989
+ });
6990
+ } catch (error) {
6991
+ signal.throwIfAborted();
6992
+ port.renderFailed?.(workId, error);
6993
+ renders.push({
6994
+ workId,
6995
+ status: "failed",
6996
+ code: safeCode(error)
6997
+ });
6998
+ }
6999
+ await new Promise((resolve) => setImmediate(resolve));
6759
7000
  }
6760
- await new Promise((resolve) => setImmediate(resolve));
6761
- }
6762
- }))).find((item) => item.status === "rejected");
6763
- if (failure) throw failure.reason;
7001
+ }))).find((item) => item.status === "rejected");
7002
+ if (failure) throw failure.reason;
7003
+ } finally {
7004
+ await scope?.close();
7005
+ }
7006
+ const order = new Map(workIds.map((workId, index) => [workId, index]));
7007
+ return renders.toSorted((a, b) => (order.get(a.workId) ?? 0) - (order.get(b.workId) ?? 0));
6764
7008
  } finally {
6765
- await scope?.close();
7009
+ lease.release();
6766
7010
  }
6767
- return renders.toSorted((a, b) => workIds.indexOf(a.workId) - workIds.indexOf(b.workId));
6768
7011
  }
6769
7012
  async function findings(value, signal = new AbortController().signal) {
6770
7013
  if (!local) throw new ApplicationError("INVALID_TRANSITION");
@@ -6854,7 +7097,32 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
6854
7097
  const current = await storage.current(input.journeyId);
6855
7098
  if (!current) throw new ApplicationError("UNKNOWN_ITEM");
6856
7099
  const expectedRevision = rebase ? current.revision : input.expectedRevision;
6857
- const state = current.revision !== expectedRevision ? current : "resolutions" in input ? resolvePrivacyTerms(current, input) : publishDecisionCards(current, input);
7100
+ let state;
7101
+ try {
7102
+ state = current.revision !== expectedRevision ? current : "resolutions" in input ? resolvePrivacyTerms(current, input) : publishDecisionCards(current, input);
7103
+ } catch (error) {
7104
+ if (!rebase || current.revision <= input.expectedRevision || !(error instanceof ApplicationError) || error.code !== "INVALID_SELECTION") throw error;
7105
+ const receipt = await storage.commit({
7106
+ expectedRevision: input.expectedRevision,
7107
+ requestKey: input.idempotencyKey,
7108
+ requestHash: digest$1(input),
7109
+ state: current,
7110
+ event: {
7111
+ journeyId: current.journeyId,
7112
+ kind: "response",
7113
+ source: "conversation",
7114
+ at: now()
7115
+ }
7116
+ }).catch((probeError) => {
7117
+ if (probeError instanceof ApplicationError && probeError.code === "OPERATION_CONFLICT") throw error;
7118
+ throw probeError;
7119
+ });
7120
+ if (receipt.status === "replayed") return {
7121
+ ...receipt,
7122
+ state: await present(receipt.state)
7123
+ };
7124
+ throw error;
7125
+ }
6858
7126
  const result = await storage.commit({
6859
7127
  expectedRevision,
6860
7128
  requestKey: input.idempotencyKey,
@@ -6952,6 +7220,7 @@ const version$1 = z.number().int().nonnegative();
6952
7220
  const sendPrepareSchema = z.object({
6953
7221
  journeyId: journeyIdSchema,
6954
7222
  expectedJourneyRevision: version$1,
7223
+ displayedScopeDigest: journeyDigestSchema.optional(),
6955
7224
  idempotencyKey: journeyIdSchema
6956
7225
  }).strict();
6957
7226
  const sendActionSchema = z.object({
@@ -6963,6 +7232,7 @@ const sendActionSchema = z.object({
6963
7232
  }).strict();
6964
7233
  const sendStartSchema = sendActionSchema.extend({
6965
7234
  scopeDigest: journeyDigestSchema,
7235
+ displayedScopeDigest: journeyDigestSchema.optional(),
6966
7236
  confirmedWorkIds: z.array(journeyIdSchema),
6967
7237
  confirmedOmittedWorkIds: z.array(journeyIdSchema)
6968
7238
  }).strict();
@@ -11713,6 +11983,48 @@ const storedCredentialSchema = accountProfileSchema.extend({
11713
11983
  expiresAt: z.string().min(1).max(64)
11714
11984
  }).refine((value) => profileUrlMatchesIdentity(value.profileUrl, value.identityUrl));
11715
11985
  //#endregion
11986
+ //#region packages/application/src/account/renew.ts
11987
+ const renewBeforeMs = 12e4;
11988
+ const renewTimeoutMs = 3e4;
11989
+ const renewals = /* @__PURE__ */ new WeakMap();
11990
+ function renew(account, credential) {
11991
+ function configured(value) {
11992
+ return value?.identityUrl === account.configuration.identityUrl && value.clientId === account.configuration.clientId ? value : null;
11993
+ }
11994
+ const running = renewals.get(account);
11995
+ if (running) return running;
11996
+ const renewing = (async () => {
11997
+ try {
11998
+ const stored = configured((await account.credentials.update((state) => state)).credential);
11999
+ if (!stored || stored.refreshToken !== credential.refreshToken) return stored;
12000
+ const token = await account.identity.refresh(credential.refreshToken, AbortSignal.timeout(renewTimeoutMs));
12001
+ if (!token) return configured((await account.credentials.update((state) => {
12002
+ if (state.credential?.refreshToken === credential.refreshToken) {
12003
+ state.credential = null;
12004
+ state.connection = null;
12005
+ state.lastError = "SIGN_IN_EXPIRED";
12006
+ }
12007
+ return state;
12008
+ })).credential);
12009
+ return configured((await account.credentials.update((value) => {
12010
+ if (value.credential?.refreshToken === credential.refreshToken) value.credential = {
12011
+ ...value.credential,
12012
+ accessToken: token.accessToken,
12013
+ refreshToken: token.refreshToken ?? credential.refreshToken,
12014
+ scope: token.scope,
12015
+ issuedAt: account.now(),
12016
+ expiresAt: token.expiresAt
12017
+ };
12018
+ return value;
12019
+ })).credential);
12020
+ } finally {
12021
+ renewals.delete(account);
12022
+ }
12023
+ })();
12024
+ renewals.set(account, renewing);
12025
+ return renewing;
12026
+ }
12027
+ //#endregion
11716
12028
  //#region packages/application/src/account/poll-interval.ts
11717
12029
  /** RFC 8628 section 3.5: each `slow_down` lengthens the interval by five seconds. */
11718
12030
  const slowDownStepMs = 5e3;
@@ -12001,11 +12313,41 @@ function createSharedAccount(ports) {
12001
12313
  return stop(true);
12002
12314
  },
12003
12315
  async verify() {
12004
- const saved = (await read()).credential;
12316
+ let saved = (await read()).credential;
12005
12317
  if (!saved || !belongsToConfiguration(saved)) throw new ApplicationError("ACCOUNT_SIGNED_OUT");
12006
- const result = await ports.connection.check(saved.accessToken, shutdown.signal);
12318
+ let result;
12319
+ let renewing = false;
12320
+ try {
12321
+ const left = Date.parse(saved.expiresAt) - Date.parse(ports.now());
12322
+ if (saved.refreshToken && (!Number.isFinite(left) || left <= 12e4)) {
12323
+ renewing = true;
12324
+ saved = await renew(ports, {
12325
+ ...saved,
12326
+ refreshToken: saved.refreshToken
12327
+ });
12328
+ renewing = false;
12329
+ if (!saved) return view(await read());
12330
+ }
12331
+ result = await ports.connection.check(saved.accessToken, shutdown.signal);
12332
+ if (result === "rejected" && saved.refreshToken) {
12333
+ renewing = true;
12334
+ saved = await renew(ports, {
12335
+ ...saved,
12336
+ refreshToken: saved.refreshToken
12337
+ });
12338
+ renewing = false;
12339
+ if (!saved) return view(await read());
12340
+ result = await ports.connection.check(saved.accessToken, shutdown.signal);
12341
+ }
12342
+ } catch (error) {
12343
+ if (safeCode(error) !== "IDENTITY_UNAVAILABLE" && !(renewing && saved && !expired(saved.expiresAt, ports.now()))) throw error;
12344
+ const detail = typeof error === "object" && error !== null && "detail" in error ? ` (${String(error.detail)})` : "";
12345
+ console.error(`Account verification failed: ${safeCode(error)}${detail}`);
12346
+ result = "unavailable";
12347
+ }
12348
+ const checked = saved;
12007
12349
  return view(await update((state) => {
12008
- if (sameCredential(state.credential, saved)) {
12350
+ if (checked && sameCredential(state.credential, checked)) {
12009
12351
  if (result === "rejected") {
12010
12352
  state.credential = null;
12011
12353
  state.connection = null;
@@ -13412,4 +13754,4 @@ function equivalenceDuplicates(batch) {
13412
13754
  return bindings.sort();
13413
13755
  }
13414
13756
  //#endregion
13415
- export { settingsBatchSchema as $, reviewTransportBytes as $n, journeyPreviewSchema as $r, projectPreparationProgress as $t, deviceAuthorizationSchema as A, maximumHideIndexTokens as An, staticReviewDigest as Ar, nativeAdapterVersions as At, inspectionStartSchema as B, confirmedHideTerms as Bn, sendResolutionSchema as Br, classificationWorks as Bt, rebindReviewOverlay as C, confirmedHideDeltaSchema as Cn, applyCardAnswers as Cr, sendPrepareSchema as Ct, staticallyHiddenFindings as D, termHideAdditions as Dn, nativeGetSchema as Dr, currentJourneyAdapters as Dt, guardedFindingText as E, settleConfirmedHides as En, nativeEventsSchema as Er, settleCorrections as Et, sourceIdentityEvidence as F, bTermMatcher as Fn, agentSourcesSchema as Fr, journeyBatchReadSchema as Ft, validateInspectionClaim as G, retireWorkDescription as Gn, isReadingComplete as Gr, journeyContextSchema as Gt, validateInspectionBatches as H, journeyConfirmedHides as Hn, hasCompleteWorkDescription as Hr, newJourneyWork as Ht, withSourceEvidence as I, bWordBounded as In, journeySendSchema as Ir, journeyReviewStorageQuerySchema as It, validateInspectionInputs as J, workCorrectionBinding as Jn, selectedWorks as Jr, hidePublishSchema as Jt, validateInspectionFence as K, workDecisionDigest as Kn, isSelectedWork as Kr, hideBeginSchema as Kt, agentSetupKey as L, confirmedHideGroupIds as Ln, sendArtifactSchema as Lr, applyClassificationProposal as Lt, profileUrlMatchesIdentity as M, bContainment as Mn, staticSessionResourceSchema as Mr, validateAttemptAppend as Mt, readAccountAvatarUrl as N, bExceptionRanges as Nn, agentSourcePathSegment as Nr, journeyGuidance as Nt, createAccount as O, addHideIndexTokens as On, nativeRespondSchema as Or, obsoleteJourney as Ot, storedCredentialSchema as P, bMaskedHits as Pn, agentSourceSchema as Pr, applyClassification as Pt, inspectionInputs as Q, reviewRepairResourceSchema as Qn, journeyFindingsSchema as Qr, journeyPolicyRespondSchema as Qt, createApplication as R, confirmedHideKey as Rn, sendReceiptSchema as Rr, classificationCommitSchema as Rt, rebindReviewAlignment as S, confirmedHideAdditions as Sn, journeyClassificationSessionsSchema as Sr, sendChoiceSchema as St, reviewBatchReuse as T, renderedHideValue as Tn, privacyDecisionDigest as Tr, sendStartSchema as Tt, validateInspectionRequest as U, maskedHitsStale as Un, isClassifying as Ur, reviewCompileSchema as Ut, validateInspectionBatch as V, dictionaryHides as Vn, sendSettingsSchema as Vr, retireScanGeneration as Vt, inspectionQuerySchema as W, syncTermGroups as Wn, isIncludedWork as Wr, journeyContextQuerySchema as Wt, validateInspectionContribution as X, reviewCandidateResourceSchema as Xn, mergePrivacyTerms as Xr, journeySearchSaveSchema as Xt, validateInspectionQuery as Y, reviewCandidateInputSchema as Yn, withReadingProgress as Yr, journeySearchResultSchema as Yt, validateDetectionSummary as Z, reviewRepairInputSchema as Zn, journeyFindingImpactSchema as Zr, searchScopeDigest as Zt, changeJourney as _, sessionUsefulnessInputSchema as _n, emptyPreparationFacts as _r, emptyTermsRevision as _t, bCrossSegmentReviewSchema as a, selectionSchema 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, stateSchema as ci, sessionReconcileInputSchema as cn, scanGitSeedsResource as cr, resolveFailureLocation as ct, resolveGroupSplit as d, approvedTreatmentSchema as di, sessionRereadInputSchema as dn, validateScanExclusion as dr, manualUploadBindingSchema as dt, journeyRenderInputSchema as ei, journeyResourceQuerySchema as en, bReviewDigest as er, settingsQuerySchema as et, selectedValueMembers as f, effectiveTreatmentSchema as fi, sessionRereadResourceSchema as fn, resolveSourceSelection as fr, manualUploadChoiceSchema as ft, semanticGroupKey as g, staticCatalogRef as gi, sessionReviewStatusSchema as gn, journeyPreparationProgressSchema as gr, candidateSortAvailable as gt, hideScopeDigest as h, selectFrozenConfiguration as hi, sessionReviewResourceSchema as hn, journeyInventorySchema as hr, manualUploadStartSchema as ht, validateBCrossSegmentReview as i, selectionBatchSchema as ii, sortingApplySchema as in, scanCanonicalResourceSchema as ir, classifyAgentSetup as it, deviceProfileScopeSchema as j, packHideIndex as jn, staticSessionId as jr, attemptBudgetCodePoints as jt, accountProfileSchema as k, hideKeyId as kn, hasCurrentStaticReview as kr, metadataProcessingVersion as kt, chooseCheapPlacement as l, sourceChangePolicy as li, sessionReconcileResultSchema as ln, verifiedScanGitSeeds as lr, validateFailureLocation as lt, activeJourneyTerms as m, freezeConfiguration as mi, sessionReviewQuerySchema as mn, journeyInventoryReadSchema as mr, manualUploadSchema as mt, scanBatchEquivalenceDigest as n, commandSchema as ni, journeyIsolateSchema as nn, planBRepair as nr, reviewInventory as nt, bRereadDigest as o, gitMetadataBudgetBytes as oi, reviewAttemptsQuerySchema as on, scanSessionResourceSchema as or, maximumAgentSetupSessions as ot, splitSelectorValues as p, defaultScopedSettings as pi, sessionReviewInputSchema as pn, journeyInventoryQuerySchema as pr, manualUploadPrepareSchema as pt, validateInspectionHead as q, workPrivacyDecisions as qn, isSkippedWork as qr, hideFinishSchema as qt, locateTermEvidence as r, packageCommandSchema as ri, journeyPlaceSchema as rn, scanAliasResourceSchema as rr, appendSensitiveTerms as rt, reconcileBSession as s, producerVersion as si, reviewAttemptsResultSchema as sn, scanGitSeedsId as sr, artifactFailureDetailSchema as st, scanBatchContextDigest as t, journeyRenderSchema as ti, journeyResourceSchema as tn, mergeBRepair as tr, readWorkspaceOutcomes as tt, reviseGroupMembership as u, recommendParallelism as ui, sessionReconciliationResourceSchema as un, scanExclusionResourceSchema as ur, manualUploadActionSchema as ut, projectJourneyInventory as v, sessionUsefulnessResourceSchema as vn, preparationBeginSchema as vr, maximumTermsFileBytes as vt, rebindReviewSubmit as w, readConfirmedHideChange as wn, assertJourneyPrivacyReady 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, sendReplaceResultSchema as zr, classificationPartition as zt };
13757
+ export { validateDetectionSummary as $, reviewRepairInputSchema as $n, isClassifying as $r, searchScopeDigest as $t, renewBeforeMs as A, addHideIndexTokens as An, displayedScopeDigest as Ar, obsoleteJourney as At, createApplication as B, confirmedHideKey as Bn, staticReviewDigest as Br, classificationPartition as Bt, rebindReviewOverlay as C, journeyEventSchema as Cn, validatePreparationFacts as Cr, sendActionSchema as Ct, staticallyHiddenFindings as D, renderedHideValue as Dn, journeyPreviewSchema as Dr, sendStartSchema as Dt, guardedFindingText as E, readConfirmedHideChange as En, journeyFindingsSchema as Er, sendRequestSchema as Et, readAccountAvatarUrl as F, bExceptionRanges as Fn, nativeEventsSchema as Fr, journeyGuidance as Ft, validateInspectionRequest as G, maskedHitsStale as Gn, agentSourcesSchema as Gr, reviewCompileSchema as Gt, inspectionStartSchema as H, confirmedHideTerms as Hn, staticSessionResourceSchema as Hr, applyClassification as Ht, storedCredentialSchema as I, bMaskedHits as In, nativeGetSchema as Ir, journeyBatchReadSchema as It, validateInspectionFence as J, workDecisionDigest as Jn, sendReceiptSchema as Jr, hideBeginSchema as Jt, inspectionQuerySchema as K, syncTermGroups as Kn, journeySendSchema as Kr, journeyContextQuerySchema as Kt, sourceIdentityEvidence as L, bTermMatcher as Ln, nativeRespondSchema as Lr, journeyReviewStorageQuerySchema as Lt, deviceAuthorizationSchema as M, maximumHideIndexTokens as Mn, applyCardAnswers as Mr, nativeAdapterVersions as Mt, deviceProfileScopeSchema as N, packHideIndex as Nn, assertJourneyPrivacyReady as Nr, attemptBudgetCodePoints as Nt, createAccount as O, settleConfirmedHides as On, journeyRenderInputSchema as Or, settleCorrections as Ot, profileUrlMatchesIdentity as P, bContainment as Pn, privacyDecisionDigest as Pr, validateAttemptAppend as Pt, validateInspectionContribution as Q, reviewCandidateResourceSchema as Qn, hasCompleteWorkDescription as Qr, journeySearchSaveSchema as Qt, withSourceEvidence as R, bWordBounded as Rn, mergePrivacyTerms as Rr, applyClassificationProposal as Rt, rebindReviewAlignment as S, journeyCommitSchema as Sn, preparationStartSchema as Sr, replaceSensitiveTermsSchema as St, reviewBatchReuse as T, confirmedHideDeltaSchema as Tn, journeyFindingImpactSchema as Tr, sendPrepareSchema as Tt, validateInspectionBatch as U, dictionaryHides as Un, agentSourcePathSegment as Ur, retireScanGeneration as Ut, inspectionCancelSchema as V, confirmedHideKinds as Vn, staticSessionId as Vr, classificationWorks as Vt, validateInspectionBatches as W, journeyConfirmedHides as Wn, agentSourceSchema as Wr, newJourneyWork as Wt, validateInspectionInputs as X, workCorrectionBinding as Xn, sendResolutionSchema as Xr, hidePublishSchema as Xt, validateInspectionHead as Y, workPrivacyDecisions as Yn, sendReplaceResultSchema as Yr, hideFinishSchema as Yt, validateInspectionQuery as Z, reviewCandidateInputSchema as Zn, sendSettingsSchema as Zr, journeySearchResultSchema as Zt, changeJourney as _, defaultScopedSettings as _i, sessionReviewResourceSchema as _n, journeyInventorySchema as _r, manualUploadStartSchema as _t, bCrossSegmentReviewSchema as a, withReadingProgress as ai, journeyPlaceSchema as an, scanAliasResourceSchema as ar, appendSensitiveTerms as at, isGenericPublicTerm as b, staticCatalogRef as bi, sessionUsefulnessResourceSchema as bn, preparationBeginSchema as br, maximumTermsFileBytes as bt, reconstructBFields as c, selectionBatchSchema as ci, reviewAttemptsQuerySchema as cn, scanSessionResourceSchema as cr, maximumAgentSetupSessions as ct, resolveGroupSplit as d, producerVersion as di, sessionReconcileResultSchema as dn, verifiedScanGitSeeds as dr, validateFailureLocation as dt, isIncludedWork as ei, journeyPolicyRespondSchema as en, reviewRepairResourceSchema as er, inspectionInputs as et, selectedValueMembers as f, stateSchema as fi, sessionReconciliationResourceSchema as fn, scanExclusionResourceSchema as fr, manualUploadActionSchema as ft, semanticGroupKey as g, effectiveTreatmentSchema as gi, sessionReviewQuerySchema as gn, journeyInventoryReadSchema as gr, manualUploadSchema as gt, hideScopeDigest as h, approvedTreatmentSchema as hi, sessionReviewInputSchema as hn, journeyInventoryQuerySchema as hr, manualUploadPrepareSchema as ht, validateBCrossSegmentReview as i, selectedWorks as ii, journeyIsolateSchema as in, planBRepair as ir, reviewInventory as it, accountProfileSchema as j, hideKeyId as jn, requestedScope as jr, metadataProcessingVersion as jt, renew as k, termHideAdditions as kn, journeyRenderSchema as kr, currentJourneyAdapters as kt, chooseCheapPlacement as l, selectionSchema as li, reviewAttemptsResultSchema as ln, scanGitSeedsId as lr, artifactFailureDetailSchema as lt, activeJourneyTerms as m, recommendParallelism as mi, sessionRereadResourceSchema as mn, resolveSourceSelection as mr, manualUploadChoiceSchema as mt, scanBatchEquivalenceDigest as n, isSelectedWork as ni, journeyResourceQuerySchema as nn, bReviewDigest as nr, settingsQuerySchema as nt, bRereadDigest as o, commandSchema as oi, sortingApplySchema as on, scanCanonicalResourceSchema as or, classifyAgentSetup as ot, splitSelectorValues as p, sourceChangePolicy as pi, sessionRereadInputSchema as pn, validateScanExclusion as pr, manualUploadBindingSchema as pt, validateInspectionClaim as q, retireWorkDescription as qn, sendArtifactSchema as qr, journeyContextSchema as qt, locateTermEvidence as r, isSkippedWork as ri, journeyResourceSchema as rn, mergeBRepair as rr, readWorkspaceOutcomes as rt, reconcileBSession as s, packageCommandSchema as si, reviewAttemptResourceSchema as sn, scanPreparationDigest as sr, defaultAgentSetupInclusion as st, scanBatchContextDigest as t, isReadingComplete as ti, projectPreparationProgress as tn, reviewTransportBytes as tr, settingsBatchSchema as tt, reviseGroupMembership as u, gitMetadataBudgetBytes as ui, sessionReconcileInputSchema as un, scanGitSeedsResource as ur, resolveFailureLocation as ut, projectJourneyInventory as v, freezeConfiguration as vi, sessionReviewStatusSchema as vn, journeyPreparationProgressSchema as vr, candidateSortAvailable as vt, rebindReviewSubmit as w, confirmedHideAdditions as wn, journeyClassificationSessionsSchema as wr, sendChoiceSchema as wt, bReviewScope as x, journeyCommitResultSchema as xn, preparationSettlementSchema as xr, normalizeSensitiveTerms as xt, preparationScope as y, selectFrozenConfiguration as yi, sessionUsefulnessInputSchema as yn, emptyPreparationFacts as yr, emptyTermsRevision as yt, agentSetupKey as z, confirmedHideGroupIds as zn, hasCurrentStaticReview as zr, classificationCommitSchema as zt };