dsh-data-cleaning-agent 0.9.13 → 0.9.15

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/lib/workflow.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  normalizeFieldSelection,
14
14
  normalizeWorkflowDraft,
15
15
  normalizeWorkflowOrigin,
16
+ requiresQcc,
16
17
  validateMappings,
17
18
  } from './workflow-contract.js';
18
19
 
@@ -106,6 +107,30 @@ function sanitizeArtifact(value = {}, timestamp) {
106
107
  };
107
108
  }
108
109
 
110
+ function sanitizeArtifacts(input, timestamp) {
111
+ const incoming = Array.isArray(input.artifacts) ? input.artifacts : [input.artifact];
112
+ const artifacts = incoming.filter(Boolean).map((artifact) => sanitizeArtifact(artifact, timestamp));
113
+ if (!artifacts.length || artifacts.some((artifact) => !artifact.id)) {
114
+ throw new WorkflowError('DC_WORKFLOW_ARTIFACT_REQUIRED', '至少需要一个 Host 制品引用。', 400);
115
+ }
116
+ return artifacts;
117
+ }
118
+
119
+ function safeMatchSummary(value) {
120
+ const summary = safeSummary(value, ['total', 'exact', 'candidate', 'confirmed', 'unresolved', 'failed', 'reviewRequired']);
121
+ assertSummaryTotal(summary, ['exact', 'candidate', 'confirmed', 'unresolved', 'failed'], 'DC_WORKFLOW_MATCH_SUMMARY');
122
+ if (summary.reviewRequired > summary.candidate) {
123
+ throw new WorkflowError('DC_WORKFLOW_MATCH_SUMMARY', '待核验数量不能超过候选数量。', 400);
124
+ }
125
+ return summary;
126
+ }
127
+
128
+ function safeEnrichmentSummary(value) {
129
+ const summary = safeSummary(value, ['total', 'completed', 'unchanged', 'failed', 'reviewRequired', 'callsUsed']);
130
+ assertSummaryTotal(summary, ['completed', 'unchanged', 'failed', 'reviewRequired'], 'DC_WORKFLOW_ENRICH_SUMMARY');
131
+ return summary;
132
+ }
133
+
109
134
  function nowIso() {
110
135
  return new Date().toISOString();
111
136
  }
@@ -271,7 +296,7 @@ export class DataCleaningWorkflowStore {
271
296
  mappings,
272
297
  matchRules: input.matchRules ?? record.matchRules,
273
298
  });
274
- return { ...draft, state: 'rules_confirmed', stage: 'match', error: null };
299
+ return { ...draft, state: 'rules_confirmed', stage: 'rules', error: null };
275
300
  });
276
301
  }
277
302
 
@@ -283,7 +308,7 @@ export class DataCleaningWorkflowStore {
283
308
  if (record.state !== 'diagnosed') contractCall(() => assertWorkflowTransition(record.state, 'diagnosed'));
284
309
  return {
285
310
  state: 'diagnosed',
286
- stage: 'match',
311
+ stage: requiresQcc(record) ? 'match' : 'enrich',
287
312
  error: null,
288
313
  qualitySummary: safeSummary(input.summary, [
289
314
  'total', 'valid', 'missingAnchor', 'duplicates', 'invalidCreditNo', 'invalidPhone', 'emptyFields',
@@ -304,11 +329,7 @@ export class DataCleaningWorkflowStore {
304
329
  if (!['rules_confirmed', 'diagnosed', 'matching', 'review_required', 'partial'].includes(record.state)) {
305
330
  throw new WorkflowError('DC_WORKFLOW_MATCH_STATE', 'Match summary cannot be recorded in the current state.', 409);
306
331
  }
307
- const summary = safeSummary(input.summary, ['total', 'exact', 'candidate', 'confirmed', 'unresolved', 'failed', 'reviewRequired']);
308
- assertSummaryTotal(summary, ['exact', 'candidate', 'confirmed', 'unresolved', 'failed'], 'DC_WORKFLOW_MATCH_SUMMARY');
309
- if (summary.reviewRequired > summary.candidate) {
310
- throw new WorkflowError('DC_WORKFLOW_MATCH_SUMMARY', 'Review-required count cannot exceed candidate count.', 400);
311
- }
332
+ const summary = safeMatchSummary(input.summary);
312
333
  const nextState = summary.reviewRequired > 0 ? 'review_required' : 'matched';
313
334
  if (record.state !== nextState) contractCall(() => assertWorkflowTransition(record.state, nextState));
314
335
  return {
@@ -343,9 +364,11 @@ export class DataCleaningWorkflowStore {
343
364
  if (!['matched', 'enriching', 'partial', 'authorization_required'].includes(record.state)) {
344
365
  throw new WorkflowError('DC_WORKFLOW_ENRICH_STATE', 'Enrichment summary cannot be recorded in the current state.', 409);
345
366
  }
346
- const summary = safeSummary(input.summary, ['total', 'completed', 'unchanged', 'failed', 'reviewRequired', 'callsUsed']);
347
- assertSummaryTotal(summary, ['completed', 'unchanged', 'failed', 'reviewRequired'], 'DC_WORKFLOW_ENRICH_SUMMARY');
367
+ const summary = safeEnrichmentSummary(input.summary);
348
368
  const nextState = summary.failed > 0 || summary.reviewRequired > 0 ? 'partial' : 'export_ready';
369
+ if (nextState === 'partial' && !record.artifacts.length) {
370
+ throw new WorkflowError('DC_WORKFLOW_PARTIAL_ARTIFACT_REQUIRED', '部分完成任务必须先生成可下载制品。', 409);
371
+ }
349
372
  if (record.state !== nextState) contractCall(() => assertWorkflowTransition(record.state, nextState));
350
373
  return {
351
374
  state: nextState,
@@ -385,6 +408,143 @@ export class DataCleaningWorkflowStore {
385
408
  });
386
409
  }
387
410
 
411
+ async completeLocalDelivery(id, input = {}) {
412
+ return this.mutate(id, input.expectedRevision, (record) => {
413
+ if (record.state !== 'diagnosed' || requiresQcc(record)) {
414
+ throw new WorkflowError('DC_WORKFLOW_LOCAL_EXPORT_STATE', '只有已完成体检的本地任务可以生成结果。', 409);
415
+ }
416
+ contractCall(() => assertWorkflowTransition(record.state, 'completed'));
417
+ const total = integer(input.summary?.total ?? record.source?.rowCount);
418
+ const summary = {
419
+ total,
420
+ completed: integer(input.summary?.completed ?? total),
421
+ unchanged: integer(input.summary?.unchanged),
422
+ failed: integer(input.summary?.failed),
423
+ reviewRequired: 0,
424
+ callsUsed: 0,
425
+ };
426
+ assertSummaryTotal(summary, ['completed', 'unchanged', 'failed'], 'DC_WORKFLOW_ENRICH_SUMMARY');
427
+ const timestamp = this.nowFn();
428
+ const artifacts = sanitizeArtifacts(input, timestamp);
429
+ return {
430
+ state: 'completed',
431
+ stage: 'download',
432
+ enrichmentSummary: summary,
433
+ artifacts: [...record.artifacts, ...artifacts].slice(-20),
434
+ error: null,
435
+ completedAt: timestamp,
436
+ };
437
+ });
438
+ }
439
+
440
+ async completeQccDelivery(id, input = {}) {
441
+ return this.mutate(id, input.expectedRevision, (record) => {
442
+ if (!['matching', 'review_required'].includes(record.state)) {
443
+ throw new WorkflowError('DC_WORKFLOW_DELIVERY_STATE', '当前任务不能提交企查查结果。', 409);
444
+ }
445
+ const matchSummary = safeMatchSummary(input.matchSummary);
446
+ if (matchSummary.reviewRequired > 0) {
447
+ throw new WorkflowError('DC_WORKFLOW_REVIEW_REQUIRED', '请先完成企业主体候选核验。', 409);
448
+ }
449
+ const enrichmentSummary = safeEnrichmentSummary(input.enrichmentSummary);
450
+ const nextState = enrichmentSummary.failed > 0 || enrichmentSummary.reviewRequired > 0 ? 'partial' : 'completed';
451
+ contractCall(() => assertWorkflowTransition(record.state, nextState));
452
+ const timestamp = this.nowFn();
453
+ const artifacts = sanitizeArtifacts(input, timestamp);
454
+ return {
455
+ state: nextState,
456
+ stage: 'download',
457
+ matchSummary,
458
+ enrichmentSummary,
459
+ qccRunId: safeText(input.qccRunId, 160) || record.qccRunId,
460
+ artifacts: [...record.artifacts, ...artifacts].slice(-20),
461
+ error: null,
462
+ completedAt: timestamp,
463
+ };
464
+ });
465
+ }
466
+
467
+ async recordRetryReview(id, input = {}) {
468
+ return this.mutate(id, input.expectedRevision, (record) => {
469
+ if (record.state !== 'partial') {
470
+ throw new WorkflowError('DC_WORKFLOW_RETRY_STATE', '只有部分完成任务可以进入重试候选核验。', 409);
471
+ }
472
+ const summary = safeMatchSummary(input.summary);
473
+ if (summary.reviewRequired === 0) {
474
+ throw new WorkflowError('DC_WORKFLOW_REVIEW_REQUIRED', '重试结果没有待核验企业主体。', 400);
475
+ }
476
+ contractCall(() => assertWorkflowTransition(record.state, 'review_required'));
477
+ return {
478
+ state: 'review_required',
479
+ stage: 'match',
480
+ matchSummary: summary,
481
+ qccRunId: safeText(input.qccRunId, 160) || record.qccRunId,
482
+ error: null,
483
+ };
484
+ });
485
+ }
486
+
487
+ async completeRetryDelivery(id, input = {}) {
488
+ return this.mutate(id, input.expectedRevision, (record) => {
489
+ const recoverableFailure = record.state === 'failed'
490
+ && record.error?.code === 'DC_DELIVERY_FAILED'
491
+ && record.qccRunId;
492
+ if (record.state !== 'partial' && record.state !== 'export_ready' && !recoverableFailure) {
493
+ throw new WorkflowError('DC_WORKFLOW_RETRY_STATE', '当前任务不能重试结果交付。', 409);
494
+ }
495
+ const matchSummary = safeMatchSummary(input.matchSummary ?? record.matchSummary);
496
+ if (matchSummary.reviewRequired > 0) {
497
+ throw new WorkflowError('DC_WORKFLOW_REVIEW_REQUIRED', '请先完成企业主体候选核验。', 409);
498
+ }
499
+ const enrichmentSummary = safeEnrichmentSummary(input.enrichmentSummary ?? record.enrichmentSummary);
500
+ const nextState = enrichmentSummary.failed > 0 || enrichmentSummary.reviewRequired > 0 ? 'partial' : 'completed';
501
+ if (record.state !== nextState) contractCall(() => assertWorkflowTransition(record.state, nextState));
502
+ const timestamp = this.nowFn();
503
+ const artifacts = sanitizeArtifacts(input, timestamp);
504
+ return {
505
+ state: nextState,
506
+ stage: 'download',
507
+ matchSummary,
508
+ enrichmentSummary,
509
+ qccRunId: safeText(input.qccRunId, 160) || record.qccRunId,
510
+ artifacts: [...record.artifacts, ...artifacts].slice(-20),
511
+ error: null,
512
+ completedAt: timestamp,
513
+ };
514
+ });
515
+ }
516
+
517
+ async recordRetryFailure(id, input = {}) {
518
+ return this.mutate(id, input.expectedRevision, (record) => {
519
+ if (record.state !== 'partial') {
520
+ throw new WorkflowError('DC_WORKFLOW_RETRY_STATE', '只有部分完成任务可以记录重试错误。', 409);
521
+ }
522
+ return {
523
+ state: 'partial',
524
+ stage: 'download',
525
+ error: { code: safeText(input.code, 80) || 'DC_RETRY_FAILED' },
526
+ };
527
+ });
528
+ }
529
+
530
+ async recordDeliveryFailure(id, input = {}) {
531
+ return this.mutate(id, input.expectedRevision, (record) => {
532
+ if (!['matching', 'review_required', 'export_ready'].includes(record.state)) {
533
+ throw new WorkflowError('DC_WORKFLOW_DELIVERY_STATE', '当前任务不能记录结果交付失败。', 409);
534
+ }
535
+ const nextState = record.artifacts.length ? 'partial' : 'failed';
536
+ if (record.state !== nextState) contractCall(() => assertWorkflowTransition(record.state, nextState));
537
+ return {
538
+ state: nextState,
539
+ stage: 'download',
540
+ matchSummary: input.matchSummary ? safeMatchSummary(input.matchSummary) : record.matchSummary,
541
+ enrichmentSummary: input.enrichmentSummary ? safeEnrichmentSummary(input.enrichmentSummary) : record.enrichmentSummary,
542
+ qccRunId: safeText(input.qccRunId, 160) || record.qccRunId,
543
+ error: { code: 'DC_DELIVERY_FAILED' },
544
+ };
545
+ });
546
+ }
547
+
388
548
  async recordExport(id, input = {}) {
389
549
  return this.mutate(id, input.expectedRevision, (record) => {
390
550
  if (!['export_ready', 'partial'].includes(record.state)) {
@@ -392,11 +552,7 @@ export class DataCleaningWorkflowStore {
392
552
  }
393
553
  contractCall(() => assertWorkflowTransition(record.state, 'completed'));
394
554
  const timestamp = this.nowFn();
395
- const incoming = Array.isArray(input.artifacts) ? input.artifacts : [input.artifact];
396
- const artifacts = incoming.filter(Boolean).map((artifact) => sanitizeArtifact(artifact, timestamp));
397
- if (!artifacts.length || artifacts.some((artifact) => !artifact.id)) {
398
- throw new WorkflowError('DC_WORKFLOW_ARTIFACT_REQUIRED', 'At least one Host artifact reference is required.', 400);
399
- }
555
+ const artifacts = sanitizeArtifacts(input, timestamp);
400
556
  return {
401
557
  state: input.preservePartial === true && record.state === 'partial' ? 'partial' : 'completed',
402
558
  stage: 'download',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-data-cleaning-agent",
3
- "version": "0.9.13",
3
+ "version": "0.9.15",
4
4
  "description": "Data cleaning and data enrichment for CSV/XLSX/JSON enterprise lists in DeepSeek Harness, including spreadsheet cleaning, deduplication, profiling, optional Qichacha MCP and exports.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -22,6 +22,8 @@
22
22
  "docs/USER-GUIDE.md",
23
23
  "docs/RELEASE-0.9.11.md",
24
24
  "docs/RELEASE-0.9.13.md",
25
+ "docs/RELEASE-0.9.14.md",
26
+ "docs/RELEASE-0.9.15.md",
25
27
  "docs/UNIFIED-HOME-1.5.4-ADOPTION.md",
26
28
  "docs/FIRST-CONTRIBUTION.md",
27
29
  "docs/COMPATIBILITY.md",