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