@gitroll/tacitt-cli 0.6.11 → 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/README.md +4 -0
- package/bin/INDEX.md +6 -4
- package/bin/tacitt.cjs +1 -0
- package/dist/{agent-setup-DdfF3zch.mjs → agent-setup-Doy_AnPY.mjs} +2 -2
- package/dist/agent-setup-worker.mjs +5 -5
- package/dist/cli.mjs +32 -32
- package/dist/codex-title-worker.mjs +2 -2
- package/dist/configured-preview-worker.mjs +5 -5
- package/dist/configured-session-worker.mjs +23 -25
- package/dist/{frame-Bq2zRh-i.mjs → frame-BPbypzmt.mjs} +1 -1
- package/dist/{heap-CV8Eca8V.mjs → heap-DCS_WXIA.mjs} +264 -23
- package/dist/inspection-worker.mjs +4 -4
- package/dist/{job-BKsifhyv.mjs → job--zxwgaPy.mjs} +155 -41
- package/dist/{locate-Cx52Zqxv.mjs → locate-PLEyLYW-.mjs} +292 -236
- package/dist/{model-C1UXkHh9.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 +28 -8
- package/dist/{query-AMZerUh8.mjs → query-BC7MIaWc.mjs} +1914 -759
- package/dist/{run-CaPktPhH.mjs → run-Dod7Vykp.mjs} +6 -6
- package/dist/session-process.mjs +20 -0
- package/dist/{src-DlVbGB32.mjs → src-CP-K2PBv.mjs} +2100 -1649
- package/dist/{src-D1iONla3.mjs → src-CttNuxOn.mjs} +925 -630
- package/dist/{src-wZx_v0Ih.mjs → src-dL6y2mcu.mjs} +180 -30
- package/dist/store-database-worker.mjs +1 -1
- package/dist/web/assets/index-BgDdPL1c.js +172 -0
- package/dist/web/index.html +2 -1
- package/package.json +2 -1
- package/skills/INDEX.md +6 -4
- package/skills/tacitt/INDEX.md +7 -6
- package/skills/tacitt/SKILL.md +7 -7
- package/skills/tacitt/references/INDEX.md +14 -11
- package/skills/tacitt/references/b-method.md +6 -4
- package/skills/tacitt/references/bootstrap.md +18 -4
- package/skills/tacitt/references/classification.md +1 -1
- package/skills/tacitt/references/commands.md +34 -10
- package/skills/tacitt/references/journey.md +7 -7
- package/skills/tacitt/references/parallel-review.md +31 -12
- package/skills/tacitt/references/remote-workers.md +6 -2
- package/skills/tacitt/references/send.md +7 -3
- package/skills/tacitt/scripts/INDEX.md +40 -37
- package/skills/tacitt/scripts/answer.mjs +31 -2
- package/skills/tacitt/scripts/describe.mjs +3 -2
- package/skills/tacitt/scripts/journey-card-consolidation.mjs +1 -1
- package/skills/tacitt/scripts/journey-classification-tasks.mjs +28 -8
- package/skills/tacitt/scripts/journey-classification.mjs +1 -0
- package/skills/tacitt/scripts/journey-panel-host.d.mts +2 -0
- package/skills/tacitt/scripts/journey-panel-host.mjs +25 -2
- package/skills/tacitt/scripts/journey-scan-timing.d.mts +4 -0
- package/skills/tacitt/scripts/journey-scan-timing.mjs +15 -2
- package/skills/tacitt/scripts/journey-state.mjs +11 -0
- package/skills/tacitt/scripts/journey-tasks.mjs +12 -5
- package/skills/tacitt/scripts/journey.mjs +11 -3
- package/skills/tacitt/scripts/privacy-validator.mjs +112 -40
- package/skills/tacitt/scripts/review.d.mts +1 -0
- package/skills/tacitt/scripts/review.mjs +680 -119
- package/dist/web/assets/index-CYl7eDUu.js +0 -172
|
@@ -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,100 +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
|
-
}
|
|
1202
|
+
//#region packages/processing/src/session/marker.ts
|
|
1206
1203
|
/**
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
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.
|
|
1209
1206
|
*/
|
|
1210
|
-
|
|
1211
|
-
|
|
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()];
|
|
1212
1237
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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;
|
|
1216
1248
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
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;
|
|
1225
1257
|
}
|
|
1226
|
-
/** A
|
|
1227
|
-
function
|
|
1228
|
-
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)));
|
|
1229
1261
|
}
|
|
1230
1262
|
/**
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
* Public (origin and channel `account`). A Public Work stored before `accessChoice` existed is
|
|
1234
|
-
* still pending. Mirrored by `phaseOf` in the Skill's `journey-state.mjs` and by the panel's
|
|
1235
|
-
* `WorkAccess`.
|
|
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.
|
|
1236
1265
|
*/
|
|
1237
|
-
function
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
function publicSource(work) {
|
|
1244
|
-
if (!isIncludedWork(work) || work.access !== "public" || work.accountAccessConflict) return null;
|
|
1245
|
-
if (!hasPublicChoice(work)) return "suggestion";
|
|
1246
|
-
return work.accessChoice?.channel === "account" ? "account" : "choice";
|
|
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);
|
|
1247
1272
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
+
};
|
|
1250
1287
|
}
|
|
1251
|
-
/**
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
+
};
|
|
1255
1306
|
}
|
|
1256
|
-
/**
|
|
1257
|
-
function
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
+
};
|
|
1262
1318
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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)}`;
|
|
1266
1330
|
}
|
|
1267
|
-
/**
|
|
1268
|
-
|
|
1269
|
-
|
|
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;
|
|
1270
1338
|
}
|
|
1271
|
-
|
|
1272
|
-
function
|
|
1273
|
-
const
|
|
1274
|
-
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
|
+
};
|
|
1275
1366
|
return {
|
|
1276
|
-
...
|
|
1277
|
-
|
|
1278
|
-
|
|
1367
|
+
...base,
|
|
1368
|
+
belief: preference === null ? .5 : preference === "hide" ? .97 : .03,
|
|
1369
|
+
choice: preference,
|
|
1370
|
+
confirmed: false,
|
|
1371
|
+
origin: preference === null ? "model" : "preference"
|
|
1279
1372
|
};
|
|
1280
1373
|
}
|
|
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;
|
|
1395
|
+
}
|
|
1281
1396
|
//#endregion
|
|
1282
|
-
//#region packages/application/src/journey/
|
|
1283
|
-
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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";
|
|
1294
1414
|
}
|
|
1295
|
-
/**
|
|
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. */
|
|
1296
1635
|
const agentSourceSchema = z.object({
|
|
1297
1636
|
kind: z.enum(["skill", "mcp"]),
|
|
1298
1637
|
name: z.string().regex(/^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,199}$/u),
|
|
@@ -1443,6 +1782,19 @@ const sendUploadInputSchema = z.object({
|
|
|
1443
1782
|
expectedScopeVersion: count$13
|
|
1444
1783
|
}).strict();
|
|
1445
1784
|
const savedSendReceiptSchema = sendReceiptSchema.omit({ upload: true });
|
|
1785
|
+
const sendSessionUploadSchema = z.object({
|
|
1786
|
+
key: journeyDigestSchema,
|
|
1787
|
+
sha256: journeyDigestSchema,
|
|
1788
|
+
/** Byte-started transport/server errors charged to this Send. */
|
|
1789
|
+
attempt: count$13.optional(),
|
|
1790
|
+
/** Transient failures used only to schedule capped zero-byte retries. */
|
|
1791
|
+
retryCount: count$13.optional(),
|
|
1792
|
+
bytesAttempted: count$13.optional(),
|
|
1793
|
+
bytesAcknowledged: count$13.optional(),
|
|
1794
|
+
contentLength: count$13.optional(),
|
|
1795
|
+
bytesSent: count$13.optional(),
|
|
1796
|
+
nextAttemptAt: z.iso.datetime().optional()
|
|
1797
|
+
}).strict();
|
|
1446
1798
|
const sendItemSchema = z.object({
|
|
1447
1799
|
workId: journeyIdSchema,
|
|
1448
1800
|
title: z.string(),
|
|
@@ -1471,22 +1823,13 @@ const sendItemSchema = z.object({
|
|
|
1471
1823
|
* uploads through staging (ADR 0231).
|
|
1472
1824
|
*/
|
|
1473
1825
|
checksumUploadRefused: z.literal(true).optional(),
|
|
1826
|
+
/** Selected session ZIPs confirmed by inventory or completion during this traversal. */
|
|
1827
|
+
storedSessions: count$13.optional(),
|
|
1474
1828
|
/** Write-ahead upload intent; completion probes recover an unacknowledged blob PUT. */
|
|
1475
|
-
sessionUpload:
|
|
1476
|
-
key: journeyDigestSchema,
|
|
1477
|
-
sha256: journeyDigestSchema,
|
|
1478
|
-
/** Byte-started transport/server failures charged to this Send. */
|
|
1479
|
-
attempt: count$13.optional(),
|
|
1480
|
-
/** Transient failures used only to schedule capped zero-byte retries. */
|
|
1481
|
-
retryCount: count$13.optional(),
|
|
1482
|
-
bytesAttempted: count$13.optional(),
|
|
1483
|
-
bytesAcknowledged: count$13.optional(),
|
|
1484
|
-
contentLength: count$13.optional(),
|
|
1485
|
-
bytesSent: count$13.optional(),
|
|
1486
|
-
nextAttemptAt: z.iso.datetime().optional()
|
|
1487
|
-
}).strict().nullable().optional(),
|
|
1829
|
+
sessionUpload: sendSessionUploadSchema.nullable().optional(),
|
|
1488
1830
|
/** Local key-rule marker; current membership replays need no artifact migration inspection. */
|
|
1489
1831
|
sessionMembershipKeyVersion: z.literal(2).optional(),
|
|
1832
|
+
sessionMembershipMetadataVersion: z.literal(1).optional(),
|
|
1490
1833
|
/** Exact write-ahead membership request; never rebase it after an uncertain PUT. */
|
|
1491
1834
|
sessionMembership: z.object({
|
|
1492
1835
|
workId: journeyIdSchema,
|
|
@@ -1658,19 +2001,34 @@ function hasCurrentStaticReview(state, work) {
|
|
|
1658
2001
|
}
|
|
1659
2002
|
//#endregion
|
|
1660
2003
|
//#region packages/application/src/journey/privacy/record.ts
|
|
1661
|
-
/**
|
|
1662
|
-
function
|
|
1663
|
-
const terms = new Map(state.privacy.terms.map((term) => [privacyTermKey(term), term]));
|
|
2004
|
+
/** Only a stored human rule or an explicitly delegated category choice can settle a reader proposal. */
|
|
2005
|
+
function currentTermRules(state) {
|
|
1664
2006
|
const overrides = /* @__PURE__ */ new Map();
|
|
1665
2007
|
for (const item of state.valueOverrides ?? []) {
|
|
1666
2008
|
const value = item.value.normalize("NFC");
|
|
1667
2009
|
if (!overrides.has(value)) overrides.set(value, item);
|
|
1668
2010
|
}
|
|
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 ?? []);
|
|
2013
|
+
function choice(term) {
|
|
2014
|
+
const value = term.value.normalize("NFC");
|
|
2015
|
+
return overrides.get(value)?.choice ?? (remembered.has(value) ? "hide" : categories.get(`type:${term.type}`));
|
|
2016
|
+
}
|
|
2017
|
+
return {
|
|
2018
|
+
overrides,
|
|
2019
|
+
choice
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
/** Merge independently reviewed occurrences into one journey-wide decision. */
|
|
2023
|
+
function mergePrivacyTerms(state, incoming) {
|
|
2024
|
+
const terms = new Map(state.privacy.terms.map((term) => [privacyTermKey(term), term]));
|
|
2025
|
+
const rules = currentTermRules(state);
|
|
2026
|
+
const overrides = rules.overrides;
|
|
1669
2027
|
for (const term of incoming) {
|
|
1670
|
-
const key = privacyTermKey(term)
|
|
1671
|
-
const
|
|
1672
|
-
const
|
|
1673
|
-
const disposition = !termTypeInfo[term.type].asked
|
|
2028
|
+
const key = privacyTermKey(term);
|
|
2029
|
+
const prior = terms.get(key);
|
|
2030
|
+
const choice = rules.choice(term);
|
|
2031
|
+
const disposition = !termTypeInfo[term.type].asked || choice === "hide" ? "hidden" : choice === "keep" ? "keep" : prior?.disposition === "hidden" || term.disposition === "hidden" ? "hidden" : "pending";
|
|
1674
2032
|
const sources = [...new Map([...prior?.sources ?? [], ...term.sources].map((source) => [JSON.stringify(source.kind === "session" ? [
|
|
1675
2033
|
source.workId,
|
|
1676
2034
|
source.batchId,
|
|
@@ -1680,7 +2038,7 @@ function mergePrivacyTerms(state, incoming) {
|
|
|
1680
2038
|
...term,
|
|
1681
2039
|
disposition,
|
|
1682
2040
|
sources,
|
|
1683
|
-
reason: prior && prior.disposition !== term.disposition ? disposition === "pending" ? "Conflicting scan judgments require a contextual decision." : disposition === "hidden" ? "A Hidden judgment for this keyword takes precedence." : term.reason : term.reason
|
|
2041
|
+
reason: term.disposition === "keep" && disposition === "pending" ? "A current decision is required to keep this value." : choice && term.disposition !== disposition ? "The current value or category decision applies." : prior && prior.disposition !== term.disposition ? disposition === "pending" ? "Conflicting scan judgments require a contextual decision." : disposition === "hidden" ? "A Hidden judgment for this keyword takes precedence." : term.reason : term.reason
|
|
1684
2042
|
});
|
|
1685
2043
|
}
|
|
1686
2044
|
const values = Map.groupBy([...terms.values()], (term) => term.value.normalize("NFC"));
|
|
@@ -1715,34 +2073,125 @@ function activePrivacyTerms(state) {
|
|
|
1715
2073
|
return state.works.some((work) => work.access !== "drop" && work.rootSessionIds.length > 0) ? state.privacy.terms : [];
|
|
1716
2074
|
}
|
|
1717
2075
|
//#endregion
|
|
1718
|
-
//#region packages/application/src/
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
2076
|
+
//#region packages/application/src/export/versions.ts
|
|
2077
|
+
/** Current built-in admission, annotation and retention contracts; implementations share these. */
|
|
2078
|
+
const nativeAdapterVersions = Object.freeze({
|
|
2079
|
+
claude: "claude-native-export-v19",
|
|
2080
|
+
codex: "codex-native-export-v16"
|
|
2081
|
+
});
|
|
2082
|
+
/** Shared metadata annotation/preparation semantics, independent of serialization formats. */
|
|
2083
|
+
const metadataProcessingVersion = "metadata-annotations-v1";
|
|
2084
|
+
//#endregion
|
|
2085
|
+
//#region packages/application/src/journey/adapters.ts
|
|
2086
|
+
/** The adapter contracts a Journey's snapshot is prepared under. */
|
|
2087
|
+
const currentJourneyAdapters = () => ({
|
|
2088
|
+
claude: `${nativeAdapterVersions.claude}/main-prose-source-scan-v3`,
|
|
2089
|
+
codex: `${nativeAdapterVersions.codex}/main-prose-source-scan-v3`
|
|
2090
|
+
});
|
|
2091
|
+
/**
|
|
2092
|
+
* An unfinished snapshot frozen under other adapter contracts (absent: an earlier release), or
|
|
2093
|
+
* read by B review instead of dictionary review (ADR 0086), cannot render its Works (ADR 0076).
|
|
2094
|
+
* A new scan replaces it unless a Work was sent or a Send may have reached Passport, which the
|
|
2095
|
+
* store keeps for the person to finish (ADR 0284).
|
|
2096
|
+
*/
|
|
2097
|
+
function obsoleteJourney(state) {
|
|
2098
|
+
return state.phase !== "completed" && state.snapshotId !== null && (state.adapters?.claude !== currentJourneyAdapters().claude || state.adapters.codex !== currentJourneyAdapters().codex || state.reviewMethod !== "dictionary");
|
|
2099
|
+
}
|
|
2100
|
+
/** A receipt or persisted session mutation intent means a Send may have reached Passport. */
|
|
2101
|
+
function sendReachedPassport(send) {
|
|
2102
|
+
return send.items.some((item) => item.receipt || item.previousReceipts.length || item.sessionUpload !== void 0 || item.sessionMembership || item.sessionAssembly);
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* ADR 0284: Passport acknowledged this Send's Work: this Send's receipt for it is past
|
|
2106
|
+
* `awaiting_upload`, or its session membership is acknowledged. An `awaiting_upload` receipt is
|
|
2107
|
+
* an empty reservation, and a reservation, a session upload intent or a membership intent is saved
|
|
2108
|
+
* before the call it announces, so none of them shows that anything left.
|
|
2109
|
+
*/
|
|
2110
|
+
function itemAtPassport(item) {
|
|
2111
|
+
return Boolean(item.receipt && item.receipt.state !== "awaiting_upload" || item.sessionAssembly);
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* The Works the next advance steps without the person to send something: an upload, a
|
|
2115
|
+
* completion or a confirmed access change. `pickBatch` selects upload work by this; a Work being
|
|
2116
|
+
* assessed is only read, and the other states wait on the person or have ended.
|
|
2117
|
+
*/
|
|
2118
|
+
function sendsOnNextAdvance(item) {
|
|
2119
|
+
return item.status === "uploading" || item.status === "ready";
|
|
2120
|
+
}
|
|
2121
|
+
/**
|
|
2122
|
+
* A completed Send that Passport has not finished with: a Work is still assessed or reapplied
|
|
2123
|
+
* (ADR 0233). A blocked Work waits on the person's Retry, not on Passport.
|
|
2124
|
+
*/
|
|
2125
|
+
function awaitingPassport(send) {
|
|
2126
|
+
return send.status === "completed" && send.items.some((item) => !["completed", "blocked"].includes(item.status));
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Whether an advance steps this Send's Works at all. `advanceSend` returns early exactly when this
|
|
2130
|
+
* is false: the person paused the journey, the Send is unconfirmed, paused or prepared, or it is
|
|
2131
|
+
* completed with every Work settled. A blocked Send is stepped only to re-check its scope.
|
|
2132
|
+
*/
|
|
2133
|
+
function advanceStepsSend(journey, send) {
|
|
2134
|
+
return journey.phase !== "paused" && send.confirmedAt !== null && send.status !== "paused" && send.status !== "prepared" && !(send.status === "completed" && send.items.every((item) => item.status === "completed" || item.status === "blocked"));
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Whether a Send in each state is stepped without the person once `advanceStepsSend` holds.
|
|
2138
|
+
* Every status is listed, so a new one fails the type check until it is classified. Unconfirmed
|
|
2139
|
+
* requests and the person's pause, refusal, sign-in and review decisions stay explicit.
|
|
2140
|
+
*/
|
|
2141
|
+
const drivenWithoutPerson = {
|
|
2142
|
+
"awaiting-packaging": false,
|
|
2143
|
+
prepared: false,
|
|
2144
|
+
"sign-in": false,
|
|
2145
|
+
review: false,
|
|
2146
|
+
paused: false,
|
|
2147
|
+
blocked: false,
|
|
2148
|
+
sending: true,
|
|
2149
|
+
waiting: true,
|
|
2150
|
+
completed: true
|
|
2151
|
+
};
|
|
2152
|
+
/**
|
|
2153
|
+
* The one decision whether the advance steps a Send without the person: the detached host's
|
|
2154
|
+
* timer drives exactly these Sends, and a CLI step defers to a live host for them (ADR 0271).
|
|
2155
|
+
*/
|
|
2156
|
+
function hostDrives(journey, send) {
|
|
2157
|
+
return advanceStepsSend(journey, send) && drivenWithoutPerson[send.status];
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* ADR 0284: a Work at Passport still owes Passport something that this Send's next advance sends
|
|
2161
|
+
* without the person. An obsolete journey gives way unless its current Send owes.
|
|
2162
|
+
*/
|
|
2163
|
+
function sendOwesPassport(journey, send) {
|
|
2164
|
+
return hostDrives(journey, send) && send.items.some((item) => itemAtPassport(item) && sendsOnNextAdvance(item));
|
|
2165
|
+
}
|
|
2166
|
+
//#endregion
|
|
2167
|
+
//#region packages/application/src/journey/policy/commands.ts
|
|
2168
|
+
const journeyFindingReferenceSchema = z.object({
|
|
2169
|
+
batchId: journeyIdSchema,
|
|
2170
|
+
findingId: journeyIdSchema
|
|
2171
|
+
}).strict();
|
|
2172
|
+
/** Exact original values as `journey.groupItems` lists them; never trimmed or normalized. */
|
|
2173
|
+
const journeyExactValuesSchema = z.array(z.string().min(1).max(240)).min(1).max(256);
|
|
2174
|
+
const partFields = {
|
|
2175
|
+
groupId: journeyIdSchema,
|
|
2176
|
+
groupKey: z.string().trim().min(1).max(120),
|
|
2177
|
+
label: z.string().trim().min(1).max(120),
|
|
2178
|
+
condition: z.string().trim().min(1).max(1e3),
|
|
2179
|
+
choice: journeyChoiceSchema.nullable()
|
|
2180
|
+
};
|
|
2181
|
+
const journeyGroupPartSchema = z.object({
|
|
2182
|
+
...partFields,
|
|
2183
|
+
members: z.array(journeyFindingReferenceSchema).min(1).max(1e4)
|
|
2184
|
+
}).strict();
|
|
2185
|
+
/** A split part names its members exactly, by exact value, or as the rest of the group. */
|
|
2186
|
+
const journeySplitPartSchema = z.union([
|
|
2187
|
+
journeyGroupPartSchema,
|
|
2188
|
+
z.object({
|
|
2189
|
+
...partFields,
|
|
2190
|
+
values: journeyExactValuesSchema
|
|
2191
|
+
}).strict(),
|
|
2192
|
+
z.object({
|
|
2193
|
+
...partFields,
|
|
2194
|
+
rest: z.literal(true)
|
|
1746
2195
|
}).strict()
|
|
1747
2196
|
]);
|
|
1748
2197
|
const journeySplitActionSchema = z.object({
|
|
@@ -2067,6 +2516,7 @@ const nativeRespondSchema = z.object({
|
|
|
2067
2516
|
const nativeEventsSchema = z.object({
|
|
2068
2517
|
journeyId: journeyIdSchema,
|
|
2069
2518
|
after: z.number().int().nonnegative(),
|
|
2519
|
+
changesOnly: z.boolean().optional(),
|
|
2070
2520
|
waitMs: z.number().int().min(0).max(3e4).default(0)
|
|
2071
2521
|
}).strict();
|
|
2072
2522
|
//#endregion
|
|
@@ -2141,6 +2591,58 @@ function assertPublicChoices(state) {
|
|
|
2141
2591
|
function assertJourneyStaticReady(state) {
|
|
2142
2592
|
if (state.works.some((work) => work.access !== "drop" && work.rootSessionIds.length > 0 && !isSkippedWork(work) && !hasCurrentStaticReview(state, work))) throw new ApplicationError("SCOPE_NOT_READY");
|
|
2143
2593
|
}
|
|
2594
|
+
/** Check a proposed or stored Keep against the current structured decisions. An answered
|
|
2595
|
+
* card covers its actual members, including the opposite choice for an unticked member. */
|
|
2596
|
+
function keepAuthority(state) {
|
|
2597
|
+
const rules = currentTermRules(state);
|
|
2598
|
+
const { keep, hide } = answeredValues(state);
|
|
2599
|
+
const mandatory = new Set(state.privacy.terms.filter((term) => !termTypeInfo[term.type].asked).map((term) => term.value.normalize("NFC")));
|
|
2600
|
+
const own = workProjectNames(state);
|
|
2601
|
+
return (term) => {
|
|
2602
|
+
const value = term.value.normalize("NFC");
|
|
2603
|
+
if (mandatory.has(value)) return "hidden";
|
|
2604
|
+
const override = rules.overrides.get(value);
|
|
2605
|
+
if (override) return override.choice === "keep" ? "keep" : "hidden";
|
|
2606
|
+
if (keep.has(value) && own.has(value.toLowerCase())) return "keep";
|
|
2607
|
+
if (hide.has(value)) return "hidden";
|
|
2608
|
+
if (keep.has(value)) return "keep";
|
|
2609
|
+
const choice = rules.choice(term);
|
|
2610
|
+
return choice === "keep" ? "keep" : choice === "hide" ? "hidden" : "pending";
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
const reconciledKeeps = /* @__PURE__ */ new WeakMap();
|
|
2614
|
+
/** Recover unresolved visibility in a stored unfinished journey without repeating source reads. */
|
|
2615
|
+
function reconcileReaderKeeps(state) {
|
|
2616
|
+
if (state.phase === "completed") return state;
|
|
2617
|
+
const cached = reconciledKeeps.get(state);
|
|
2618
|
+
if (cached) return cached;
|
|
2619
|
+
let result = state;
|
|
2620
|
+
if (state.privacy.terms.some((term) => term.disposition === "keep")) {
|
|
2621
|
+
const authority = keepAuthority(state);
|
|
2622
|
+
let changed = false;
|
|
2623
|
+
const terms = state.privacy.terms.map((term) => {
|
|
2624
|
+
if (term.disposition !== "keep") return term;
|
|
2625
|
+
const disposition = authority(term);
|
|
2626
|
+
if (disposition === "keep") return term;
|
|
2627
|
+
changed = true;
|
|
2628
|
+
return {
|
|
2629
|
+
...term,
|
|
2630
|
+
disposition,
|
|
2631
|
+
reason: "Current privacy decisions require review of this value."
|
|
2632
|
+
};
|
|
2633
|
+
});
|
|
2634
|
+
if (changed) result = {
|
|
2635
|
+
...retirePrivacyArtifacts(state),
|
|
2636
|
+
privacy: {
|
|
2637
|
+
...state.privacy,
|
|
2638
|
+
terms,
|
|
2639
|
+
packageApproval: null
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
reconciledKeeps.set(state, result);
|
|
2644
|
+
return result;
|
|
2645
|
+
}
|
|
2144
2646
|
/**
|
|
2145
2647
|
* Cards need only Track A, AI review and metadata; this final gate also needs Track B.
|
|
2146
2648
|
* `publicChoices` defaults to `packaging`: the Send click checks it before packaging, and the
|
|
@@ -2149,7 +2651,7 @@ function assertJourneyStaticReady(state) {
|
|
|
2149
2651
|
function assertJourneyPrivacyReady(state, packaging = false, { publicChoices = packaging } = {}) {
|
|
2150
2652
|
assertJourneyScanned(state);
|
|
2151
2653
|
if (publicChoices) assertPublicChoices(state);
|
|
2152
|
-
if (activePrivacyTerms(state).some((term) => term.disposition === "pending") || state.privacy.cards.length > 0 && (state.privacy.cards.some((card) => !card.answer) || state.privacy.confirmedAnswersRevision !== state.privacy.answersRevision)) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
2654
|
+
if (reconcileReaderKeeps(state) !== state || activePrivacyTerms(state).some((term) => term.disposition === "pending") || state.privacy.cards.length > 0 && (state.privacy.cards.some((card) => !card.answer) || state.privacy.confirmedAnswersRevision !== state.privacy.answersRevision)) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
2153
2655
|
assertJourneyStaticReady(state);
|
|
2154
2656
|
if (packaging && state.privacy.packageApproval !== privacyDecisionDigest(state)) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
2155
2657
|
}
|
|
@@ -2169,7 +2671,7 @@ function ownIdentityValues(state) {
|
|
|
2169
2671
|
}
|
|
2170
2672
|
/**
|
|
2171
2673
|
* D3: a batch covers only Pending values no card covers yet. Values added to an existing card
|
|
2172
|
-
* take its answer
|
|
2674
|
+
* take its answer, except new literals on an answered Keep card arrive Hidden off-card.
|
|
2173
2675
|
*/
|
|
2174
2676
|
function appendDecisionCards(state, input) {
|
|
2175
2677
|
assertCardsAllowed(state);
|
|
@@ -2196,12 +2698,27 @@ function appendDecisionCards(state, input) {
|
|
|
2196
2698
|
if (term?.type === "people" && ownCard && !ownValues.has(identityValue(term.value))) throw new ApplicationError("INVALID_SELECTION", "cards.append.other-person");
|
|
2197
2699
|
}
|
|
2198
2700
|
}
|
|
2199
|
-
const
|
|
2200
|
-
const
|
|
2701
|
+
const keepCards = state.privacy.cards.filter((card) => card.answer?.optionId === "keep");
|
|
2702
|
+
const keptValues = new Set(keepCards.flatMap((card) => card.termKeys.flatMap((key) => {
|
|
2703
|
+
const term = terms.get(key);
|
|
2704
|
+
return term?.disposition === "keep" && !card.answer?.excludedTermKeys?.includes(key) ? [term.value.normalize("NFC")] : [];
|
|
2705
|
+
})));
|
|
2706
|
+
const diverted = new Set(extend.flatMap((item) => keepCards.some((card) => card.id === item.cardId) ? item.termKeys.filter((key) => !keptValues.has(terms.get(key)?.value.normalize("NFC") ?? "")) : []));
|
|
2707
|
+
const joined = extend.map((item) => ({
|
|
2708
|
+
...item,
|
|
2709
|
+
termKeys: item.termKeys.filter((key) => !diverted.has(key))
|
|
2710
|
+
}));
|
|
2711
|
+
const additions = new Map(joined.map((item) => [item.cardId, item.termKeys]));
|
|
2712
|
+
const inherited = new Map(joined.flatMap((item) => item.termKeys.map((key) => [key, item.cardId])));
|
|
2201
2713
|
return applyCardAnswers({
|
|
2202
2714
|
...retirePrivacyArtifacts(state),
|
|
2203
2715
|
privacy: {
|
|
2204
2716
|
...state.privacy,
|
|
2717
|
+
terms: state.privacy.terms.map((term) => diverted.has(privacyTermKey(term)) ? {
|
|
2718
|
+
...term,
|
|
2719
|
+
disposition: "hidden",
|
|
2720
|
+
reason: "Found after you answered Keep on its card; hidden."
|
|
2721
|
+
} : term),
|
|
2205
2722
|
cards: [...state.privacy.cards.map((card) => {
|
|
2206
2723
|
const added = additions.get(card.id);
|
|
2207
2724
|
if (!added) return card;
|
|
@@ -2264,11 +2781,14 @@ function resolvePrivacyTerms(state, input) {
|
|
|
2264
2781
|
terms
|
|
2265
2782
|
}
|
|
2266
2783
|
}), (term) => term.value.normalize("NFC")).values()) if (same.some((term) => term.disposition === "pending") && input.resolutions.some((item) => item.value.normalize("NFC") === same[0]?.value.normalize("NFC"))) throw new ApplicationError("INVALID_SELECTION");
|
|
2784
|
+
const normalized = normalizeValues(state, terms);
|
|
2785
|
+
const canKeep = keepAuthority(state);
|
|
2786
|
+
if (normalized.some((term) => resolutions.has(privacyTermKey(term)) && term.disposition === "keep" && canKeep(term) !== "keep")) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
2267
2787
|
return {
|
|
2268
2788
|
...retirePrivacyArtifacts(state),
|
|
2269
2789
|
privacy: {
|
|
2270
2790
|
...state.privacy,
|
|
2271
|
-
terms:
|
|
2791
|
+
terms: normalized,
|
|
2272
2792
|
packageApproval: null
|
|
2273
2793
|
}
|
|
2274
2794
|
};
|
|
@@ -2281,6 +2801,34 @@ function answeredKeys(state, optionId) {
|
|
|
2281
2801
|
return card.answer.optionId === optionId ? card.termKeys.filter((key) => !excluded.has(key)) : [...excluded];
|
|
2282
2802
|
}));
|
|
2283
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
|
+
}
|
|
2816
|
+
function answeredValues(state) {
|
|
2817
|
+
const keepKeys = answeredKeys(state, "keep");
|
|
2818
|
+
const hideKeys = answeredKeys(state, "hide");
|
|
2819
|
+
const keep = /* @__PURE__ */ new Set();
|
|
2820
|
+
const hide = /* @__PURE__ */ new Set();
|
|
2821
|
+
for (const term of state.privacy.terms) {
|
|
2822
|
+
const key = privacyTermKey(term);
|
|
2823
|
+
const value = term.value.normalize("NFC");
|
|
2824
|
+
if (keepKeys.has(key)) keep.add(value);
|
|
2825
|
+
if (hideKeys.has(key)) hide.add(value);
|
|
2826
|
+
}
|
|
2827
|
+
return {
|
|
2828
|
+
keep,
|
|
2829
|
+
hide
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2284
2832
|
/** One NFC value has one outcome across term types, cards and per-value rules. */
|
|
2285
2833
|
function normalizeValues(state, input) {
|
|
2286
2834
|
const active = activePrivacyTerms({
|
|
@@ -2290,17 +2838,8 @@ function normalizeValues(state, input) {
|
|
|
2290
2838
|
terms: input
|
|
2291
2839
|
}
|
|
2292
2840
|
});
|
|
2293
|
-
const overrides =
|
|
2294
|
-
|
|
2295
|
-
const value = item.value.normalize("NFC");
|
|
2296
|
-
if (!overrides.has(value)) overrides.set(value, item);
|
|
2297
|
-
}
|
|
2298
|
-
const cardValues = (optionId) => {
|
|
2299
|
-
const keys = answeredKeys(state, optionId);
|
|
2300
|
-
return new Set(state.privacy.terms.filter((term) => keys.has(privacyTermKey(term))).map((term) => term.value.normalize("NFC")));
|
|
2301
|
-
};
|
|
2302
|
-
const hideCardValues = cardValues("hide");
|
|
2303
|
-
const keepCardValues = cardValues("keep");
|
|
2841
|
+
const { overrides } = currentTermRules(state);
|
|
2842
|
+
const { keep: keepCardValues, hide: hideCardValues } = answeredValues(state);
|
|
2304
2843
|
const ownNames = workProjectNames(state);
|
|
2305
2844
|
const normalized = /* @__PURE__ */ new Map();
|
|
2306
2845
|
for (const same of Map.groupBy(active, (term) => term.value.normalize("NFC")).values()) {
|
|
@@ -2372,6 +2911,7 @@ function respondPrivacy(state, action, source) {
|
|
|
2372
2911
|
rule: [option.rule, action.rule?.trim()].filter(Boolean).join("\n"),
|
|
2373
2912
|
additionalRule: action.rule?.trim() ?? "",
|
|
2374
2913
|
remember: action.remember,
|
|
2914
|
+
source,
|
|
2375
2915
|
...excluded.length ? { excludedTermKeys: excluded } : {}
|
|
2376
2916
|
}
|
|
2377
2917
|
});
|
|
@@ -2404,6 +2944,7 @@ function respondPrivacy(state, action, source) {
|
|
|
2404
2944
|
};
|
|
2405
2945
|
}
|
|
2406
2946
|
if (action.kind === "package-confirm") {
|
|
2947
|
+
if (obsoleteJourney(state)) throw new ApplicationError("JOURNEY_OBSOLETE");
|
|
2407
2948
|
assertPublicChoices(state);
|
|
2408
2949
|
assertJourneyPrivacyReady(state);
|
|
2409
2950
|
return {
|
|
@@ -2552,7 +3093,9 @@ const journeyClassificationReadSchema = z.object({
|
|
|
2552
3093
|
journeyId: journeyIdSchema,
|
|
2553
3094
|
workId: journeyIdSchema,
|
|
2554
3095
|
rootSessionId: z.string().min(1).max(128).optional(),
|
|
2555
|
-
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()
|
|
2556
3099
|
}).strict();
|
|
2557
3100
|
const journeyClassificationSubmitSchema = z.object({
|
|
2558
3101
|
journeyId: journeyIdSchema,
|
|
@@ -2579,6 +3122,12 @@ const journeyClassificationSubmitSchema = z.object({
|
|
|
2579
3122
|
*/
|
|
2580
3123
|
expectedClassificationRevision: count$10.optional()
|
|
2581
3124
|
}).strict();
|
|
3125
|
+
/** Restore only the split families that can be proven against the accepted original proposal. */
|
|
3126
|
+
const isolatedMetadataRecoverySchema = z.object({
|
|
3127
|
+
proposal: journeyClassificationSubmitSchema,
|
|
3128
|
+
workIds: z.array(journeyIdSchema).min(1).max(1e3),
|
|
3129
|
+
idempotencyKey: journeyIdSchema
|
|
3130
|
+
}).strict();
|
|
2582
3131
|
const journeyClassificationSessionsSchema = z.object({
|
|
2583
3132
|
journeyId: journeyIdSchema,
|
|
2584
3133
|
workId: journeyIdSchema,
|
|
@@ -2716,401 +3265,86 @@ const correctionReportSchema = z.object({
|
|
|
2716
3265
|
groupId: journeyIdSchema,
|
|
2717
3266
|
kind: journeyKindSchema,
|
|
2718
3267
|
reason: z.string().trim().min(1).max(1e3),
|
|
2719
|
-
anchors: z.array(correctionAnchorSchema).min(1).max(256)
|
|
2720
|
-
}).strict();
|
|
2721
|
-
const reviewCorrectSchema = z.object({
|
|
2722
|
-
journeyId: journeyIdSchema,
|
|
2723
|
-
expectedRevision: z.number().int().nonnegative(),
|
|
2724
|
-
idempotencyKey: journeyIdSchema,
|
|
2725
|
-
report: correctionReportSchema
|
|
2726
|
-
}).strict();
|
|
2727
|
-
/** Immutable private report receipt; the only place where correction quotes are retained. */
|
|
2728
|
-
const correctionReportResourceSchema = z.object({
|
|
2729
|
-
journeyId: journeyIdSchema,
|
|
2730
|
-
kind: z.literal("correction-report"),
|
|
2731
|
-
id: journeyIdSchema,
|
|
2732
|
-
requestHash: journeyDigestSchema,
|
|
2733
|
-
at: z.iso.datetime(),
|
|
2734
|
-
snapshotId: journeyDigestSchema,
|
|
2735
|
-
batchDigest: journeyDigestSchema,
|
|
2736
|
-
decisionDigest: journeyDigestSchema,
|
|
2737
|
-
report: correctionReportSchema
|
|
2738
|
-
}).strict();
|
|
2739
|
-
const correctionAuditSchema = z.object({
|
|
2740
|
-
reportId: journeyIdSchema,
|
|
2741
|
-
action: z.enum([
|
|
2742
|
-
"report",
|
|
2743
|
-
"hide",
|
|
2744
|
-
"dismiss"
|
|
2745
|
-
]),
|
|
2746
|
-
actor: z.enum([
|
|
2747
|
-
"observer",
|
|
2748
|
-
"human",
|
|
2749
|
-
"delegate"
|
|
2750
|
-
]),
|
|
2751
|
-
authorityId: journeyIdSchema.nullable(),
|
|
2752
|
-
decisionDigest: journeyDigestSchema
|
|
2753
|
-
}).strict();
|
|
2754
|
-
//#endregion
|
|
2755
|
-
//#region packages/application/src/journey/correction/binding.ts
|
|
2756
|
-
function workCorrections(state, workId) {
|
|
2757
|
-
return (state.corrections ?? []).filter((item) => item.workId === workId);
|
|
2758
|
-
}
|
|
2759
|
-
/** Binds rendered output and generated metadata to every correction disposition of the Work. */
|
|
2760
|
-
function workCorrectionBinding(state, workId) {
|
|
2761
|
-
const corrections = workCorrections(state, workId);
|
|
2762
|
-
return corrections.length ? corrections.map(({ id, groupId, inputRevision, batchId, status, disposition }) => ({
|
|
2763
|
-
id,
|
|
2764
|
-
groupId,
|
|
2765
|
-
inputRevision,
|
|
2766
|
-
batchId,
|
|
2767
|
-
status,
|
|
2768
|
-
choice: disposition?.choice ?? null,
|
|
2769
|
-
decisionDigest: disposition?.decisionDigest ?? null
|
|
2770
|
-
})) : null;
|
|
2771
|
-
}
|
|
2772
|
-
//#endregion
|
|
2773
|
-
//#region packages/application/src/journey/placement/decisions.ts
|
|
2774
|
-
function workPrivacyDecisions(state, work) {
|
|
2775
|
-
return state.groups.filter((group) => work.groupIds.includes(group.id)).map(({ id, key, kind, label, condition, choice, confirmed }) => ({
|
|
2776
|
-
id,
|
|
2777
|
-
key,
|
|
2778
|
-
kind,
|
|
2779
|
-
label,
|
|
2780
|
-
condition,
|
|
2781
|
-
choice,
|
|
2782
|
-
confirmed
|
|
2783
|
-
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
2784
|
-
}
|
|
2785
|
-
/** Corrections are additive under an unchanged Hide group, so they bind metadata review too. */
|
|
2786
|
-
function workDecisionDigest(state, work) {
|
|
2787
|
-
const decisions = workPrivacyDecisions(state, work);
|
|
2788
|
-
const corrections = workCorrectionBinding(state, work.id);
|
|
2789
|
-
return textDigest(JSON.stringify(corrections ? {
|
|
2790
|
-
decisions,
|
|
2791
|
-
corrections
|
|
2792
|
-
} : decisions));
|
|
2793
|
-
}
|
|
2794
|
-
/** Retire generated text until it is reviewed; the processed project name keeps rows distinct. */
|
|
2795
|
-
function retireWorkDescription(work) {
|
|
2796
|
-
return {
|
|
2797
|
-
...work,
|
|
2798
|
-
suggestion: null
|
|
2799
|
-
};
|
|
2800
|
-
}
|
|
2801
|
-
//#endregion
|
|
2802
|
-
//#region packages/application/src/journey/terms/groups.ts
|
|
2803
|
-
const keyPrefix = "type:";
|
|
2804
|
-
const termGroupKey = (type) => `${keyPrefix}${type}`;
|
|
2805
|
-
/** RFC 9562 UUID version 5 of `name` in the `namespace` UUID. */
|
|
2806
|
-
function uuidV5(namespace, name) {
|
|
2807
|
-
const bytes = createHash("sha1").update(Buffer.from(namespace.replaceAll("-", ""), "hex")).update(name, "utf8").digest().subarray(0, 16);
|
|
2808
|
-
bytes.writeUInt8(bytes.readUInt8(6) & 15 | 80, 6);
|
|
2809
|
-
bytes.writeUInt8(bytes.readUInt8(8) & 63 | 128, 8);
|
|
2810
|
-
const hex = bytes.toString("hex");
|
|
2811
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
2812
|
-
}
|
|
2813
|
-
/** Deterministic: every reader and writer of a journey names a type's group alike. */
|
|
2814
|
-
const termGroupId = (journeyId, type) => uuidV5(journeyId, type);
|
|
2815
|
-
/** The term type a group answers, or null for any other category. */
|
|
2816
|
-
function termGroupType(group) {
|
|
2817
|
-
if (!group.key.startsWith(keyPrefix)) return null;
|
|
2818
|
-
const parsed = termTypeSchema.safeParse(group.key.slice(5));
|
|
2819
|
-
return parsed.success ? parsed.data : null;
|
|
2820
|
-
}
|
|
2821
|
-
const typeGroup = (state, type) => state.groups.find((group) => termGroupType(group) === type);
|
|
2822
|
-
function newTypeGroup(state, type, preference) {
|
|
2823
|
-
const info = termTypeInfo[type];
|
|
2824
|
-
const base = {
|
|
2825
|
-
id: termGroupId(state.journeyId, type),
|
|
2826
|
-
kind: info.kind,
|
|
2827
|
-
key: termGroupKey(type),
|
|
2828
|
-
label: info.label,
|
|
2829
|
-
condition: info.condition,
|
|
2830
|
-
count: 0,
|
|
2831
|
-
preferenceScope: null,
|
|
2832
|
-
representative: null
|
|
2833
|
-
};
|
|
2834
|
-
if (!info.asked) return {
|
|
2835
|
-
...base,
|
|
2836
|
-
belief: .97,
|
|
2837
|
-
choice: "hide",
|
|
2838
|
-
confirmed: true,
|
|
2839
|
-
origin: "policy"
|
|
2840
|
-
};
|
|
2841
|
-
if (state.questionsPaused) return {
|
|
2842
|
-
...base,
|
|
2843
|
-
belief: .97,
|
|
2844
|
-
choice: "hide",
|
|
2845
|
-
confirmed: false,
|
|
2846
|
-
origin: "paused"
|
|
2847
|
-
};
|
|
2848
|
-
return {
|
|
2849
|
-
...base,
|
|
2850
|
-
belief: preference === null ? .5 : preference === "hide" ? .97 : .03,
|
|
2851
|
-
choice: preference,
|
|
2852
|
-
confirmed: false,
|
|
2853
|
-
origin: preference === null ? "model" : "preference"
|
|
2854
|
-
};
|
|
2855
|
-
}
|
|
2856
|
-
/**
|
|
2857
|
-
* One group per type with at least one dictionary value, `count` its distinct values. Existing
|
|
2858
|
-
* groups keep their identity and answer; other groups are unchanged.
|
|
2859
|
-
*/
|
|
2860
|
-
function syncTermGroups(state, counts, preference) {
|
|
2861
|
-
const groups = state.groups.map((group) => {
|
|
2862
|
-
const type = termGroupType(group);
|
|
2863
|
-
return type === null ? group : {
|
|
2864
|
-
...group,
|
|
2865
|
-
count: counts.get(type) ?? group.count
|
|
2866
|
-
};
|
|
2867
|
-
});
|
|
2868
|
-
for (const type of termTypes) {
|
|
2869
|
-
const count = counts.get(type) ?? 0;
|
|
2870
|
-
if (count === 0 || groups.some((group) => termGroupType(group) === type)) continue;
|
|
2871
|
-
groups.push({
|
|
2872
|
-
...newTypeGroup(state, type, preference(termGroupKey(type))),
|
|
2873
|
-
count
|
|
2874
|
-
});
|
|
2875
|
-
}
|
|
2876
|
-
return groups;
|
|
2877
|
-
}
|
|
2878
|
-
//#endregion
|
|
2879
|
-
//#region packages/application/src/journey/review/confirmed-hides.ts
|
|
2880
|
-
/** Kinds from lowest to highest mask priority. */
|
|
2881
|
-
const confirmedHideKinds = [
|
|
2882
|
-
"internal",
|
|
2883
|
-
"identity",
|
|
2884
|
-
"private",
|
|
2885
|
-
"credentials"
|
|
2886
|
-
];
|
|
2887
|
-
const kinds = confirmedHideKinds;
|
|
2888
|
-
const rank$1 = (kind) => kinds.indexOf(kind);
|
|
2889
|
-
/**
|
|
2890
|
-
* ADR 0086: a type's values are hidden unless its group is answered Keep; before an answer the
|
|
2891
|
-
* default is Hide. Credentials and contact details are always hidden (D06).
|
|
2892
|
-
*/
|
|
2893
|
-
function typeHides(state, type) {
|
|
2894
|
-
const group = typeGroup(state, type);
|
|
2895
|
-
return !termTypeInfo[type].asked || !group?.confirmed || group.choice !== "keep";
|
|
2896
|
-
}
|
|
2897
|
-
/** B review (superseded by ADR 0086): IDs of the categories confirmed as Hide. */
|
|
2898
|
-
const confirmedHideGroupIds = (state) => state.groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => group.id);
|
|
2899
|
-
const overrideKey = (state) => (state.valueOverrides ?? []).map((item) => JSON.stringify([
|
|
2900
|
-
item.type,
|
|
2901
|
-
item.value,
|
|
2902
|
-
item.choice
|
|
2903
|
-
])).toSorted();
|
|
2904
|
-
/**
|
|
2905
|
-
* Identifies the hidden set (ADR 0079, 0086). The dictionary only grows and a type's group
|
|
2906
|
-
* counts its distinct values, so each type's count and Hide answer, with the value overrides,
|
|
2907
|
-
* name exactly one hidden set.
|
|
2908
|
-
*/
|
|
2909
|
-
function confirmedHideKey(state) {
|
|
2910
|
-
return createHash("sha256").update(JSON.stringify({
|
|
2911
|
-
types: termTypes.map((type) => [
|
|
2912
|
-
type,
|
|
2913
|
-
typeGroup(state, type)?.count ?? 0,
|
|
2914
|
-
typeHides(state, type)
|
|
2915
|
-
]),
|
|
2916
|
-
overrides: overrideKey(state),
|
|
2917
|
-
terms: state.privacy.terms.filter((term) => term.disposition === "hidden").map((term) => [term.type, term.value]).toSorted((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))
|
|
2918
|
-
})).digest("hex");
|
|
2919
|
-
}
|
|
2920
|
-
/** Whether two states hold the same value overrides. */
|
|
2921
|
-
const sameValueOverrides = (left, right) => JSON.stringify(overrideKey(left)) === JSON.stringify(overrideKey(right));
|
|
2922
|
-
/**
|
|
2923
|
-
* A preparation's waived hits stay waived only while every category recorded for them is still a
|
|
2924
|
-
* confirmed Hide; otherwise render may no longer mask them, so the session needs reconciliation.
|
|
2925
|
-
*/
|
|
2926
|
-
function maskedHitsStale(maskedHits, hiding) {
|
|
2927
|
-
const current = new Set(hiding);
|
|
2928
|
-
return !!maskedHits?.some((hit) => hit.groupIds.some((id) => !current.has(id)));
|
|
2929
|
-
}
|
|
2930
|
-
/**
|
|
2931
|
-
* ADR 0086 hidden set: values of types not answered Keep, minus Keep overrides, plus Hide
|
|
2932
|
-
* overrides, plus every value of an always-hidden type. A value of several types takes the
|
|
2933
|
-
* highest mask kind among the types hiding it.
|
|
2934
|
-
*/
|
|
2935
|
-
function dictionaryHides(state, terms) {
|
|
2936
|
-
const overrides = state.valueOverrides ?? [];
|
|
2937
|
-
const kept = new Set(overrides.flatMap((item) => item.choice === "keep" ? [item.value] : []));
|
|
2938
|
-
const hidden = /* @__PURE__ */ new Map();
|
|
2939
|
-
const add = (value, type) => {
|
|
2940
|
-
const prior = hidden.get(value);
|
|
2941
|
-
const kind = termTypeInfo[type].kind;
|
|
2942
|
-
hidden.set(value, {
|
|
2943
|
-
kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
|
|
2944
|
-
groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], termGroupId(state.journeyId, type)])].sort()
|
|
2945
|
-
});
|
|
2946
|
-
};
|
|
2947
|
-
const all = [...terms];
|
|
2948
|
-
const hiddenValues = new Set(state.privacy.terms.filter((item) => item.disposition === "hidden").map((item) => item.value));
|
|
2949
|
-
for (const term of all) if (hiddenValues.has(term.value) && !kept.has(term.value)) add(term.value, term.type);
|
|
2950
|
-
for (const item of overrides) if (item.choice === "hide") add(item.value, item.type);
|
|
2951
|
-
for (const term of all) if (!termTypeInfo[term.type].asked) add(term.value, term.type);
|
|
2952
|
-
return hidden;
|
|
2953
|
-
}
|
|
2954
|
-
/** The journey's current hidden set, applied to every selected Work (ADR 0086, D03). */
|
|
2955
|
-
async function journeyConfirmedHides(reader, state) {
|
|
2956
|
-
return dictionaryHides(state, [...await reader.journeyTerms({ journeyId: state.journeyId }), ...state.privacy.terms]);
|
|
2957
|
-
}
|
|
2958
|
-
function confirmedHideTerms(groups, records) {
|
|
2959
|
-
const confirmed = new Map(groups.filter((group) => group.confirmed && group.choice === "hide").map((group) => [group.id, group]));
|
|
2960
|
-
const fields = /* @__PURE__ */ new Map();
|
|
2961
|
-
for (const record of records) {
|
|
2962
|
-
const assigned = new Map(record.assignments.map((item) => [item.findingId, item.groupId]));
|
|
2963
|
-
const chunks = new Map(record.chunks.map((chunk) => [chunk.id, chunk]));
|
|
2964
|
-
for (const finding of record.findings) {
|
|
2965
|
-
const group = confirmed.get(assigned.get(finding.id) ?? "");
|
|
2966
|
-
const chunk = chunks.get(finding.chunkId);
|
|
2967
|
-
if (!group || !chunk || finding.class !== "gray") continue;
|
|
2968
|
-
const key = JSON.stringify([chunk.fieldId, chunk.fieldHash]);
|
|
2969
|
-
fields.set(key, [...fields.get(key) ?? [], {
|
|
2970
|
-
start: chunk.offset + finding.start,
|
|
2971
|
-
end: chunk.offset + finding.end,
|
|
2972
|
-
text: chunk.text.slice(finding.start, finding.end),
|
|
2973
|
-
split: [finding.start === 0, finding.end === chunk.text.length],
|
|
2974
|
-
group
|
|
2975
|
-
}]);
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
const terms = /* @__PURE__ */ new Map();
|
|
2979
|
-
const add = (text, groupIds, kind) => {
|
|
2980
|
-
for (const term of bLiteralTerms(text)) {
|
|
2981
|
-
const prior = terms.get(term);
|
|
2982
|
-
terms.set(term, {
|
|
2983
|
-
kind: prior && rank$1(prior.kind) > rank$1(kind) ? prior.kind : kind,
|
|
2984
|
-
groupIds: [.../* @__PURE__ */ new Set([...prior?.groupIds ?? [], ...groupIds])].sort()
|
|
2985
|
-
});
|
|
2986
|
-
}
|
|
2987
|
-
};
|
|
2988
|
-
for (const spans of fields.values()) {
|
|
2989
|
-
let open;
|
|
2990
|
-
for (const span of spans.toSorted((a, b) => a.start - b.start || b.end - a.end)) {
|
|
2991
|
-
if (open && span.split[0] && span.start === open.end) {
|
|
2992
|
-
open.text += span.text;
|
|
2993
|
-
open.end = span.end;
|
|
2994
|
-
open.groupIds.push(span.group.id);
|
|
2995
|
-
} else {
|
|
2996
|
-
if (open) add(open.text, open.groupIds, open.kind);
|
|
2997
|
-
open = {
|
|
2998
|
-
end: span.end,
|
|
2999
|
-
text: span.text,
|
|
3000
|
-
groupIds: [span.group.id],
|
|
3001
|
-
kind: span.group.kind
|
|
3002
|
-
};
|
|
3003
|
-
}
|
|
3004
|
-
if (!span.split[1]) {
|
|
3005
|
-
add(open.text, open.groupIds, open.kind);
|
|
3006
|
-
open = void 0;
|
|
3007
|
-
}
|
|
3008
|
-
}
|
|
3009
|
-
if (open) add(open.text, open.groupIds, open.kind);
|
|
3010
|
-
}
|
|
3011
|
-
return terms;
|
|
3012
|
-
}
|
|
3013
|
-
//#endregion
|
|
3014
|
-
//#region packages/application/src/journey/b/matches.ts
|
|
3015
|
-
function bDictionaryHits(fields, literals, signal = new AbortController().signal) {
|
|
3016
|
-
const matcher = createEntityMatcher([...new Set(literals.map((part) => part.term))].map((value) => ({
|
|
3017
|
-
kind: "custom-term",
|
|
3018
|
-
value
|
|
3019
|
-
})));
|
|
3020
|
-
const hits = [];
|
|
3021
|
-
for (const field of fields) {
|
|
3022
|
-
const index = createCodePointIndex(field.text);
|
|
3023
|
-
const base = field.start ?? 0;
|
|
3024
|
-
for (const match of matcher.find(field.text, signal, 1e5)) {
|
|
3025
|
-
if (hits.length >= 1e6) throw new ApplicationError("PROCESSING_LIMIT");
|
|
3026
|
-
const body = {
|
|
3027
|
-
fieldId: field.id,
|
|
3028
|
-
fieldHash: field.fieldHash,
|
|
3029
|
-
recordId: field.recordId,
|
|
3030
|
-
start: base + index.codePointOffset(match.range.start),
|
|
3031
|
-
end: base + index.codePointOffset(match.range.end),
|
|
3032
|
-
exactQuote: field.text.slice(match.range.start, match.range.end),
|
|
3033
|
-
term: match.entity.value
|
|
3034
|
-
};
|
|
3035
|
-
hits.push({
|
|
3036
|
-
...body,
|
|
3037
|
-
digest: bDigest(JSON.stringify(body))
|
|
3038
|
-
});
|
|
3039
|
-
}
|
|
3040
|
-
}
|
|
3041
|
-
return [...new Map(hits.map((hit) => [hit.digest, hit])).values()];
|
|
3042
|
-
}
|
|
3043
|
-
function unclassifiedBHits(hits, literals, negative) {
|
|
3044
|
-
const issues = [];
|
|
3045
|
-
const positiveFields = new Map([...Map.groupBy(literals, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
|
|
3046
|
-
const negativeFields = new Map([...Map.groupBy(negative, (part) => part.fieldId)].map(([id, parts]) => [id, indexBRanges(parts)]));
|
|
3047
|
-
for (const hit of hits) {
|
|
3048
|
-
const positive = positiveFields.get(hit.fieldId)?.relation(hit).covers ?? false;
|
|
3049
|
-
const negative = negativeFields.get(hit.fieldId)?.relation(hit);
|
|
3050
|
-
if (!positive && !negative?.covers || positive && negative?.overlaps) issues.push(bIssue(positive ? "CONFLICTING_PRIVACY_DECISIONS" : "UNCLASSIFIED_MATCH", hit));
|
|
3051
|
-
}
|
|
3052
|
-
return issues;
|
|
3053
|
-
}
|
|
3054
|
-
const bSpacedWord = (value) => /[\p{L}\p{N}_]/u.test(value) && !/[\p{sc=Han}\p{sc=Hiragana}\p{sc=Katakana}\p{sc=Thai}\p{sc=Lao}\p{sc=Khmer}\p{sc=Myanmar}]/u.test(value);
|
|
3055
|
-
/** A confirmed Hide value matches only where it does not run into a neighbouring word (ADR 0069). */
|
|
3056
|
-
function bWordBounded(text, start, end) {
|
|
3057
|
-
return !(bSpacedWord(scalarBefore(text, start)) && bSpacedWord(scalarAfter(text, start)) || bSpacedWord(scalarBefore(text, end)) && bSpacedWord(scalarAfter(text, end)));
|
|
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);
|
|
3058
3307
|
}
|
|
3059
|
-
/**
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
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;
|
|
3069
3320
|
}
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
return low > 0 && (reach[low - 1] ?? -1) >= range.end;
|
|
3083
|
-
};
|
|
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));
|
|
3084
3333
|
}
|
|
3085
|
-
/**
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
})));
|
|
3094
|
-
const excepted = bExceptionRanges(exceptions);
|
|
3095
|
-
return (text, signal = new AbortController().signal) => {
|
|
3096
|
-
const hits = matcher.find(text, signal, 1e5).filter(({ range }) => bWordBounded(text, range.start, range.end));
|
|
3097
|
-
const kept = hits.length ? bContainment(excepted(text, signal)) : () => false;
|
|
3098
|
-
return hits.filter(({ range }) => !kept(range)).map(({ range, entity }) => ({
|
|
3099
|
-
...range,
|
|
3100
|
-
term: entity.value
|
|
3101
|
-
}));
|
|
3102
|
-
};
|
|
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));
|
|
3103
3342
|
}
|
|
3104
|
-
/**
|
|
3105
|
-
function
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
base: field.start ?? 0
|
|
3110
|
-
}]));
|
|
3111
|
-
return (hit) => {
|
|
3112
|
-
const field = byId.get(hit.fieldId);
|
|
3113
|
-
return !!field && terms.has(hit.term) && bWordBounded(field.text, field.index.offset(hit.start - field.base), field.index.offset(hit.end - field.base));
|
|
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
|
|
3114
3348
|
};
|
|
3115
3349
|
}
|
|
3116
3350
|
const segmenter = new Intl.Segmenter("und", { granularity: "grapheme" });
|
|
@@ -3126,6 +3360,14 @@ const maximumHideIndexTokens = 2e6;
|
|
|
3126
3360
|
* has too many such positions to list.
|
|
3127
3361
|
*/
|
|
3128
3362
|
function addHideIndexTokens(text, add) {
|
|
3363
|
+
let complete = ordinaryTokens(text, add);
|
|
3364
|
+
if (text.includes("\\")) {
|
|
3365
|
+
if (/^\s*[[{]/u.test(text) && text.includes("\\u")) return false;
|
|
3366
|
+
for (const [, word] of text.matchAll(/\\[nrt]([\p{L}\p{N}\p{M}_]+)/gu)) if (word) complete = ordinaryTokens(word, add) && complete;
|
|
3367
|
+
}
|
|
3368
|
+
return complete;
|
|
3369
|
+
}
|
|
3370
|
+
function ordinaryTokens(text, add) {
|
|
3129
3371
|
let complete = true;
|
|
3130
3372
|
let from = 0;
|
|
3131
3373
|
for (const { index } of text.matchAll(/[^\p{ASCII}]+/gu)) {
|
|
@@ -3340,8 +3582,9 @@ function memberChange(previous, next) {
|
|
|
3340
3582
|
return counts.every((count) => count >= 0) ? "additions" : counts.every((count) => count <= 0) ? "removals" : "other";
|
|
3341
3583
|
}
|
|
3342
3584
|
/**
|
|
3343
|
-
* Whether a Work's output may change with the delta (
|
|
3344
|
-
*
|
|
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
|
|
3345
3588
|
* members were only removed: no value is then added, so nothing can be under-hidden, and an
|
|
3346
3589
|
* output may keep masking a value that left its category until it is rendered again.
|
|
3347
3590
|
*/
|
|
@@ -3944,6 +4187,40 @@ const journeyPreparationProgressSchema = z.object({
|
|
|
3944
4187
|
capturedBytes: preparationCountSchema
|
|
3945
4188
|
}).strict();
|
|
3946
4189
|
//#endregion
|
|
4190
|
+
//#region packages/application/src/journey/inventory/progress.ts
|
|
4191
|
+
/** State-only gate: the scope is confirmed and a selected Work still waits for preparation. */
|
|
4192
|
+
function preparationInProgress(state) {
|
|
4193
|
+
return state.consent !== null && state.snapshotId !== null && state.scope.status === "confirmed" && state.works.some((work) => work.status === "pending" && isSelectedWork(work));
|
|
4194
|
+
}
|
|
4195
|
+
/**
|
|
4196
|
+
* Capture counts cover only the selected Works' frozen parts, so Dropped Works never hold the
|
|
4197
|
+
* total open. Captured bytes can end below the frozen size at a complete record boundary.
|
|
4198
|
+
* Current Works are the pending selected Works whose newest attempt has no settlement, in
|
|
4199
|
+
* Work order; several prepare at once.
|
|
4200
|
+
*/
|
|
4201
|
+
function projectPreparationProgress(state, families, captures, starts, settlements) {
|
|
4202
|
+
const selected = state.works.filter(isSelectedWork);
|
|
4203
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
4204
|
+
for (const id of selected.flatMap((work) => work.rootSessionIds)) {
|
|
4205
|
+
const members = families.members.get(id);
|
|
4206
|
+
if (!members) throw new ApplicationError("CORRUPTED_STATE");
|
|
4207
|
+
for (const part of members.flatMap((session) => session.parts)) sizes.set(part.id, part.snapshot.size);
|
|
4208
|
+
}
|
|
4209
|
+
const captured = captures.filter((resource) => sizes.has(resource.partId));
|
|
4210
|
+
const settled = new Set(settlements.map((item) => item.id));
|
|
4211
|
+
return {
|
|
4212
|
+
currentWorkIds: selected.filter((work) => {
|
|
4213
|
+
if (work.status !== "pending") return false;
|
|
4214
|
+
const newest = starts.filter((start) => start.workId === work.id && start.inputRevision === work.inputRevision).reduce((latest, start) => !latest || start.sequence > latest.sequence ? start : latest, void 0);
|
|
4215
|
+
return newest !== void 0 && !settled.has(newest.id);
|
|
4216
|
+
}).map((work) => work.id),
|
|
4217
|
+
targetParts: sizes.size,
|
|
4218
|
+
capturedParts: captured.length,
|
|
4219
|
+
targetBytes: [...sizes.values()].reduce((sum, size) => sum + size, 0),
|
|
4220
|
+
capturedBytes: captured.reduce((sum, resource) => sum + resource.bytes, 0)
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
//#endregion
|
|
3947
4224
|
//#region packages/application/src/source/resolve.ts
|
|
3948
4225
|
/** Queue-based ancestry resolution cannot leak deep descendants through a recursion cutoff. */
|
|
3949
4226
|
function resolveParts(inventory, route) {
|
|
@@ -4646,7 +4923,7 @@ const sessionReconcileResultSchema = z.object({
|
|
|
4646
4923
|
//#endregion
|
|
4647
4924
|
//#region packages/application/src/journey/review/attempt-model.ts
|
|
4648
4925
|
/**
|
|
4649
|
-
* 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
|
|
4650
4927
|
* 240,000 code points and 48 units; attempts dispatched with the same `packId` share one agent.
|
|
4651
4928
|
*/
|
|
4652
4929
|
const reviewAgentCodePoints = 24e4;
|
|
@@ -4857,6 +5134,31 @@ const journeyCaptureSchema = z.object({
|
|
|
4857
5134
|
source: sourcePartSchema
|
|
4858
5135
|
}).strict();
|
|
4859
5136
|
const base = { journeyId: journeyIdSchema };
|
|
5137
|
+
const journeyOutputSchema = z.object({
|
|
5138
|
+
...base,
|
|
5139
|
+
kind: z.literal("output"),
|
|
5140
|
+
workId: journeyIdSchema,
|
|
5141
|
+
revision: z.number().int().positive(),
|
|
5142
|
+
bundleId: journeyIdSchema,
|
|
5143
|
+
decisionDigest: journeyDigestSchema,
|
|
5144
|
+
primary: journeyFileSchema,
|
|
5145
|
+
companion: journeyFileSchema,
|
|
5146
|
+
finalMaskLedger: journeyFileSchema.extend({
|
|
5147
|
+
primarySha256: journeyDigestSchema,
|
|
5148
|
+
basePrimarySha256: journeyDigestSchema,
|
|
5149
|
+
workId: journeyIdSchema,
|
|
5150
|
+
inputRevision: z.number().int().positive(),
|
|
5151
|
+
decisionDigest: journeyDigestSchema,
|
|
5152
|
+
occurrences: z.number().int().nonnegative(),
|
|
5153
|
+
contributors: z.number().int().nonnegative(),
|
|
5154
|
+
categories: z.object({
|
|
5155
|
+
identity: z.number().int().nonnegative(),
|
|
5156
|
+
credentials: z.number().int().nonnegative(),
|
|
5157
|
+
internal: z.number().int().nonnegative(),
|
|
5158
|
+
private: z.number().int().nonnegative()
|
|
5159
|
+
}).strict()
|
|
5160
|
+
}).strict()
|
|
5161
|
+
}).strict();
|
|
4860
5162
|
const journeyResourceSchema = z.discriminatedUnion("kind", [
|
|
4861
5163
|
z.object({
|
|
4862
5164
|
...base,
|
|
@@ -4976,31 +5278,7 @@ const journeyResourceSchema = z.discriminatedUnion("kind", [
|
|
|
4976
5278
|
bundleSourceId: z.string().min(1)
|
|
4977
5279
|
}).strict())
|
|
4978
5280
|
}).strict(),
|
|
4979
|
-
|
|
4980
|
-
...base,
|
|
4981
|
-
kind: z.literal("output"),
|
|
4982
|
-
workId: journeyIdSchema,
|
|
4983
|
-
revision: z.number().int().positive(),
|
|
4984
|
-
bundleId: journeyIdSchema,
|
|
4985
|
-
decisionDigest: journeyDigestSchema,
|
|
4986
|
-
primary: journeyFileSchema,
|
|
4987
|
-
companion: journeyFileSchema,
|
|
4988
|
-
finalMaskLedger: journeyFileSchema.extend({
|
|
4989
|
-
primarySha256: journeyDigestSchema,
|
|
4990
|
-
basePrimarySha256: journeyDigestSchema,
|
|
4991
|
-
workId: journeyIdSchema,
|
|
4992
|
-
inputRevision: z.number().int().positive(),
|
|
4993
|
-
decisionDigest: journeyDigestSchema,
|
|
4994
|
-
occurrences: z.number().int().nonnegative(),
|
|
4995
|
-
contributors: z.number().int().nonnegative(),
|
|
4996
|
-
categories: z.object({
|
|
4997
|
-
identity: z.number().int().nonnegative(),
|
|
4998
|
-
credentials: z.number().int().nonnegative(),
|
|
4999
|
-
internal: z.number().int().nonnegative(),
|
|
5000
|
-
private: z.number().int().nonnegative()
|
|
5001
|
-
}).strict()
|
|
5002
|
-
}).strict()
|
|
5003
|
-
}).strict()
|
|
5281
|
+
journeyOutputSchema
|
|
5004
5282
|
]);
|
|
5005
5283
|
const journeyResourceQuerySchema = z.union([
|
|
5006
5284
|
z.object({
|
|
@@ -5057,40 +5335,37 @@ const journeyResourceQuerySchema = z.union([
|
|
|
5057
5335
|
}).strict()
|
|
5058
5336
|
]);
|
|
5059
5337
|
//#endregion
|
|
5060
|
-
//#region packages/application/src/journey/
|
|
5061
|
-
/**
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
capturedBytes: captured.reduce((sum, resource) => sum + resource.capture.facts.byteLength, 0)
|
|
5092
|
-
};
|
|
5093
|
-
}
|
|
5338
|
+
//#region packages/application/src/journey/send/port.ts
|
|
5339
|
+
/** Storage validates current package approval before releasing one Work's frozen descriptor. */
|
|
5340
|
+
const sendOutputQuerySchema = z.object({
|
|
5341
|
+
journeyId: z.uuid(),
|
|
5342
|
+
workId: z.uuid(),
|
|
5343
|
+
revision: z.number().int().positive(),
|
|
5344
|
+
sessions: z.boolean()
|
|
5345
|
+
}).strict();
|
|
5346
|
+
const sendOutputSchema = z.object({
|
|
5347
|
+
output: journeyOutputSchema,
|
|
5348
|
+
extension: z.object({
|
|
5349
|
+
approval: z.string(),
|
|
5350
|
+
groups: z.array(z.array(z.string())),
|
|
5351
|
+
sources: z.array(z.object({
|
|
5352
|
+
session: z.string(),
|
|
5353
|
+
source: z.string(),
|
|
5354
|
+
part: sourcePartSchema
|
|
5355
|
+
})),
|
|
5356
|
+
outputs: z.array(z.object({
|
|
5357
|
+
workId: z.uuid(),
|
|
5358
|
+
revision: z.number().int(),
|
|
5359
|
+
sha256: z.string(),
|
|
5360
|
+
bundleId: z.string()
|
|
5361
|
+
}))
|
|
5362
|
+
}).strict().optional(),
|
|
5363
|
+
identities: z.array(z.object({
|
|
5364
|
+
archiveSessionId: z.string(),
|
|
5365
|
+
relativePath: z.string(),
|
|
5366
|
+
lastAt: z.iso.datetime()
|
|
5367
|
+
}).strict())
|
|
5368
|
+
}).strict();
|
|
5094
5369
|
//#endregion
|
|
5095
5370
|
//#region packages/application/src/journey/policy/port.ts
|
|
5096
5371
|
const journeyPolicyRespondSchema = z.object({
|
|
@@ -5224,6 +5499,14 @@ function newJourneyWork(id, projectName, rootSessionIds, sourceWorkspaceIds, pla
|
|
|
5224
5499
|
binding: null
|
|
5225
5500
|
};
|
|
5226
5501
|
}
|
|
5502
|
+
/** Stable blocked Work IDs per parent, so later failing roots join one instead of multiplying. */
|
|
5503
|
+
function isolatedWorkId(parentId, ordinal) {
|
|
5504
|
+
const bytes = createHash("sha256").update(`isolated-roots:${parentId}:${ordinal}`).digest().subarray(0, 16);
|
|
5505
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 64;
|
|
5506
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
5507
|
+
const hex = bytes.toString("hex");
|
|
5508
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
5509
|
+
}
|
|
5227
5510
|
//#endregion
|
|
5228
5511
|
//#region packages/application/src/journey/scan-generation.ts
|
|
5229
5512
|
/** Retire one scan generation while preserving the person's confirmed classification. */
|
|
@@ -5890,6 +6173,7 @@ function respondToJourney(state, action, source, now, decision, preferenceScope,
|
|
|
5890
6173
|
reason: action.reason
|
|
5891
6174
|
}];
|
|
5892
6175
|
} else {
|
|
6176
|
+
if (source !== "panel") throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
5893
6177
|
if (previous?.reason !== "retry-exhausted") throw new ApplicationError("INVALID_TRANSITION");
|
|
5894
6178
|
nextHolds = holds.filter((item) => item.packId !== action.packId);
|
|
5895
6179
|
}
|
|
@@ -6001,7 +6285,7 @@ function answeredRule(card) {
|
|
|
6001
6285
|
/** A dispatch without a pack ID is its own analysis agent. */
|
|
6002
6286
|
const agentKey = (attemptId, packId) => packId === void 0 ? `attempt:${attemptId}` : `pack:${packId}`;
|
|
6003
6287
|
/**
|
|
6004
|
-
* 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
|
|
6005
6289
|
* 240,000 code points or 48 units. A full reservation (repair or reconciliation) fills an agent,
|
|
6006
6290
|
* so it always runs alone.
|
|
6007
6291
|
*/
|
|
@@ -6020,7 +6304,7 @@ function assertAgentCapacity(open, dispatches, attemptId, packId, budgetCodePoin
|
|
|
6020
6304
|
}
|
|
6021
6305
|
const key = agentKey(attemptId, packId);
|
|
6022
6306
|
const agent = agents.get(key);
|
|
6023
|
-
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");
|
|
6024
6308
|
}
|
|
6025
6309
|
//#endregion
|
|
6026
6310
|
//#region packages/application/src/journey/review/attempt.ts
|
|
@@ -6282,30 +6566,6 @@ async function readGroupItems(storage, value) {
|
|
|
6282
6566
|
};
|
|
6283
6567
|
}
|
|
6284
6568
|
//#endregion
|
|
6285
|
-
//#region packages/application/src/export/versions.ts
|
|
6286
|
-
/** Current built-in admission, annotation and retention contracts; implementations share these. */
|
|
6287
|
-
const nativeAdapterVersions = Object.freeze({
|
|
6288
|
-
claude: "claude-native-export-v19",
|
|
6289
|
-
codex: "codex-native-export-v16"
|
|
6290
|
-
});
|
|
6291
|
-
/** Shared metadata annotation/preparation semantics, independent of serialization formats. */
|
|
6292
|
-
const metadataProcessingVersion = "metadata-annotations-v1";
|
|
6293
|
-
//#endregion
|
|
6294
|
-
//#region packages/application/src/journey/adapters.ts
|
|
6295
|
-
/** The adapter contracts a Journey's snapshot is prepared under. */
|
|
6296
|
-
const currentJourneyAdapters = () => ({
|
|
6297
|
-
claude: `${nativeAdapterVersions.claude}/main-prose-source-scan-v2`,
|
|
6298
|
-
codex: `${nativeAdapterVersions.codex}/main-prose-source-scan-v2`
|
|
6299
|
-
});
|
|
6300
|
-
/**
|
|
6301
|
-
* An unfinished snapshot frozen under other adapter contracts (absent: an earlier release), or
|
|
6302
|
-
* read by B review instead of dictionary review (ADR 0086), cannot render its Works (ADR 0076).
|
|
6303
|
-
* A new scan replaces it unless it holds a Send, which the store keeps for the person to finish.
|
|
6304
|
-
*/
|
|
6305
|
-
function obsoleteJourney(state) {
|
|
6306
|
-
return state.phase !== "completed" && state.snapshotId !== null && (state.adapters?.claude !== currentJourneyAdapters().claude || state.adapters.codex !== currentJourneyAdapters().codex || state.reviewMethod !== "dictionary");
|
|
6307
|
-
}
|
|
6308
|
-
//#endregion
|
|
6309
6569
|
//#region packages/application/src/journey/correction/anchors.ts
|
|
6310
6570
|
/** Exact frozen-field validation only: no fuzzy relocation, context ownership or other unit. */
|
|
6311
6571
|
function locateCorrectionAnchors(batch, anchors) {
|
|
@@ -6650,6 +6910,25 @@ async function sessionReviewStatus(storage, value) {
|
|
|
6650
6910
|
}
|
|
6651
6911
|
//#endregion
|
|
6652
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
|
+
}
|
|
6653
6932
|
function digest$1(value) {
|
|
6654
6933
|
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
6655
6934
|
}
|
|
@@ -6658,8 +6937,9 @@ function digest$1(value) {
|
|
|
6658
6937
|
* quotes stay out. The agent reads override values from `journey.get` and the guidance.
|
|
6659
6938
|
*/
|
|
6660
6939
|
function journeyView(state, now) {
|
|
6661
|
-
const { exceptions, instructions, corrections, valueOverrides, consent, accountRead, ...safe } = state;
|
|
6940
|
+
const { exceptions, instructions, corrections, valueOverrides, consent, accountRead, rememberedHides: _remembered, ...safe } = state;
|
|
6662
6941
|
const terms = activePrivacyTerms(state);
|
|
6942
|
+
const hidden = dictionaryHides(state, state.privacy.terms);
|
|
6663
6943
|
return {
|
|
6664
6944
|
...safe,
|
|
6665
6945
|
consent: consent && {
|
|
@@ -6680,15 +6960,17 @@ function journeyView(state, now) {
|
|
|
6680
6960
|
const localTitle = work.suggestion || work.explicitTitleId ? work.title : work.classificationMetadata?.title ?? work.title;
|
|
6681
6961
|
const imagesOmitted = hasCurrentStaticReview(state, work) ? work.staticReview?.imagesOmitted ?? 0 : 0;
|
|
6682
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));
|
|
6683
6964
|
if (!work.classificationMetadata) return {
|
|
6684
6965
|
...work,
|
|
6966
|
+
title,
|
|
6685
6967
|
localTitle,
|
|
6686
6968
|
imagesOmitted,
|
|
6687
6969
|
publicSource: source
|
|
6688
6970
|
};
|
|
6689
6971
|
const metadata = work.classificationMetadata;
|
|
6690
6972
|
const displayed = {
|
|
6691
|
-
title
|
|
6973
|
+
title,
|
|
6692
6974
|
projectName: metadata.projectName,
|
|
6693
6975
|
description: work.suggestion ? work.description : metadata.description,
|
|
6694
6976
|
reason: work.suggestion ? work.reason : metadata.reason
|
|
@@ -6786,7 +7068,7 @@ async function joinRender(port, journeyId, workId, run, signal, retryConflict =
|
|
|
6786
7068
|
}
|
|
6787
7069
|
function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).toISOString(), local) {
|
|
6788
7070
|
/** Callers present after their write is durable, so a concurrent commit must not turn it into an error. */
|
|
6789
|
-
async function present(committed) {
|
|
7071
|
+
async function present(committed, includePreparationProgress = true) {
|
|
6790
7072
|
let state = committed;
|
|
6791
7073
|
for (let attempt = 1;; attempt++) {
|
|
6792
7074
|
const view = journeyView(state, now());
|
|
@@ -6806,7 +7088,7 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
|
|
|
6806
7088
|
review: null
|
|
6807
7089
|
};
|
|
6808
7090
|
}
|
|
6809
|
-
const preparationProgress = preparationInProgress(state) ? await storage.readPreparationProgress({ journeyId: state.journeyId }) : null;
|
|
7091
|
+
const preparationProgress = includePreparationProgress && preparationInProgress(state) ? await storage.readPreparationProgress({ journeyId: state.journeyId }) : null;
|
|
6810
7092
|
const classificationProgress = state.consent && state.snapshotId ? (await storage.resources({
|
|
6811
7093
|
journeyId: state.journeyId,
|
|
6812
7094
|
kind: "classification-progress",
|
|
@@ -6865,8 +7147,8 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
|
|
|
6865
7147
|
const query = nativeGetSchema.parse(input);
|
|
6866
7148
|
const state = await storage.current(query.journeyId);
|
|
6867
7149
|
if (!state) return null;
|
|
6868
|
-
const view = await present(state);
|
|
6869
|
-
return audience
|
|
7150
|
+
const view = await present(state, audience !== "review-helper");
|
|
7151
|
+
return audience !== "panel" && state.valueOverrides ? {
|
|
6870
7152
|
...view,
|
|
6871
7153
|
valueOverrides: state.valueOverrides
|
|
6872
7154
|
} : view;
|
|
@@ -6933,6 +7215,7 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
|
|
|
6933
7215
|
};
|
|
6934
7216
|
};
|
|
6935
7217
|
if (current && current.phase !== "completed" && !current.abandonedAt && !obsoleteJourney(current)) return resume(current);
|
|
7218
|
+
const carried = current && !current.abandonedAt && obsoleteJourney(current) ? (current.instructions ?? []).filter((item) => item.actor === "human") : [];
|
|
6936
7219
|
const state = {
|
|
6937
7220
|
formatVersion: 10,
|
|
6938
7221
|
classificationRevision: null,
|
|
@@ -6961,6 +7244,7 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
|
|
|
6961
7244
|
exceptions: [],
|
|
6962
7245
|
adapters: currentJourneyAdapters(),
|
|
6963
7246
|
lastEvent: 0,
|
|
7247
|
+
...carried.length ? { instructions: carried } : {},
|
|
6964
7248
|
...input.period?.since || input.period?.until ? { period: input.period } : {}
|
|
6965
7249
|
};
|
|
6966
7250
|
const result = await storage.commit({
|
|
@@ -7142,7 +7426,7 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
|
|
|
7142
7426
|
const rows = await storage.events(input.journeyId, input.after);
|
|
7143
7427
|
if (rows.length || Date.now() >= deadline) return {
|
|
7144
7428
|
events: rows,
|
|
7145
|
-
state: await get({ journeyId: input.journeyId })
|
|
7429
|
+
state: rows.length || !input.changesOnly ? await get({ journeyId: input.journeyId }) : null
|
|
7146
7430
|
};
|
|
7147
7431
|
await setTimeout$1(Math.min(1e3, Math.max(0, deadline - Date.now())), void 0, { signal });
|
|
7148
7432
|
}
|
|
@@ -7445,6 +7729,14 @@ function createNativeJourney(storage, now = () => (/* @__PURE__ */ new Date()).t
|
|
|
7445
7729
|
if (!local) throw new ApplicationError("INVALID_TRANSITION");
|
|
7446
7730
|
return local.works.classificationRead(journeyClassificationReadSchema.parse(value), signal);
|
|
7447
7731
|
},
|
|
7732
|
+
recoverIsolatedMetadata: async (value, signal = new AbortController().signal) => {
|
|
7733
|
+
if (!local) throw new ApplicationError("INVALID_TRANSITION");
|
|
7734
|
+
const result = await local.works.recoverIsolatedMetadata(isolatedMetadataRecoverySchema.parse(value), signal);
|
|
7735
|
+
return {
|
|
7736
|
+
...result,
|
|
7737
|
+
state: await present(result.state)
|
|
7738
|
+
};
|
|
7739
|
+
},
|
|
7448
7740
|
classificationSubmit: async (value, signal = new AbortController().signal) => {
|
|
7449
7741
|
if (!local) throw new ApplicationError("INVALID_TRANSITION");
|
|
7450
7742
|
const result = await local.works.classificationSubmit(journeyClassificationSubmitSchema.parse(value), signal);
|
|
@@ -7577,7 +7869,8 @@ function safeJourneySender(sender, now) {
|
|
|
7577
7869
|
async get(input) {
|
|
7578
7870
|
const result = await sender.get({
|
|
7579
7871
|
journeyId: input.journeyId,
|
|
7580
|
-
...input.sendId ? { sendId: input.sendId } : {}
|
|
7872
|
+
...input.sendId ? { sendId: input.sendId } : {},
|
|
7873
|
+
...input.observedRevision !== void 0 ? { observedRevision: input.observedRevision } : {}
|
|
7581
7874
|
});
|
|
7582
7875
|
return result ? safe(result) : null;
|
|
7583
7876
|
},
|
|
@@ -7605,6 +7898,8 @@ function safeJourneySender(sender, now) {
|
|
|
7605
7898
|
async advance(input, signal = new AbortController().signal) {
|
|
7606
7899
|
return safe(await sender.advance(input, signal));
|
|
7607
7900
|
},
|
|
7901
|
+
uploadAssessments: (input) => sender.uploadAssessments(input),
|
|
7902
|
+
ownedPassport: (input, signal = new AbortController().signal) => sender.ownedPassport(input, signal),
|
|
7608
7903
|
ownerProfile: (signal = new AbortController().signal) => sender.ownerProfile(signal),
|
|
7609
7904
|
addressTaken: (input, signal = new AbortController().signal) => sender.addressTaken(input, signal),
|
|
7610
7905
|
saveSentProfile: (input, signal = new AbortController().signal) => sender.saveSentProfile(input, signal)
|
|
@@ -14050,4 +14345,4 @@ function equivalenceDuplicates(batch) {
|
|
|
14050
14345
|
return bindings.sort();
|
|
14051
14346
|
}
|
|
14052
14347
|
//#endregion
|
|
14053
|
-
export { validateDetectionSummary as $,
|
|
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 };
|