@gitroll/tacitt-cli 0.6.12 → 0.6.13

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 (38) hide show
  1. package/dist/{agent-setup-B12VbvN_.mjs → agent-setup-Doy_AnPY.mjs} +2 -2
  2. package/dist/agent-setup-worker.mjs +5 -5
  3. package/dist/cli.mjs +10 -10
  4. package/dist/codex-title-worker.mjs +1 -1
  5. package/dist/configured-preview-worker.mjs +5 -5
  6. package/dist/configured-session-worker.mjs +5 -5
  7. package/dist/{frame-B3He21G8.mjs → frame-BPbypzmt.mjs} +1 -1
  8. package/dist/{heap-Blrat--4.mjs → heap-DCS_WXIA.mjs} +7 -3
  9. package/dist/inspection-worker.mjs +4 -4
  10. package/dist/{job-CBuIMTQ0.mjs → job--zxwgaPy.mjs} +135 -36
  11. package/dist/{locate-D-ZaHpI0.mjs → locate-PLEyLYW-.mjs} +287 -236
  12. package/dist/{model-DR9odhWP.mjs → model-CyXvsWLz.mjs} +1 -1
  13. package/dist/operation-executor.mjs +3 -3
  14. package/dist/preview-worker.mjs +1 -1
  15. package/dist/privacy-validator.mjs +1 -1
  16. package/dist/{query-BYDcNKy2.mjs → query-BC7MIaWc.mjs} +335 -115
  17. package/dist/{run-DQ9xyKSt.mjs → run-Dod7Vykp.mjs} +6 -6
  18. package/dist/{src-DeNJ8vOD.mjs → src-CP-K2PBv.mjs} +1779 -1686
  19. package/dist/{src-Do-qzuMn.mjs → src-CttNuxOn.mjs} +590 -542
  20. package/dist/{src-CKQlTA2Z.mjs → src-dL6y2mcu.mjs} +180 -30
  21. package/dist/store-database-worker.mjs +1 -1
  22. package/package.json +1 -1
  23. package/skills/tacitt/SKILL.md +3 -3
  24. package/skills/tacitt/references/b-method.md +4 -2
  25. package/skills/tacitt/references/bootstrap.md +1 -1
  26. package/skills/tacitt/references/commands.md +10 -4
  27. package/skills/tacitt/references/journey.md +3 -3
  28. package/skills/tacitt/references/parallel-review.md +22 -9
  29. package/skills/tacitt/references/remote-workers.md +6 -2
  30. package/skills/tacitt/scripts/INDEX.md +4 -4
  31. package/skills/tacitt/scripts/answer.mjs +12 -1
  32. package/skills/tacitt/scripts/describe.mjs +3 -2
  33. package/skills/tacitt/scripts/journey-classification.mjs +1 -0
  34. package/skills/tacitt/scripts/journey-scan-timing.mjs +3 -2
  35. package/skills/tacitt/scripts/journey.mjs +1 -1
  36. package/skills/tacitt/scripts/privacy-validator.mjs +84 -33
  37. package/skills/tacitt/scripts/review.d.mts +1 -0
  38. package/skills/tacitt/scripts/review.mjs +296 -35
@@ -1,4 +1,4 @@
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-D-ZaHpI0.mjs";
1
+ import { $ as createCodePointIndex, $n as validateProfile, A as bLegacyBindings, An as currentDetectorVersion, B as bIssue, Bt as reviewTermsResultSchema, C as validateBReview, Ct as hideConfirmSchema, Dt as cardAnswerActionSchema, E as validateBUnit, Et as journeyConfigurationSchema, F as bLiterals, Fn as createStaticProcessor, G as bReviewSubmissionSchema, Gt as termTypes, H as unownedBObligations, Hn as cleanupImplementationVersion, Ht as termOverrideValueSchema, I as uncoveredBObligations, In as staticDefaults, J as bAnchorKey, Jt as sessionPeriodSchema, K as bSessionDecisionSchema, Kt as deselectOutsidePeriod, L as indexBRanges, Ln as staticOptions, Lt as emptyReviewCorrectionSchema, M as semanticFindingSchema, Mt as packageConfirmActionSchema, N as bLiteralAdmitted, Nn as gapReasons, Nt as privacyResolveSchema, O as bDuplicateMirror, Ot as cardsConfirmActionSchema, P as bLiteralTerms, Pt as privacyTermKey, Q as codePointSlice, R as bAdoptedObligations, S as validateBClassification, T as validateBFields, Tt as hidePlanResourceSchema, U as bRepresentationSchema, Un as entityDictionaryVersion, Ut as termTypeInfo, V as bOverlaps, Vt as termAssessmentSchema, W as bRetainedGroupSchema, Wt as termTypeSchema, X as bDigest, Xt as journeyProfileAddressActionSchema, Y as bEvidenceReader, Yt as emptyJourneyProfile, Z as codePointLength, Zt as journeyProfileLinksActionSchema, _ as reviewAlignmentInputSchema, _r as defaultDetectionLimits, _t as reviewInterruptionSchema, an as UnfinishedJourneyError, at as bMethod, b as validateReviewAlignment, bn as nativeSourceIdentitySchema, bt as delegatedDecisionSchema, c as reviewBatchSchema, cn as familyNeedsReview, ct as journeyChoiceSchema, d as reviewSubmitSchema, dn as sourceGraph, dt as journeyIdSchema, er as orderProfile, et as subtractBRanges, f as textDigest, fn as sourceKey, fr as createEntityMatcher, ft as journeyKindSchema, g as reviewAlignmentAuditSchema, gn as inventorySchema, gr as scalarBefore, gt as nativeJourneySchema, hn as fileIdentitySchema, hr as scalarAfter, ht as journeyWorkTypeSchema, in as ApplicationError, it as bIdSchema, j as bUnitFragments, jn as inspectionSemanticVersions, jt as emptyJourneyPrivacy, kt as cardsSubmitSchema, l as reviewChunkSchema, ln as familySelected, lr as staticCategories, lt as journeyDigestSchema, m as findingReferenceKey, mn as sourceSnapshotId, mt as journeyProjectTypeSchema, nn as respondProfileLinks, nr as validatePlan, nt as bAnchorSchema, o as compileBFindings, on as safeCode, ot as consentedWithoutAccount, p as validateReview, pn as observeSource, pt as journeyPreferenceScopeSchema, q as bTermSchema, rn as classificationTitleMaskSchema, rr as copyData, rt as bCountSchema, s as reviewBatchChunks, sr as detectStatic, st as journeyAccessSchema, u as reviewDigest, un as reviewFamilies, ut as journeyExceptionSchema, v as reviewAlignmentSchema, vn as sourcePartSchema, vt as journeyGuidanceSchema, w as createBAnalysisUnits, wt as hidePlanInputSchema, xt as journeyDescribeSchema, y as reviewFindingsDigest, yt as journeyInstructionInputSchema, zt as reviewTermsInputSchema } from "./locate-PLEyLYW-.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";
@@ -1199,108 +1199,439 @@ function requireSelecting(run) {
1199
1199
  if (run.phase !== "selecting") throw new ApplicationError("INVALID_TRANSITION");
1200
1200
  }
1201
1201
  //#endregion
1202
- //#region packages/application/src/journey/placement/readiness.ts
1203
- function hasCompleteWorkDescription(work) {
1204
- return work.placement === "placed" && !!work.description.trim() && work.classificationRecommendation !== null;
1205
- }
1206
- /** Explain why a requested Work still has no usable output after rendering. */
1207
- function unfinishedPackagingCode(work) {
1208
- if (!work) return "REVISION_CONFLICT";
1209
- if (work.renderFailure?.revision === work.revision) return work.renderFailure.code;
1210
- if (!hasCompleteWorkDescription(work)) return "WORK_METADATA_INCOMPLETE";
1211
- if (!work.accessConfirmed) return "CONFIRMATION_REQUIRED";
1212
- return "PACKAGING_FAILED";
1213
- }
1202
+ //#region packages/processing/src/session/marker.ts
1214
1203
  /**
1215
- * Dropped Works are not AI-read; a Work emptied by repartition has nothing left to read, and
1216
- * roots classification has not grouped yet are read once they join a Work (E2).
1204
+ * One complete replacement marker as bindings and semantic markers write it. Group 1 names the
1205
+ * static method, or `semantic` for an AI-review span. Dependency-free so the browser can use it.
1217
1206
  */
1218
- function isSelectedWork(work) {
1219
- return work.access !== "drop" && work.rootSessionIds.length > 0 && work.placement !== "classifying";
1207
+ const replacementMarkerPattern = /\[sp_[a-z0-9]{8,48}_([a-z][a-z0-9_]{0,47})_[a-f0-9]{24,64}\]/gu;
1208
+ //#endregion
1209
+ //#region packages/application/src/journey/b/matches.ts
1210
+ function bDictionaryHits(fields, literals, signal = new AbortController().signal) {
1211
+ const matcher = createEntityMatcher([...new Set(literals.map((part) => part.term))].map((value) => ({
1212
+ kind: "custom-term",
1213
+ value
1214
+ })));
1215
+ const hits = [];
1216
+ for (const field of fields) {
1217
+ const index = createCodePointIndex(field.text);
1218
+ const base = field.start ?? 0;
1219
+ for (const match of matcher.find(field.text, signal, 1e5)) {
1220
+ if (hits.length >= 1e6) throw new ApplicationError("PROCESSING_LIMIT");
1221
+ const body = {
1222
+ fieldId: field.id,
1223
+ fieldHash: field.fieldHash,
1224
+ recordId: field.recordId,
1225
+ start: base + index.codePointOffset(match.range.start),
1226
+ end: base + index.codePointOffset(match.range.end),
1227
+ exactQuote: field.text.slice(match.range.start, match.range.end),
1228
+ term: match.entity.value
1229
+ };
1230
+ hits.push({
1231
+ ...body,
1232
+ digest: bDigest(JSON.stringify(body))
1233
+ });
1234
+ }
1235
+ }
1236
+ return [...new Map(hits.map((hit) => [hit.digest, hit])).values()];
1220
1237
  }
1221
- /** Classification published some Works and is still grouping the remaining roots (E2). */
1222
- function isClassifying(state) {
1223
- return state.works.some((work) => work.placement === "classifying");
1238
+ function unclassifiedBHits(hits, literals, negative) {
1239
+ const issues = [];
1240
+ const positiveFields = new Map([...Map.groupBy(literals, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
1241
+ const negativeFields = new Map([...Map.groupBy(negative, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
1242
+ for (const hit of hits) {
1243
+ const positive = positiveFields.get(hit.fieldId)?.relation(hit).covers ?? false;
1244
+ const negative = negativeFields.get(hit.fieldId)?.relation(hit);
1245
+ if (!positive && !negative?.covers || positive && negative?.overlaps) issues.push(bIssue(positive ? "CONFLICTING_PRIVACY_DECISIONS" : "UNCLASSIFIED_MATCH", hit));
1246
+ }
1247
+ return issues;
1224
1248
  }
1225
- /**
1226
- * A Work whose read failed: it is skipped for this Send without a decision (ADR 0236). It holds no
1227
- * reading, card, packaging or Send gate and is never sent; Retry or a move out of Drop reads it
1228
- * again. A pending Hide also blocks the Works it changes, but those wait for its recovery and are
1229
- * not skipped. The Skill helpers mirror this rule in `journey-state.mjs`.
1230
- */
1231
- function isSkippedWork(work) {
1232
- return work.status === "blocked" && !work.blockedCodes.includes("PRIVACY_CHANGE_PENDING") && currentReviewInterruptions(work).length === 0;
1249
+ 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);
1250
+ /** In decoded tool text an odd backslash run escapes its following n, r or t. */
1251
+ function afterEscapedSpace(text, start) {
1252
+ const letter = text[start - 1];
1253
+ if (letter !== "n" && letter !== "r" && letter !== "t") return false;
1254
+ let index = start - 2;
1255
+ while (index >= 0 && text[index] === "\\") index--;
1256
+ return (start - 2 - index) % 2 === 1;
1233
1257
  }
1234
- /** A selected Work that is not skipped (ADR 0236). */
1235
- function isIncludedWork(work) {
1236
- return isSelectedWork(work) && !isSkippedWork(work);
1258
+ /** A confirmed Hide value matches only where it does not run into a neighbouring word (ADR 0069). */
1259
+ function bWordBounded(text, start, end, toolResultEscapes = false) {
1260
+ return !(bSpacedWord(scalarBefore(text, start)) && bSpacedWord(scalarAfter(text, start)) && !(toolResultEscapes && afterEscapedSpace(text, start)) || bSpacedWord(scalarBefore(text, end)) && bSpacedWord(scalarAfter(text, end)));
1237
1261
  }
1238
1262
  /**
1239
- * ADR 0274: Public needs a recorded choice: the person's panel click or named conversation
1240
- * answer (origin `explicit`), or, under ADR 0275, the account proof that every root was already
1241
- * Public (origin and channel `account`). A Public Work stored before `accessChoice` existed is
1242
- * still pending. Mirrored by `phaseOf` in the Skill's `journey-state.mjs` and by the panel's
1243
- * `WorkAccess`.
1263
+ * Occurrences of a Work's explicit exceptions, matched like Hide everywhere: exact NFC values
1264
+ * over complete graphemes. A confirmed Hide value inside one stays visible there.
1244
1265
  */
1245
- function hasPublicChoice(work) {
1246
- if (work.access !== "public") return true;
1247
- if (!work.accessConfirmed || !work.accessChoice) return false;
1248
- return work.accessChoice.channel === "account" ? work.accessOrigin === "account" : work.accessOrigin === "explicit";
1249
- }
1250
- /** ADR 0277 slice 2 display projection. Send consent remains a separate gate. */
1251
- function publicSource(work) {
1252
- if (!isIncludedWork(work) || work.access !== "public" || work.accountAccessConflict) return null;
1253
- if (!hasPublicChoice(work)) return "suggestion";
1254
- return work.accessChoice?.channel === "account" ? "account" : "choice";
1266
+ function bExceptionRanges(values) {
1267
+ const matcher = createEntityMatcher([...new Set([...values].map((value) => value.normalize("NFC")))].map((value) => ({
1268
+ kind: "custom-term",
1269
+ value
1270
+ })));
1271
+ 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);
1255
1272
  }
1256
- function selectedWorks(state) {
1257
- return state.works.filter(isSelectedWork);
1273
+ /** Whether a range lies inside one of `ranges`: a prefix maximum of ends, O(log n) per query. */
1274
+ function bContainment(ranges) {
1275
+ const sorted = ranges.toSorted((a, b) => a.start - b.start);
1276
+ const reach = [];
1277
+ for (const [index, range] of sorted.entries()) reach.push(Math.max(range.end, reach[index - 1] ?? range.end));
1278
+ return (range) => {
1279
+ let low = 0, high = sorted.length;
1280
+ while (low < high) {
1281
+ const middle = low + high >> 1;
1282
+ if ((sorted[middle]?.start ?? Infinity) <= range.start) low = middle + 1;
1283
+ else high = middle;
1284
+ }
1285
+ return low > 0 && (reach[low - 1] ?? -1) >= range.end;
1286
+ };
1258
1287
  }
1259
- /** Categories found only in Dropped Works are neither asked nor counted. */
1260
- function unselectedGroupIds(state) {
1261
- const selected = new Set(selectedWorks(state).flatMap((work) => work.groupIds));
1262
- return new Set(state.works.filter((work) => !isSelectedWork(work)).flatMap((work) => work.groupIds).filter((id) => !selected.has(id)));
1288
+ /**
1289
+ * Word-bounded, exact, case-sensitive NFC occurrences of the terms, in UTF-16 coordinates,
1290
+ * except where an explicit exception occurrence contains one.
1291
+ */
1292
+ function bTermMatcher(terms, exceptions = []) {
1293
+ const matcher = createEntityMatcher([...new Set(terms)].map((value) => ({
1294
+ kind: "custom-term",
1295
+ value
1296
+ })));
1297
+ const excepted = bExceptionRanges(exceptions);
1298
+ return (text, signal = new AbortController().signal) => {
1299
+ const hits = matcher.find(text, signal, 1e5).filter(({ range }) => bWordBounded(text, range.start, range.end));
1300
+ const kept = hits.length ? bContainment(excepted(text, signal)) : () => false;
1301
+ return hits.filter(({ range }) => !kept(range)).map(({ range, entity }) => ({
1302
+ ...range,
1303
+ term: entity.value
1304
+ }));
1305
+ };
1263
1306
  }
1264
- /** Every root without batches is covered by an empty-input or explicit exclusion proof. */
1265
- function hasCertifiedNoBatchReview(work, snapshotId) {
1266
- const proofs = [work.emptyReview, work.excludedReview].filter((proof) => proof !== void 0);
1267
- if (!proofs.length || proofs.some((proof) => proof.snapshotId !== snapshotId || proof.inputRevision !== work.inputRevision)) return false;
1268
- const roots = proofs.flatMap((proof) => proof.rootSessionIds);
1269
- return roots.length === work.rootSessionIds.length && new Set(roots).size === roots.length && work.rootSessionIds.every((id) => roots.includes(id));
1307
+ /** Dictionary hits that a confirmed Hide value masks at render, so they need no classification. */
1308
+ function bMaskedHits(fields, terms) {
1309
+ const byId = new Map(fields.map((field) => [field.id, {
1310
+ text: field.text,
1311
+ index: createCodePointIndex(field.text),
1312
+ base: field.start ?? 0
1313
+ }]));
1314
+ return (hit) => {
1315
+ const field = byId.get(hit.fieldId);
1316
+ return !!field && terms.has(hit.term) && bWordBounded(field.text, field.index.offset(hit.start - field.base), field.index.offset(hit.end - field.base));
1317
+ };
1270
1318
  }
1271
- /** A reused Work ID cannot carry an old input generation's failed review forward. */
1272
- function currentReviewInterruptions(work) {
1273
- return (work.reviewInterruptions ?? []).filter((item) => item.inputRevision === work.inputRevision);
1319
+ //#endregion
1320
+ //#region packages/application/src/journey/terms/groups.ts
1321
+ const keyPrefix = "type:";
1322
+ const termGroupKey = (type) => `${keyPrefix}${type}`;
1323
+ /** RFC 9562 UUID version 5 of `name` in the `namespace` UUID. */
1324
+ function uuidV5(namespace, name) {
1325
+ const bytes = createHash("sha1").update(Buffer.from(namespace.replaceAll("-", ""), "hex")).update(name, "utf8").digest().subarray(0, 16);
1326
+ bytes.writeUInt8(bytes.readUInt8(6) & 15 | 80, 6);
1327
+ bytes.writeUInt8(bytes.readUInt8(8) & 63 | 128, 8);
1328
+ const hex = bytes.toString("hex");
1329
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1274
1330
  }
1275
- /** Reading completes over included Works once the person has confirmed the scan scope. */
1276
- function isReadingComplete(state) {
1277
- 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);
1331
+ /** Deterministic: every reader and writer of a journey names a type's group alike. */
1332
+ const termGroupId = (journeyId, type) => uuidV5(journeyId, type);
1333
+ /** The term type a group answers, or null for any other category. */
1334
+ function termGroupType(group) {
1335
+ if (!group.key.startsWith(keyPrefix)) return null;
1336
+ const parsed = termTypeSchema.safeParse(group.key.slice(5));
1337
+ return parsed.success ? parsed.data : null;
1278
1338
  }
1279
- /** Recompute completion after a selection or reading change without leaving a paused/send phase. */
1280
- function withReadingProgress(state) {
1281
- const readingComplete = isReadingComplete(state);
1282
- const phase = state.phase === "paused" || !state.snapshotId ? state.phase : readingComplete ? state.phase === "processing" ? "review" : state.phase : state.phase === "review" || state.phase === "ready" ? "processing" : state.phase;
1339
+ const typeGroup = (state, type) => state.groups.find((group) => termGroupType(group) === type);
1340
+ function newTypeGroup(state, type, preference) {
1341
+ const info = termTypeInfo[type];
1342
+ const base = {
1343
+ id: termGroupId(state.journeyId, type),
1344
+ kind: info.kind,
1345
+ key: termGroupKey(type),
1346
+ label: info.label,
1347
+ condition: info.condition,
1348
+ count: 0,
1349
+ preferenceScope: null,
1350
+ representative: null
1351
+ };
1352
+ if (!info.asked) return {
1353
+ ...base,
1354
+ belief: .97,
1355
+ choice: "hide",
1356
+ confirmed: true,
1357
+ origin: "policy"
1358
+ };
1359
+ if (state.questionsPaused) return {
1360
+ ...base,
1361
+ belief: .97,
1362
+ choice: "hide",
1363
+ confirmed: false,
1364
+ origin: "paused"
1365
+ };
1283
1366
  return {
1284
- ...state,
1285
- readingComplete,
1286
- phase
1367
+ ...base,
1368
+ belief: preference === null ? .5 : preference === "hide" ? .97 : .03,
1369
+ choice: preference,
1370
+ confirmed: false,
1371
+ origin: preference === null ? "model" : "preference"
1287
1372
  };
1288
1373
  }
1289
- //#endregion
1290
- //#region packages/application/src/journey/send/model.ts
1291
- const count$13 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
1292
- const access = z.enum(["public", "private"]);
1293
- const sendSourceSchema = z.object({
1294
- id: z.string().regex(/^w[0-9]{6,}$/u),
1295
- pathHash: journeyDigestSchema.nullable(),
1296
- remoteHashes: z.array(journeyDigestSchema),
1297
- primaryRemoteHash: journeyDigestSchema.nullable(),
1298
- challengeId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/u).optional()
1299
- }).strict();
1300
- function agentSourcePathSegment(value) {
1301
- return /^[A-Za-z0-9.][A-Za-z0-9._-]*$/u.test(value) && value !== "." && value !== "..";
1374
+ /**
1375
+ * One group per type with at least one dictionary value, `count` its distinct values. Existing
1376
+ * groups keep their identity and answer; other groups are unchanged.
1377
+ */
1378
+ function syncTermGroups(state, counts, preference) {
1379
+ const groups = state.groups.map((group) => {
1380
+ const type = termGroupType(group);
1381
+ return type === null ? group : {
1382
+ ...group,
1383
+ count: counts.get(type) ?? group.count
1384
+ };
1385
+ });
1386
+ for (const type of termTypes) {
1387
+ const count = counts.get(type) ?? 0;
1388
+ if (count === 0 || groups.some((group) => termGroupType(group) === type)) continue;
1389
+ groups.push({
1390
+ ...newTypeGroup(state, type, preference(termGroupKey(type))),
1391
+ count
1392
+ });
1393
+ }
1394
+ return groups;
1302
1395
  }
1303
- /** Local candidates become public only after anonymous upload-time evidence checks. */
1396
+ //#endregion
1397
+ //#region packages/application/src/journey/review/confirmed-hides.ts
1398
+ /** Kinds from lowest to highest mask priority. */
1399
+ const confirmedHideKinds = [
1400
+ "internal",
1401
+ "identity",
1402
+ "private",
1403
+ "credentials"
1404
+ ];
1405
+ const kinds = confirmedHideKinds;
1406
+ const rank$1 = (kind) => kinds.indexOf(kind);
1407
+ /**
1408
+ * ADR 0086: a type's values are hidden unless its group is answered Keep; before an answer the
1409
+ * default is Hide. Credentials and contact details are always hidden (D06).
1410
+ */
1411
+ function typeHides(state, type) {
1412
+ const group = typeGroup(state, type);
1413
+ return !termTypeInfo[type].asked || !group?.confirmed || group.choice !== "keep";
1414
+ }
1415
+ /** B review (superseded by ADR 0086): IDs of the categories confirmed as Hide. */
1416
+ const confirmedHideGroupIds = (state) => state.groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => group.id);
1417
+ const overrideKey = (state) => (state.valueOverrides ?? []).map((item) => JSON.stringify([
1418
+ item.type,
1419
+ item.value,
1420
+ item.choice
1421
+ ])).toSorted();
1422
+ /**
1423
+ * Identifies the hidden set (ADR 0079, 0086). The dictionary only grows and a type's group
1424
+ * counts its distinct values, so each type's count and Hide answer, with the value overrides,
1425
+ * name exactly one hidden set.
1426
+ */
1427
+ function confirmedHideKey(state) {
1428
+ return createHash("sha256").update(JSON.stringify({
1429
+ types: termTypes.map((type) => [
1430
+ type,
1431
+ typeGroup(state, type)?.count ?? 0,
1432
+ typeHides(state, type)
1433
+ ]),
1434
+ overrides: overrideKey(state),
1435
+ 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)))
1436
+ })).digest("hex");
1437
+ }
1438
+ /** Whether two states hold the same value overrides. */
1439
+ const sameValueOverrides = (left, right) => JSON.stringify(overrideKey(left)) === JSON.stringify(overrideKey(right));
1440
+ /**
1441
+ * A preparation's waived hits stay waived only while every category recorded for them is still a
1442
+ * confirmed Hide; otherwise render may no longer mask them, so the session needs reconciliation.
1443
+ */
1444
+ function maskedHitsStale(maskedHits, hiding) {
1445
+ const current = new Set(hiding);
1446
+ return !!maskedHits?.some((hit) => hit.groupIds.some((id) => !current.has(id)));
1447
+ }
1448
+ /**
1449
+ * ADR 0086 hidden set: values of types not answered Keep, minus Keep overrides, plus Hide
1450
+ * overrides, plus every value of an always-hidden type. A value of several types takes the
1451
+ * highest mask kind among the types hiding it.
1452
+ */
1453
+ function dictionaryHides(state, terms) {
1454
+ const overrides = state.valueOverrides ?? [];
1455
+ const kept = new Set(overrides.flatMap((item) => item.choice === "keep" ? [item.value] : []));
1456
+ const hidden = /* @__PURE__ */ new Map();
1457
+ const add = (value, type) => {
1458
+ const prior = hidden.get(value);
1459
+ const kind = termTypeInfo[type].kind;
1460
+ hidden.set(value, {
1461
+ kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
1462
+ groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], termGroupId(state.journeyId, type)])].sort()
1463
+ });
1464
+ };
1465
+ const all = [...terms];
1466
+ const hiddenValues = new Set(state.privacy.terms.filter((item) => item.disposition === "hidden").map((item) => item.value));
1467
+ for (const term of all) if (hiddenValues.has(term.value) && !kept.has(term.value)) add(term.value, term.type);
1468
+ for (const item of overrides) if (item.choice === "hide") add(item.value, item.type);
1469
+ for (const term of all) if (!termTypeInfo[term.type].asked) add(term.value, term.type);
1470
+ for (const value of state.rememberedHides ?? []) if (!hidden.has(value) && !kept.has(value)) add(value, "projects");
1471
+ return hidden;
1472
+ }
1473
+ /** The journey's current hidden set, applied to every selected Work (ADR 0086, D03). */
1474
+ async function journeyConfirmedHides(reader, state) {
1475
+ return dictionaryHides(state, [...await reader.journeyTerms({ journeyId: state.journeyId }), ...state.privacy.terms]);
1476
+ }
1477
+ function confirmedHideTerms(groups, records) {
1478
+ const confirmed = new Map(groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => [group.id, group]));
1479
+ const fields = /* @__PURE__ */ new Map();
1480
+ for (const record of records) {
1481
+ const assigned = new Map(record.assignments.map((item) => [item.findingId, item.groupId]));
1482
+ const chunks = new Map(record.chunks.map((chunk) => [chunk.id, chunk]));
1483
+ for (const finding of record.findings) {
1484
+ const group = confirmed.get(assigned.get(finding.id) ?? "");
1485
+ const chunk = chunks.get(finding.chunkId);
1486
+ if (!group || !chunk || finding.class !== "gray") continue;
1487
+ const key = JSON.stringify([chunk.fieldId, chunk.fieldHash]);
1488
+ fields.set(key, [...fields.get(key) ?? [], {
1489
+ start: chunk.offset + finding.start,
1490
+ end: chunk.offset + finding.end,
1491
+ text: chunk.text.slice(finding.start, finding.end),
1492
+ split: [finding.start === 0, finding.end === chunk.text.length],
1493
+ group
1494
+ }]);
1495
+ }
1496
+ }
1497
+ const terms = /* @__PURE__ */ new Map();
1498
+ const add = (text, groupIds, kind) => {
1499
+ for (const term of bLiteralTerms(text)) {
1500
+ const prior = terms.get(term);
1501
+ terms.set(term, {
1502
+ kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
1503
+ groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], ...groupIds])].sort()
1504
+ });
1505
+ }
1506
+ };
1507
+ for (const spans of fields.values()) {
1508
+ let open;
1509
+ for (const span of spans.toSorted((a, b) => a.start - b.start || b.end - a.end)) {
1510
+ if (open && span.split[0] && span.start === open.end) {
1511
+ open.text += span.text;
1512
+ open.end = span.end;
1513
+ open.groupIds.push(span.group.id);
1514
+ } else {
1515
+ if (open) add(open.text, open.groupIds, open.kind);
1516
+ open = {
1517
+ end: span.end,
1518
+ text: span.text,
1519
+ groupIds: [span.group.id],
1520
+ kind: span.group.kind
1521
+ };
1522
+ }
1523
+ if (!span.split[1]) {
1524
+ add(open.text, open.groupIds, open.kind);
1525
+ open = void 0;
1526
+ }
1527
+ }
1528
+ if (open) add(open.text, open.groupIds, open.kind);
1529
+ }
1530
+ return terms;
1531
+ }
1532
+ //#endregion
1533
+ //#region packages/application/src/journey/placement/readiness.ts
1534
+ function hasCompleteWorkDescription(work) {
1535
+ return work.placement === "placed" && !!work.description.trim() && work.classificationRecommendation !== null;
1536
+ }
1537
+ /** Explain why a requested Work still has no usable output after rendering. */
1538
+ function unfinishedPackagingCode(work) {
1539
+ if (!work) return "REVISION_CONFLICT";
1540
+ if (work.renderFailure?.revision === work.revision) return work.renderFailure.code;
1541
+ if (!hasCompleteWorkDescription(work)) return "WORK_METADATA_INCOMPLETE";
1542
+ if (!work.accessConfirmed) return "CONFIRMATION_REQUIRED";
1543
+ return "PACKAGING_FAILED";
1544
+ }
1545
+ /**
1546
+ * Dropped Works are not AI-read; a Work emptied by repartition has nothing left to read, and
1547
+ * roots classification has not grouped yet are read once they join a Work (E2).
1548
+ */
1549
+ function isSelectedWork(work) {
1550
+ return work.access !== "drop" && work.rootSessionIds.length > 0 && work.placement !== "classifying";
1551
+ }
1552
+ /** Classification published some Works and is still grouping the remaining roots (E2). */
1553
+ function isClassifying(state) {
1554
+ return state.works.some((work) => work.placement === "classifying");
1555
+ }
1556
+ /**
1557
+ * A Work whose read failed: it is skipped for this Send without a decision (ADR 0236). It holds no
1558
+ * reading, card, packaging or Send gate and is never sent; Retry or a move out of Drop reads it
1559
+ * again. A pending Hide also blocks the Works it changes, but those wait for its recovery and are
1560
+ * not skipped. The Skill helpers mirror this rule in `journey-state.mjs`.
1561
+ */
1562
+ function isSkippedWork(work) {
1563
+ return work.status === "blocked" && !work.blockedCodes.includes("PRIVACY_CHANGE_PENDING") && currentReviewInterruptions(work).length === 0;
1564
+ }
1565
+ /** A selected Work that is not skipped (ADR 0236). */
1566
+ function isIncludedWork(work) {
1567
+ return isSelectedWork(work) && !isSkippedWork(work);
1568
+ }
1569
+ /**
1570
+ * ADR 0274: Public needs a recorded choice: the person's panel click or named conversation
1571
+ * answer (origin `explicit`), or, under ADR 0275, the account proof that every root was already
1572
+ * Public (origin and channel `account`). A Public Work stored before `accessChoice` existed is
1573
+ * still pending. Mirrored by `phaseOf` in the Skill's `journey-state.mjs` and by the panel's
1574
+ * `WorkAccess`.
1575
+ */
1576
+ function hasPublicChoice(work) {
1577
+ if (work.access !== "public") return true;
1578
+ if (!work.accessConfirmed || !work.accessChoice) return false;
1579
+ return work.accessChoice.channel === "account" ? work.accessOrigin === "account" : work.accessOrigin === "explicit";
1580
+ }
1581
+ /** ADR 0277 slice 2 display projection. Send consent remains a separate gate. */
1582
+ function publicSource(work) {
1583
+ if (!isIncludedWork(work) || work.access !== "public" || work.accountAccessConflict) return null;
1584
+ if (!hasPublicChoice(work)) return "suggestion";
1585
+ return work.accessChoice?.channel === "account" ? "account" : "choice";
1586
+ }
1587
+ function selectedWorks(state) {
1588
+ return state.works.filter(isSelectedWork);
1589
+ }
1590
+ /** Categories found only in Dropped Works are neither asked nor counted. */
1591
+ function unselectedGroupIds(state) {
1592
+ const selected = new Set(selectedWorks(state).flatMap((work) => work.groupIds));
1593
+ return new Set(state.works.filter((work) => !isSelectedWork(work)).flatMap((work) => work.groupIds).filter((id) => !selected.has(id)));
1594
+ }
1595
+ /** Every root without batches is covered by an empty-input or explicit exclusion proof. */
1596
+ function hasCertifiedNoBatchReview(work, snapshotId) {
1597
+ const proofs = [work.emptyReview, work.excludedReview].filter((proof) => proof !== void 0);
1598
+ if (!proofs.length || proofs.some((proof) => proof.snapshotId !== snapshotId || proof.inputRevision !== work.inputRevision)) return false;
1599
+ const roots = proofs.flatMap((proof) => proof.rootSessionIds);
1600
+ return roots.length === work.rootSessionIds.length && new Set(roots).size === roots.length && work.rootSessionIds.every((id) => roots.includes(id));
1601
+ }
1602
+ /** A reused Work ID cannot carry an old input generation's failed review forward. */
1603
+ function currentReviewInterruptions(work) {
1604
+ return (work.reviewInterruptions ?? []).filter((item) => item.inputRevision === work.inputRevision);
1605
+ }
1606
+ /** Reading completes over included Works once the person has confirmed the scan scope. */
1607
+ function isReadingComplete(state) {
1608
+ 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);
1609
+ }
1610
+ /** Recompute completion after a selection or reading change without leaving a paused/send phase. */
1611
+ function withReadingProgress(state) {
1612
+ const readingComplete = isReadingComplete(state);
1613
+ const phase = state.phase === "paused" || !state.snapshotId ? state.phase : readingComplete ? state.phase === "processing" ? "review" : state.phase : state.phase === "review" || state.phase === "ready" ? "processing" : state.phase;
1614
+ return {
1615
+ ...state,
1616
+ readingComplete,
1617
+ phase
1618
+ };
1619
+ }
1620
+ //#endregion
1621
+ //#region packages/application/src/journey/send/model.ts
1622
+ const count$13 = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
1623
+ const access = z.enum(["public", "private"]);
1624
+ const sendSourceSchema = z.object({
1625
+ id: z.string().regex(/^w[0-9]{6,}$/u),
1626
+ pathHash: journeyDigestSchema.nullable(),
1627
+ remoteHashes: z.array(journeyDigestSchema),
1628
+ primaryRemoteHash: journeyDigestSchema.nullable(),
1629
+ challengeId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/u).optional()
1630
+ }).strict();
1631
+ function agentSourcePathSegment(value) {
1632
+ return /^[A-Za-z0-9.][A-Za-z0-9._-]*$/u.test(value) && value !== "." && value !== "..";
1633
+ }
1634
+ /** Local candidates become public only after anonymous upload-time evidence checks. */
1304
1635
  const agentSourceSchema = z.object({
1305
1636
  kind: z.enum(["skill", "mcp"]),
1306
1637
  name: z.string().regex(/^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,199}$/u),
@@ -1678,8 +2009,10 @@ function currentTermRules(state) {
1678
2009
  if (!overrides.has(value)) overrides.set(value, item);
1679
2010
  }
1680
2011
  const categories = new Map(state.groups.filter((group) => group.origin === "preference" || group.confirmed && (group.origin === "explicit" || group.origin === "delegate")).map((group) => [group.key, group.choice]));
2012
+ const remembered = new Set(state.rememberedHides ?? []);
1681
2013
  function choice(term) {
1682
- return overrides.get(term.value.normalize("NFC"))?.choice ?? categories.get(`type:${term.type}`);
2014
+ const value = term.value.normalize("NFC");
2015
+ return overrides.get(value)?.choice ?? (remembered.has(value) ? "hide" : categories.get(`type:${term.type}`));
1683
2016
  }
1684
2017
  return {
1685
2018
  overrides,
@@ -2468,6 +2801,18 @@ function answeredKeys(state, optionId) {
2468
2801
  return card.answer.optionId === optionId ? card.termKeys.filter((key) => !excluded.has(key)) : [...excluded];
2469
2802
  }));
2470
2803
  }
2804
+ /**
2805
+ * Values the person hid with their own panel answer, still on that card and Hidden now; the next
2806
+ * journey hides them on arrival (issue #126). Never sent anywhere.
2807
+ */
2808
+ function rememberedHideValues(state) {
2809
+ const keys = new Set(state.privacy.cards.flatMap((card) => {
2810
+ if (card.answer?.optionId !== "hide" || card.answer.source !== "panel") return [];
2811
+ const excluded = new Set(card.answer.excludedTermKeys ?? []);
2812
+ return card.termKeys.filter((key) => !excluded.has(key));
2813
+ }));
2814
+ return [...new Set(state.privacy.terms.filter((term) => term.disposition === "hidden" && keys.has(privacyTermKey(term))).map((term) => term.value.normalize("NFC")))];
2815
+ }
2471
2816
  function answeredValues(state) {
2472
2817
  const keepKeys = answeredKeys(state, "keep");
2473
2818
  const hideKeys = answeredKeys(state, "hide");
@@ -2566,6 +2911,7 @@ function respondPrivacy(state, action, source) {
2566
2911
  rule: [option.rule, action.rule?.trim()].filter(Boolean).join("\n"),
2567
2912
  additionalRule: action.rule?.trim() ?? "",
2568
2913
  remember: action.remember,
2914
+ source,
2569
2915
  ...excluded.length ? { excludedTermKeys: excluded } : {}
2570
2916
  }
2571
2917
  });
@@ -2747,7 +3093,9 @@ const journeyClassificationReadSchema = z.object({
2747
3093
  journeyId: journeyIdSchema,
2748
3094
  workId: journeyIdSchema,
2749
3095
  rootSessionId: z.string().min(1).max(128).optional(),
2750
- cursor: classificationCursorSchema.optional()
3096
+ cursor: classificationCursorSchema.optional(),
3097
+ /** Classification-only prefix; omitted callers retain full capture verification. */
3098
+ limit: z.number().int().min(1).max(8).optional()
2751
3099
  }).strict();
2752
3100
  const journeyClassificationSubmitSchema = z.object({
2753
3101
  journeyId: journeyIdSchema,
@@ -2865,461 +3213,138 @@ const preparationUpdateSchema = z.object({
2865
3213
  journeyId: journeyIdSchema,
2866
3214
  expectedRevision: preparationCountSchema,
2867
3215
  requestKey: journeyIdSchema,
2868
- at: z.iso.datetime(),
2869
- kind: z.enum(["snapshot", "batch"]),
2870
- works: z.array(nativeJourneySchema.shape.works.element),
2871
- phase: nativeJourneySchema.shape.phase,
2872
- readingComplete: z.boolean(),
2873
- readingStartedAt: nativeJourneySchema.shape.readingStartedAt,
2874
- clearPackageApproval: z.boolean()
2875
- }).strict();
2876
- function emptyPreparationFacts() {
2877
- return {
2878
- stage: "capture",
2879
- unsupported: {
2880
- known: 0,
2881
- evaluated: 0
2882
- },
2883
- resourceLimited: {
2884
- known: 0,
2885
- evaluated: 0
2886
- },
2887
- excluded: {
2888
- known: 0,
2889
- evaluated: 0
2890
- },
2891
- emptyReviewRoots: {
2892
- known: 0,
2893
- evaluated: 0
2894
- }
2895
- };
2896
- }
2897
- function validatePreparationFacts(start, facts) {
2898
- preparationFactsSchema.parse(facts);
2899
- 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");
2900
- }
2901
- //#endregion
2902
- //#region packages/application/src/journey/correction/model.ts
2903
- /** Original field-relative code-point coordinates, end-exclusive, exactly as B anchors. */
2904
- const correctionAnchorSchema = z.object({
2905
- fieldId: bIdSchema,
2906
- recordId: bIdSchema,
2907
- fieldHash: journeyDigestSchema,
2908
- start: bCountSchema,
2909
- end: bCountSchema,
2910
- quote: z.string().min(1).max(4096)
2911
- }).strict();
2912
- const correctionReportSchema = z.object({
2913
- id: journeyIdSchema,
2914
- workId: journeyIdSchema,
2915
- inputRevision: z.number().int().min(1),
2916
- batchId: journeyIdSchema,
2917
- groupId: journeyIdSchema,
2918
- kind: journeyKindSchema,
2919
- reason: z.string().trim().min(1).max(1e3),
2920
- anchors: z.array(correctionAnchorSchema).min(1).max(256)
2921
- }).strict();
2922
- const reviewCorrectSchema = z.object({
2923
- journeyId: journeyIdSchema,
2924
- expectedRevision: z.number().int().nonnegative(),
2925
- idempotencyKey: journeyIdSchema,
2926
- report: correctionReportSchema
2927
- }).strict();
2928
- /** Immutable private report receipt; the only place where correction quotes are retained. */
2929
- const correctionReportResourceSchema = z.object({
2930
- journeyId: journeyIdSchema,
2931
- kind: z.literal("correction-report"),
2932
- id: journeyIdSchema,
2933
- requestHash: journeyDigestSchema,
2934
- at: z.iso.datetime(),
2935
- snapshotId: journeyDigestSchema,
2936
- batchDigest: journeyDigestSchema,
2937
- decisionDigest: journeyDigestSchema,
2938
- report: correctionReportSchema
2939
- }).strict();
2940
- const correctionAuditSchema = z.object({
2941
- reportId: journeyIdSchema,
2942
- action: z.enum([
2943
- "report",
2944
- "hide",
2945
- "dismiss"
2946
- ]),
2947
- actor: z.enum([
2948
- "observer",
2949
- "human",
2950
- "delegate"
2951
- ]),
2952
- authorityId: journeyIdSchema.nullable(),
2953
- decisionDigest: journeyDigestSchema
2954
- }).strict();
2955
- //#endregion
2956
- //#region packages/application/src/journey/correction/binding.ts
2957
- function workCorrections(state, workId) {
2958
- return (state.corrections ?? []).filter((item) => item.workId === workId);
2959
- }
2960
- /** Binds rendered output and generated metadata to every correction disposition of the Work. */
2961
- function workCorrectionBinding(state, workId) {
2962
- const corrections = workCorrections(state, workId);
2963
- return corrections.length ? corrections.map(({ id, groupId, inputRevision, batchId, status, disposition }) => ({
2964
- id,
2965
- groupId,
2966
- inputRevision,
2967
- batchId,
2968
- status,
2969
- choice: disposition?.choice ?? null,
2970
- decisionDigest: disposition?.decisionDigest ?? null
2971
- })) : null;
2972
- }
2973
- //#endregion
2974
- //#region packages/application/src/journey/placement/decisions.ts
2975
- function workPrivacyDecisions(state, work) {
2976
- return state.groups.filter((group) => work.groupIds.includes(group.id)).map(({ id, key, kind, label, condition, choice, confirmed }) => ({
2977
- id,
2978
- key,
2979
- kind,
2980
- label,
2981
- condition,
2982
- choice,
2983
- confirmed
2984
- })).sort((a, b) => a.id.localeCompare(b.id));
2985
- }
2986
- /** Corrections are additive under an unchanged Hide group, so they bind metadata review too. */
2987
- function workDecisionDigest(state, work) {
2988
- const decisions = workPrivacyDecisions(state, work);
2989
- const corrections = workCorrectionBinding(state, work.id);
2990
- return textDigest(JSON.stringify(corrections ? {
2991
- decisions,
2992
- corrections
2993
- } : decisions));
2994
- }
2995
- /** Retire generated text until it is reviewed; the processed project name keeps rows distinct. */
2996
- function retireWorkDescription(work) {
2997
- return {
2998
- ...work,
2999
- suggestion: null
3000
- };
3001
- }
3002
- //#endregion
3003
- //#region packages/application/src/journey/terms/groups.ts
3004
- const keyPrefix = "type:";
3005
- const termGroupKey = (type) => `${keyPrefix}${type}`;
3006
- /** RFC 9562 UUID version 5 of `name` in the `namespace` UUID. */
3007
- function uuidV5(namespace, name) {
3008
- const bytes = createHash("sha1").update(Buffer.from(namespace.replaceAll("-", ""), "hex")).update(name, "utf8").digest().subarray(0, 16);
3009
- bytes.writeUInt8(bytes.readUInt8(6) & 15 | 80, 6);
3010
- bytes.writeUInt8(bytes.readUInt8(8) & 63 | 128, 8);
3011
- const hex = bytes.toString("hex");
3012
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
3013
- }
3014
- /** Deterministic: every reader and writer of a journey names a type's group alike. */
3015
- const termGroupId = (journeyId, type) => uuidV5(journeyId, type);
3016
- /** The term type a group answers, or null for any other category. */
3017
- function termGroupType(group) {
3018
- if (!group.key.startsWith(keyPrefix)) return null;
3019
- const parsed = termTypeSchema.safeParse(group.key.slice(5));
3020
- return parsed.success ? parsed.data : null;
3021
- }
3022
- const typeGroup = (state, type) => state.groups.find((group) => termGroupType(group) === type);
3023
- function newTypeGroup(state, type, preference) {
3024
- const info = termTypeInfo[type];
3025
- const base = {
3026
- id: termGroupId(state.journeyId, type),
3027
- kind: info.kind,
3028
- key: termGroupKey(type),
3029
- label: info.label,
3030
- condition: info.condition,
3031
- count: 0,
3032
- preferenceScope: null,
3033
- representative: null
3034
- };
3035
- if (!info.asked) return {
3036
- ...base,
3037
- belief: .97,
3038
- choice: "hide",
3039
- confirmed: true,
3040
- origin: "policy"
3041
- };
3042
- if (state.questionsPaused) return {
3043
- ...base,
3044
- belief: .97,
3045
- choice: "hide",
3046
- confirmed: false,
3047
- origin: "paused"
3048
- };
3049
- return {
3050
- ...base,
3051
- belief: preference === null ? .5 : preference === "hide" ? .97 : .03,
3052
- choice: preference,
3053
- confirmed: false,
3054
- origin: preference === null ? "model" : "preference"
3055
- };
3056
- }
3057
- /**
3058
- * One group per type with at least one dictionary value, `count` its distinct values. Existing
3059
- * groups keep their identity and answer; other groups are unchanged.
3060
- */
3061
- function syncTermGroups(state, counts, preference) {
3062
- const groups = state.groups.map((group) => {
3063
- const type = termGroupType(group);
3064
- return type === null ? group : {
3065
- ...group,
3066
- count: counts.get(type) ?? group.count
3067
- };
3068
- });
3069
- for (const type of termTypes) {
3070
- const count = counts.get(type) ?? 0;
3071
- if (count === 0 || groups.some((group) => termGroupType(group) === type)) continue;
3072
- groups.push({
3073
- ...newTypeGroup(state, type, preference(termGroupKey(type))),
3074
- count
3075
- });
3076
- }
3077
- return groups;
3078
- }
3079
- //#endregion
3080
- //#region packages/application/src/journey/review/confirmed-hides.ts
3081
- /** Kinds from lowest to highest mask priority. */
3082
- const confirmedHideKinds = [
3083
- "internal",
3084
- "identity",
3085
- "private",
3086
- "credentials"
3087
- ];
3088
- const kinds = confirmedHideKinds;
3089
- const rank$1 = (kind) => kinds.indexOf(kind);
3090
- /**
3091
- * ADR 0086: a type's values are hidden unless its group is answered Keep; before an answer the
3092
- * default is Hide. Credentials and contact details are always hidden (D06).
3093
- */
3094
- function typeHides(state, type) {
3095
- const group = typeGroup(state, type);
3096
- return !termTypeInfo[type].asked || !group?.confirmed || group.choice !== "keep";
3097
- }
3098
- /** B review (superseded by ADR 0086): IDs of the categories confirmed as Hide. */
3099
- const confirmedHideGroupIds = (state) => state.groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => group.id);
3100
- const overrideKey = (state) => (state.valueOverrides ?? []).map((item) => JSON.stringify([
3101
- item.type,
3102
- item.value,
3103
- item.choice
3104
- ])).toSorted();
3105
- /**
3106
- * Identifies the hidden set (ADR 0079, 0086). The dictionary only grows and a type's group
3107
- * counts its distinct values, so each type's count and Hide answer, with the value overrides,
3108
- * name exactly one hidden set.
3109
- */
3110
- function confirmedHideKey(state) {
3111
- return createHash("sha256").update(JSON.stringify({
3112
- types: termTypes.map((type) => [
3113
- type,
3114
- typeGroup(state, type)?.count ?? 0,
3115
- typeHides(state, type)
3116
- ]),
3117
- overrides: overrideKey(state),
3118
- 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)))
3119
- })).digest("hex");
3120
- }
3121
- /** Whether two states hold the same value overrides. */
3122
- const sameValueOverrides = (left, right) => JSON.stringify(overrideKey(left)) === JSON.stringify(overrideKey(right));
3123
- /**
3124
- * A preparation's waived hits stay waived only while every category recorded for them is still a
3125
- * confirmed Hide; otherwise render may no longer mask them, so the session needs reconciliation.
3126
- */
3127
- function maskedHitsStale(maskedHits, hiding) {
3128
- const current = new Set(hiding);
3129
- return !!maskedHits?.some((hit) => hit.groupIds.some((id) => !current.has(id)));
3130
- }
3131
- /**
3132
- * ADR 0086 hidden set: values of types not answered Keep, minus Keep overrides, plus Hide
3133
- * overrides, plus every value of an always-hidden type. A value of several types takes the
3134
- * highest mask kind among the types hiding it.
3135
- */
3136
- function dictionaryHides(state, terms) {
3137
- const overrides = state.valueOverrides ?? [];
3138
- const kept = new Set(overrides.flatMap((item) => item.choice === "keep" ? [item.value] : []));
3139
- const hidden = /* @__PURE__ */ new Map();
3140
- const add = (value, type) => {
3141
- const prior = hidden.get(value);
3142
- const kind = termTypeInfo[type].kind;
3143
- hidden.set(value, {
3144
- kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
3145
- groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], termGroupId(state.journeyId, type)])].sort()
3146
- });
3147
- };
3148
- const all = [...terms];
3149
- const hiddenValues = new Set(state.privacy.terms.filter((item) => item.disposition === "hidden").map((item) => item.value));
3150
- for (const term of all) if (hiddenValues.has(term.value) && !kept.has(term.value)) add(term.value, term.type);
3151
- for (const item of overrides) if (item.choice === "hide") add(item.value, item.type);
3152
- for (const term of all) if (!termTypeInfo[term.type].asked) add(term.value, term.type);
3153
- return hidden;
3154
- }
3155
- /** The journey's current hidden set, applied to every selected Work (ADR 0086, D03). */
3156
- async function journeyConfirmedHides(reader, state) {
3157
- return dictionaryHides(state, [...await reader.journeyTerms({ journeyId: state.journeyId }), ...state.privacy.terms]);
3158
- }
3159
- function confirmedHideTerms(groups, records) {
3160
- const confirmed = new Map(groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => [group.id, group]));
3161
- const fields = /* @__PURE__ */ new Map();
3162
- for (const record of records) {
3163
- const assigned = new Map(record.assignments.map((item) => [item.findingId, item.groupId]));
3164
- const chunks = new Map(record.chunks.map((chunk) => [chunk.id, chunk]));
3165
- for (const finding of record.findings) {
3166
- const group = confirmed.get(assigned.get(finding.id) ?? "");
3167
- const chunk = chunks.get(finding.chunkId);
3168
- if (!group || !chunk || finding.class !== "gray") continue;
3169
- const key = JSON.stringify([chunk.fieldId, chunk.fieldHash]);
3170
- fields.set(key, [...fields.get(key) ?? [], {
3171
- start: chunk.offset + finding.start,
3172
- end: chunk.offset + finding.end,
3173
- text: chunk.text.slice(finding.start, finding.end),
3174
- split: [finding.start === 0, finding.end === chunk.text.length],
3175
- group
3176
- }]);
3177
- }
3178
- }
3179
- const terms = /* @__PURE__ */ new Map();
3180
- const add = (text, groupIds, kind) => {
3181
- for (const term of bLiteralTerms(text)) {
3182
- const prior = terms.get(term);
3183
- terms.set(term, {
3184
- kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
3185
- groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], ...groupIds])].sort()
3186
- });
3216
+ at: z.iso.datetime(),
3217
+ kind: z.enum(["snapshot", "batch"]),
3218
+ works: z.array(nativeJourneySchema.shape.works.element),
3219
+ phase: nativeJourneySchema.shape.phase,
3220
+ readingComplete: z.boolean(),
3221
+ readingStartedAt: nativeJourneySchema.shape.readingStartedAt,
3222
+ clearPackageApproval: z.boolean()
3223
+ }).strict();
3224
+ function emptyPreparationFacts() {
3225
+ return {
3226
+ stage: "capture",
3227
+ unsupported: {
3228
+ known: 0,
3229
+ evaluated: 0
3230
+ },
3231
+ resourceLimited: {
3232
+ known: 0,
3233
+ evaluated: 0
3234
+ },
3235
+ excluded: {
3236
+ known: 0,
3237
+ evaluated: 0
3238
+ },
3239
+ emptyReviewRoots: {
3240
+ known: 0,
3241
+ evaluated: 0
3187
3242
  }
3188
3243
  };
3189
- for (const spans of fields.values()) {
3190
- let open;
3191
- for (const span of spans.toSorted((a, b) => a.start - b.start || b.end - a.end)) {
3192
- if (open && span.split[0] && span.start === open.end) {
3193
- open.text += span.text;
3194
- open.end = span.end;
3195
- open.groupIds.push(span.group.id);
3196
- } else {
3197
- if (open) add(open.text, open.groupIds, open.kind);
3198
- open = {
3199
- end: span.end,
3200
- text: span.text,
3201
- groupIds: [span.group.id],
3202
- kind: span.group.kind
3203
- };
3204
- }
3205
- if (!span.split[1]) {
3206
- add(open.text, open.groupIds, open.kind);
3207
- open = void 0;
3208
- }
3209
- }
3210
- if (open) add(open.text, open.groupIds, open.kind);
3211
- }
3212
- return terms;
3213
- }
3214
- //#endregion
3215
- //#region packages/application/src/journey/b/matches.ts
3216
- function bDictionaryHits(fields, literals, signal = new AbortController().signal) {
3217
- const matcher = createEntityMatcher([...new Set(literals.map((part) => part.term))].map((value) => ({
3218
- kind: "custom-term",
3219
- value
3220
- })));
3221
- const hits = [];
3222
- for (const field of fields) {
3223
- const index = createCodePointIndex(field.text);
3224
- const base = field.start ?? 0;
3225
- for (const match of matcher.find(field.text, signal, 1e5)) {
3226
- if (hits.length >= 1e6) throw new ApplicationError("PROCESSING_LIMIT");
3227
- const body = {
3228
- fieldId: field.id,
3229
- fieldHash: field.fieldHash,
3230
- recordId: field.recordId,
3231
- start: base + index.codePointOffset(match.range.start),
3232
- end: base + index.codePointOffset(match.range.end),
3233
- exactQuote: field.text.slice(match.range.start, match.range.end),
3234
- term: match.entity.value
3235
- };
3236
- hits.push({
3237
- ...body,
3238
- digest: bDigest(JSON.stringify(body))
3239
- });
3240
- }
3241
- }
3242
- return [...new Map(hits.map((hit) => [hit.digest, hit])).values()];
3243
3244
  }
3244
- function unclassifiedBHits(hits, literals, negative) {
3245
- const issues = [];
3246
- const positiveFields = new Map([...Map.groupBy(literals, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
3247
- const negativeFields = new Map([...Map.groupBy(negative, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
3248
- for (const hit of hits) {
3249
- const positive = positiveFields.get(hit.fieldId)?.relation(hit).covers ?? false;
3250
- const negative = negativeFields.get(hit.fieldId)?.relation(hit);
3251
- if (!positive && !negative?.covers || positive && negative?.overlaps) issues.push(bIssue(positive ? "CONFLICTING_PRIVACY_DECISIONS" : "UNCLASSIFIED_MATCH", hit));
3252
- }
3253
- return issues;
3254
- }
3255
- 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);
3256
- /** In decoded tool text an odd backslash run escapes its following n, r or t. */
3257
- function afterEscapedSpace(text, start) {
3258
- const letter = text[start - 1];
3259
- if (letter !== "n" && letter !== "r" && letter !== "t") return false;
3260
- let index = start - 2;
3261
- while (index >= 0 && text[index] === "\\") index--;
3262
- return (start - 2 - index) % 2 === 1;
3245
+ function validatePreparationFacts(start, facts) {
3246
+ preparationFactsSchema.parse(facts);
3247
+ 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");
3263
3248
  }
3264
- /** A confirmed Hide value matches only where it does not run into a neighbouring word (ADR 0069). */
3265
- function bWordBounded(text, start, end, toolResultEscapes = false) {
3266
- return !(bSpacedWord(scalarBefore(text, start)) && bSpacedWord(scalarAfter(text, start)) && !(toolResultEscapes && afterEscapedSpace(text, start)) || bSpacedWord(scalarBefore(text, end)) && bSpacedWord(scalarAfter(text, end)));
3249
+ //#endregion
3250
+ //#region packages/application/src/journey/correction/model.ts
3251
+ /** Original field-relative code-point coordinates, end-exclusive, exactly as B anchors. */
3252
+ const correctionAnchorSchema = z.object({
3253
+ fieldId: bIdSchema,
3254
+ recordId: bIdSchema,
3255
+ fieldHash: journeyDigestSchema,
3256
+ start: bCountSchema,
3257
+ end: bCountSchema,
3258
+ quote: z.string().min(1).max(4096)
3259
+ }).strict();
3260
+ const correctionReportSchema = z.object({
3261
+ id: journeyIdSchema,
3262
+ workId: journeyIdSchema,
3263
+ inputRevision: z.number().int().min(1),
3264
+ batchId: journeyIdSchema,
3265
+ groupId: journeyIdSchema,
3266
+ kind: journeyKindSchema,
3267
+ reason: z.string().trim().min(1).max(1e3),
3268
+ anchors: z.array(correctionAnchorSchema).min(1).max(256)
3269
+ }).strict();
3270
+ const reviewCorrectSchema = z.object({
3271
+ journeyId: journeyIdSchema,
3272
+ expectedRevision: z.number().int().nonnegative(),
3273
+ idempotencyKey: journeyIdSchema,
3274
+ report: correctionReportSchema
3275
+ }).strict();
3276
+ /** Immutable private report receipt; the only place where correction quotes are retained. */
3277
+ const correctionReportResourceSchema = z.object({
3278
+ journeyId: journeyIdSchema,
3279
+ kind: z.literal("correction-report"),
3280
+ id: journeyIdSchema,
3281
+ requestHash: journeyDigestSchema,
3282
+ at: z.iso.datetime(),
3283
+ snapshotId: journeyDigestSchema,
3284
+ batchDigest: journeyDigestSchema,
3285
+ decisionDigest: journeyDigestSchema,
3286
+ report: correctionReportSchema
3287
+ }).strict();
3288
+ const correctionAuditSchema = z.object({
3289
+ reportId: journeyIdSchema,
3290
+ action: z.enum([
3291
+ "report",
3292
+ "hide",
3293
+ "dismiss"
3294
+ ]),
3295
+ actor: z.enum([
3296
+ "observer",
3297
+ "human",
3298
+ "delegate"
3299
+ ]),
3300
+ authorityId: journeyIdSchema.nullable(),
3301
+ decisionDigest: journeyDigestSchema
3302
+ }).strict();
3303
+ //#endregion
3304
+ //#region packages/application/src/journey/correction/binding.ts
3305
+ function workCorrections(state, workId) {
3306
+ return (state.corrections ?? []).filter((item) => item.workId === workId);
3267
3307
  }
3268
- /**
3269
- * Occurrences of a Work's explicit exceptions, matched like Hide everywhere: exact NFC values
3270
- * over complete graphemes. A confirmed Hide value inside one stays visible there.
3271
- */
3272
- function bExceptionRanges(values) {
3273
- const matcher = createEntityMatcher([...new Set([...values].map((value) => value.normalize("NFC")))].map((value) => ({
3274
- kind: "custom-term",
3275
- value
3276
- })));
3277
- 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);
3308
+ /** Binds rendered output and generated metadata to every correction disposition of the Work. */
3309
+ function workCorrectionBinding(state, workId) {
3310
+ const corrections = workCorrections(state, workId);
3311
+ return corrections.length ? corrections.map(({ id, groupId, inputRevision, batchId, status, disposition }) => ({
3312
+ id,
3313
+ groupId,
3314
+ inputRevision,
3315
+ batchId,
3316
+ status,
3317
+ choice: disposition?.choice ?? null,
3318
+ decisionDigest: disposition?.decisionDigest ?? null
3319
+ })) : null;
3278
3320
  }
3279
- /** Whether a range lies inside one of `ranges`: a prefix maximum of ends, O(log n) per query. */
3280
- function bContainment(ranges) {
3281
- const sorted = ranges.toSorted((a, b) => a.start - b.start);
3282
- const reach = [];
3283
- for (const [index, range] of sorted.entries()) reach.push(Math.max(range.end, reach[index - 1] ?? range.end));
3284
- return (range) => {
3285
- let low = 0, high = sorted.length;
3286
- while (low < high) {
3287
- const middle = low + high >> 1;
3288
- if ((sorted[middle]?.start ?? Infinity) <= range.start) low = middle + 1;
3289
- else high = middle;
3290
- }
3291
- return low > 0 && (reach[low - 1] ?? -1) >= range.end;
3292
- };
3321
+ //#endregion
3322
+ //#region packages/application/src/journey/placement/decisions.ts
3323
+ function workPrivacyDecisions(state, work) {
3324
+ return state.groups.filter((group) => work.groupIds.includes(group.id)).map(({ id, key, kind, label, condition, choice, confirmed }) => ({
3325
+ id,
3326
+ key,
3327
+ kind,
3328
+ label,
3329
+ condition,
3330
+ choice,
3331
+ confirmed
3332
+ })).sort((a, b) => a.id.localeCompare(b.id));
3293
3333
  }
3294
- /**
3295
- * Word-bounded, exact, case-sensitive NFC occurrences of the terms, in UTF-16 coordinates,
3296
- * except where an explicit exception occurrence contains one.
3297
- */
3298
- function bTermMatcher(terms, exceptions = []) {
3299
- const matcher = createEntityMatcher([...new Set(terms)].map((value) => ({
3300
- kind: "custom-term",
3301
- value
3302
- })));
3303
- const excepted = bExceptionRanges(exceptions);
3304
- return (text, signal = new AbortController().signal) => {
3305
- const hits = matcher.find(text, signal, 1e5).filter(({ range }) => bWordBounded(text, range.start, range.end));
3306
- const kept = hits.length ? bContainment(excepted(text, signal)) : () => false;
3307
- return hits.filter(({ range }) => !kept(range)).map(({ range, entity }) => ({
3308
- ...range,
3309
- term: entity.value
3310
- }));
3311
- };
3334
+ /** Corrections are additive under an unchanged Hide group, so they bind metadata review too. */
3335
+ function workDecisionDigest(state, work) {
3336
+ const decisions = workPrivacyDecisions(state, work);
3337
+ const corrections = workCorrectionBinding(state, work.id);
3338
+ return textDigest(JSON.stringify(corrections ? {
3339
+ decisions,
3340
+ corrections
3341
+ } : decisions));
3312
3342
  }
3313
- /** Dictionary hits that a confirmed Hide value masks at render, so they need no classification. */
3314
- function bMaskedHits(fields, terms) {
3315
- const byId = new Map(fields.map((field) => [field.id, {
3316
- text: field.text,
3317
- index: createCodePointIndex(field.text),
3318
- base: field.start ?? 0
3319
- }]));
3320
- return (hit) => {
3321
- const field = byId.get(hit.fieldId);
3322
- return !!field && terms.has(hit.term) && bWordBounded(field.text, field.index.offset(hit.start - field.base), field.index.offset(hit.end - field.base));
3343
+ /** Retire generated text until it is reviewed; the processed project name keeps rows distinct. */
3344
+ function retireWorkDescription(work) {
3345
+ return {
3346
+ ...work,
3347
+ suggestion: null
3323
3348
  };
3324
3349
  }
3325
3350
  const segmenter = new Intl.Segmenter("und", { granularity: "grapheme" });
@@ -3557,8 +3582,9 @@ function memberChange(previous, next) {
3557
3582
  return counts.every((count) => count >= 0) ? "additions" : counts.every((count) => count <= 0) ? "removals" : "other";
3558
3583
  }
3559
3584
  /**
3560
- * Whether a Work's output may change with the delta (Work metadata is never masked, so a
3561
- * 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
3585
+ * Whether a Work's output may change with the delta (generated descriptions remain unmasked
3586
+ * and never retire output on their own; titles are covered by the output index). Without a
3587
+ * delta that covers this exact change every Work is affected, as ADR 0069 first decided, except when
3562
3588
  * members were only removed: no value is then added, so nothing can be under-hidden, and an
3563
3589
  * output may keep masking a value that left its category until it is rendered again.
3564
3590
  */
@@ -4897,7 +4923,7 @@ const sessionReconcileResultSchema = z.object({
4897
4923
  //#endregion
4898
4924
  //#region packages/application/src/journey/review/attempt-model.ts
4899
4925
  /**
4900
- * At most twenty small-model analysis agents run at once (D12, raised on 2026-10-20). One agent receives at most
4926
+ * At most twenty-four small-model analysis agents run at once (raised on 2026-11-14). One agent receives at most
4901
4927
  * 240,000 code points and 48 units; attempts dispatched with the same `packId` share one agent.
4902
4928
  */
4903
4929
  const reviewAgentCodePoints = 24e4;
@@ -6259,7 +6285,7 @@ function answeredRule(card) {
6259
6285
  /** A dispatch without a pack ID is its own analysis agent. */
6260
6286
  const agentKey = (attemptId, packId) => packId === void 0 ? `attempt:${attemptId}` : `pack:${packId}`;
6261
6287
  /**
6262
- * Refuses a dispatch that would run more than twenty agents at once, or give one agent more than
6288
+ * Refuses a dispatch that would run more than twenty-four agents at once, or give one agent more than
6263
6289
  * 240,000 code points or 48 units. A full reservation (repair or reconciliation) fills an agent,
6264
6290
  * so it always runs alone.
6265
6291
  */
@@ -6278,7 +6304,7 @@ function assertAgentCapacity(open, dispatches, attemptId, packId, budgetCodePoin
6278
6304
  }
6279
6305
  const key = agentKey(attemptId, packId);
6280
6306
  const agent = agents.get(key);
6281
- if (agent === void 0 && agents.size >= 16 || (agent?.codePoints ?? 0) + budgetCodePoints > 24e4 || (agent?.units ?? 0) + 1 > 48) throw new ApplicationError("OPERATION_CONFLICT");
6307
+ if (agent === void 0 && agents.size >= 24 || (agent?.codePoints ?? 0) + budgetCodePoints > 24e4 || (agent?.units ?? 0) + 1 > 48) throw new ApplicationError("OPERATION_CONFLICT");
6282
6308
  }
6283
6309
  //#endregion
6284
6310
  //#region packages/application/src/journey/review/attempt.ts
@@ -6884,6 +6910,25 @@ async function sessionReviewStatus(storage, value) {
6884
6910
  }
6885
6911
  //#endregion
6886
6912
  //#region packages/application/src/journey/service.ts
6913
+ /** Display uses the same bounded literal/exception matcher without depending on artifact I/O. */
6914
+ function presentedTitle(text, terms, exceptions) {
6915
+ const markers = [...text.matchAll(replacementMarkerPattern)].map((match) => ({
6916
+ start: match.index,
6917
+ end: match.index + match[0].length
6918
+ }));
6919
+ const spans = [];
6920
+ for (const hit of bTermMatcher(terms, exceptions)(text).sort((left, right) => left.start - right.start || right.end - left.end)) {
6921
+ if (markers.some((marker) => marker.start < hit.end && marker.end > hit.start)) continue;
6922
+ const previous = spans.at(-1);
6923
+ if (previous && hit.start < previous.end) previous.end = Math.max(previous.end, hit.end);
6924
+ else spans.push({
6925
+ start: hit.start,
6926
+ end: hit.end
6927
+ });
6928
+ }
6929
+ for (const span of spans.toReversed()) text = text.slice(0, span.start) + "[hidden]" + text.slice(span.end);
6930
+ return text;
6931
+ }
6887
6932
  function digest$1(value) {
6888
6933
  return createHash("sha256").update(JSON.stringify(value)).digest("hex");
6889
6934
  }
@@ -6892,8 +6937,9 @@ function digest$1(value) {
6892
6937
  * quotes stay out. The agent reads override values from `journey.get` and the guidance.
6893
6938
  */
6894
6939
  function journeyView(state, now) {
6895
- const { exceptions, instructions, corrections, valueOverrides, consent, accountRead, ...safe } = state;
6940
+ const { exceptions, instructions, corrections, valueOverrides, consent, accountRead, rememberedHides: _remembered, ...safe } = state;
6896
6941
  const terms = activePrivacyTerms(state);
6942
+ const hidden = dictionaryHides(state, state.privacy.terms);
6897
6943
  return {
6898
6944
  ...safe,
6899
6945
  consent: consent && {
@@ -6914,15 +6960,17 @@ function journeyView(state, now) {
6914
6960
  const localTitle = work.suggestion || work.explicitTitleId ? work.title : work.classificationMetadata?.title ?? work.title;
6915
6961
  const imagesOmitted = hasCurrentStaticReview(state, work) ? work.staticReview?.imagesOmitted ?? 0 : 0;
6916
6962
  const source = publicSource(work);
6963
+ const title = work.titleOrigin === "explicit" ? localTitle : presentedTitle(localTitle, hidden.keys(), state.exceptions.filter((item) => item.workId === work.id).map((item) => item.value));
6917
6964
  if (!work.classificationMetadata) return {
6918
6965
  ...work,
6966
+ title,
6919
6967
  localTitle,
6920
6968
  imagesOmitted,
6921
6969
  publicSource: source
6922
6970
  };
6923
6971
  const metadata = work.classificationMetadata;
6924
6972
  const displayed = {
6925
- title: work.suggestion || work.explicitTitleId ? work.title : metadata.title,
6973
+ title,
6926
6974
  projectName: metadata.projectName,
6927
6975
  description: work.suggestion ? work.description : metadata.description,
6928
6976
  reason: work.suggestion ? work.reason : metadata.reason
@@ -14297,4 +14345,4 @@ function equivalenceDuplicates(batch) {
14297
14345
  return bindings.sort();
14298
14346
  }
14299
14347
  //#endregion
14300
- export { validateDetectionSummary as $, maximumHideIndexTokens as $n, nativeAdapterVersions as $r, journeyPlaceSchema as $t, renewBeforeMs as A, stateSchema as Ai, scanExclusionResourceSchema as An, journeyRenderInputSchema as Ar, validateAttemptAppend as At, createApplication as B, journeySearchSaveSchema as Bn, nativeRespondSchema as Br, newJourneyWork as Bt, rebindReviewOverlay as C, withReadingProgress as Ci, scanAliasResourceSchema as Cn, preparationStateSchema as Cr, sendActionSchema as Ct, staticallyHiddenFindings as D, selectionSchema as Di, scanGitSeedsId as Dn, journeyFindingImpactSchema as Dr, sendStartSchema as Dt, guardedFindingText as E, selectionBatchSchema as Ei, scanSessionResourceSchema as En, journeyClassificationSessionsSchema as Er, sendRequestSchema as Et, readAccountAvatarUrl as F, defaultScopedSettings as Fi, journeyInventoryReadSchema as Fn, assertJourneyPrivacyReady as Fr, classificationCommitSchema as Ft, validateInspectionRequest as G, confirmedHideAdditions as Gn, currentJourneyAdapters as Gr, hideFinishSchema as Gt, inspectionStartSchema as H, journeyCommitResultSchema as Hn, retryReviewActionSchema as Hr, journeyContextQuerySchema as Ht, storedCredentialSchema as I, freezeConfiguration as Ii, journeyInventorySchema as In, privacyDecisionDigest as Ir, classificationPartition as It, validateInspectionFence as J, renderedHideValue as Jn, obsoleteJourney as Jr, sendOutputQuerySchema as Jt, inspectionQuerySchema as K, confirmedHideDeltaSchema as Kn, hostDrives as Kr, hidePublishSchema as Kt, sourceIdentityEvidence as L, selectFrozenConfiguration as Li, journeyPreparationProgressSchema as Ln, reconcileReaderKeeps as Lr, classificationWorks as Lt, deviceAuthorizationSchema as M, recommendParallelism as Mi, resolveSourceSelection as Mn, displayedScopeDigest as Mr, journeyBatchReadSchema as Mt, deviceProfileScopeSchema as N, approvedTreatmentSchema as Ni, projectPreparationProgress as Nn, requestedScope as Nr, journeyReviewStorageQuerySchema as Nt, createAccount as O, gitMetadataBudgetBytes as Oi, scanGitSeedsResource as On, journeyFindingsSchema as Or, settleCorrections as Ot, profileUrlMatchesIdentity as P, effectiveTreatmentSchema as Pi, journeyInventoryQuerySchema as Pn, applyCardAnswers as Pr, applyClassificationProposal as Pt, validateInspectionContribution as Q, hideKeyId as Qn, metadataProcessingVersion as Qr, journeyIsolateSchema as Qt, withSourceEvidence as R, staticCatalogRef as Ri, applyClassification as Rn, nativeEventsSchema as Rr, retireScanGeneration as Rt, rebindReviewAlignment as S, unfinishedPackagingCode as Si, planBRepair as Sn, preparationStartSchema as Sr, replaceSensitiveTermsSchema as St, reviewBatchReuse as T, packageCommandSchema as Ti, scanPreparationDigest as Tn, validatePreparationFacts as Tr, sendPrepareSchema as Tt, validateInspectionBatch as U, journeyCommitSchema as Un, advanceStepsSend as Ur, journeyContextSchema as Ut, inspectionCancelSchema as V, searchScopeDigest as Vn, nativeStartSchema as Vr, reviewCompileSchema as Vt, validateInspectionBatches as W, journeyEventSchema as Wn, awaitingPassport as Wr, hideBeginSchema as Wt, validateInspectionInputs as X, termHideAdditions as Xn, sendReachedPassport as Xr, journeyResourceQuerySchema as Xt, validateInspectionHead as Y, settleConfirmedHides as Yn, sendOwesPassport as Yr, sendOutputSchema as Yt, validateInspectionQuery as Z, addHideIndexTokens as Zn, sendsOnNextAdvance as Zr, journeyResourceSchema as Zt, changeJourney as _, isIncludedWork as _i, reviewRepairInputSchema as _n, workCorrectionBinding as _r, manualUploadStartSchema as _t, bCrossSegmentReviewSchema as a, agentSourcePathSegment as ai, sessionReconcileResultSchema as an, bWordBounded as ar, appendSensitiveTerms as at, isGenericPublicTerm as b, isSkippedWork as bi, bReviewDigest as bn, preparationPermissionSchema as br, maximumTermsFileBytes as bt, reconstructBFields as c, journeySendSchema as ci, sessionRereadResourceSchema as cn, confirmedHideKinds as cr, maximumAgentSetupSessions as ct, resolveGroupSplit as d, sendReplaceResultSchema as di, sessionReviewResourceSchema as dn, journeyConfirmedHides as dr, validateFailureLocation as dt, mergePrivacyTerms as ei, sortingApplySchema as en, packHideIndex as er, inspectionInputs as et, selectedValueMembers as f, sendResolutionSchema as fi, sessionReviewStatusSchema as fn, maskedHitsStale as fr, manualUploadActionSchema as ft, semanticGroupKey as g, isClassifying as gi, reviewCandidateResourceSchema as gn, workPrivacyDecisions as gr, manualUploadSchema as gt, hideScopeDigest as h, hasCompleteWorkDescription as hi, reviewCandidateInputSchema as hn, workDecisionDigest as hr, manualUploadPrepareSchema as ht, validateBCrossSegmentReview as i, staticSessionResourceSchema as ii, sessionReconcileInputSchema as in, bTermMatcher as ir, reviewInventory as it, accountProfileSchema as j, sourceChangePolicy as ji, validateScanExclusion as jn, journeyRenderSchema as jr, journeyGuidance as jt, renew as k, producerVersion as ki, verifiedScanGitSeeds as kn, journeyPreviewSchema as kr, attemptBudgetCodePoints as kt, chooseCheapPlacement as l, sendArtifactSchema as li, sessionReviewInputSchema as ln, confirmedHideTerms as lr, artifactFailureDetailSchema as lt, activeJourneyTerms as m, sendSettingsSchema as mi, sessionUsefulnessResourceSchema as mn, retireWorkDescription as mr, manualUploadChoiceSchema as mt, scanBatchEquivalenceDigest as n, staticReviewDigest as ni, reviewAttemptsQuerySchema as nn, bExceptionRanges as nr, settingsQuerySchema as nt, bRereadDigest as o, agentSourceSchema as oi, sessionReconciliationResourceSchema as on, confirmedHideGroupIds as or, classifyAgentSetup as ot, splitSelectorValues as p, sendSessionUploadSchema as pi, sessionUsefulnessInputSchema as pn, syncTermGroups as pr, manualUploadBindingSchema as pt, validateInspectionClaim as q, readConfirmedHideChange as qn, itemAtPassport as qr, journeyPolicyRespondSchema as qt, locateTermEvidence as r, staticSessionId as ri, reviewAttemptsResultSchema as rn, bMaskedHits as rr, readWorkspaceOutcomes as rt, reconcileBSession as s, agentSourcesSchema as si, sessionRereadInputSchema as sn, confirmedHideKey as sr, defaultAgentSetupInclusion as st, scanBatchContextDigest as t, hasCurrentStaticReview as ti, reviewAttemptResourceSchema as tn, bContainment as tr, settingsBatchSchema as tt, reviseGroupMembership as u, sendReceiptSchema as ui, sessionReviewQuerySchema as un, dictionaryHides as ur, resolveFailureLocation as ut, projectJourneyInventory as v, isReadingComplete as vi, reviewRepairResourceSchema as vn, emptyPreparationFacts as vr, candidateSortAvailable as vt, rebindReviewSubmit as w, commandSchema as wi, scanCanonicalResourceSchema as wn, preparationUpdateSchema as wr, sendChoiceSchema as wt, bReviewScope as x, selectedWorks as xi, mergeBRepair as xn, preparationSettlementSchema as xr, normalizeSensitiveTerms as xt, preparationScope as y, isSelectedWork as yi, reviewTransportBytes as yn, preparationBeginSchema as yr, emptyTermsRevision as yt, agentSetupKey as z, journeySearchResultSchema as zn, nativeGetSchema as zr, isolatedWorkId as zt };
14348
+ export { validateDetectionSummary as $, maximumHideIndexTokens as $n, sendReplaceResultSchema as $r, journeyPlaceSchema as $t, renewBeforeMs as A, gitMetadataBudgetBytes as Ai, scanExclusionResourceSchema as An, retryReviewActionSchema as Ar, validateAttemptAppend as At, createApplication as B, staticCatalogRef as Bi, journeySearchSaveSchema as Bn, metadataProcessingVersion as Br, newJourneyWork as Bt, rebindReviewOverlay as C, bTermMatcher as Ci, scanAliasResourceSchema as Cn, privacyDecisionDigest as Cr, sendActionSchema as Ct, staticallyHiddenFindings as D, packageCommandSchema as Di, scanGitSeedsId as Dn, nativeGetSchema as Dr, sendStartSchema as Dt, guardedFindingText as E, commandSchema as Ei, scanSessionResourceSchema as En, nativeEventsSchema as Er, sendRequestSchema as Et, readAccountAvatarUrl as F, approvedTreatmentSchema as Fi, journeyInventoryReadSchema as Fn, itemAtPassport as Fr, classificationCommitSchema as Ft, validateInspectionRequest as G, confirmedHideAdditions as Gn, staticSessionId as Gr, hideFinishSchema as Gt, inspectionStartSchema as H, journeyCommitResultSchema as Hn, mergePrivacyTerms as Hr, journeyContextQuerySchema as Ht, storedCredentialSchema as I, effectiveTreatmentSchema as Ii, journeyInventorySchema as In, obsoleteJourney as Ir, classificationPartition as It, validateInspectionFence as J, renderedHideValue as Jn, agentSourceSchema as Jr, sendOutputQuerySchema as Jt, inspectionQuerySchema as K, confirmedHideDeltaSchema as Kn, staticSessionResourceSchema as Kr, hidePublishSchema as Kt, sourceIdentityEvidence as L, defaultScopedSettings as Li, journeyPreparationProgressSchema as Ln, sendOwesPassport as Lr, classificationWorks as Lt, deviceAuthorizationSchema as M, stateSchema as Mi, resolveSourceSelection as Mn, awaitingPassport as Mr, journeyBatchReadSchema as Mt, deviceProfileScopeSchema as N, sourceChangePolicy as Ni, projectPreparationProgress as Nn, currentJourneyAdapters as Nr, journeyReviewStorageQuerySchema as Nt, createAccount as O, selectionBatchSchema as Oi, scanGitSeedsResource as On, nativeRespondSchema as Or, settleCorrections as Ot, profileUrlMatchesIdentity as P, recommendParallelism as Pi, journeyInventoryQuerySchema as Pn, hostDrives as Pr, applyClassificationProposal as Pt, validateInspectionContribution as Q, hideKeyId as Qn, sendReceiptSchema as Qr, journeyIsolateSchema as Qt, withSourceEvidence as R, freezeConfiguration as Ri, applyClassification as Rn, sendReachedPassport as Rr, retireScanGeneration as Rt, rebindReviewAlignment as S, bMaskedHits as Si, planBRepair as Sn, assertJourneyPrivacyReady as Sr, replaceSensitiveTermsSchema as St, reviewBatchReuse as T, replacementMarkerPattern as Ti, scanPreparationDigest as Tn, rememberedHideValues as Tr, sendPrepareSchema as Tt, validateInspectionBatch as U, journeyCommitSchema as Un, hasCurrentStaticReview as Ur, journeyContextSchema as Ut, inspectionCancelSchema as V, searchScopeDigest as Vn, nativeAdapterVersions as Vr, reviewCompileSchema as Vt, validateInspectionBatches as W, journeyEventSchema as Wn, staticReviewDigest as Wr, hideBeginSchema as Wt, validateInspectionInputs as X, termHideAdditions as Xn, journeySendSchema as Xr, journeyResourceQuerySchema as Xt, validateInspectionHead as Y, settleConfirmedHides as Yn, agentSourcesSchema as Yr, sendOutputSchema as Yt, validateInspectionQuery as Z, addHideIndexTokens as Zn, sendArtifactSchema as Zr, journeyResourceSchema as Zt, changeJourney as _, journeyConfirmedHides as _i, reviewRepairInputSchema as _n, journeyRenderInputSchema as _r, manualUploadStartSchema as _t, bCrossSegmentReviewSchema as a, isIncludedWork as ai, sessionReconcileResultSchema as an, emptyPreparationFacts as ar, appendSensitiveTerms as at, isGenericPublicTerm as b, bContainment as bi, bReviewDigest as bn, requestedScope as br, maximumTermsFileBytes as bt, reconstructBFields as c, isSkippedWork as ci, sessionRereadResourceSchema as cn, preparationSettlementSchema as cr, maximumAgentSetupSessions as ct, resolveGroupSplit as d, withReadingProgress as di, sessionReviewResourceSchema as dn, preparationUpdateSchema as dr, validateFailureLocation as dt, sendResolutionSchema as ei, sortingApplySchema as en, packHideIndex as er, inspectionInputs as et, selectedValueMembers as f, confirmedHideGroupIds as fi, sessionReviewStatusSchema as fn, validatePreparationFacts as fr, manualUploadActionSchema as ft, semanticGroupKey as g, dictionaryHides as gi, reviewCandidateResourceSchema as gn, journeyPreviewSchema as gr, manualUploadSchema as gt, hideScopeDigest as h, confirmedHideTerms as hi, reviewCandidateInputSchema as hn, journeyFindingsSchema as hr, manualUploadPrepareSchema as ht, validateBCrossSegmentReview as i, isClassifying as ii, sessionReconcileInputSchema as in, workCorrectionBinding as ir, reviewInventory as it, accountProfileSchema as j, producerVersion as ji, validateScanExclusion as jn, advanceStepsSend as jr, journeyGuidance as jt, renew as k, selectionSchema as ki, verifiedScanGitSeeds as kn, nativeStartSchema as kr, attemptBudgetCodePoints as kt, chooseCheapPlacement as l, selectedWorks as li, sessionReviewInputSchema as ln, preparationStartSchema as lr, artifactFailureDetailSchema as lt, activeJourneyTerms as m, confirmedHideKinds as mi, sessionUsefulnessResourceSchema as mn, journeyFindingImpactSchema as mr, manualUploadChoiceSchema as mt, scanBatchEquivalenceDigest as n, sendSettingsSchema as ni, reviewAttemptsQuerySchema as nn, workDecisionDigest as nr, settingsQuerySchema as nt, bRereadDigest as o, isReadingComplete as oi, sessionReconciliationResourceSchema as on, preparationBeginSchema as or, classifyAgentSetup as ot, splitSelectorValues as p, confirmedHideKey as pi, sessionUsefulnessInputSchema as pn, journeyClassificationSessionsSchema as pr, manualUploadBindingSchema as pt, validateInspectionClaim as q, readConfirmedHideChange as qn, agentSourcePathSegment as qr, journeyPolicyRespondSchema as qt, locateTermEvidence as r, hasCompleteWorkDescription as ri, reviewAttemptsResultSchema as rn, workPrivacyDecisions as rr, readWorkspaceOutcomes as rt, reconcileBSession as s, isSelectedWork as si, sessionRereadInputSchema as sn, preparationPermissionSchema as sr, defaultAgentSetupInclusion as st, scanBatchContextDigest as t, sendSessionUploadSchema as ti, reviewAttemptResourceSchema as tn, retireWorkDescription as tr, settingsBatchSchema as tt, reviseGroupMembership as u, unfinishedPackagingCode as ui, sessionReviewQuerySchema as un, preparationStateSchema as ur, resolveFailureLocation as ut, projectJourneyInventory as v, maskedHitsStale as vi, reviewRepairResourceSchema as vn, journeyRenderSchema as vr, candidateSortAvailable as vt, rebindReviewSubmit as w, bWordBounded as wi, scanCanonicalResourceSchema as wn, reconcileReaderKeeps as wr, sendChoiceSchema as wt, bReviewScope as x, bExceptionRanges as xi, mergeBRepair as xn, applyCardAnswers as xr, normalizeSensitiveTerms as xt, preparationScope as y, syncTermGroups as yi, reviewTransportBytes as yn, displayedScopeDigest as yr, emptyTermsRevision as yt, agentSetupKey as z, selectFrozenConfiguration as zi, journeySearchResultSchema as zn, sendsOnNextAdvance as zr, isolatedWorkId as zt };