job-application-agent 3.4.2 → 3.5.0

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.
@@ -7,6 +7,7 @@ import { platform } from 'node:os';
7
7
  import { basename, join, resolve } from 'node:path';
8
8
  import { pathToFileURL } from 'node:url';
9
9
 
10
+ import { ACCOUNTING_CAPABILITY, accountingApplicationKey, canonicalUrl, deliveryProjection, discoveryProjection, validateDelivery, validateDeliveryReferences, validateLead, validateLeadReferences, stableJson } from './application-accounting.mjs';
10
11
  import { createSecretStore, migrateLegacyStateDir, resolveStateDir } from './secret-store.mjs';
11
12
  import { SourceCommunityClient } from './source-community-client.mjs';
12
13
  import { normalizeCommunityJob, normalizeCommunitySource } from './source-community-schema.mjs';
@@ -506,12 +507,7 @@ function rolesLikelySame(left, right) {
506
507
  return shared / Math.min(leftTokens.size, rightTokens.size) >= 0.75;
507
508
  }
508
509
 
509
- function canonicalApplicationKey(entry, fallback = '') {
510
- if (entry.employerJobId) return `job:${normalizedText(entry.company)}:${String(entry.employerJobId).toLowerCase()}`;
511
- if (entry.company && entry.role) return `legacy-role:${normalizedText(entry.company)}:${normalizedText(entry.role)}`;
512
- if (entry.url) return `url:${normalizeUrl(entry.url)}`;
513
- return `id:${entry.id ?? fallback}`;
514
- }
510
+ const canonicalApplicationKey = accountingApplicationKey;
515
511
 
516
512
  function businessDaysBetween(startValue, endValue) {
517
513
  const start = new Date(startValue);
@@ -525,8 +521,10 @@ function businessDaysBetween(startValue, endValue) {
525
521
  return days;
526
522
  }
527
523
 
528
- export function buildReview(entries, outcomeEntries = [], acknowledgements = [], now = new Date()) {
524
+ export function buildReview(entries, outcomeEntries = [], acknowledgements = [], now = new Date(), deliveryEvents = []) {
529
525
  const submissions = entries.filter((entry) => !Number.isNaN(Date.parse(entry.submittedAt)));
526
+ const delivery = deliveryProjection(submissions, deliveryEvents);
527
+ const effectiveKeys = new Set(submissions.filter((entry, i) => delivery.applications[i].counted).map(canonicalApplicationKey));
530
528
  const explicitOutcomes = outcomeEntries.length > 0;
531
529
  const outcomes = explicitOutcomes ? outcomeEntries : entries.filter((entry) => ['interview', 'rejected', 'offer', 'withdrawn'].includes(entry.status));
532
530
  const groups = new Map();
@@ -535,7 +533,8 @@ export function buildReview(entries, outcomeEntries = [], acknowledgements = [],
535
533
  if (!groups.has(key)) groups.set(key, []);
536
534
  groups.get(key).push(entry);
537
535
  });
538
- const canonical = [...groups.values()].map((group) => [...group].sort((a, b) => Date.parse(a.submittedAt) - Date.parse(b.submittedAt))[0]);
536
+ const recordedCanonical = [...groups.values()].map(group => [...group].sort((a, b) => Date.parse(a.submittedAt) - Date.parse(b.submittedAt))[0]);
537
+ const canonical = [...groups].filter(([key]) => effectiveKeys.has(key)).map(([,group]) => [...group].sort((a, b) => Date.parse(a.submittedAt) - Date.parse(b.submittedAt))[0]);
539
538
  const outcomesById = new Map();
540
539
  for (const outcome of outcomes) {
541
540
  if (!outcomesById.has(outcome.id)) outcomesById.set(outcome.id, []);
@@ -544,7 +543,7 @@ export function buildReview(entries, outcomeEntries = [], acknowledgements = [],
544
543
  const canonicalOutcomes = [];
545
544
  const matureCanonicalOutcomes = [];
546
545
  const canonicalInterviewDetails = [];
547
- for (const group of groups.values()) {
546
+ for (const [groupKey, group] of groups) {
548
547
  const candidates = explicitOutcomes
549
548
  ? group.flatMap((entry) => outcomesById.get(entry.id) ?? [])
550
549
  : group.filter((entry) => ['interview', 'rejected', 'offer', 'withdrawn'].includes(entry.status));
@@ -559,14 +558,15 @@ export function buildReview(entries, outcomeEntries = [], acknowledgements = [],
559
558
  source: canonicalApplication.source,
560
559
  score: canonicalApplication.score,
561
560
  });
562
- if (businessDaysBetween(canonicalApplication.submittedAt, now) >= 10) matureCanonicalOutcomes.push(latest);
561
+ if (effectiveKeys.has(groupKey) && businessDaysBetween(canonicalApplication.submittedAt, now) >= 10) matureCanonicalOutcomes.push(latest);
563
562
  }
564
563
  }
565
564
  const maturedApplications = canonical.filter((entry) => businessDaysBetween(entry.submittedAt, now) >= 10).length;
566
565
  const lastAck = acknowledgements.length ? acknowledgements[acknowledgements.length - 1] : {};
567
- const submittedSinceLastReview = Math.max(0, canonical.length - (lastAck.uniqueSubmissionCount ?? 0));
566
+ const recordedMaturedApplicationCount = recordedCanonical.filter(entry => businessDaysBetween(entry.submittedAt, now) >= 10).length;
567
+ const submittedSinceLastReview = Math.max(0, recordedCanonical.length - (lastAck.uniqueSubmissionCount ?? 0));
568
568
  const hygieneDue = submittedSinceLastReview >= 10;
569
- const outcomeDue = maturedApplications - (lastAck.maturedApplicationCount ?? 0) >= 20;
569
+ const outcomeDue = recordedMaturedApplicationCount - (lastAck.maturedApplicationCount ?? 0) >= 20;
570
570
  const reviewReasons = [...(hygieneDue ? ['submission-hygiene'] : []), ...(outcomeDue ? ['outcome-effectiveness'] : [])];
571
571
  const outcomeCounts = Object.fromEntries(['interview', 'rejected', 'offer', 'withdrawn'].map((status) => [status, canonicalOutcomes.filter((entry) => entry.status === status).length]));
572
572
  const matureOutcomeCounts = Object.fromEntries(['interview', 'rejected', 'offer', 'withdrawn'].map((status) => [status, matureCanonicalOutcomes.filter((entry) => entry.status === status).length]));
@@ -598,9 +598,14 @@ export function buildReview(entries, outcomeEntries = [], acknowledgements = [],
598
598
  submittedTotal: canonical.length,
599
599
  uniqueSubmittedTotal: canonical.length,
600
600
  rawSubmissionRows: submissions.length,
601
- duplicateSubmissionRows: submissions.length - canonical.length,
601
+ duplicateSubmissionRows: submissions.length - groups.size,
602
+ recordedSubmissionCount: groups.size,
603
+ effectiveSubmissionCount: canonical.length,
604
+ failedDeliveryCount: delivery.failedDeliveryCount,
605
+ receiptUnknownEmailCount: delivery.receiptUnknownEmailCount,
602
606
  submittedSinceLastReview,
603
607
  maturedApplications,
608
+ recordedMaturedApplicationCount,
604
609
  outcomeCounts,
605
610
  matureOutcomeCounts,
606
611
  reasonCounts,
@@ -1081,7 +1086,7 @@ async function ledgerAdd(entryInput, duplicateOverride, companyReapplyOverride,
1081
1086
  if (entry.roundId) await cloudState.appendRecord('rounds', { type: 'submission-confirmed', roundId: entry.roundId, applicationId: entry.id, occurredAt: entry.submittedAt }, { recordKey: entry.roundId, idempotencyKey: `round-confirmation:${entry.roundId}:${entry.id}`, occurredAt: entry.submittedAt, queueOnFailure: true });
1082
1087
  }
1083
1088
  }
1084
- return { recorded: storedEntry.id, review: buildReview([...entries, storedEntry]), ...(cloudIntent ? { cloudIntent: cloudIntent.intentId } : {}) };
1089
+ return { recorded: storedEntry.id, review: buildReview([...entries, storedEntry], outcomes, [], new Date(), await jsonLines(join(dir, 'delivery.ndjson'))), ...(cloudIntent ? { cloudIntent: cloudIntent.intentId } : {}) };
1085
1090
  });
1086
1091
  }
1087
1092
 
@@ -1107,6 +1112,63 @@ function isoDate(value, label) {
1107
1112
  return result;
1108
1113
  }
1109
1114
 
1115
+ async function appendAccounting(stream, event, { cloudConfirmed = false } = {}) {
1116
+ const file = join(await ensureStateDir(), `${stream}.ndjson`);
1117
+ const previous = await jsonLines(file);
1118
+ const matches = previous.filter(e => e.id === event.id);
1119
+ if (matches.some(e => stableJson(e) !== stableJson(event))) throw new Error('Conflicting accounting event ID.');
1120
+ if (matches.length) return { recorded: false, duplicate: true, event };
1121
+ await appendFile(file, `${JSON.stringify(event)}\n`, { mode: 0o600 });
1122
+ await chmod(file, 0o600);
1123
+ if (!cloudConfirmed && await cloudState.configured()) await cloudState.appendRecord(stream, event, { recordKey: event.id, idempotencyKey: `accounting:${event.id}`, occurredAt: event.occurredAt ?? event.observedAt, queueOnFailure: true });
1124
+ return { recorded: true, duplicate: false, event };
1125
+ }
1126
+
1127
+ async function recordDelivery(input, retry = false) {
1128
+ const { cloudIntentId, cloudLeaseId, ...raw } = object(input, 'delivery');
1129
+ const event = validateDelivery({ ...raw, ...(retry ? { type: 'retry-confirmed' } : {}) });
1130
+ if (!retry && event.type === 'retry-confirmed') throw new Error('Use ledger retry for replacement transmissions.');
1131
+ return withStateLock('applications', () => withStateLock('delivery', async dir => {
1132
+ const applications = await jsonLines(join(dir, 'applications.ndjson'));
1133
+ const events = await jsonLines(join(dir, 'delivery.ndjson'));
1134
+ const cloudRetry = retry && await cloudState.configured();
1135
+ if (cloudRetry && !events.some(e => e.id === event.id) && (!cloudIntentId || !cloudLeaseId || event.attemptId !== cloudIntentId)) throw new Error('Cloud retry requires its intent and live lease; attemptId must equal cloudIntentId.');
1136
+ // Cloud transmission eligibility was checked at intent preparation. The
1137
+ // Worker verifies that intent before we persist the observed transmission.
1138
+ validateDeliveryReferences(event, applications, events, { preparedRetry: cloudRetry });
1139
+ let cloudConfirmed = false;
1140
+ if (cloudRetry && !events.some(e => e.id === event.id)) {
1141
+ await cloudState.confirmRetry(cloudIntentId, event, cloudLeaseId);
1142
+ cloudConfirmed = true;
1143
+ }
1144
+ const result = await appendAccounting('delivery', event, { cloudConfirmed });
1145
+ return { ...result, delivery: deliveryProjection(applications, [...events, event]) };
1146
+ }));
1147
+ }
1148
+ async function deliveryHistory(applicationId) {
1149
+ const dir = await ensureStateDir();
1150
+ const apps = (await jsonLines(join(dir, 'applications.ndjson'))).filter(a => !applicationId || a.id === applicationId);
1151
+ const events = (await jsonLines(join(dir, 'delivery.ndjson'))).filter(e => !applicationId || e.applicationId === applicationId);
1152
+ return { ...deliveryProjection(apps, events), events };
1153
+ }
1154
+ async function recordLead(input) {
1155
+ const event = validateLead(input);
1156
+ knownDiscoverySourceId(event.sourceId, 'lead.sourceId');
1157
+ return withStateLock('rounds', async dir => {
1158
+ const round = await roundStatus(event.roundId);
1159
+ const events = await jsonLines(join(dir, 'discovery.ndjson'));
1160
+ if (round.completed && !event.supersedes && !events.some(e => e.id === event.id)) throw new Error('Completed rounds accept only corrections to existing leads.');
1161
+ validateLeadReferences(event, events);
1162
+ return appendAccounting('discovery', event);
1163
+ });
1164
+ }
1165
+ async function leadHistory(roundId) {
1166
+ const dir = await ensureStateDir();
1167
+ const id = roundId ?? (await roundStatus()).roundId;
1168
+ const history = (await jsonLines(join(dir, 'discovery.ndjson'))).filter(e => e.roundId === id);
1169
+ return { ...discoveryProjection(history, { roundId: id }), history };
1170
+ }
1171
+
1110
1172
  async function roundStart(input) {
1111
1173
  const value = object(input, 'round');
1112
1174
  const allowed = new Set(['requestedCount', 'startedAt']);
@@ -1114,6 +1176,7 @@ async function roundStart(input) {
1114
1176
  const event = {
1115
1177
  type: 'started',
1116
1178
  roundId: `round-${new Date().toISOString().slice(0, 10)}-${randomUUID()}`,
1179
+ discoveryPolicyVersion: 2,
1117
1180
  requestedCount: integer(value.requestedCount, 'round.requestedCount', 1, 1000),
1118
1181
  occurredAt: isoDate(value.startedAt, 'round.startedAt'),
1119
1182
  };
@@ -1127,7 +1190,7 @@ function discoveryGroup(id) {
1127
1190
  return id;
1128
1191
  }
1129
1192
 
1130
- function discoverySummary(events, applications, completion) {
1193
+ function discoverySummary(events, applications, completion, audit = null) {
1131
1194
  const latest = new Map();
1132
1195
  const searchedIds = new Set();
1133
1196
  const attribution = new Map();
@@ -1136,7 +1199,10 @@ function discoverySummary(events, applications, completion) {
1136
1199
  if (event.status === 'searched') searchedIds.add(event.sourceId);
1137
1200
  for (const id of event.applicationIds ?? []) attribution.set(id, event.sourceId);
1138
1201
  }
1139
- const sources = [...latest.values()];
1202
+ const sources = [...latest.values()].map(report => {
1203
+ const leads = audit?.leads.filter(lead => lead.sourceId === report.sourceId) ?? [];
1204
+ return audit ? { ...report, reviewedCount: leads.length, qualifiedCount: leads.filter(lead => !lead.conflict && lead.disposition === 'qualified').length, accounting: 'per-lead' } : { ...report, accounting: 'legacy-unverified' };
1205
+ });
1140
1206
  const eligible = sources.filter((item) => !['recruiter-inbound', 'user-supplied-leads'].includes(item.sourceId));
1141
1207
  const attempted = new Set(eligible.map((item) => discoveryGroup(item.sourceId)));
1142
1208
  const searched = new Set(eligible.filter((item) => searchedIds.has(item.sourceId)).map((item) => discoveryGroup(item.sourceId)));
@@ -1172,17 +1238,23 @@ async function roundSource(input) {
1172
1238
  const sourceId = knownDiscoverySourceId(value.sourceId, 'coverage.sourceId');
1173
1239
  const status = string(value.status, 'coverage.status', 20);
1174
1240
  if (!['searched', 'blocked'].includes(status)) throw new Error('coverage.status must be searched or blocked.');
1175
- const reviewedCount = integer(value.reviewedCount, 'coverage.reviewedCount', 0, 10000);
1176
- const qualifiedCount = integer(value.qualifiedCount, 'coverage.qualifiedCount', 0, reviewedCount);
1241
+ const roundId = string(value.roundId, 'coverage.roundId', 180);
1177
1242
  const blocker = value.blocker == null ? null : string(value.blocker, 'coverage.blocker', 40);
1178
- if (status === 'blocked' && (!SOURCE_BLOCKERS.has(blocker) || reviewedCount !== 0 || qualifiedCount !== 0)) throw new Error('Blocked sources require a documented blocker and zero counts.');
1179
1243
  if (status === 'searched' && blocker != null) throw new Error('Searched sources cannot have a blocker.');
1180
1244
  const evidence = string(value.evidence, 'coverage.evidence', 2000);
1181
1245
  const applicationIds = value.applicationIds == null ? [] : [...new Set(stringArray(value.applicationIds, 'coverage.applicationIds'))];
1182
1246
  if (applicationIds.length > 1000 || (status === 'blocked' && applicationIds.length)) throw new Error('Invalid coverage.applicationIds.');
1183
1247
  const event = await withStateLock('rounds', async (dir) => {
1184
- const round = await roundStatus(string(value.roundId, 'coverage.roundId', 180));
1248
+ const round = await roundStatus(roundId);
1185
1249
  if (round.completed) throw new Error('Cannot record coverage for a completed round.');
1250
+ const audit = discoveryProjection(await jsonLines(join(dir, 'discovery.ndjson')), { roundId: value.roundId });
1251
+ const leads = audit.leads.filter(lead => lead.sourceId === sourceId);
1252
+ const derivedReviewed = status === 'blocked' ? 0 : leads.length;
1253
+ const derivedQualified = status === 'blocked' ? 0 : leads.filter(lead => !lead.conflict && lead.disposition === 'qualified').length;
1254
+ const reviewedCount = round.discoveryPolicyVersion === 2 ? derivedReviewed : integer(value.reviewedCount, 'coverage.reviewedCount', 0, 10000);
1255
+ const qualifiedCount = round.discoveryPolicyVersion === 2 ? derivedQualified : integer(value.qualifiedCount, 'coverage.qualifiedCount', 0, reviewedCount);
1256
+ if (round.discoveryPolicyVersion === 2 && ((value.reviewedCount != null && value.reviewedCount !== reviewedCount) || (value.qualifiedCount != null && value.qualifiedCount !== qualifiedCount))) throw new Error('Source count assertions do not match recorded leads.');
1257
+ if (status === 'blocked' && (!SOURCE_BLOCKERS.has(blocker) || reviewedCount !== 0 || qualifiedCount !== 0)) throw new Error('Blocked sources require a documented blocker and zero counts.');
1186
1258
  const applications = await jsonLines(join(dir, 'applications.ndjson'));
1187
1259
  for (const id of applicationIds) {
1188
1260
  const entry = applications.find((item) => item.id === id && item.roundId === round.roundId && item.status === 'submitted');
@@ -1217,6 +1289,16 @@ function replayAttention(events, roundId = null) {
1217
1289
  async function attentionList(roundId = null) {
1218
1290
  const events = await jsonLines(join(await ensureStateDir(), 'attention.ndjson'));
1219
1291
  const items = replayAttention(events, roundId);
1292
+ const dir = await ensureStateDir();
1293
+ const apps = await jsonLines(join(dir, 'applications.ndjson'));
1294
+ const delivery = deliveryProjection(apps, await jsonLines(join(dir, 'delivery.ndjson')));
1295
+ for (const item of delivery.applications.filter(a => a.failed || a.conflict)) {
1296
+ const app = apps.find(a => a.id === item.applicationId);
1297
+ if (roundId && app.roundId !== roundId) continue;
1298
+ items.push({ id: `delivery:${app.id}`, applicationId: app.id, roundId: app.roundId ?? null, url: app.url, stage: 'confirmation', blocker: item.conflict ? 'delivery-conflict' : 'delivery-failed', requiredActions: ['review-delivery'], derived: true });
1299
+ }
1300
+ const discovery = discoveryProjection(await jsonLines(join(dir, 'discovery.ndjson')), { roundId });
1301
+ for (const lead of discovery.leads.filter(l => l.conflict)) items.push({ id: `discovery:${lead.key}`, roundId: lead.roundId, url: lead.url, stage: 'discovery', blocker: 'assessment-conflict', requiredActions: ['review-assessment'], derived: true });
1220
1302
  return { count: items.length, items };
1221
1303
  }
1222
1304
 
@@ -1251,6 +1333,7 @@ async function attentionResolve(input) {
1251
1333
  for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`Unknown attention resolution property: ${key}.`);
1252
1334
  const id = string(value.id, 'attention.id', 180);
1253
1335
  const current = await attentionList();
1336
+ if (current.items.some(item => item.id === id && item.derived)) throw new Error('Resolve accounting attention with a delivery correction, verified recovery, or lead revision.');
1254
1337
  if (!current.items.some((item) => item.id === id)) throw new Error('Attention item is not active.');
1255
1338
  const event = { type: 'resolved', id, resolvedAt: isoDate(value.resolvedAt, 'attention.resolvedAt') };
1256
1339
  await appendPrivateEvent('attention', event);
@@ -1267,7 +1350,11 @@ async function roundStatus(roundId = null) {
1267
1350
  if (!started) throw new Error('Application round was not found.');
1268
1351
  const matching = (await jsonLines(join(dir, 'applications.ndjson'))).filter((entry) => entry.roundId === id && entry.status === 'submitted');
1269
1352
  const applications = [...new Map(matching.map((entry, index) => [canonicalApplicationKey(entry, String(index)), entry])).values()];
1270
- const confirmedCount = new Set(applications.map((entry, index) => canonicalApplicationKey(entry, String(index)))).size;
1353
+ const delivery = deliveryProjection(matching, await jsonLines(join(dir, 'delivery.ndjson')));
1354
+ const effectiveKeys = new Set(matching.filter((entry,i) => delivery.applications[i].counted).map(canonicalApplicationKey));
1355
+ const effectiveApplications = applications.filter(entry => effectiveKeys.has(canonicalApplicationKey(entry)));
1356
+ const confirmedCount = delivery.effectiveSubmissionCount;
1357
+ const audit = started.discoveryPolicyVersion === 2 ? discoveryProjection(await jsonLines(join(dir, 'discovery.ndjson')), { roundId: id }) : null;
1271
1358
  const attention = await attentionList(id);
1272
1359
  const completion = events.find((event) => event.type === 'completed' && event.roundId === id);
1273
1360
  return {
@@ -1277,7 +1364,16 @@ async function roundStatus(roundId = null) {
1277
1364
  remainingCount: Math.max(0, started.requestedCount - confirmedCount),
1278
1365
  blockedCount: attention.count,
1279
1366
  completed: Boolean(completion),
1280
- discovery: discoverySummary(events.filter((event) => event.roundId === id), applications, completion),
1367
+ discoveryPolicyVersion: started.discoveryPolicyVersion ?? 1,
1368
+ recordedSubmissionCount: delivery.recordedSubmissionCount,
1369
+ effectiveSubmissionCount: confirmedCount,
1370
+ failedDeliveryCount: delivery.failedDeliveryCount,
1371
+ receiptUnknownEmailCount: delivery.receiptUnknownEmailCount,
1372
+ shortfallCount: Math.max(0, started.requestedCount - confirmedCount),
1373
+ needsRecovery: Boolean(completion) && confirmedCount < started.requestedCount,
1374
+ discovery: { ...discoverySummary(events.filter((event) => event.roundId === id), effectiveApplications, completion, audit),
1375
+ ...(audit ? { reviewedCount: audit.reviewedCount, qualifiedCount: audit.qualifiedCount, uniqueLeadCount: audit.uniqueLeadCount, dispositionCounts: audit.dispositionCounts, conflicts: audit.conflicts,
1376
+ missingLeadApplicationIds: effectiveApplications.filter(app => !audit.leads.some(lead => !lead.conflict && lead.disposition === 'qualified' && lead.applicationId === app.id && lead.sourceId === (app.discoverySourceId ?? events.filter(e => e.type === 'source-checked' && e.applicationIds?.includes(app.id)).at(-1)?.sourceId) && (normalizedText(lead.company) === normalizedText(app.company) && (lead.employerJobId && app.employerJobId ? lead.employerJobId.toLowerCase() === app.employerJobId.toLowerCase() : canonicalUrl(lead.url) === canonicalUrl(app.url))))).map(app => app.id) } : { accounting: 'legacy-unverified' }) },
1281
1377
  startedAt: started.occurredAt,
1282
1378
  ...(completion ? { completedAt: completion.occurredAt } : {}),
1283
1379
  };
@@ -1294,6 +1390,7 @@ async function roundComplete(input) {
1294
1390
  if (status.confirmedCount < status.requestedCount) throw new Error(`Round requires ${status.requestedCount} confirmed submissions before completion.`);
1295
1391
  if (!status.discovery.coverageSatisfied) throw new Error('Round requires attempts across at least 3 distinct discovery sources, including at least one searched source. Record coverage and blockers with round source --stdin.');
1296
1392
  if (status.discovery.unattributedCount) throw new Error('Round requires discovery source attribution for every confirmed submission. Supply missing attribution using round source applicationIds; do not rewrite the ledger.');
1393
+ if (status.discovery.conflicts?.length || status.discovery.missingLeadApplicationIds?.length) throw new Error('Round requires qualified lead records for every submission and resolution of conflicting assessments.');
1297
1394
  let explanation = {};
1298
1395
  if (status.discovery.concentrationNeedsExplanation || value.concentrationReason != null || value.concentrationEvidence != null) {
1299
1396
  if (!CONCENTRATION_REASONS.has(value.concentrationReason)) throw new Error('Source concentration requires a documented concentrationReason and private concentrationEvidence.');
@@ -1399,7 +1496,7 @@ async function ledgerReview() {
1399
1496
  const applications = await jsonLines(join(dir, 'applications.ndjson'));
1400
1497
  const outcomes = await jsonLines(join(dir, 'outcomes.ndjson'));
1401
1498
  const acknowledgements = await jsonLines(join(dir, 'reviews.ndjson'));
1402
- return buildReview(applications, outcomes, acknowledgements);
1499
+ return buildReview(applications, outcomes, acknowledgements, new Date(), await jsonLines(join(dir, 'delivery.ndjson')));
1403
1500
  }
1404
1501
 
1405
1502
  async function ledgerReviewAcknowledge(input) {
@@ -1407,7 +1504,7 @@ async function ledgerReviewAcknowledge(input) {
1407
1504
  const reviewedAt = string(value.reviewedAt ?? new Date().toISOString(), 'reviewedAt', 80);
1408
1505
  if (Number.isNaN(Date.parse(reviewedAt))) throw new Error('reviewedAt must be an ISO date.');
1409
1506
  const review = await ledgerReview();
1410
- const event = { reviewedAt, uniqueSubmissionCount: review.uniqueSubmittedTotal, maturedApplicationCount: review.maturedApplications };
1507
+ const event = { reviewedAt, uniqueSubmissionCount: review.recordedSubmissionCount, maturedApplicationCount: review.recordedMaturedApplicationCount };
1411
1508
  await withStateLock('reviews', async (dir) => {
1412
1509
  const file = join(dir, 'reviews.ndjson');
1413
1510
  await appendFile(file, `${JSON.stringify(event)}\n`, { mode: 0o600 });
@@ -1547,6 +1644,9 @@ async function executeCommand([area, action, value], telemetry, session, communi
1547
1644
  const event = await telemetryJobAssessed(job, result);
1548
1645
  if (event) domainEvents.push(event);
1549
1646
  } else if (area === 'ledger' && action === 'check' && value === '--stdin') result = await ledgerCheck(await jsonStdin());
1647
+ else if (area === 'ledger' && action === 'delivery' && value === '--stdin') result = await recordDelivery(await jsonStdin());
1648
+ else if (area === 'ledger' && action === 'retry' && value === '--stdin') result = await recordDelivery(await jsonStdin(), true);
1649
+ else if (area === 'ledger' && action === 'deliveries') result = await deliveryHistory(value);
1550
1650
  else if (area === 'ledger' && action === 'add' && value === '--stdin') {
1551
1651
  const input = await jsonStdin();
1552
1652
  const telemetryDetails = validateSubmissionTelemetry(input.telemetry);
@@ -1569,6 +1669,8 @@ async function executeCommand([area, action, value], telemetry, session, communi
1569
1669
  else if (area === 'autonomy' && action === 'preview' && value == null) result = await autonomyStatus();
1570
1670
  else if (area === 'autonomy' && action === 'revoke' && value == null) result = await autonomyRevoke();
1571
1671
  else if (area === 'round' && action === 'start' && value === '--stdin') result = await roundStart(await jsonStdin());
1672
+ else if (area === 'round' && action === 'lead' && value === '--stdin') result = await recordLead(await jsonStdin());
1673
+ else if (area === 'round' && action === 'leads') result = await leadHistory(value);
1572
1674
  else if (area === 'round' && action === 'source' && value === '--stdin') {
1573
1675
  result = await roundSource(await jsonStdin());
1574
1676
  domainEvents.push({ event: 'source_checked', properties: {
@@ -1 +1 @@
1
- export const SKILL_VERSION = '3.4.2';
1
+ export const SKILL_VERSION = '3.5.0';
@@ -0,0 +1,86 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync, spawnSync } from 'node:child_process';
3
+ import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import test from 'node:test';
7
+ import { fileURLToPath } from 'node:url';
8
+ const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
9
+ async function fixture(t) {
10
+ const dir = await mkdtemp(join(tmpdir(), 'accounting-cli-'));
11
+ t.after(() => rm(dir, { recursive: true, force: true }));
12
+ await writeFile(join(dir, 'telemetry.json'), JSON.stringify({ enabled: false, disclosed: true }));
13
+ const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: dir, JOB_APPLICATION_AGENT_CLOUD_CONFIG: join(dir, 'absent.json'), JOB_APPLICATION_AGENT_SOURCE_COMMUNITY_URL: 'http://127.0.0.1:9' };
14
+ const run = (args, input) => JSON.parse(execFileSync(process.execPath, [script, ...args], { env, encoding: 'utf8', input: JSON.stringify(input) }));
15
+ const fail = (args, input) => spawnSync(process.execPath, [script, ...args], { env, encoding: 'utf8', input: JSON.stringify(input) });
16
+ return { dir, run, fail };
17
+ }
18
+ const application = (roundId) => ({ id: 'app', company: 'Example', role: 'Senior Engineer', url: 'https://example.test/jobs/1', source: 'email', discoverySourceId: 'indeed', score: 90, status: 'submitted', submittedAt: '2026-01-01T00:00:00Z', approval: 'STANDING AUTHORIZATION', roundId });
19
+ const failure = { id: 'bounce-1', applicationId: 'app', attemptId: 'initial:app', type: 'delivery-failed', evidenceType: 'final-delivery-failure', evidence: 'Matched final failure for the original recruiting message.', occurredAt: '2026-01-02T00:00:00Z' };
20
+
21
+ test('late delivery failure corrects completed round and review without rewriting applications', async (t) => {
22
+ const {dir, run} = await fixture(t);
23
+ const roundId = 'legacy-round';
24
+ await writeFile(join(dir, 'rounds.ndjson'), [ { type:'started', roundId, requestedCount:1, occurredAt:'2026-01-01T00:00:00Z'}, {type:'completed',roundId,occurredAt:'2026-01-01T01:00:00Z'} ].map(JSON.stringify).join('\n')+'\n');
25
+ const raw = JSON.stringify(application(roundId))+'\n';
26
+ await writeFile(join(dir, 'applications.ndjson'), raw);
27
+ run(['ledger','delivery','--stdin'], failure);
28
+ run(['ledger','delivery','--stdin'], failure);
29
+ const status = run(['round','status',roundId]);
30
+ assert.equal(status.completed,true);
31
+ assert.equal(status.confirmedCount,0);
32
+ assert.equal(status.shortfallCount,1);
33
+ assert.equal(status.needsRecovery,true);
34
+ assert.equal(run(['ledger','review']).submittedTotal,0);
35
+ assert.equal(await readFile(join(dir,'applications.ndjson'),'utf8'),raw);
36
+ assert.equal((await readFile(join(dir,'delivery.ndjson'),'utf8')).trim().split('\n').length,1);
37
+ });
38
+
39
+ test('new rounds derive source totals and require qualified lead linkage', async (t) => {
40
+ const {run,fail} = await fixture(t);
41
+ const {roundId} = run(['round','start','--stdin'],{requestedCount:1});
42
+ assert.notEqual(fail(['round','source','--stdin'],{roundId,sourceId:'indeed',status:'searched',reviewedCount:20,qualifiedCount:1,evidence:'Unsupported summary'}).status,0);
43
+ const lead = { id:'lead-1',roundId,sourceId:'indeed',company:'Example',role:'Senior Engineer',url:'https://example.test/jobs/1',disposition:'qualified',evidence:'Meets the evidenced target requirements.',applicationId:'app',observedAt:'2026-01-01T00:00:00Z' };
44
+ run(['round','lead','--stdin'],lead);
45
+ run(['round','lead','--stdin'],lead);
46
+ for(const sourceId of ['indeed','linkedin-jobs-feed','hacker-news-who-is-hiring']) run(['round','source','--stdin'],{roundId,sourceId,status:'searched',evidence:'Actual synthetic search'});
47
+ run(['ledger','add','--stdin'],application(roundId));
48
+ const status=run(['round','status',roundId]);
49
+ assert.equal(status.discovery.sources.find(s=>s.sourceId==='indeed').reviewedCount,1);
50
+ assert.equal(run(['round','leads',roundId]).leads.length,1);
51
+ assert.equal(run(['round','complete','--stdin'],{roundId,concentrationReason:'stronger-fit',concentrationEvidence:'Only this source had a qualified role.'}).completed,true);
52
+ run(['round','lead','--stdin'],{...lead,id:'revision',supersedes:lead.id,disposition:'closed-stale'});
53
+ assert.equal(run(['round','status',roundId]).completed,true);
54
+ assert.equal(run(['round','leads',roundId]).qualifiedCount,0);
55
+ assert.notEqual(fail(['round','lead','--stdin'],{...lead,id:'new-lead',url:'https://different.example/job'}).status,0);
56
+ });
57
+
58
+ for (const scenario of [
59
+ { name: 'different requisition IDs at the same URL', application: { employerJobId: 'req-1' }, lead: { employerJobId: 'req-2' }, matched: false },
60
+ { name: 'different companies sharing a requisition ID and URL', application: { employerJobId: 'req-1' }, lead: { employerJobId: 'req-1', company: 'Another Company' }, matched: false },
61
+ { name: 'different companies sharing a URL without requisition IDs', application: {}, lead: { company: 'Another Company' }, matched: false },
62
+ { name: 'matching company and requisition ID across URL aliases', application: { employerJobId: 'req-1' }, lead: { employerJobId: 'req-1', url: 'https://ats.example.test/alias/1' }, matched: true },
63
+ { name: 'matching company and URL when only one requisition ID is known', application: { employerJobId: 'req-1' }, lead: {}, matched: true },
64
+ ]) {
65
+ test(`qualified lead attribution checks ${scenario.name}`, async (t) => {
66
+ const { dir, run, fail } = await fixture(t);
67
+ const { roundId } = run(['round', 'start', '--stdin'], { requestedCount: 1 });
68
+ const app = { ...application(roundId), ...scenario.application };
69
+ await writeFile(join(dir, 'applications.ndjson'), JSON.stringify(app) + '\n');
70
+ run(['round', 'lead', '--stdin'], {
71
+ id: 'qualified-lead', roundId, sourceId: 'indeed', company: 'Example', role: app.role,
72
+ url: app.url, disposition: 'qualified', applicationId: app.id,
73
+ observedAt: '2026-01-01T00:00:00Z', evidence: 'Synthetic verified requisition assessment.',
74
+ ...scenario.lead,
75
+ });
76
+ for (const sourceId of ['indeed', 'linkedin-jobs-feed', 'hacker-news-who-is-hiring']) {
77
+ run(['round', 'source', '--stdin'], { roundId, sourceId, status: 'searched', evidence: 'Synthetic search performed.' });
78
+ }
79
+ const status = run(['round', 'status', roundId]);
80
+ assert.deepEqual(status.discovery.missingLeadApplicationIds, scenario.matched ? [] : [app.id]);
81
+ const completion = fail(['round', 'complete', '--stdin'], {
82
+ roundId, concentrationReason: 'stronger-fit', concentrationEvidence: 'Only the selected source had a qualifying opening.',
83
+ });
84
+ assert.equal(completion.status === 0, scenario.matched, completion.stderr);
85
+ });
86
+ }
@@ -0,0 +1,183 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { CloudStateClient, readCachedCloudProfile, saveCloudConfig } from '../scripts/cloud-state-client.mjs';
8
+ import { deliveryProjection, discoveryProjection, validateDelivery, validateLead } from '../scripts/application-accounting.mjs';
9
+ import worker, { sha256Hex } from '../../state-worker/src/worker.mjs';
10
+ import { createBackup, restoreBackup } from '../../state-worker/src/backup.mjs';
11
+ import { createMemoryD1, hasNodeSqlite } from '../../state-worker/tests/d1-mock.mjs';
12
+ import { createMemoryR2 } from '../../state-worker/tests/r2-mock.mjs';
13
+
14
+ const TOKEN = 'accounting-cloud-client-synthetic-token-long-enough';
15
+ const sqliteTest = hasNodeSqlite ? test : test.skip;
16
+ const now = '2026-09-13T10:00:00.000Z';
17
+ const application = { id: 'app', company: 'Example', role: 'Engineer', source: 'email', url: 'https://example.com/jobs/app', status: 'submitted', submittedAt: '2026-09-12T10:00:00.000Z' };
18
+ const failure = validateDelivery({ version: 1, id: 'failure-event', applicationId: 'app', attemptId: 'initial:app', type: 'delivery-failed', occurredAt: now, evidenceType: 'final-delivery-failure', evidence: 'Synthetic final failure matched to the original application email.' });
19
+ const retry = validateDelivery({ version: 1, id: 'retry-event', applicationId: 'app', attemptId: 'recovery-attempt', type: 'retry-confirmed', channel: 'browser', url: 'https://example.com/jobs/app', channelVerifiedAt: now, approval: 'STANDING AUTHORIZATION', evidenceType: 'browser-confirmation', evidence: 'Synthetic visible ATS success.', occurredAt: '2026-09-13T11:00:00.000Z' });
20
+ const qualifiedLead = validateLead({ version: 1, id: 'lead-event', type: 'lead-reviewed', roundId: 'round-1', sourceId: 'direct', url: application.url, company: application.company, role: application.role, disposition: 'qualified', observedAt: now, evidence: 'Synthetic active employer posting matches requirements.' });
21
+
22
+ async function setup(t) {
23
+ const root = await mkdtemp(join(tmpdir(), 'job-agent-accounting-cloud-'));
24
+ t.after(() => rm(root, { recursive: true, force: true }));
25
+ const stateDir = join(root, 'state');
26
+ const configPath = join(root, 'cloud', 'config.json');
27
+ await mkdir(stateDir, { recursive: true });
28
+ const schema = await readFile(new URL('../../state-worker/migrations/0001_private_state.sql', import.meta.url), 'utf8');
29
+ const DB = createMemoryD1(schema);
30
+ await DB.prepare('INSERT INTO clients (client_id, name, token_hash, created_at) VALUES (?, ?, ?, ?)')
31
+ .bind('client-test', 'Synthetic Accounting Client', sha256Hex(TOKEN), now).run();
32
+ const bindings = { DB, STATE: createMemoryR2(), STATE_TOKEN: 'legacy', LEGACY_WRITES_DISABLED: '1' };
33
+ const fetchImpl = (input, init) => worker.fetch(new Request(input, init), bindings);
34
+ await saveCloudConfig({ version: 2, url: 'https://state.example.com', token: TOKEN, clientId: 'client-test' }, { configPath });
35
+ const client = new CloudStateClient({ stateDir, configPath, fetchImpl });
36
+ return { root, stateDir, configPath, schema, bindings, fetchImpl, client };
37
+ }
38
+
39
+ async function writeRows(stateDir, stream, rows) {
40
+ await writeFile(join(stateDir, `${stream}.ndjson`), `${rows.map(row => JSON.stringify(row)).join('\n')}\n`, { mode: 0o600 });
41
+ }
42
+
43
+ async function readRows(stateDir, stream) {
44
+ return (await readFile(join(stateDir, `${stream}.ndjson`), 'utf8')).split('\n').filter(Boolean).map(JSON.parse);
45
+ }
46
+
47
+ sqliteTest('local and cloud accounting projections agree after repeated bidirectional reconciliation', async t => {
48
+ const ctx = await setup(t);
49
+ await writeRows(ctx.stateDir, 'applications', [application]);
50
+ await writeRows(ctx.stateDir, 'delivery', [failure]);
51
+ await writeRows(ctx.stateDir, 'discovery', [qualifiedLead]);
52
+ const originalProjection = deliveryProjection([application], [failure]);
53
+ assert.equal(originalProjection.effectiveSubmissionCount, 0);
54
+ await ctx.client.reconcile({ dryRun: false });
55
+ const correction = validateDelivery({ version: 1, id: 'correction-event', applicationId: 'app', attemptId: 'initial:app', type: 'correction', supersedes: failure.id, status: 'receipt-confirmed', occurredAt: '2026-09-14T10:00:00.000Z', evidenceType: 'employer-acknowledgement', evidence: 'Synthetic employer acknowledgement corrects the matched failure.' });
56
+ await ctx.client.appendRecord('delivery', correction);
57
+ const downloaded = await ctx.client.reconcile({ dryRun: false });
58
+ assert.equal(downloaded.streams.delivery.cloudOnly, 1);
59
+ const repeated = await ctx.client.reconcile({ dryRun: false });
60
+ assert.equal(repeated.imported, 0);
61
+ assert.equal(repeated.downloaded, 0);
62
+ const localApplications = await readRows(ctx.stateDir, 'applications');
63
+ const localDelivery = await readRows(ctx.stateDir, 'delivery');
64
+ const cloudApplications = (await ctx.client.listStream('applications')).map(row => row.value);
65
+ const cloudDelivery = (await ctx.client.listStream('delivery')).map(row => row.value);
66
+ assert.equal(cloudDelivery.length, 2);
67
+ assert.deepEqual(deliveryProjection(localApplications, localDelivery), deliveryProjection(cloudApplications, cloudDelivery));
68
+ assert.equal(deliveryProjection(localApplications, localDelivery).effectiveSubmissionCount, 1);
69
+ const localDiscovery = await readRows(ctx.stateDir, 'discovery');
70
+ const cloudDiscovery = (await ctx.client.listStream('discovery')).map(row => row.value);
71
+ assert.deepEqual(discoveryProjection(localDiscovery, { roundId: 'round-1' }), discoveryProjection(cloudDiscovery, { roundId: 'round-1' }));
72
+ });
73
+
74
+ sqliteTest('default reconciliation migrates failed-email recovery history and exports one application with two delivery records', async t => {
75
+ const ctx = await setup(t);
76
+ await writeRows(ctx.stateDir, 'applications', [application]);
77
+ await writeRows(ctx.stateDir, 'delivery', [failure, retry]);
78
+ const result = await ctx.client.reconcile({ dryRun: false });
79
+ assert.equal(result.imported, 3);
80
+ const again = await ctx.client.reconcile({ dryRun: false });
81
+ assert.equal(again.imported, 0);
82
+ const exported = await ctx.client.exportTo(join(ctx.root, 'private-export.json'));
83
+ const archive = JSON.parse(await readFile(exported.path, 'utf8'));
84
+ assert.equal(archive.streams.applications.length, 1);
85
+ assert.equal(archive.streams.delivery.length, 2);
86
+ assert.ok(archive.streams.delivery.every(row => row.provenance === 'local-reconcile'));
87
+ assert.deepEqual(archive.streams.delivery.map(row => row.value), [failure, retry]);
88
+ assert.equal(deliveryProjection(archive.streams.applications.map(row => row.value), archive.streams.delivery.map(row => row.value)).effectiveSubmissionCount, 1);
89
+ if (process.platform !== 'win32') assert.equal((await stat(exported.path)).mode & 0o777, 0o600);
90
+ });
91
+
92
+ sqliteTest('private backup restores delivery attempts and projection without duplicating a second restore', async t => {
93
+ const ctx = await setup(t);
94
+ await writeRows(ctx.stateDir, 'applications', [application]);
95
+ await writeRows(ctx.stateDir, 'delivery', [failure, retry]);
96
+ await ctx.client.reconcile({ dryRun: false });
97
+ const archive = await createBackup(ctx.bindings.DB, now);
98
+ const restored = createMemoryD1(ctx.schema);
99
+ assert.equal((await restoreBackup(restored, archive)).records, 3);
100
+ assert.equal((await restoreBackup(restored, archive)).records, 0);
101
+ const values = async stream => (await restored.prepare('SELECT payload_json FROM records WHERE stream = ? ORDER BY sequence').bind(stream).all()).results.map(row => JSON.parse(row.payload_json));
102
+ const restoredApplications = await values('applications');
103
+ const restoredDelivery = await values('delivery');
104
+ assert.deepEqual(restoredDelivery, [failure, retry]);
105
+ assert.deepEqual(deliveryProjection(restoredApplications, restoredDelivery), deliveryProjection([application], [failure, retry]));
106
+ });
107
+
108
+ sqliteTest('an older backend is rejected before any local-only accounting data uploads', async t => {
109
+ const ctx = await setup(t);
110
+ await writeRows(ctx.stateDir, 'applications', [application]);
111
+ await writeRows(ctx.stateDir, 'delivery', [failure]);
112
+ const mutations = [];
113
+ const older = new CloudStateClient({ stateDir: ctx.stateDir, configPath: ctx.configPath, fetchImpl: async (input, init) => {
114
+ if (new URL(input).pathname === '/v2/status') return Response.json({ apiVersion: 2, backend: 'cloudflare-d1-r2', capabilities: [] });
115
+ if (init.method && init.method !== 'GET') mutations.push({ input, method: init.method });
116
+ return ctx.fetchImpl(input, init);
117
+ } });
118
+ await assert.rejects(() => older.reconcile({ dryRun: false }), /backend upgrade required.*application-accounting-v1/i);
119
+ assert.equal(mutations.length, 0);
120
+ assert.equal((await ctx.bindings.DB.prepare('SELECT COUNT(*) AS count FROM records').first()).count, 0);
121
+ assert.deepEqual(await readRows(ctx.stateDir, 'delivery'), [failure]);
122
+ });
123
+
124
+ sqliteTest('cached accounting permits offline research and durable observation queues while blocking transmission intents', async t => {
125
+ const ctx = await setup(t);
126
+ await ctx.client.putDocument('profile', { name: 'Synthetic Candidate', roleFamilies: ['engineering'] }, 0);
127
+ await ctx.client.refreshProfileCache();
128
+ await writeRows(ctx.stateDir, 'applications', [application]);
129
+ await ctx.client.reconcile({ dryRun: false });
130
+ const offline = new CloudStateClient({ stateDir: ctx.stateDir, configPath: ctx.configPath, fetchImpl: async () => { throw new Error('synthetic network outage'); } });
131
+ await offline.requireAccounting();
132
+ assert.equal((await readCachedCloudProfile(ctx.stateDir)).name, 'Synthetic Candidate');
133
+ assert.deepEqual(await readRows(ctx.stateDir, 'applications'), [application]);
134
+ const queued = await offline.appendRecord('delivery', failure, { queueOnFailure: true });
135
+ assert.equal(queued.queued, true);
136
+ const restarted = new CloudStateClient({ stateDir: ctx.stateDir, configPath: ctx.configPath, fetchImpl: offline.fetchImpl });
137
+ const pending = await restarted.pendingWrites();
138
+ assert.equal(pending.length, 1);
139
+ assert.deepEqual(pending[0].payload.value, failure);
140
+ assert.equal(pending[0].stream, 'delivery');
141
+ if (process.platform !== 'win32') assert.equal((await stat(join(ctx.stateDir, 'cloud-pending.ndjson'))).mode & 0o777, 0o600);
142
+ await assert.rejects(() => restarted.createIntent({ applicationId: 'another-app', canonicalUrl: 'https://example.com/jobs/another', leaseId: 'expired-lease' }), /cloud state unavailable/i);
143
+ assert.equal((await ctx.bindings.DB.prepare('SELECT COUNT(*) AS count FROM application_intents').first()).count, 0);
144
+ await ctx.client.reconcile({dryRun:false});
145
+ assert.equal((await ctx.client.pendingWrites()).length,0);
146
+ assert.equal((await readRows(ctx.stateDir,'delivery')).length,1);
147
+ });
148
+
149
+ sqliteTest('a delivery replay followed by repeated synchronization preserves one logical evidence event', async t => {
150
+ const ctx = await setup(t);
151
+ await writeRows(ctx.stateDir, 'applications', [application]);
152
+ await writeRows(ctx.stateDir, 'delivery', [failure]);
153
+ await ctx.client.reconcile({ dryRun: false });
154
+ await ctx.client.appendRecord('delivery', failure, { idempotencyKey: 'different-network-replay-id' });
155
+ const sync = await ctx.client.reconcile({ dryRun: false });
156
+ const repeated = await ctx.client.reconcile({ dryRun: false });
157
+ assert.equal(sync.imported + sync.downloaded + repeated.imported + repeated.downloaded, 0);
158
+ assert.equal((await ctx.client.listStream('delivery')).length, 1);
159
+ assert.equal((await readRows(ctx.stateDir, 'delivery')).length, 1);
160
+ assert.equal(deliveryProjection([application], await readRows(ctx.stateDir, 'delivery')).failedDeliveryCount, 1);
161
+ });
162
+
163
+ sqliteTest('cross-client JSON field ordering does not duplicate the same delivery event during synchronization', async t => {
164
+ const ctx = await setup(t);
165
+ await writeRows(ctx.stateDir, 'applications', [application]);
166
+ await ctx.client.reconcile({ dryRun: false });
167
+ await writeRows(ctx.stateDir, 'delivery', [failure]);
168
+ const reorderedFailure = Object.fromEntries(Object.entries(failure).reverse());
169
+ await ctx.client.appendRecord('delivery', reorderedFailure);
170
+ await ctx.client.reconcile({ dryRun: false });
171
+ await ctx.client.reconcile({ dryRun: false });
172
+ assert.equal((await ctx.client.listStream('delivery')).length, 1);
173
+ assert.equal((await readRows(ctx.stateDir, 'delivery')).length, 1);
174
+ });
175
+
176
+ sqliteTest('unfamiliar versioned discovery records survive synchronization unchanged', async t => {
177
+ const ctx = await setup(t);
178
+ const legacy = {version:7,type:'legacy-discovery-report',id:'legacy',roundId:'old',results:{reviewed:12}};
179
+ await writeRows(ctx.stateDir,'discovery',[legacy]);
180
+ await ctx.client.reconcile({dryRun:false});
181
+ assert.deepEqual((await ctx.client.listStream('discovery'))[0].value,legacy);
182
+ assert.deepEqual(discoveryProjection(await readRows(ctx.stateDir,'discovery')).leads,[]);
183
+ });