@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.
- package/dist/{agent-setup-B12VbvN_.mjs → agent-setup-Doy_AnPY.mjs} +2 -2
- package/dist/agent-setup-worker.mjs +5 -5
- package/dist/cli.mjs +10 -10
- package/dist/codex-title-worker.mjs +1 -1
- package/dist/configured-preview-worker.mjs +5 -5
- package/dist/configured-session-worker.mjs +5 -5
- package/dist/{frame-B3He21G8.mjs → frame-BPbypzmt.mjs} +1 -1
- package/dist/{heap-Blrat--4.mjs → heap-DCS_WXIA.mjs} +7 -3
- package/dist/inspection-worker.mjs +4 -4
- package/dist/{job-CBuIMTQ0.mjs → job--zxwgaPy.mjs} +135 -36
- package/dist/{locate-D-ZaHpI0.mjs → locate-PLEyLYW-.mjs} +287 -236
- package/dist/{model-DR9odhWP.mjs → model-CyXvsWLz.mjs} +1 -1
- package/dist/operation-executor.mjs +3 -3
- package/dist/preview-worker.mjs +1 -1
- package/dist/privacy-validator.mjs +1 -1
- package/dist/{query-BYDcNKy2.mjs → query-BC7MIaWc.mjs} +335 -115
- package/dist/{run-DQ9xyKSt.mjs → run-Dod7Vykp.mjs} +6 -6
- package/dist/{src-DeNJ8vOD.mjs → src-CP-K2PBv.mjs} +1779 -1686
- package/dist/{src-Do-qzuMn.mjs → src-CttNuxOn.mjs} +590 -542
- package/dist/{src-CKQlTA2Z.mjs → src-dL6y2mcu.mjs} +180 -30
- package/dist/store-database-worker.mjs +1 -1
- package/package.json +1 -1
- package/skills/tacitt/SKILL.md +3 -3
- package/skills/tacitt/references/b-method.md +4 -2
- package/skills/tacitt/references/bootstrap.md +1 -1
- package/skills/tacitt/references/commands.md +10 -4
- package/skills/tacitt/references/journey.md +3 -3
- package/skills/tacitt/references/parallel-review.md +22 -9
- package/skills/tacitt/references/remote-workers.md +6 -2
- package/skills/tacitt/scripts/INDEX.md +4 -4
- package/skills/tacitt/scripts/answer.mjs +12 -1
- package/skills/tacitt/scripts/describe.mjs +3 -2
- package/skills/tacitt/scripts/journey-classification.mjs +1 -0
- package/skills/tacitt/scripts/journey-scan-timing.mjs +3 -2
- package/skills/tacitt/scripts/journey.mjs +1 -1
- package/skills/tacitt/scripts/privacy-validator.mjs +84 -33
- package/skills/tacitt/scripts/review.d.mts +1 -0
- package/skills/tacitt/scripts/review.mjs +296 -35
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
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/
|
|
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
|
-
*
|
|
1216
|
-
*
|
|
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
|
-
|
|
1219
|
-
|
|
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
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
return
|
|
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
|
|
1235
|
-
function
|
|
1236
|
-
return
|
|
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
|
-
*
|
|
1240
|
-
*
|
|
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
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
|
|
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
|
-
|
|
1257
|
-
|
|
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
|
-
/**
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
-
/**
|
|
1265
|
-
function
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
-
/**
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
1280
|
-
function
|
|
1281
|
-
const
|
|
1282
|
-
const
|
|
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
|
-
...
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3245
|
-
|
|
3246
|
-
|
|
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
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
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
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
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
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
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
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
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
|
-
/**
|
|
3314
|
-
function
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
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 (
|
|
3561
|
-
*
|
|
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 (
|
|
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 >=
|
|
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
|
|
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,
|
|
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 };
|