@zq-silk/yui 0.6.0 → 0.6.2

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.
Files changed (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. package/skills/yui-leader/SKILL.md +8 -0
@@ -4,7 +4,7 @@ import { normalizeCheckResult } from "./checkResult.js";
4
4
  export function createIntegrationAttempt(input, now) {
5
5
  const timestamp = now.toISOString();
6
6
  return validateIntegrationAttempt({
7
- schemaVersion: 2,
7
+ schemaVersion: 4,
8
8
  id: input.id,
9
9
  taskId: input.taskId,
10
10
  projectId: input.projectId,
@@ -17,6 +17,26 @@ export function createIntegrationAttempt(input, now) {
17
17
  updatedAt: timestamp
18
18
  });
19
19
  }
20
+ /**
21
+ * Bind the check DurableJob to a running attempt. Set-once: the job is the
22
+ * durable source of truth for the checks, so a retry of the start path must
23
+ * find the recorded job instead of spawning a second one.
24
+ */
25
+ export function recordIntegrationCheckJob(attempt, jobId, now) {
26
+ validateIntegrationAttempt(attempt);
27
+ if (attempt.status !== "running") {
28
+ throw new Error(`Integration check job can only bind a running attempt: ${attempt.status}.`);
29
+ }
30
+ if (attempt.jobId !== undefined) {
31
+ throw new Error(`Integration check job is already recorded: ${attempt.jobId}.`);
32
+ }
33
+ validateTaskRecordReference({ taskId: attempt.taskId, localId: jobId }, "durableJob");
34
+ return validateIntegrationAttempt({
35
+ ...attempt,
36
+ jobId,
37
+ updatedAt: now.toISOString()
38
+ });
39
+ }
20
40
  export function requireLeaderDecision(attempt, report, now) {
21
41
  validateIntegrationAttempt(attempt);
22
42
  const continuingAfterResolution = attempt.status === "blocked"
@@ -54,10 +74,11 @@ export function recordResolutionDecision(attempt, decision, now) {
54
74
  ...(decision.action === "reject" ? { endedAt: timestamp } : {})
55
75
  });
56
76
  }
77
+ const TERMINAL_STATUSES = ["committed", "superseded", "failed"];
57
78
  export function updateIntegrationAttempt(attempt, patch, now) {
58
79
  validateIntegrationAttempt(attempt);
59
80
  const status = patch.status ?? attempt.status;
60
- const terminal = ["committed", "failed"].includes(status);
81
+ const terminal = TERMINAL_STATUSES.includes(status);
61
82
  const updated = {
62
83
  ...attempt,
63
84
  ...patch,
@@ -71,9 +92,33 @@ export function updateIntegrationAttempt(attempt, patch, now) {
71
92
  }
72
93
  return validateIntegrationAttempt(updated);
73
94
  }
95
+ /**
96
+ * Mark a committed Integration as superseded (obsolete). A superseded
97
+ * Integration retains its evidence but is excluded from latest-committed
98
+ * selection, allowing the next valid committed Integration to become the
99
+ * Task's delivery baseline. Only a committed Integration may be superseded;
100
+ * a reason is required for the audit trail.
101
+ */
102
+ export function supersedeIntegration(attempt, reason, now) {
103
+ validateIntegrationAttempt(attempt);
104
+ if (attempt.status !== "committed") {
105
+ throw new Error(`Integration cannot be superseded from ${attempt.status}: ${attempt.id}.`);
106
+ }
107
+ const timestamp = now.toISOString();
108
+ return validateIntegrationAttempt({
109
+ ...attempt,
110
+ status: "superseded",
111
+ checks: [
112
+ ...(attempt.checks ?? []),
113
+ { name: "superseded", outcome: "failed", details: requireText(reason, "Supersede reason") }
114
+ ],
115
+ updatedAt: timestamp,
116
+ endedAt: timestamp
117
+ });
118
+ }
74
119
  export function validateIntegrationAttempt(attempt) {
75
- if (attempt.schemaVersion !== 2) {
76
- throw new Error("IntegrationAttempt must use schemaVersion 2.");
120
+ if (attempt.schemaVersion !== 4) {
121
+ throw new Error("IntegrationAttempt must use schemaVersion 4.");
77
122
  }
78
123
  validateTaskRecordReference({
79
124
  taskId: attempt.taskId,
@@ -93,11 +138,15 @@ export function validateIntegrationAttempt(attempt) {
93
138
  if (attempt.candidateCommit !== undefined) {
94
139
  requireCommit(attempt.candidateCommit, "Integration candidate commit");
95
140
  }
141
+ if (attempt.jobId !== undefined) {
142
+ validateTaskRecordReference({ taskId: attempt.taskId, localId: attempt.jobId }, "durableJob");
143
+ }
96
144
  if (![
97
145
  "running",
98
146
  "blocked",
99
147
  "validating",
100
148
  "committed",
149
+ "superseded",
101
150
  "failed"
102
151
  ].includes(attempt.status)) {
103
152
  throw new Error(`Integration status is invalid: ${String(attempt.status)}.`);
@@ -121,7 +170,7 @@ export function validateIntegrationAttempt(attempt) {
121
170
  attempt.checks?.forEach(normalizeCheckResult);
122
171
  requireTimestamp(attempt.createdAt, "Integration Attempt createdAt");
123
172
  requireTimestamp(attempt.updatedAt, "Integration Attempt updatedAt");
124
- if (["committed", "failed"].includes(attempt.status)) {
173
+ if (TERMINAL_STATUSES.includes(attempt.status)) {
125
174
  requireTimestamp(attempt.endedAt ?? "", "Integration Attempt endedAt");
126
175
  }
127
176
  return attempt;
@@ -0,0 +1,221 @@
1
+ import { normalizedUniqueIdentities, normalizedUniqueText, requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
2
+ import { validateTaskRecordReference } from "../task/taskRecordReference.js";
3
+ export function createIntegrationQueueEntry(input, now) {
4
+ const timestamp = now.toISOString();
5
+ return validateIntegrationQueueEntry({
6
+ schemaVersion: 1,
7
+ id: input.id,
8
+ taskId: input.taskId,
9
+ projectId: input.projectId,
10
+ changeSetId: input.changeSetId,
11
+ targetRef: input.targetRef,
12
+ status: "queued",
13
+ checkCommands: normalizedUniqueText(input.checkCommands ?? [], "Integration queue check command"),
14
+ evidenceRefs: normalizedUniqueText(input.evidenceRefs ?? [], "Integration queue evidence"),
15
+ createdAt: timestamp,
16
+ updatedAt: timestamp
17
+ });
18
+ }
19
+ /**
20
+ * An entry whose ChangeSet is already represented on the target (an ancestor
21
+ * or a same-tree commit) converges directly to committed with its proof. The
22
+ * entry still names the committed IntegrationAttempt that recorded the
23
+ * convergence, so every acceptance and provenance consumer reads one
24
+ * integration authority.
25
+ */
26
+ export function createConvergedIntegrationQueueEntry(input, now) {
27
+ const timestamp = now.toISOString();
28
+ return validateIntegrationQueueEntry({
29
+ schemaVersion: 1,
30
+ id: input.id,
31
+ taskId: input.taskId,
32
+ projectId: input.projectId,
33
+ changeSetId: input.changeSetId,
34
+ targetRef: input.targetRef,
35
+ status: "committed",
36
+ checkCommands: [],
37
+ targetBefore: input.targetHead,
38
+ targetAfter: input.targetHead,
39
+ evidenceRefs: [input.proof],
40
+ ...(input.integrationAttemptId === undefined
41
+ ? {}
42
+ : { integrationAttemptId: input.integrationAttemptId }),
43
+ createdAt: timestamp,
44
+ updatedAt: timestamp,
45
+ endedAt: timestamp
46
+ });
47
+ }
48
+ export function markIntegrationQueueRunning(entry, targetBefore, now) {
49
+ validateIntegrationQueueEntry(entry);
50
+ if (!["queued", "conflicted", "validated"].includes(entry.status)) {
51
+ throw new Error(`Integration queue entry cannot run from ${entry.status}: ${entry.id}.`);
52
+ }
53
+ const { conflictSummary: _previousConflict, ...rest } = entry;
54
+ return validateIntegrationQueueEntry({
55
+ ...rest,
56
+ status: "running",
57
+ targetBefore,
58
+ updatedAt: now.toISOString()
59
+ });
60
+ }
61
+ export function recordIntegrationQueueAttempt(entry, integrationAttemptId, now) {
62
+ validateIntegrationQueueEntry(entry);
63
+ if (entry.status !== "running") {
64
+ throw new Error(`Integration queue entry is not running: ${entry.id}/${entry.status}.`);
65
+ }
66
+ return validateIntegrationQueueEntry({
67
+ ...entry,
68
+ integrationAttemptId,
69
+ updatedAt: now.toISOString()
70
+ });
71
+ }
72
+ export function markIntegrationQueueBlocked(entry, conflictSummary, now) {
73
+ validateIntegrationQueueEntry(entry);
74
+ if (entry.status !== "running") {
75
+ throw new Error(`Integration queue entry is not running: ${entry.id}/${entry.status}.`);
76
+ }
77
+ return validateIntegrationQueueEntry({
78
+ ...entry,
79
+ status: "conflicted",
80
+ conflictSummary: requireText(conflictSummary, "Integration queue conflict summary"),
81
+ updatedAt: now.toISOString()
82
+ });
83
+ }
84
+ export function markIntegrationQueueCommitted(entry, targetAfter, now) {
85
+ validateIntegrationQueueEntry(entry);
86
+ if (entry.status !== "running" && entry.status !== "conflicted") {
87
+ throw new Error(`Integration queue entry cannot commit from ${entry.status}: ${entry.id}.`);
88
+ }
89
+ const timestamp = now.toISOString();
90
+ return validateIntegrationQueueEntry({
91
+ ...entry,
92
+ status: "committed",
93
+ targetAfter,
94
+ updatedAt: timestamp,
95
+ endedAt: timestamp
96
+ });
97
+ }
98
+ /**
99
+ * Records that this entry's reusable evidence was checked against an exact
100
+ * target head. The evidence only covers `evidenceTargetHead`; a target
101
+ * advance invalidates it unless the delta is provably empty. Path
102
+ * disjointness alone never justifies this transition: a gate may read any
103
+ * file, so a non-empty target increment cannot be proven irrelevant.
104
+ */
105
+ export function markIntegrationQueueValidated(entry, now, evidenceTargetHead) {
106
+ validateIntegrationQueueEntry(entry);
107
+ if (entry.status !== "queued") {
108
+ throw new Error(`Integration queue entry cannot validate from ${entry.status}: ${entry.id}.`);
109
+ }
110
+ if (entry.evidenceRefs.length === 0) {
111
+ throw new Error(`Integration queue entry has no reusable evidence: ${entry.id}.`);
112
+ }
113
+ return validateIntegrationQueueEntry({
114
+ ...entry,
115
+ status: "validated",
116
+ ...(evidenceTargetHead === undefined ? {} : { evidenceTargetHead }),
117
+ updatedAt: now.toISOString()
118
+ });
119
+ }
120
+ /**
121
+ * A validated entry whose ChangeSet became affected by a later target advance
122
+ * loses its evidence coverage and runs its checks again; a conflicted entry is
123
+ * manually retried after its underlying issue was fixed.
124
+ */
125
+ export function markIntegrationQueueRequeued(entry, now) {
126
+ validateIntegrationQueueEntry(entry);
127
+ if (entry.status !== "validated" && entry.status !== "conflicted") {
128
+ throw new Error(`Integration queue entry cannot requeue from ${entry.status}: ${entry.id}.`);
129
+ }
130
+ return validateIntegrationQueueEntry({
131
+ ...entry,
132
+ status: "queued",
133
+ updatedAt: now.toISOString()
134
+ });
135
+ }
136
+ export function recordIntegrationQueueAffectedPaths(entry, affectedPaths, now) {
137
+ validateIntegrationQueueEntry(entry);
138
+ if (!["queued", "validated"].includes(entry.status)) {
139
+ throw new Error(`Integration queue entry cannot recompute overlap from ${entry.status}: ${entry.id}.`);
140
+ }
141
+ const previous = new Set(entry.affectedPaths ?? []);
142
+ const merged = [...new Set([...previous, ...affectedPaths])].sort();
143
+ return validateIntegrationQueueEntry({
144
+ ...entry,
145
+ affectedPaths: merged,
146
+ updatedAt: now.toISOString()
147
+ });
148
+ }
149
+ export function markIntegrationQueueSuperseded(entry, reason, now) {
150
+ validateIntegrationQueueEntry(entry);
151
+ if (!["queued", "conflicted", "validated"].includes(entry.status)) {
152
+ throw new Error(`Integration queue entry cannot be superseded from ${entry.status}: ${entry.id}.`);
153
+ }
154
+ const timestamp = now.toISOString();
155
+ return validateIntegrationQueueEntry({
156
+ ...entry,
157
+ status: "superseded",
158
+ supersedeReason: requireText(reason, "Integration queue supersede reason"),
159
+ updatedAt: timestamp,
160
+ endedAt: timestamp
161
+ });
162
+ }
163
+ export function validateIntegrationQueueEntry(entry) {
164
+ if (entry.schemaVersion !== 1) {
165
+ throw new Error("IntegrationQueueEntry must use schemaVersion 1.");
166
+ }
167
+ validateTaskRecordReference({ taskId: entry.taskId, localId: entry.id }, "integrationQueue");
168
+ validateTaskRecordReference({ taskId: entry.taskId, localId: entry.changeSetId }, "changeSet");
169
+ requireIdentity(entry.projectId, "Project id");
170
+ requireText(entry.targetRef, "Integration queue target ref");
171
+ normalizedUniqueText(entry.checkCommands, "Integration queue check command");
172
+ normalizedUniqueText(entry.evidenceRefs, "Integration queue evidence");
173
+ if (![
174
+ "queued",
175
+ "running",
176
+ "conflicted",
177
+ "validated",
178
+ "committed",
179
+ "superseded"
180
+ ].includes(entry.status)) {
181
+ throw new Error(`Integration queue status is invalid: ${String(entry.status)}.`);
182
+ }
183
+ if (entry.targetBefore !== undefined)
184
+ requireCommit(entry.targetBefore, "Integration queue targetBefore");
185
+ if (entry.targetAfter !== undefined)
186
+ requireCommit(entry.targetAfter, "Integration queue targetAfter");
187
+ if (entry.evidenceTargetHead !== undefined) {
188
+ requireCommit(entry.evidenceTargetHead, "Integration queue evidenceTargetHead");
189
+ }
190
+ if (entry.integrationAttemptId !== undefined) {
191
+ normalizedUniqueIdentities([entry.integrationAttemptId], "Integration attempt id");
192
+ }
193
+ if (entry.conflictSummary !== undefined) {
194
+ requireText(entry.conflictSummary, "Integration queue conflict summary");
195
+ }
196
+ if (entry.affectedPaths !== undefined) {
197
+ normalizedUniqueText(entry.affectedPaths, "Integration queue affected path");
198
+ }
199
+ if (entry.supersedeReason !== undefined) {
200
+ requireText(entry.supersedeReason, "Integration queue supersede reason");
201
+ }
202
+ requireTimestamp(entry.createdAt, "Integration queue entry createdAt");
203
+ requireTimestamp(entry.updatedAt, "Integration queue entry updatedAt");
204
+ if (["committed", "superseded"].includes(entry.status)) {
205
+ requireTimestamp(entry.endedAt ?? "", "Integration queue entry endedAt");
206
+ }
207
+ if (entry.status === "committed") {
208
+ requireCommit(entry.targetAfter ?? "", "Integration queue targetAfter");
209
+ }
210
+ if (entry.status === "running") {
211
+ requireCommit(entry.targetBefore ?? "", "Integration queue targetBefore");
212
+ }
213
+ return entry;
214
+ }
215
+ function requireCommit(value, label) {
216
+ const normalized = requireText(value, label).toLowerCase();
217
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(normalized)) {
218
+ throw new Error(`${label} is invalid.`);
219
+ }
220
+ return normalized;
221
+ }