ultra-builder-pro-cli 0.24.0 → 0.24.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.
@@ -660,27 +660,10 @@ function storedReadyInvariantBlockers(baseline) {
660
660
  if (!Array.isArray(baseline.scope) || baseline.scope.length === 0) {
661
661
  blockers.push('BASELINE_SCOPE_MISSING');
662
662
  }
663
- if (Array.isArray(baseline.spec_refs)) {
664
- for (const kind of ['discovery', 'product', 'architecture']) {
665
- if (!baseline.spec_refs.some((item) => item?.kind === kind)) {
666
- blockers.push(`BASELINE_SPEC_MISSING:${kind}`);
667
- }
668
- }
669
- }
670
- if (!Array.isArray(baseline.evidence) || baseline.evidence.length === 0) {
671
- blockers.push('BASELINE_EVIDENCE_MISSING');
672
- } else if (baseline.mode === 'brownfield'
673
- && !baseline.evidence.some((item) => item?.kind === 'source')) {
674
- blockers.push('BASELINE_SOURCE_EVIDENCE_MISSING');
675
- }
676
- if (!Array.isArray(baseline.verification) || baseline.verification.length === 0) {
677
- blockers.push('BASELINE_VERIFICATION_MISSING');
678
- } else {
663
+ if (Array.isArray(baseline.verification)) {
679
664
  for (const item of baseline.verification) {
680
665
  if (!item || typeof item !== 'object') {
681
666
  blockers.push('BASELINE_STATE_INVALID:verification');
682
- } else if (item.status === 'not_run') {
683
- blockers.push(`BASELINE_VERIFICATION_NOT_RUN:${item.name || 'unknown'}`);
684
667
  } else if (item.status === 'known_red') {
685
668
  if (!String(item.rationale || '').trim()) {
686
669
  blockers.push(`BASELINE_KNOWN_RED_RATIONALE_MISSING:${item.name || 'unknown'}`);
@@ -688,7 +671,7 @@ function storedReadyInvariantBlockers(baseline) {
688
671
  if (!baseline.known_red_accepted) {
689
672
  blockers.push(`BASELINE_KNOWN_RED_NOT_ACCEPTED:${item.name || 'unknown'}`);
690
673
  }
691
- } else if (item.status !== 'pass') {
674
+ } else if (!['pass', 'not_run'].includes(item.status)) {
692
675
  blockers.push(`BASELINE_VERIFICATION_STATUS_INVALID:${item.name || 'unknown'}`);
693
676
  }
694
677
  }
@@ -697,25 +680,54 @@ function storedReadyInvariantBlockers(baseline) {
697
680
  blockers.push('BASELINE_REVISION_MISSING');
698
681
  }
699
682
  if (!String(baseline.approved_by || '').trim()) blockers.push('BASELINE_APPROVER_MISSING');
700
- if (String(baseline.approval_note || '').trim().length < 3) {
683
+ if (!String(baseline.approval_note || '').trim()) {
701
684
  blockers.push('BASELINE_APPROVAL_NOTE_MISSING');
702
685
  }
703
686
  if (!baseline.converged_at || Number.isNaN(Date.parse(baseline.converged_at))) {
704
687
  blockers.push('BASELINE_CONVERGENCE_TIMESTAMP_MISSING');
705
688
  }
689
+ return [...new Set(blockers)];
690
+ }
691
+
692
+ function storedReadySemanticWarnings(baseline) {
693
+ const warnings = [];
694
+ if (Array.isArray(baseline.spec_refs)) {
695
+ for (const kind of ['discovery', 'product', 'architecture']) {
696
+ if (!baseline.spec_refs.some((item) => item?.kind === kind)) {
697
+ warnings.push(`BASELINE_SPEC_MISSING:${kind}`);
698
+ }
699
+ }
700
+ }
701
+ if (Array.isArray(baseline.evidence) && baseline.evidence.length === 0) {
702
+ warnings.push('BASELINE_EVIDENCE_MISSING');
703
+ }
704
+ if (baseline.mode === 'brownfield'
705
+ && Array.isArray(baseline.evidence)
706
+ && !baseline.evidence.some((item) => item?.kind === 'source')) {
707
+ warnings.push('BASELINE_SOURCE_EVIDENCE_MISSING');
708
+ }
709
+ if (Array.isArray(baseline.verification) && baseline.verification.length === 0) {
710
+ warnings.push('BASELINE_VERIFICATION_MISSING');
711
+ } else if (Array.isArray(baseline.verification)) {
712
+ for (const item of baseline.verification) {
713
+ if (item?.status === 'not_run') {
714
+ warnings.push(`BASELINE_VERIFICATION_NOT_RUN:${item.name || 'unknown'}`);
715
+ }
716
+ }
717
+ }
706
718
  if (Array.isArray(baseline.unknowns)) {
707
719
  for (const item of baseline.unknowns.filter((unknown) => unknown?.blocking === true)) {
708
- blockers.push(`BASELINE_UNKNOWN_BLOCKING:${item.summary || 'unknown'}`);
720
+ warnings.push(`BASELINE_UNKNOWN_RECORDED:${item.summary || 'unknown'}`);
709
721
  }
710
722
  }
711
723
  if (Array.isArray(baseline.gaps)) {
712
724
  for (const gap of baseline.gaps.filter((item) => (
713
725
  item?.status === 'open' && item?.blocking === true
714
726
  ))) {
715
- blockers.push(`BASELINE_GAP_BLOCKING:${gap.id || 'unknown'}`);
727
+ warnings.push(`BASELINE_GAP_RECORDED:${gap.id || 'unknown'}`);
716
728
  }
717
729
  }
718
- return [...new Set(blockers)];
730
+ return [...new Set(warnings)];
719
731
  }
720
732
 
721
733
  function acceptedBaselineResearch(db, baseline) {
@@ -736,7 +748,7 @@ function convergenceResearchBlockers(db, baseline) {
736
748
  function storedResearchHealth(db, baseline) {
737
749
  const checkpointId = baseline.research_checkpoint_id || baseline.research_run_id;
738
750
  if (!checkpointId) {
739
- return { blockers: ['BASELINE_RESEARCH_PROVENANCE_MISSING'], warnings: [] };
751
+ return { blockers: [], warnings: ['BASELINE_RESEARCH_PROVENANCE_MISSING'] };
740
752
  }
741
753
  let checkpoint;
742
754
  try {
@@ -751,10 +763,19 @@ function storedResearchHealth(db, baseline) {
751
763
  || checkpoint.stage !== 'research'
752
764
  || checkpoint.scope_type !== 'baseline'
753
765
  || checkpoint.scope_id !== baseline.id
754
- || checkpoint.status !== 'accepted'
755
766
  || checkpoints.checkpointDigest(checkpoint) !== checkpoint.digest) {
756
767
  return { blockers: ['BASELINE_RESEARCH_RECORD_INVALID'], warnings: [] };
757
768
  }
769
+ if (checkpoint.status !== 'accepted') {
770
+ return {
771
+ blockers: [],
772
+ warnings: [
773
+ checkpoint.status === 'superseded'
774
+ ? 'BASELINE_RESEARCH_SUPERSEDED'
775
+ : `BASELINE_RESEARCH_NOT_ACCEPTED:${checkpoint.status}`,
776
+ ],
777
+ };
778
+ }
758
779
  return { blockers: [], warnings: checkpoint.diagnostics || [] };
759
780
  }
760
781
 
@@ -774,10 +795,6 @@ function convergenceBlockers(db, baseline, input, rootDir) {
774
795
  if (baseline.repository_branch && snapshot.branch
775
796
  && baseline.repository_branch !== snapshot.branch) blockers.add('BASELINE_BRANCH_STALE');
776
797
  if (!Array.isArray(baseline.scope) || baseline.scope.length === 0) blockers.add('BASELINE_SCOPE_MISSING');
777
- for (const kind of ['discovery', 'product', 'architecture']) {
778
- if (!baseline.spec_refs.some((ref) => ref.kind === kind)) blockers.add(`BASELINE_SPEC_MISSING:${kind}`);
779
- }
780
- for (const blocker of convergenceResearchBlockers(db, baseline)) blockers.add(blocker);
781
798
  for (const blocker of storedSpecBlockers(baseline, rootDir)) blockers.add(blocker);
782
799
  for (const blocker of storedEvidenceBlockers(baseline, rootDir)) blockers.add(blocker);
783
800
  for (const blocker of storedScopeBlockers(baseline, rootDir)) blockers.add(blocker);
@@ -788,29 +805,23 @@ function convergenceBlockers(db, baseline, input, rootDir) {
788
805
  if (baseline.worktree_state === 'dirty' && input.accept_dirty_worktree !== true) {
789
806
  blockers.add('BASELINE_DIRTY_WORKTREE_NOT_ACCEPTED');
790
807
  }
791
- if (baseline.evidence.length === 0) blockers.add('BASELINE_EVIDENCE_MISSING');
792
- if (baseline.mode === 'brownfield' && !baseline.evidence.some((item) => item.kind === 'source')) {
793
- blockers.add('BASELINE_SOURCE_EVIDENCE_MISSING');
794
- }
795
- if (baseline.verification.length === 0) blockers.add('BASELINE_VERIFICATION_MISSING');
796
808
  for (const item of baseline.verification) {
797
- if (item.status === 'not_run') blockers.add(`BASELINE_VERIFICATION_NOT_RUN:${item.name}`);
798
809
  if (item.status === 'known_red') {
799
810
  if (input.accept_known_red !== true) blockers.add(`BASELINE_KNOWN_RED_NOT_ACCEPTED:${item.name}`);
800
811
  if (!item.rationale) blockers.add(`BASELINE_KNOWN_RED_RATIONALE_MISSING:${item.name}`);
801
812
  }
802
813
  }
803
- for (const item of baseline.unknowns.filter((unknown) => unknown.blocking)) {
804
- blockers.add(`BASELINE_UNKNOWN_BLOCKING:${item.summary}`);
805
- }
806
- for (const gap of baseline.gaps.filter((item) => item.status === 'open' && item.blocking)) {
807
- blockers.add(`BASELINE_GAP_BLOCKING:${gap.id}`);
808
- }
809
814
  if (!String(input.approved_by || '').trim()) blockers.add('BASELINE_APPROVER_REQUIRED');
810
- if (String(input.approval_note || '').trim().length < 3) blockers.add('BASELINE_APPROVAL_NOTE_REQUIRED');
815
+ if (!String(input.approval_note || '').trim()) blockers.add('BASELINE_APPROVAL_NOTE_REQUIRED');
811
816
  return [...blockers].sort();
812
817
  }
813
818
 
819
+ function convergenceWarnings(db, baseline) {
820
+ const warnings = new Set(storedReadySemanticWarnings(baseline));
821
+ for (const warning of convergenceResearchBlockers(db, baseline)) warnings.add(warning);
822
+ return [...warnings].sort();
823
+ }
824
+
814
825
  function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = true } = {}) {
815
826
  const current = readBaseline(db, input.id);
816
827
  if (!current) throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id} not found`);
@@ -818,6 +829,7 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
818
829
  throw new BaselineWorkflowError('BASELINE_NOT_CONVERGEABLE', `baseline ${current.id} is ${current.status}`);
819
830
  }
820
831
  const blockers = convergenceBlockers(db, current, input, rootDir);
832
+ const warnings = convergenceWarnings(db, current);
821
833
  const ts = nowIso();
822
834
  if (blockers.length > 0) {
823
835
  ops.tx(db, () => {
@@ -829,7 +841,13 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
829
841
  });
830
842
  }
831
843
  });
832
- return { ready: false, status: 'blocked', blockers, baseline: readBaseline(db, current.id) };
844
+ return {
845
+ ready: false,
846
+ status: 'blocked',
847
+ blockers,
848
+ warnings,
849
+ baseline: readBaseline(db, current.id),
850
+ };
833
851
  }
834
852
  const research = acceptedBaselineResearch(db, current);
835
853
  ops.tx(db, () => {
@@ -841,7 +859,7 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
841
859
  ).run(
842
860
  String(input.approved_by).trim(), String(input.approval_note).trim(),
843
861
  input.accept_dirty_worktree === true ? 1 : 0,
844
- input.accept_known_red === true ? 1 : 0, research.id, ts, ts, current.id,
862
+ input.accept_known_red === true ? 1 : 0, research?.id || null, ts, ts, current.id,
845
863
  );
846
864
  if (emitEvent) {
847
865
  ops.appendEventInTx(db, {
@@ -849,7 +867,13 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
849
867
  });
850
868
  }
851
869
  });
852
- return { ready: true, status: 'ready', blockers: [], baseline: readBaseline(db, current.id) };
870
+ return {
871
+ ready: true,
872
+ status: 'ready',
873
+ blockers: [],
874
+ warnings,
875
+ baseline: readBaseline(db, current.id),
876
+ };
853
877
  }
854
878
 
855
879
  function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
@@ -880,6 +904,10 @@ function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
880
904
  ...(Array.isArray(baseline.evidence) ? storedEvidenceBlockers(baseline, rootDir) : []),
881
905
  ...research.blockers,
882
906
  ];
907
+ const warnings = [
908
+ ...storedReadySemanticWarnings(baseline),
909
+ ...research.warnings,
910
+ ];
883
911
  const snapshot = gitWorktreeSnapshot(
884
912
  rootDir, Array.isArray(baseline.scope) && baseline.scope.length > 0 ? baseline.scope : ['.'],
885
913
  );
@@ -909,7 +937,7 @@ function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
909
937
  }
910
938
  return {
911
939
  status: blockers.length === 0 ? 'pass' : 'fail', blockers: [...new Set(blockers)].sort(),
912
- warnings: research.warnings, baseline,
940
+ warnings: [...new Set(warnings)], baseline,
913
941
  };
914
942
  }
915
943
 
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+
5
+ function parseJson(value, fallback = {}) {
6
+ if (typeof value !== 'string' || !value.trim()) return fallback;
7
+ try { return JSON.parse(value); } catch { return fallback; }
8
+ }
9
+
10
+ function changeStateDigest(change) {
11
+ if (!change) return null;
12
+ const payload = {
13
+ id: change.id,
14
+ kind: change.kind,
15
+ intent: change.intent,
16
+ docs_impact: change.docs_impact || parseJson(change.docs_impact_json, {}),
17
+ provider_refs: change.provider_refs || parseJson(change.provider_refs_json, {}),
18
+ contract: change.contract || parseJson(change.contract_json, {}),
19
+ classification: change.classification || parseJson(change.classification_json, {}),
20
+ research_disposition: change.research_disposition
21
+ || parseJson(change.research_disposition_json, {}),
22
+ };
23
+ return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
24
+ }
25
+
26
+ module.exports = { changeStateDigest };
@@ -9,6 +9,7 @@ const Ajv = require('ajv/dist/2020');
9
9
  const ops = require('./state-ops.cjs');
10
10
  const artifacts = require('./artifact-registry.cjs');
11
11
  const canonical = require('./canonical-json.cjs');
12
+ const changeAuthority = require('./change-authority.cjs');
12
13
  const { writeManagedFile } = require('./managed-file-write.cjs');
13
14
  const providerRefs = require('./provider-refs.cjs');
14
15
  const baselines = require('./baseline-workflow.cjs');
@@ -30,10 +31,6 @@ function loadLegacyModule(filename) {
30
31
  return module.require(path.join(__dirname, filename));
31
32
  }
32
33
 
33
- function legacyContextSpine() {
34
- return loadLegacyModule('context-spine.cjs');
35
- }
36
-
37
34
  function legacySpecLearning() {
38
35
  return loadLegacyModule('spec-learning.cjs');
39
36
  }
@@ -46,6 +43,10 @@ function legacyDecisions() {
46
43
  return loadLegacyModule('decision-dialogue.cjs');
47
44
  }
48
45
 
46
+ function legacyContextSpine() {
47
+ return loadLegacyModule('context-spine.cjs');
48
+ }
49
+
49
50
  const CHANGE_ID = /^[a-zA-Z0-9_-]+$/;
50
51
  const CHANGE_PATCH_FIELDS = new Set([
51
52
  'title', 'intent', 'status', 'docs_impact', 'provider_refs',
@@ -1191,8 +1192,8 @@ function updateChange(db, id, patch = {}, {
1191
1192
  const semanticFields = Object.keys(patch)
1192
1193
  .filter((field) => SEMANTIC_AUTHORITY_FIELDS.has(field));
1193
1194
  if (semanticFields.length > 0
1194
- && legacyContextSpine().changeStateDigest(current)
1195
- !== legacyContextSpine().changeStateDigest(updated)) {
1195
+ && changeAuthority.changeStateDigest(current)
1196
+ !== changeAuthority.changeStateDigest(updated)) {
1196
1197
  invalidated.push(...artifacts.invalidateConsumersFromEndpointInTx(
1197
1198
  db,
1198
1199
  { type: 'change', id },
@@ -1354,6 +1355,60 @@ function generatePacketReconciliation(db, change, packet, rootDir) {
1354
1355
  }
1355
1356
  }
1356
1357
 
1358
+ function generateCallerNoChangeReconciliation(db, change, input, rootDir) {
1359
+ const baseline = baselines.readBaseline(db);
1360
+ const manifest = {
1361
+ $schema: 'ultra-baseline-reconciliation-v1',
1362
+ change_id: change.id,
1363
+ baseline_id: baseline?.id || null,
1364
+ baseline_updates: [],
1365
+ semantic_changes: [],
1366
+ semantic_no_change_reason: String(input.no_baseline_change_reason).trim(),
1367
+ resolved_gap_ids: [],
1368
+ resolved_unknowns: [],
1369
+ verification: [{
1370
+ name: 'explicit archive handoff',
1371
+ command: 'ultra.archive',
1372
+ status: 'pass',
1373
+ evidence: String(input.summary).trim(),
1374
+ }],
1375
+ };
1376
+ const relative = `${change.artifact_root}/baseline-reconciliation.json`;
1377
+ const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
1378
+ const file = safeRelativePath(rootDir, relative);
1379
+ const previous = fs.existsSync(file) ? fs.readFileSync(file) : null;
1380
+ const published = writeManagedFile(rootDir, relative, Buffer.from(serialized));
1381
+ try {
1382
+ const recorded = artifacts.recordArtifact(db, {
1383
+ id: `${change.id}-baseline-reconciliation`,
1384
+ owner_type: 'change',
1385
+ owner_id: change.id,
1386
+ kind: 'baseline_reconciliation',
1387
+ path: relative,
1388
+ content_digest: published.digest,
1389
+ source_refs: [],
1390
+ consumer_refs: [],
1391
+ provenance: { writer: 'ultra.archive', generated: true },
1392
+ metadata: {
1393
+ terminal_role: true,
1394
+ schema: manifest.$schema,
1395
+ caller_declared: true,
1396
+ },
1397
+ }, { rootDir }).artifact;
1398
+ return {
1399
+ manifest,
1400
+ relative,
1401
+ file,
1402
+ digest: recorded.digest,
1403
+ artifact: recorded,
1404
+ };
1405
+ } catch (error) {
1406
+ if (previous === null) fs.rmSync(file, { force: true });
1407
+ else fs.writeFileSync(file, previous);
1408
+ throw error;
1409
+ }
1410
+ }
1411
+
1357
1412
  function baselineConvergenceBlockers(db, change, rootDir) {
1358
1413
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
1359
1414
  return legacyContextSpine().baselineGateForChange(db, change, baselineHealth).blockers;
@@ -2487,35 +2542,6 @@ function archiveChange(db, input, {
2487
2542
  }
2488
2543
 
2489
2544
  let packet = null;
2490
- if (kernelMode) {
2491
- const requiredStages = ['plan', 'test', 'review', 'deliver'];
2492
- const missing = requiredStages.filter((stage) => !stageCheckpoints.currentCheckpoint(
2493
- db,
2494
- stage,
2495
- { change_id: change.id },
2496
- { includeDraft: false },
2497
- ));
2498
- const tasks = ops.listTasks(db, { change_id: change.id });
2499
- if (tasks.length === 0) missing.push('tasks');
2500
- for (const task of tasks.filter((item) => item.status !== 'expanded')) {
2501
- if (task.status !== 'completed') missing.push(`task:${task.id}:completed`);
2502
- if (!stageCheckpoints.currentCheckpoint(
2503
- db,
2504
- 'dev',
2505
- { task_id: task.id },
2506
- { includeDraft: false },
2507
- )) {
2508
- missing.push(`task:${task.id}:dev`);
2509
- }
2510
- }
2511
- if (missing.length > 0) {
2512
- throw new ChangeWorkflowError(
2513
- 'CHANGE_EVIDENCE_INCOMPLETE',
2514
- `change ${change.id} lacks accepted delivery checkpoints`,
2515
- { missing },
2516
- );
2517
- }
2518
- }
2519
2545
  if (changePacket.readDeltaAuthority(db, change.id)) {
2520
2546
  if (!kernelMode) {
2521
2547
  legacyWorkflows().validateDeliveryPrerequisites(
@@ -2553,7 +2579,14 @@ function archiveChange(db, input, {
2553
2579
 
2554
2580
  const reconciliation = packet
2555
2581
  ? generatePacketReconciliation(db, change, packet, rootDir)
2556
- : readReconciliationManifest(db, change, input, rootDir);
2582
+ : (
2583
+ kernelMode && updates.length === 0 && !String(input.reconciliation_path || '').trim()
2584
+ ? generateCallerNoChangeReconciliation(db, change, {
2585
+ summary: input.summary,
2586
+ no_baseline_change_reason: noChangeReason,
2587
+ }, rootDir)
2588
+ : readReconciliationManifest(db, change, input, rootDir)
2589
+ );
2557
2590
  if (kernelMode) {
2558
2591
  materializeKernelArchiveAuthority(db, change, rootDir);
2559
2592
  }
@@ -21,7 +21,7 @@ const STATUS_TRANSITIONS = Object.freeze({
21
21
  in_progress: new Set(['completed', 'blocked', 'pending']),
22
22
  blocked: new Set(['pending', 'in_progress']),
23
23
  expanded: new Set(['completed']),
24
- completed: new Set(),
24
+ completed: new Set(['in_progress']),
25
25
  });
26
26
 
27
27
  const TASK_FIELDS = Object.freeze([
@@ -529,6 +529,16 @@ function patchTask(db, id, patch = {}) {
529
529
  `cannot transition task ${id} from ${current.status} to ${nextStatus}`,
530
530
  );
531
531
  }
532
+ if (current.status === 'completed' && nextStatus === 'in_progress') {
533
+ if (!Object.prototype.hasOwnProperty.call(patch, 'completion_commit')) {
534
+ sets.push('completion_commit = ?');
535
+ params.push(null);
536
+ }
537
+ if (!Object.prototype.hasOwnProperty.call(patch, 'session_id')) {
538
+ sets.push('session_id = ?');
539
+ params.push(null);
540
+ }
541
+ }
532
542
  sets.push('status = ?');
533
543
  params.push(nextStatus);
534
544
  }
@@ -542,10 +552,18 @@ function patchTask(db, id, patch = {}) {
542
552
 
543
553
  if (nextStatus && nextStatus !== current.status) {
544
554
  appendEventInTx(db, {
545
- type: statusEventType(nextStatus),
555
+ type: current.status === 'completed' && nextStatus === 'in_progress'
556
+ ? 'task_reopened'
557
+ : statusEventType(nextStatus),
546
558
  task_id: id,
547
559
  change_id: patch.change_id !== undefined ? patch.change_id : current.change_id,
548
- payload: { from: current.status, to: nextStatus },
560
+ payload: {
561
+ from: current.status,
562
+ to: nextStatus,
563
+ ...(current.status === 'completed' && nextStatus === 'in_progress'
564
+ ? { previous_completion_commit: current.completion_commit }
565
+ : {}),
566
+ },
549
567
  });
550
568
  }
551
569
  const contractFields = Object.keys(patch).filter((field) => TASK_CONTRACT_PATCH_FIELDS.has(field));