trackly-cli 0.17.0 → 0.17.1
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/bin/trackly +2 -2
- package/contracts/trackly-apply-contract-history.json +9 -0
- package/contracts/trackly-apply-tools.json +16 -3
- package/lib/agent.js +1 -1
- package/lib/formatters.js +106 -2
- package/mcp/apply-tools.js +94 -22
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/trackly-apply/SKILL.md +6 -4
- package/skills/trackly-apply/references/answer-resolution.md +25 -0
- package/skills/trackly-apply/references/browser-upload.md +26 -8
- package/skills/trackly-apply/references/form-integrity.md +22 -0
- package/skills/trackly-apply/references/operational-checkpoints.md +141 -10
- package/skills/trackly-apply/references/review-handoff.md +21 -0
- package/skills/trackly-apply/scripts/validate-phase-checkpoint.js +670 -23
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { createHash } = require('node:crypto');
|
|
4
5
|
const { StringDecoder } = require('node:string_decoder');
|
|
5
6
|
|
|
6
7
|
const ACCESS_STATES = new Set([
|
|
@@ -24,6 +25,79 @@ const TAB_STATES = new Set(['open', 'missing', 'closure_unverified', 'closed_ver
|
|
|
24
25
|
const MAX_RECEIPT_BYTES = 64 * 1024;
|
|
25
26
|
const WORK_MODES = new Set(['accessible_execution', 'fixed_inspection']);
|
|
26
27
|
const COMMON_LINEAGE_FIELDS = ['workMode', 'batchId', 'memberId', 'jobId', 'runId', 'inspectionEpoch'];
|
|
28
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
29
|
+
const COMMITTED_CONTROL_FIELDS = [
|
|
30
|
+
'filledExactProfile',
|
|
31
|
+
'filledSafeDerivation',
|
|
32
|
+
'filledSupportedDraft',
|
|
33
|
+
'preservedUserEdit',
|
|
34
|
+
];
|
|
35
|
+
const EXCEPTION_CONTROL_FIELDS = [
|
|
36
|
+
'missingFact',
|
|
37
|
+
'liveConsent',
|
|
38
|
+
'authenticationBlocker',
|
|
39
|
+
'unobservableCommit',
|
|
40
|
+
'unsupportedControl',
|
|
41
|
+
'notApplicable',
|
|
42
|
+
];
|
|
43
|
+
const CONTROL_ACCOUNTING_FIELDS = [
|
|
44
|
+
...COMMITTED_CONTROL_FIELDS,
|
|
45
|
+
...EXCEPTION_CONTROL_FIELDS,
|
|
46
|
+
];
|
|
47
|
+
const ANSWER_SCOPE_FIELDS = ['run', 'question', 'office', 'jurisdiction', 'company', 'provider', 'global'];
|
|
48
|
+
const HISTORY_FIELDS = [
|
|
49
|
+
'canonicalEducationRecordCount',
|
|
50
|
+
'accountedEducationRecordCount',
|
|
51
|
+
'canonicalEmploymentPositionCount',
|
|
52
|
+
'accountedEmploymentPositionCount',
|
|
53
|
+
'educationOrderVerified',
|
|
54
|
+
'employmentOrderVerified',
|
|
55
|
+
'datePrecisionInvented',
|
|
56
|
+
'educationReconciliationFingerprint',
|
|
57
|
+
'employmentReconciliationFingerprint',
|
|
58
|
+
];
|
|
59
|
+
const RESUME_AUDIT_FIELDS = [
|
|
60
|
+
'control',
|
|
61
|
+
'mode',
|
|
62
|
+
'approval',
|
|
63
|
+
'preAttachVerification',
|
|
64
|
+
'attachmentCommit',
|
|
65
|
+
'filenameVerification',
|
|
66
|
+
'parserRecheck',
|
|
67
|
+
'finalSweep',
|
|
68
|
+
];
|
|
69
|
+
const EMPLOYER_APPLICATION_STATES = new Set([
|
|
70
|
+
'not_opened',
|
|
71
|
+
'access_probed',
|
|
72
|
+
'form_reached',
|
|
73
|
+
'partially_filled',
|
|
74
|
+
'needs_answers',
|
|
75
|
+
'review_state_prepared',
|
|
76
|
+
'manually_submitted',
|
|
77
|
+
'ats_success_observed',
|
|
78
|
+
'blocked',
|
|
79
|
+
]);
|
|
80
|
+
const TRACKLY_MEMBER_STATES = new Set([
|
|
81
|
+
'reserved',
|
|
82
|
+
'inspecting',
|
|
83
|
+
'needs_input',
|
|
84
|
+
'review_ready',
|
|
85
|
+
'awaiting_manual_submit',
|
|
86
|
+
'submitted',
|
|
87
|
+
'blocked',
|
|
88
|
+
'parked',
|
|
89
|
+
'stopped',
|
|
90
|
+
]);
|
|
91
|
+
const TRACKLY_JOB_STATES = new Set(['new', 'check_later', 'not_interested', 'applied_confirmed', 'unknown']);
|
|
92
|
+
const BROWSER_HANDOFF_STATES = new Set([
|
|
93
|
+
'no_known_tab',
|
|
94
|
+
'controller_owned',
|
|
95
|
+
'user_inventory',
|
|
96
|
+
'visible',
|
|
97
|
+
'durable_handoff_proven',
|
|
98
|
+
'missing',
|
|
99
|
+
'closure_unverified',
|
|
100
|
+
]);
|
|
27
101
|
|
|
28
102
|
const PHASE_FIELDS = {
|
|
29
103
|
selection: new Set([
|
|
@@ -59,13 +133,21 @@ const PHASE_FIELDS = {
|
|
|
59
133
|
'jobId',
|
|
60
134
|
'runId',
|
|
61
135
|
'inspectionEpoch',
|
|
136
|
+
'profileRevision',
|
|
62
137
|
'visibleControlCount',
|
|
63
138
|
'committedControlCount',
|
|
64
139
|
'typedExceptionCount',
|
|
140
|
+
'controlAccounting',
|
|
141
|
+
'formInventoryFingerprint',
|
|
65
142
|
'knownOmissionCount',
|
|
66
143
|
'knownFieldsFilledBeforeQuestions',
|
|
144
|
+
'answerLookupCompleted',
|
|
145
|
+
'answerLookupScopeCounts',
|
|
146
|
+
'answerLookupFingerprint',
|
|
67
147
|
'parserSensitiveFieldsRechecked',
|
|
68
148
|
'educationAndEmploymentVerified',
|
|
149
|
+
'historyReconciliation',
|
|
150
|
+
'resumeAudit',
|
|
69
151
|
'writingPresent',
|
|
70
152
|
'localWritingGate',
|
|
71
153
|
'humanizerAvailability',
|
|
@@ -81,11 +163,56 @@ const PHASE_FIELDS = {
|
|
|
81
163
|
'jobId',
|
|
82
164
|
'runId',
|
|
83
165
|
'inspectionEpoch',
|
|
166
|
+
'profileRevision',
|
|
167
|
+
'formInventoryFingerprint',
|
|
84
168
|
'finalIntegrityPassed',
|
|
85
169
|
'truthConfirmationRecorded',
|
|
170
|
+
'truthCertificationAttestationId',
|
|
171
|
+
'truthCertificationDependencyHash',
|
|
172
|
+
'truthCertificationExpiresAt',
|
|
173
|
+
'truthCertificationMemberRuns',
|
|
174
|
+
'truthCertificationRunSetHash',
|
|
175
|
+
'truthCertificationStatus',
|
|
86
176
|
'submitActivated',
|
|
87
177
|
'reviewTabPreserved',
|
|
88
178
|
'userVisibleHandoffProven',
|
|
179
|
+
'browserBindingHash',
|
|
180
|
+
'handoffEvidenceFingerprint',
|
|
181
|
+
'handoffEvidenceType',
|
|
182
|
+
'checkpointAction',
|
|
183
|
+
'continuationAllowed',
|
|
184
|
+
'resolvedActionCount',
|
|
185
|
+
'resolvedActionIdsFingerprint',
|
|
186
|
+
'checkpointStatus',
|
|
187
|
+
'checkpointMemberVersion',
|
|
188
|
+
'checkpointInspectionEpoch',
|
|
189
|
+
'checkpointLifecycle',
|
|
190
|
+
'checkpointActionCount',
|
|
191
|
+
'checkpointActionIdsFingerprint',
|
|
192
|
+
]),
|
|
193
|
+
handoff: new Set([
|
|
194
|
+
'workMode',
|
|
195
|
+
'executionId',
|
|
196
|
+
'batchId',
|
|
197
|
+
'memberId',
|
|
198
|
+
'jobId',
|
|
199
|
+
'runId',
|
|
200
|
+
'inspectionEpoch',
|
|
201
|
+
'employerApplicationState',
|
|
202
|
+
'tracklyMemberState',
|
|
203
|
+
'tracklyJobState',
|
|
204
|
+
'browserTabState',
|
|
205
|
+
'handoffVisibility',
|
|
206
|
+
'reviewReadyClaimed',
|
|
207
|
+
'browserBindingHash',
|
|
208
|
+
'handoffEvidenceFingerprint',
|
|
209
|
+
'handoffEvidenceType',
|
|
210
|
+
'checkpointStatus',
|
|
211
|
+
'checkpointMemberVersion',
|
|
212
|
+
'checkpointInspectionEpoch',
|
|
213
|
+
'checkpointLifecycle',
|
|
214
|
+
'checkpointAction',
|
|
215
|
+
'checkpointActionCount',
|
|
89
216
|
]),
|
|
90
217
|
reconciliation: new Set([
|
|
91
218
|
'workMode',
|
|
@@ -126,7 +253,108 @@ function requireCount(errors, receipt, field) {
|
|
|
126
253
|
}
|
|
127
254
|
}
|
|
128
255
|
|
|
129
|
-
function
|
|
256
|
+
function requireFingerprint(errors, value, field) {
|
|
257
|
+
if (typeof value !== 'string' || !SHA256_PATTERN.test(value)) {
|
|
258
|
+
errors.push(`${field} must be a lowercase SHA-256 fingerprint`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function requirePositiveDecimalId(errors, value, field) {
|
|
263
|
+
if (typeof value !== 'string' || !/^[1-9][0-9]*$/.test(value)) {
|
|
264
|
+
errors.push(`${field} must be a positive decimal identifier`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function requireFutureIsoDateTime(errors, value, field, validationTimeMs) {
|
|
269
|
+
const canonicalPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
270
|
+
const parsedTime = typeof value === 'string' && canonicalPattern.test(value)
|
|
271
|
+
? Date.parse(value)
|
|
272
|
+
: Number.NaN;
|
|
273
|
+
if (Number.isNaN(parsedTime) || new Date(parsedTime).toISOString() !== value) {
|
|
274
|
+
errors.push(`${field} must be an ISO-8601 date-time`);
|
|
275
|
+
} else if (parsedTime <= validationTimeMs) {
|
|
276
|
+
errors.push(`${field} must be later than validation time`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function canonicalJson(value) {
|
|
281
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number') {
|
|
282
|
+
return JSON.stringify(value);
|
|
283
|
+
}
|
|
284
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
285
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
286
|
+
return `{${Object.entries(value)
|
|
287
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
288
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
|
|
289
|
+
.join(',')}}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function validateTruthCertificationMemberRuns(errors, memberRuns, receipt, side) {
|
|
293
|
+
const push = (message) => errors.push(`${side}: ${message}`);
|
|
294
|
+
if (!Array.isArray(memberRuns) || memberRuns.length < 1 || memberRuns.length > 100) {
|
|
295
|
+
push('truthCertificationMemberRuns must contain 1 to 100 member bindings');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const allowedFields = ['memberId', 'runId', 'memberVersion', 'inspectionEpoch'];
|
|
299
|
+
const normalized = [];
|
|
300
|
+
for (const member of memberRuns) {
|
|
301
|
+
if (!member || typeof member !== 'object' || Array.isArray(member)
|
|
302
|
+
|| Object.keys(member).length !== allowedFields.length
|
|
303
|
+
|| Object.keys(member).some((field) => !allowedFields.includes(field))) {
|
|
304
|
+
push('truthCertificationMemberRuns entries must contain only memberId, runId, memberVersion, and inspectionEpoch');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (!Number.isSafeInteger(member.memberId) || member.memberId < 1
|
|
308
|
+
|| !Number.isSafeInteger(member.runId) || member.runId < 1
|
|
309
|
+
|| !Number.isSafeInteger(member.memberVersion) || member.memberVersion < 1
|
|
310
|
+
|| !Number.isSafeInteger(member.inspectionEpoch) || member.inspectionEpoch < 0) {
|
|
311
|
+
push('truthCertificationMemberRuns entries contain an invalid identifier, version, or epoch');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
normalized.push({
|
|
315
|
+
memberId: member.memberId,
|
|
316
|
+
runId: member.runId,
|
|
317
|
+
memberVersion: member.memberVersion,
|
|
318
|
+
inspectionEpoch: member.inspectionEpoch,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
normalized.sort((left, right) => left.memberId - right.memberId || left.runId - right.runId);
|
|
322
|
+
if (new Set(normalized.map((member) => member.memberId)).size !== normalized.length) {
|
|
323
|
+
push('truthCertificationMemberRuns memberId values must be unique');
|
|
324
|
+
}
|
|
325
|
+
const runSetHash = createHash('sha256').update(canonicalJson(normalized)).digest('hex');
|
|
326
|
+
if (runSetHash !== receipt.truthCertificationRunSetHash) {
|
|
327
|
+
push('truthCertificationMemberRuns must match truthCertificationRunSetHash');
|
|
328
|
+
}
|
|
329
|
+
const currentMemberMatches = normalized.filter((member) => (
|
|
330
|
+
member.memberId === receipt.memberId
|
|
331
|
+
&& member.runId === receipt.runId
|
|
332
|
+
&& member.memberVersion === receipt.checkpointMemberVersion
|
|
333
|
+
&& member.inspectionEpoch === receipt.inspectionEpoch
|
|
334
|
+
));
|
|
335
|
+
if (currentMemberMatches.length !== 1) {
|
|
336
|
+
push('truth certification must cover the current member, run, checkpoint version, and inspection epoch');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function requireExactObject(errors, value, field, allowedFields) {
|
|
341
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
342
|
+
errors.push(`${field} must be an object`);
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
for (const key of Object.keys(value)) {
|
|
346
|
+
if (!allowedFields.includes(key)) errors.push(`${field} contains an unexpected field`);
|
|
347
|
+
}
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function validateCurrentLineage(
|
|
352
|
+
errors,
|
|
353
|
+
receipt,
|
|
354
|
+
expectedContext,
|
|
355
|
+
jobSetField = 'approvedJobIds',
|
|
356
|
+
additionalExpectedFields = [],
|
|
357
|
+
) {
|
|
130
358
|
if (!WORK_MODES.has(receipt.workMode)) errors.push('workMode must be accessible_execution or fixed_inspection');
|
|
131
359
|
for (const field of ['batchId', 'memberId', 'jobId', 'runId']) requireTracklyId(errors, receipt, field);
|
|
132
360
|
if (!Number.isSafeInteger(receipt.inspectionEpoch) || receipt.inspectionEpoch < 0) {
|
|
@@ -141,10 +369,10 @@ function validateCurrentLineage(errors, receipt, expectedContext, jobSetField =
|
|
|
141
369
|
errors.push('expectedContext is required');
|
|
142
370
|
return;
|
|
143
371
|
}
|
|
144
|
-
const expectedFields = new Set([...COMMON_LINEAGE_FIELDS, jobSetField]);
|
|
372
|
+
const expectedFields = new Set([...COMMON_LINEAGE_FIELDS, jobSetField, ...additionalExpectedFields]);
|
|
145
373
|
if (receipt.workMode === 'accessible_execution') expectedFields.add('executionId');
|
|
146
374
|
for (const field of Object.keys(expectedContext)) {
|
|
147
|
-
if (!expectedFields.has(field)) errors.push(
|
|
375
|
+
if (!expectedFields.has(field)) errors.push('expectedContext contains an unexpected field');
|
|
148
376
|
}
|
|
149
377
|
for (const field of COMMON_LINEAGE_FIELDS) {
|
|
150
378
|
if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
|
|
@@ -179,7 +407,7 @@ function validateSelection(receipt, expectedContext) {
|
|
|
179
407
|
if (!Number.isInteger(receipt.latestExplicitTarget)
|
|
180
408
|
|| receipt.latestExplicitTarget < 1
|
|
181
409
|
|| receipt.latestExplicitTarget > maximumTarget) {
|
|
182
|
-
errors.push(`latestExplicitTarget must be an integer from 1 to ${maximumTarget} for
|
|
410
|
+
errors.push(`latestExplicitTarget must be an integer from 1 to ${maximumTarget} for the selected work mode`);
|
|
183
411
|
}
|
|
184
412
|
if (!Array.isArray(receipt.approvedJobIds)) {
|
|
185
413
|
errors.push('approvedJobIds must be an array');
|
|
@@ -205,7 +433,7 @@ function validateSelection(receipt, expectedContext) {
|
|
|
205
433
|
const expectedFields = new Set(['workMode', 'batchId', 'latestExplicitTarget', 'selectableJobIds', 'queueExhausted']);
|
|
206
434
|
if (receipt.workMode === 'accessible_execution') expectedFields.add('executionId');
|
|
207
435
|
for (const field of Object.keys(expectedContext)) {
|
|
208
|
-
if (!expectedFields.has(field)) errors.push(
|
|
436
|
+
if (!expectedFields.has(field)) errors.push('expectedContext contains an unexpected field');
|
|
209
437
|
}
|
|
210
438
|
if (receipt.workMode !== expectedContext.workMode) errors.push('workMode must match expectedContext');
|
|
211
439
|
if (receipt.batchId !== expectedContext.batchId) errors.push('batchId must match expectedContext');
|
|
@@ -251,20 +479,131 @@ function validateAccess(receipt, expectedContext) {
|
|
|
251
479
|
|
|
252
480
|
function validateFill(receipt, expectedContext) {
|
|
253
481
|
const errors = [];
|
|
254
|
-
validateCurrentLineage(errors, receipt, expectedContext
|
|
482
|
+
validateCurrentLineage(errors, receipt, expectedContext, 'approvedJobIds', [
|
|
483
|
+
'profileRevision',
|
|
484
|
+
'canonicalEducationRecordCount',
|
|
485
|
+
'canonicalEmploymentPositionCount',
|
|
486
|
+
'formInventoryFingerprint',
|
|
487
|
+
'resumeControl',
|
|
488
|
+
]);
|
|
489
|
+
if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
|
|
490
|
+
if (receipt.workMode === 'accessible_execution') {
|
|
491
|
+
requireTracklyId(errors, receipt, 'profileRevision');
|
|
492
|
+
requireTracklyId(errors, expectedContext, 'profileRevision');
|
|
493
|
+
} else {
|
|
494
|
+
requireCount(errors, receipt, 'profileRevision');
|
|
495
|
+
requireCount(errors, expectedContext, 'profileRevision');
|
|
496
|
+
}
|
|
497
|
+
if (receipt.profileRevision !== expectedContext.profileRevision) {
|
|
498
|
+
errors.push('profileRevision must match expectedContext');
|
|
499
|
+
}
|
|
500
|
+
requireCount(errors, expectedContext, 'canonicalEducationRecordCount');
|
|
501
|
+
requireCount(errors, expectedContext, 'canonicalEmploymentPositionCount');
|
|
502
|
+
requireFingerprint(errors, expectedContext.formInventoryFingerprint, 'expectedContext.formInventoryFingerprint');
|
|
503
|
+
if (!['required', 'optional', 'absent'].includes(expectedContext.resumeControl)) {
|
|
504
|
+
errors.push('expectedContext.resumeControl must be required, optional, or absent');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
255
507
|
for (const field of ['visibleControlCount', 'committedControlCount', 'typedExceptionCount', 'knownOmissionCount']) {
|
|
256
508
|
requireCount(errors, receipt, field);
|
|
257
509
|
}
|
|
510
|
+
if (receipt.visibleControlCount === 0) errors.push('visibleControlCount must be at least 1 for a fill checkpoint');
|
|
258
511
|
if (Number.isSafeInteger(receipt.visibleControlCount)
|
|
259
512
|
&& Number.isSafeInteger(receipt.committedControlCount)
|
|
260
513
|
&& Number.isSafeInteger(receipt.typedExceptionCount)
|
|
261
514
|
&& receipt.committedControlCount + receipt.typedExceptionCount !== receipt.visibleControlCount) {
|
|
262
515
|
errors.push('committedControlCount plus typedExceptionCount must equal visibleControlCount');
|
|
263
516
|
}
|
|
517
|
+
if (requireExactObject(errors, receipt.controlAccounting, 'controlAccounting', CONTROL_ACCOUNTING_FIELDS)) {
|
|
518
|
+
for (const field of CONTROL_ACCOUNTING_FIELDS) requireCount(errors, receipt.controlAccounting, field);
|
|
519
|
+
const accountingCount = CONTROL_ACCOUNTING_FIELDS
|
|
520
|
+
.reduce((total, field) => total + (Number.isSafeInteger(receipt.controlAccounting[field]) ? receipt.controlAccounting[field] : 0), 0);
|
|
521
|
+
const committedCount = COMMITTED_CONTROL_FIELDS
|
|
522
|
+
.reduce((total, field) => total + (Number.isSafeInteger(receipt.controlAccounting[field]) ? receipt.controlAccounting[field] : 0), 0);
|
|
523
|
+
const exceptionCount = EXCEPTION_CONTROL_FIELDS
|
|
524
|
+
.reduce((total, field) => total + (Number.isSafeInteger(receipt.controlAccounting[field]) ? receipt.controlAccounting[field] : 0), 0);
|
|
525
|
+
if (Number.isSafeInteger(receipt.visibleControlCount) && accountingCount !== receipt.visibleControlCount) {
|
|
526
|
+
errors.push('controlAccounting counts must equal visibleControlCount');
|
|
527
|
+
}
|
|
528
|
+
if (Number.isSafeInteger(receipt.committedControlCount) && committedCount !== receipt.committedControlCount) {
|
|
529
|
+
errors.push('committed control accounting must equal committedControlCount');
|
|
530
|
+
}
|
|
531
|
+
if (Number.isSafeInteger(receipt.typedExceptionCount) && exceptionCount !== receipt.typedExceptionCount) {
|
|
532
|
+
errors.push('typed exception accounting must equal typedExceptionCount');
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
requireFingerprint(errors, receipt.formInventoryFingerprint, 'formInventoryFingerprint');
|
|
536
|
+
if (receipt.formInventoryFingerprint !== expectedContext?.formInventoryFingerprint) {
|
|
537
|
+
errors.push('formInventoryFingerprint must match expectedContext');
|
|
538
|
+
}
|
|
264
539
|
if (receipt.knownOmissionCount !== 0) errors.push('knownOmissionCount must be 0');
|
|
265
540
|
requireTrue(errors, receipt, 'knownFieldsFilledBeforeQuestions');
|
|
541
|
+
requireTrue(errors, receipt, 'answerLookupCompleted');
|
|
542
|
+
if (requireExactObject(errors, receipt.answerLookupScopeCounts, 'answerLookupScopeCounts', ANSWER_SCOPE_FIELDS)) {
|
|
543
|
+
for (const field of ANSWER_SCOPE_FIELDS) requireCount(errors, receipt.answerLookupScopeCounts, field);
|
|
544
|
+
}
|
|
545
|
+
requireFingerprint(errors, receipt.answerLookupFingerprint, 'answerLookupFingerprint');
|
|
266
546
|
requireTrue(errors, receipt, 'parserSensitiveFieldsRechecked');
|
|
267
547
|
requireTrue(errors, receipt, 'educationAndEmploymentVerified');
|
|
548
|
+
if (requireExactObject(errors, receipt.historyReconciliation, 'historyReconciliation', HISTORY_FIELDS)) {
|
|
549
|
+
for (const field of [
|
|
550
|
+
'canonicalEducationRecordCount',
|
|
551
|
+
'accountedEducationRecordCount',
|
|
552
|
+
'canonicalEmploymentPositionCount',
|
|
553
|
+
'accountedEmploymentPositionCount',
|
|
554
|
+
]) requireCount(errors, receipt.historyReconciliation, field);
|
|
555
|
+
if (receipt.historyReconciliation.canonicalEducationRecordCount
|
|
556
|
+
!== receipt.historyReconciliation.accountedEducationRecordCount) {
|
|
557
|
+
errors.push('education record counts must match');
|
|
558
|
+
}
|
|
559
|
+
if (receipt.historyReconciliation.canonicalEmploymentPositionCount
|
|
560
|
+
!== receipt.historyReconciliation.accountedEmploymentPositionCount) {
|
|
561
|
+
errors.push('employment position counts must match');
|
|
562
|
+
}
|
|
563
|
+
if (receipt.historyReconciliation.canonicalEducationRecordCount
|
|
564
|
+
!== expectedContext?.canonicalEducationRecordCount) {
|
|
565
|
+
errors.push('canonical education record count must match expectedContext');
|
|
566
|
+
}
|
|
567
|
+
if (receipt.historyReconciliation.canonicalEmploymentPositionCount
|
|
568
|
+
!== expectedContext?.canonicalEmploymentPositionCount) {
|
|
569
|
+
errors.push('canonical employment position count must match expectedContext');
|
|
570
|
+
}
|
|
571
|
+
requireTrue(errors, receipt.historyReconciliation, 'educationOrderVerified');
|
|
572
|
+
requireTrue(errors, receipt.historyReconciliation, 'employmentOrderVerified');
|
|
573
|
+
requireFalse(errors, receipt.historyReconciliation, 'datePrecisionInvented');
|
|
574
|
+
requireFingerprint(errors, receipt.historyReconciliation.educationReconciliationFingerprint, 'historyReconciliation.educationReconciliationFingerprint');
|
|
575
|
+
requireFingerprint(errors, receipt.historyReconciliation.employmentReconciliationFingerprint, 'historyReconciliation.employmentReconciliationFingerprint');
|
|
576
|
+
}
|
|
577
|
+
if (requireExactObject(errors, receipt.resumeAudit, 'resumeAudit', RESUME_AUDIT_FIELDS)) {
|
|
578
|
+
if (!['required', 'optional', 'absent'].includes(receipt.resumeAudit.control)) {
|
|
579
|
+
errors.push('resumeAudit.control must be required, optional, or absent');
|
|
580
|
+
}
|
|
581
|
+
if (receipt.resumeAudit.control !== expectedContext?.resumeControl) {
|
|
582
|
+
errors.push('resumeAudit.control must match expectedContext');
|
|
583
|
+
}
|
|
584
|
+
const attachmentExpected = ['required', 'optional'].includes(receipt.resumeAudit.control);
|
|
585
|
+
const allowedModes = attachmentExpected
|
|
586
|
+
? ['automated_verified', 'manual_unbound']
|
|
587
|
+
: ['not_applicable'];
|
|
588
|
+
if (!allowedModes.includes(receipt.resumeAudit.mode)) {
|
|
589
|
+
errors.push('resumeAudit.mode must match the declared control mode');
|
|
590
|
+
}
|
|
591
|
+
const expectedOutcomes = attachmentExpected
|
|
592
|
+
? {
|
|
593
|
+
approval: 'passed',
|
|
594
|
+
preAttachVerification: receipt.resumeAudit.mode === 'manual_unbound' ? 'not_applicable' : 'passed',
|
|
595
|
+
attachmentCommit: receipt.resumeAudit.mode === 'manual_unbound' ? 'user_confirmed' : 'passed',
|
|
596
|
+
filenameVerification: 'passed',
|
|
597
|
+
parserRecheck: 'passed',
|
|
598
|
+
finalSweep: 'passed',
|
|
599
|
+
}
|
|
600
|
+
: Object.fromEntries(RESUME_AUDIT_FIELDS.slice(2).map((field) => [field, 'not_applicable']));
|
|
601
|
+
for (const [field, expected] of Object.entries(expectedOutcomes)) {
|
|
602
|
+
if (receipt.resumeAudit[field] !== expected) {
|
|
603
|
+
errors.push(`resumeAudit.${field} must match the declared control mode`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
268
607
|
if (typeof receipt.writingPresent !== 'boolean') errors.push('writingPresent must be a boolean');
|
|
269
608
|
if (!['passed', 'not_applicable'].includes(receipt.localWritingGate)) {
|
|
270
609
|
errors.push('localWritingGate must be passed or not_applicable');
|
|
@@ -295,23 +634,330 @@ function validateFill(receipt, expectedContext) {
|
|
|
295
634
|
return errors;
|
|
296
635
|
}
|
|
297
636
|
|
|
298
|
-
function validateReview(receipt, expectedContext) {
|
|
637
|
+
function validateReview(receipt, expectedContext, priorFill, validationTimeMs) {
|
|
299
638
|
const errors = [];
|
|
300
|
-
|
|
639
|
+
const fillBaselineFields = [
|
|
640
|
+
'profileRevision',
|
|
641
|
+
'formInventoryFingerprint',
|
|
642
|
+
];
|
|
643
|
+
const resolutionFields = [
|
|
644
|
+
'resolvedActionCount',
|
|
645
|
+
'resolvedActionIdsFingerprint',
|
|
646
|
+
];
|
|
647
|
+
const checkpointFields = [
|
|
648
|
+
'checkpointAction',
|
|
649
|
+
'continuationAllowed',
|
|
650
|
+
'checkpointStatus',
|
|
651
|
+
'checkpointMemberVersion',
|
|
652
|
+
'checkpointInspectionEpoch',
|
|
653
|
+
'checkpointLifecycle',
|
|
654
|
+
'checkpointActionCount',
|
|
655
|
+
'checkpointActionIdsFingerprint',
|
|
656
|
+
];
|
|
657
|
+
const truthCertificationFields = [
|
|
658
|
+
'truthCertificationAttestationId',
|
|
659
|
+
'truthCertificationDependencyHash',
|
|
660
|
+
'truthCertificationExpiresAt',
|
|
661
|
+
'truthCertificationMemberRuns',
|
|
662
|
+
'truthCertificationRunSetHash',
|
|
663
|
+
'truthCertificationStatus',
|
|
664
|
+
];
|
|
665
|
+
const visibilityFields = [
|
|
666
|
+
'browserBindingHash',
|
|
667
|
+
'handoffEvidenceFingerprint',
|
|
668
|
+
'handoffEvidenceType',
|
|
669
|
+
];
|
|
670
|
+
validateCurrentLineage(
|
|
671
|
+
errors,
|
|
672
|
+
receipt,
|
|
673
|
+
expectedContext,
|
|
674
|
+
'approvedJobIds',
|
|
675
|
+
[
|
|
676
|
+
...fillBaselineFields,
|
|
677
|
+
...resolutionFields,
|
|
678
|
+
...checkpointFields,
|
|
679
|
+
...truthCertificationFields,
|
|
680
|
+
...visibilityFields,
|
|
681
|
+
],
|
|
682
|
+
);
|
|
683
|
+
if (receipt.workMode === 'accessible_execution') {
|
|
684
|
+
requireTracklyId(errors, receipt, 'profileRevision');
|
|
685
|
+
} else {
|
|
686
|
+
requireCount(errors, receipt, 'profileRevision');
|
|
687
|
+
}
|
|
688
|
+
requireFingerprint(errors, receipt.formInventoryFingerprint, 'formInventoryFingerprint');
|
|
689
|
+
if (!priorFill || typeof priorFill !== 'object' || Array.isArray(priorFill)) {
|
|
690
|
+
errors.push('priorFill is required for review');
|
|
691
|
+
} else {
|
|
692
|
+
const priorFillFields = Object.keys(priorFill);
|
|
693
|
+
if (priorFillFields.length !== 2
|
|
694
|
+
|| !priorFillFields.includes('receipt')
|
|
695
|
+
|| !priorFillFields.includes('expectedContext')) {
|
|
696
|
+
errors.push('priorFill must contain only receipt and expectedContext');
|
|
697
|
+
} else {
|
|
698
|
+
const priorFillErrors = validateCheckpoint(
|
|
699
|
+
'fill',
|
|
700
|
+
priorFill.receipt,
|
|
701
|
+
priorFill.expectedContext,
|
|
702
|
+
);
|
|
703
|
+
errors.push(...priorFillErrors.map((error) => `priorFill: ${error}`));
|
|
704
|
+
if (priorFill.receipt && typeof priorFill.receipt === 'object' && !Array.isArray(priorFill.receipt)) {
|
|
705
|
+
for (const field of [
|
|
706
|
+
'missingFact',
|
|
707
|
+
'liveConsent',
|
|
708
|
+
'authenticationBlocker',
|
|
709
|
+
'unobservableCommit',
|
|
710
|
+
'unsupportedControl',
|
|
711
|
+
]) {
|
|
712
|
+
if (priorFill.receipt.controlAccounting?.[field] !== 0) {
|
|
713
|
+
errors.push(`priorFill.controlAccounting.${field} must be 0 before review`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const lineageFields = [...COMMON_LINEAGE_FIELDS];
|
|
717
|
+
if (receipt.workMode === 'accessible_execution') lineageFields.push('executionId');
|
|
718
|
+
for (const field of [...lineageFields, ...fillBaselineFields]) {
|
|
719
|
+
if (receipt[field] !== priorFill.receipt[field]) {
|
|
720
|
+
errors.push(`${field} must match the validated priorFill receipt`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
301
726
|
requireTrue(errors, receipt, 'finalIntegrityPassed');
|
|
302
727
|
requireTrue(errors, receipt, 'truthConfirmationRecorded');
|
|
728
|
+
requirePositiveDecimalId(
|
|
729
|
+
errors,
|
|
730
|
+
receipt.truthCertificationAttestationId,
|
|
731
|
+
'truthCertificationAttestationId',
|
|
732
|
+
);
|
|
733
|
+
requireFingerprint(
|
|
734
|
+
errors,
|
|
735
|
+
receipt.truthCertificationDependencyHash,
|
|
736
|
+
'truthCertificationDependencyHash',
|
|
737
|
+
);
|
|
738
|
+
requireFutureIsoDateTime(
|
|
739
|
+
errors,
|
|
740
|
+
receipt.truthCertificationExpiresAt,
|
|
741
|
+
'truthCertificationExpiresAt',
|
|
742
|
+
validationTimeMs,
|
|
743
|
+
);
|
|
744
|
+
requireFingerprint(
|
|
745
|
+
errors,
|
|
746
|
+
receipt.truthCertificationRunSetHash,
|
|
747
|
+
'truthCertificationRunSetHash',
|
|
748
|
+
);
|
|
749
|
+
validateTruthCertificationMemberRuns(errors, receipt.truthCertificationMemberRuns, receipt, 'receipt');
|
|
750
|
+
validateTruthCertificationMemberRuns(
|
|
751
|
+
errors,
|
|
752
|
+
expectedContext?.truthCertificationMemberRuns,
|
|
753
|
+
expectedContext || {},
|
|
754
|
+
'expectedContext',
|
|
755
|
+
);
|
|
756
|
+
if (Array.isArray(receipt.truthCertificationMemberRuns)
|
|
757
|
+
&& Array.isArray(expectedContext?.truthCertificationMemberRuns)
|
|
758
|
+
&& canonicalJson(receipt.truthCertificationMemberRuns)
|
|
759
|
+
!== canonicalJson(expectedContext.truthCertificationMemberRuns)) {
|
|
760
|
+
errors.push('truthCertificationMemberRuns must match the authoritative expectedContext');
|
|
761
|
+
}
|
|
762
|
+
if (!['recorded', 'replayed'].includes(receipt.truthCertificationStatus)) {
|
|
763
|
+
errors.push('truthCertificationStatus must be recorded or replayed');
|
|
764
|
+
}
|
|
303
765
|
requireFalse(errors, receipt, 'submitActivated');
|
|
304
766
|
requireTrue(errors, receipt, 'reviewTabPreserved');
|
|
305
767
|
requireTrue(errors, receipt, 'userVisibleHandoffProven');
|
|
768
|
+
requireFingerprint(errors, receipt.browserBindingHash, 'browserBindingHash');
|
|
769
|
+
requireFingerprint(errors, receipt.handoffEvidenceFingerprint, 'handoffEvidenceFingerprint');
|
|
770
|
+
if (!['visible_presentation_receipt', 'user_visible_handoff_receipt'].includes(receipt.handoffEvidenceType)) {
|
|
771
|
+
errors.push('handoffEvidenceType must identify visible presentation or an exact tab-bound user-visible handoff receipt');
|
|
772
|
+
}
|
|
773
|
+
if (receipt.checkpointAction !== 'review/manual_submit') {
|
|
774
|
+
errors.push('checkpointAction must be review/manual_submit');
|
|
775
|
+
}
|
|
776
|
+
if (receipt.continuationAllowed !== false) {
|
|
777
|
+
errors.push('continuationAllowed must be false for review/manual_submit');
|
|
778
|
+
}
|
|
779
|
+
requireCount(errors, receipt, 'resolvedActionCount');
|
|
780
|
+
requireFingerprint(errors, receipt.resolvedActionIdsFingerprint, 'resolvedActionIdsFingerprint');
|
|
781
|
+
if (!['recorded', 'replayed'].includes(receipt.checkpointStatus)) {
|
|
782
|
+
errors.push('checkpointStatus must be recorded or replayed');
|
|
783
|
+
}
|
|
784
|
+
requireTracklyId(errors, receipt, 'checkpointMemberVersion');
|
|
785
|
+
if (!Number.isSafeInteger(receipt.checkpointInspectionEpoch) || receipt.checkpointInspectionEpoch < 0) {
|
|
786
|
+
errors.push('checkpointInspectionEpoch must be a non-negative safe integer');
|
|
787
|
+
}
|
|
788
|
+
if (receipt.checkpointInspectionEpoch !== receipt.inspectionEpoch) {
|
|
789
|
+
errors.push('checkpointInspectionEpoch must equal inspectionEpoch');
|
|
790
|
+
}
|
|
791
|
+
if (receipt.checkpointLifecycle !== 'review_ready') {
|
|
792
|
+
errors.push('checkpointLifecycle must be review_ready');
|
|
793
|
+
}
|
|
794
|
+
requireCount(errors, receipt, 'checkpointActionCount');
|
|
795
|
+
if (receipt.checkpointActionCount !== 1) {
|
|
796
|
+
errors.push('review/manual_submit checkpoint must contain exactly one action');
|
|
797
|
+
}
|
|
798
|
+
requireFingerprint(errors, receipt.checkpointActionIdsFingerprint, 'checkpointActionIdsFingerprint');
|
|
799
|
+
if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
|
|
800
|
+
for (const field of [
|
|
801
|
+
...fillBaselineFields,
|
|
802
|
+
...resolutionFields,
|
|
803
|
+
...checkpointFields,
|
|
804
|
+
...truthCertificationFields.filter((field) => field !== 'truthCertificationMemberRuns'),
|
|
805
|
+
...visibilityFields,
|
|
806
|
+
]) {
|
|
807
|
+
if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return errors;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function validateHandoff(receipt, expectedContext) {
|
|
814
|
+
const errors = [];
|
|
815
|
+
const evidenceFields = ['browserBindingHash', 'handoffEvidenceFingerprint', 'handoffEvidenceType'];
|
|
816
|
+
const stateAuthorityFields = [
|
|
817
|
+
'employerApplicationState',
|
|
818
|
+
'tracklyMemberState',
|
|
819
|
+
'tracklyJobState',
|
|
820
|
+
];
|
|
821
|
+
const checkpointAuthorityFields = [
|
|
822
|
+
'checkpointStatus',
|
|
823
|
+
'checkpointMemberVersion',
|
|
824
|
+
'checkpointInspectionEpoch',
|
|
825
|
+
'checkpointLifecycle',
|
|
826
|
+
'checkpointAction',
|
|
827
|
+
'checkpointActionCount',
|
|
828
|
+
];
|
|
829
|
+
const reviewAuthorityFields = [...stateAuthorityFields, ...checkpointAuthorityFields];
|
|
830
|
+
validateCurrentLineage(
|
|
831
|
+
errors,
|
|
832
|
+
receipt,
|
|
833
|
+
expectedContext,
|
|
834
|
+
'approvedJobIds',
|
|
835
|
+
[...evidenceFields, ...reviewAuthorityFields],
|
|
836
|
+
);
|
|
837
|
+
if (!EMPLOYER_APPLICATION_STATES.has(receipt.employerApplicationState)) {
|
|
838
|
+
errors.push('employerApplicationState is invalid');
|
|
839
|
+
}
|
|
840
|
+
if (!TRACKLY_MEMBER_STATES.has(receipt.tracklyMemberState)) {
|
|
841
|
+
errors.push('tracklyMemberState is invalid');
|
|
842
|
+
}
|
|
843
|
+
if (!TRACKLY_JOB_STATES.has(receipt.tracklyJobState)) {
|
|
844
|
+
errors.push('tracklyJobState is invalid');
|
|
845
|
+
}
|
|
846
|
+
if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
|
|
847
|
+
for (const field of stateAuthorityFields) {
|
|
848
|
+
if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (receipt.browserTabState === 'closed_verified') {
|
|
852
|
+
errors.push('closed_verified is reserved for reconciliation receipts');
|
|
853
|
+
} else if (!BROWSER_HANDOFF_STATES.has(receipt.browserTabState)) {
|
|
854
|
+
errors.push('browserTabState is invalid');
|
|
855
|
+
}
|
|
856
|
+
if (!['verified', 'unverified'].includes(receipt.handoffVisibility)) {
|
|
857
|
+
errors.push('handoffVisibility must be verified or unverified');
|
|
858
|
+
}
|
|
859
|
+
if (typeof receipt.reviewReadyClaimed !== 'boolean') {
|
|
860
|
+
errors.push('reviewReadyClaimed must be a boolean');
|
|
861
|
+
}
|
|
862
|
+
if (receipt.handoffVisibility === 'unverified' && receipt.reviewReadyClaimed !== false) {
|
|
863
|
+
errors.push('reviewReadyClaimed must be false when handoff visibility is unverified');
|
|
864
|
+
}
|
|
865
|
+
if (receipt.browserTabState === 'durable_handoff_proven'
|
|
866
|
+
&& receipt.handoffVisibility !== 'verified') {
|
|
867
|
+
errors.push('durable_handoff_proven requires verified visibility');
|
|
868
|
+
}
|
|
869
|
+
if (receipt.handoffVisibility === 'verified'
|
|
870
|
+
&& !['visible', 'durable_handoff_proven'].includes(receipt.browserTabState)) {
|
|
871
|
+
errors.push('verified handoff requires a visible or durably handed-off browser tab');
|
|
872
|
+
}
|
|
873
|
+
if (receipt.handoffVisibility === 'verified') {
|
|
874
|
+
requireFingerprint(errors, receipt.browserBindingHash, 'browserBindingHash');
|
|
875
|
+
requireFingerprint(errors, receipt.handoffEvidenceFingerprint, 'handoffEvidenceFingerprint');
|
|
876
|
+
if (![
|
|
877
|
+
'visible_presentation_receipt',
|
|
878
|
+
'user_visible_handoff_receipt',
|
|
879
|
+
'durable_handoff_receipt',
|
|
880
|
+
].includes(receipt.handoffEvidenceType)) {
|
|
881
|
+
errors.push('handoffEvidenceType must identify visible presentation, a user-visible handoff receipt, or a durable handoff receipt');
|
|
882
|
+
}
|
|
883
|
+
if (receipt.browserTabState === 'visible'
|
|
884
|
+
&& !['visible_presentation_receipt', 'user_visible_handoff_receipt'].includes(receipt.handoffEvidenceType)) {
|
|
885
|
+
errors.push('visible browser state requires presentation or exact tab-bound user-visible handoff evidence');
|
|
886
|
+
}
|
|
887
|
+
if (receipt.browserTabState === 'durable_handoff_proven'
|
|
888
|
+
&& receipt.handoffEvidenceType !== 'durable_handoff_receipt') {
|
|
889
|
+
errors.push('durable_handoff_proven requires durable_handoff_receipt evidence');
|
|
890
|
+
}
|
|
891
|
+
if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
|
|
892
|
+
for (const field of evidenceFields) {
|
|
893
|
+
if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} else if (evidenceFields.some((field) => Object.hasOwn(receipt, field))) {
|
|
897
|
+
errors.push('handoff evidence fields must be omitted when visibility is unverified');
|
|
898
|
+
}
|
|
899
|
+
if (receipt.reviewReadyClaimed === true
|
|
900
|
+
&& (receipt.employerApplicationState !== 'review_state_prepared'
|
|
901
|
+
|| receipt.tracklyMemberState !== 'awaiting_manual_submit'
|
|
902
|
+
|| receipt.tracklyJobState !== 'check_later'
|
|
903
|
+
|| receipt.handoffVisibility !== 'verified')) {
|
|
904
|
+
errors.push('reviewReadyClaimed requires prepared employer state, awaiting_manual_submit member state, check_later job state, and verified visibility');
|
|
905
|
+
}
|
|
906
|
+
if (receipt.reviewReadyClaimed === true) {
|
|
907
|
+
if (!['recorded', 'replayed'].includes(receipt.checkpointStatus)) {
|
|
908
|
+
errors.push('reviewReadyClaimed requires a recorded or replayed checkpointStatus');
|
|
909
|
+
}
|
|
910
|
+
requireTracklyId(errors, receipt, 'checkpointMemberVersion');
|
|
911
|
+
if (!Number.isSafeInteger(receipt.checkpointInspectionEpoch) || receipt.checkpointInspectionEpoch < 0) {
|
|
912
|
+
errors.push('checkpointInspectionEpoch must be a non-negative safe integer');
|
|
913
|
+
}
|
|
914
|
+
if (receipt.checkpointInspectionEpoch !== receipt.inspectionEpoch) {
|
|
915
|
+
errors.push('checkpointInspectionEpoch must equal inspectionEpoch');
|
|
916
|
+
}
|
|
917
|
+
if (receipt.checkpointLifecycle !== 'review_ready') {
|
|
918
|
+
errors.push('reviewReadyClaimed requires checkpointLifecycle review_ready');
|
|
919
|
+
}
|
|
920
|
+
if (receipt.checkpointAction !== 'review/manual_submit') {
|
|
921
|
+
errors.push('reviewReadyClaimed requires checkpointAction review/manual_submit');
|
|
922
|
+
}
|
|
923
|
+
if (receipt.checkpointActionCount !== 1) {
|
|
924
|
+
errors.push('reviewReadyClaimed requires checkpointActionCount 1');
|
|
925
|
+
}
|
|
926
|
+
if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
|
|
927
|
+
for (const field of checkpointAuthorityFields) {
|
|
928
|
+
if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
if (checkpointAuthorityFields.some((field) => Object.hasOwn(receipt, field))) {
|
|
933
|
+
errors.push('checkpoint authority fields must be omitted when reviewReadyClaimed is false');
|
|
934
|
+
}
|
|
935
|
+
}
|
|
306
936
|
return errors;
|
|
307
937
|
}
|
|
308
938
|
|
|
309
939
|
function validateReconciliation(receipt, expectedContext) {
|
|
310
940
|
const errors = [];
|
|
311
|
-
|
|
941
|
+
const reconciliationAuthorityFields = [
|
|
942
|
+
'positiveSubmissionEvidenceRecorded',
|
|
943
|
+
'memberLifecycle',
|
|
944
|
+
'tracklyJobStatus',
|
|
945
|
+
];
|
|
946
|
+
validateCurrentLineage(
|
|
947
|
+
errors,
|
|
948
|
+
receipt,
|
|
949
|
+
expectedContext,
|
|
950
|
+
'approvedJobIds',
|
|
951
|
+
reconciliationAuthorityFields,
|
|
952
|
+
);
|
|
312
953
|
requireTrue(errors, receipt, 'positiveSubmissionEvidenceRecorded');
|
|
313
954
|
if (receipt.memberLifecycle !== 'submitted') errors.push('memberLifecycle must be submitted');
|
|
314
955
|
if (receipt.tracklyJobStatus !== 'applied_confirmed') errors.push('tracklyJobStatus must be applied_confirmed');
|
|
956
|
+
if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
|
|
957
|
+
for (const field of reconciliationAuthorityFields) {
|
|
958
|
+
if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
315
961
|
if (!CLEANUP_PREFERENCES.has(receipt.cleanupPreference)) errors.push('cleanupPreference is invalid');
|
|
316
962
|
if (!TAB_STATES.has(receipt.browserTabStatus)) errors.push('browserTabStatus is invalid');
|
|
317
963
|
if (receipt.cleanupPreference === 'never' && receipt.browserTabStatus === 'closed_verified') {
|
|
@@ -336,36 +982,37 @@ const VALIDATORS = {
|
|
|
336
982
|
access: validateAccess,
|
|
337
983
|
fill: validateFill,
|
|
338
984
|
review: validateReview,
|
|
985
|
+
handoff: validateHandoff,
|
|
339
986
|
reconciliation: validateReconciliation,
|
|
340
987
|
};
|
|
341
988
|
|
|
342
|
-
function validateCheckpoint(phase, receipt, expectedContext) {
|
|
989
|
+
function validateCheckpoint(phase, receipt, expectedContext, priorFill, validationTimeMs = Date.now()) {
|
|
343
990
|
if (!Object.hasOwn(VALIDATORS, phase)) return [`unknown phase: ${phase}`];
|
|
344
991
|
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) return ['receipt must be a JSON object'];
|
|
992
|
+
if (phase !== 'review' && priorFill !== undefined) return ['priorFill is permitted only for review'];
|
|
345
993
|
const unexpectedFields = Object.keys(receipt)
|
|
346
994
|
.filter((field) => !PHASE_FIELDS[phase].has(field))
|
|
347
|
-
.map((
|
|
348
|
-
return [
|
|
995
|
+
.map(() => 'receipt contains an unexpected field');
|
|
996
|
+
return [
|
|
997
|
+
...unexpectedFields,
|
|
998
|
+
...VALIDATORS[phase](receipt, expectedContext, priorFill, validationTimeMs),
|
|
999
|
+
];
|
|
349
1000
|
}
|
|
350
1001
|
|
|
351
1002
|
async function readReceiptInput(stream) {
|
|
352
1003
|
const decoder = new StringDecoder('utf8');
|
|
353
1004
|
let input = '';
|
|
354
1005
|
let inputBytes = 0;
|
|
355
|
-
let oversized = false;
|
|
356
1006
|
for await (const chunk of stream) {
|
|
357
|
-
if (oversized) continue;
|
|
358
1007
|
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
359
1008
|
inputBytes += bytes.length;
|
|
360
1009
|
if (inputBytes > MAX_RECEIPT_BYTES) {
|
|
361
|
-
oversized
|
|
362
|
-
input = '';
|
|
363
|
-
continue;
|
|
1010
|
+
return { input: '', oversized: true };
|
|
364
1011
|
}
|
|
365
1012
|
input += decoder.write(bytes);
|
|
366
1013
|
}
|
|
367
|
-
|
|
368
|
-
return { input, oversized };
|
|
1014
|
+
input += decoder.end();
|
|
1015
|
+
return { input, oversized: false };
|
|
369
1016
|
}
|
|
370
1017
|
|
|
371
1018
|
async function main() {
|
|
@@ -391,18 +1038,18 @@ async function main() {
|
|
|
391
1038
|
}
|
|
392
1039
|
if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)
|
|
393
1040
|
|| !Object.hasOwn(envelope, 'receipt')) {
|
|
394
|
-
process.stderr.write('input must be an envelope with receipt and
|
|
1041
|
+
process.stderr.write('input must be an envelope with receipt, expectedContext, and priorFill for review\n');
|
|
395
1042
|
process.exitCode = 1;
|
|
396
1043
|
return;
|
|
397
1044
|
}
|
|
398
1045
|
const unexpectedEnvelopeFields = Object.keys(envelope)
|
|
399
|
-
.filter((field) => !['receipt', 'expectedContext'].includes(field));
|
|
1046
|
+
.filter((field) => !['receipt', 'expectedContext', 'priorFill'].includes(field));
|
|
400
1047
|
if (unexpectedEnvelopeFields.length > 0) {
|
|
401
|
-
process.stderr.write(
|
|
1048
|
+
process.stderr.write('input envelope contains unsupported fields\n');
|
|
402
1049
|
process.exitCode = 1;
|
|
403
1050
|
return;
|
|
404
1051
|
}
|
|
405
|
-
const errors = validateCheckpoint(phase, envelope.receipt, envelope.expectedContext);
|
|
1052
|
+
const errors = validateCheckpoint(phase, envelope.receipt, envelope.expectedContext, envelope.priorFill);
|
|
406
1053
|
if (errors.length > 0) {
|
|
407
1054
|
process.stderr.write(`${errors.join('\n')}\n`);
|
|
408
1055
|
process.exitCode = 1;
|