@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.
- package/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
|
|
2
|
+
/**
|
|
3
|
+
* Issue 09 — historical compaction.
|
|
4
|
+
*
|
|
5
|
+
* Progress events that were appended to semantic Task event history (legacy
|
|
6
|
+
* behavior) are folded into the telemetry tables inside the Home's `yui.db`:
|
|
7
|
+
* the newest `keep` rows per Run/generation become the retained window, and
|
|
8
|
+
* every Run/generation gets an accurate aggregate
|
|
9
|
+
* (count/first/last/maxSequence/errorCount). The semantic progress events are
|
|
10
|
+
* then removed through the store's public transaction adapter. Semantic
|
|
11
|
+
* evidence (Run dispatch/delivery/terminal receipts, Session lifecycle,
|
|
12
|
+
* Review, Decision, Integration, ...) is never touched.
|
|
13
|
+
*
|
|
14
|
+
* Compaction runs on a staged copy of a Home. The caller promotes the staged
|
|
15
|
+
* copy only after this module validates the result; a validation failure
|
|
16
|
+
* throws and leaves the original history unchanged.
|
|
17
|
+
*/
|
|
18
|
+
export const PROGRESS_EVENT_TYPE = "runtime.provider-turn-progress";
|
|
19
|
+
/**
|
|
20
|
+
* Scan a store and build the compaction plan. Read-only: nothing is modified.
|
|
21
|
+
*/
|
|
22
|
+
export function planTelemetryCompaction(store, options = {}) {
|
|
23
|
+
const keep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
|
|
24
|
+
const tasks = [];
|
|
25
|
+
let progressEvents = 0;
|
|
26
|
+
let generations = 0;
|
|
27
|
+
let telemetryRows = 0;
|
|
28
|
+
for (const task of store.listTasks()) {
|
|
29
|
+
const taskPlan = planTaskCompaction(task.id, store.listEvents(task.id), keep);
|
|
30
|
+
if (taskPlan === null)
|
|
31
|
+
continue;
|
|
32
|
+
tasks.push(taskPlan);
|
|
33
|
+
progressEvents += taskPlan.progressEventIds.length;
|
|
34
|
+
generations += taskPlan.generations.length;
|
|
35
|
+
for (const generation of taskPlan.generations)
|
|
36
|
+
telemetryRows += generation.window.length;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
tasks,
|
|
40
|
+
totals: { tasks: tasks.length, progressEvents, generations, telemetryRows }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function planTaskCompaction(taskId, events, keep) {
|
|
44
|
+
const byGeneration = new Map();
|
|
45
|
+
const progressEventIds = [];
|
|
46
|
+
let malformedKept = 0;
|
|
47
|
+
for (const event of events) {
|
|
48
|
+
if (event.type !== PROGRESS_EVENT_TYPE)
|
|
49
|
+
continue;
|
|
50
|
+
const runId = event.payload.runId;
|
|
51
|
+
const progressId = event.payload.progressId;
|
|
52
|
+
if (typeof runId !== "string" || typeof progressId !== "string") {
|
|
53
|
+
// Fail closed: an anomalous progress row stays semantic and is reported.
|
|
54
|
+
malformedKept++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const roleName = typeof event.payload.roleName === "string" ? event.payload.roleName : "unknown";
|
|
58
|
+
const generation = typeof event.payload.launchId === "string" && event.payload.launchId !== ""
|
|
59
|
+
? event.payload.launchId
|
|
60
|
+
: typeof event.payload.nativeSessionId === "string" && event.payload.nativeSessionId !== ""
|
|
61
|
+
? event.payload.nativeSessionId
|
|
62
|
+
: "unbound";
|
|
63
|
+
const key = `${roleName}\u0000${runId}\u0000${generation}`;
|
|
64
|
+
const group = byGeneration.get(key);
|
|
65
|
+
if (group === undefined)
|
|
66
|
+
byGeneration.set(key, [event]);
|
|
67
|
+
else
|
|
68
|
+
group.push(event);
|
|
69
|
+
progressEventIds.push(event.id);
|
|
70
|
+
}
|
|
71
|
+
if (progressEventIds.length === 0 && malformedKept === 0)
|
|
72
|
+
return null;
|
|
73
|
+
const generations = [];
|
|
74
|
+
for (const group of byGeneration.values()) {
|
|
75
|
+
generations.push(planGenerationCompaction(taskId, group, keep));
|
|
76
|
+
}
|
|
77
|
+
return { taskId, progressEventIds, malformedKept, generations };
|
|
78
|
+
}
|
|
79
|
+
function planGenerationCompaction(taskId, events, keep) {
|
|
80
|
+
const first = events[0];
|
|
81
|
+
const roleName = typeof first.payload.roleName === "string" ? first.payload.roleName : "unknown";
|
|
82
|
+
const runId = first.payload.runId;
|
|
83
|
+
const generation = typeof first.payload.launchId === "string" && first.payload.launchId !== ""
|
|
84
|
+
? first.payload.launchId
|
|
85
|
+
: typeof first.payload.nativeSessionId === "string" && first.payload.nativeSessionId !== ""
|
|
86
|
+
? first.payload.nativeSessionId
|
|
87
|
+
: "unbound";
|
|
88
|
+
const ordered = [...events].sort(compareProgressEvents);
|
|
89
|
+
let firstAt = progressAt(ordered[0]);
|
|
90
|
+
let lastAt = firstAt;
|
|
91
|
+
let maxSequence = null;
|
|
92
|
+
let errorCount = 0;
|
|
93
|
+
for (const event of ordered) {
|
|
94
|
+
const at = progressAt(event);
|
|
95
|
+
if (at < firstAt)
|
|
96
|
+
firstAt = at;
|
|
97
|
+
if (at > lastAt)
|
|
98
|
+
lastAt = at;
|
|
99
|
+
const sequence = event.payload.sequence;
|
|
100
|
+
if (typeof sequence === "string") {
|
|
101
|
+
const value = Number(sequence);
|
|
102
|
+
if (Number.isSafeInteger(value) && (maxSequence === null || value > maxSequence)) {
|
|
103
|
+
maxSequence = value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if ((typeof event.payload.error === "string" && event.payload.error !== "")
|
|
107
|
+
|| (typeof event.payload.errorKind === "string" && event.payload.errorKind !== "")) {
|
|
108
|
+
errorCount++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const window = ordered.slice(-keep).map((event) => ({
|
|
112
|
+
taskId,
|
|
113
|
+
roleName,
|
|
114
|
+
runId,
|
|
115
|
+
generation,
|
|
116
|
+
progressId: event.payload.progressId,
|
|
117
|
+
...(typeof event.payload.sequence === "string"
|
|
118
|
+
&& Number.isSafeInteger(Number(event.payload.sequence))
|
|
119
|
+
? { sequence: Number(event.payload.sequence) }
|
|
120
|
+
: {}),
|
|
121
|
+
payload: { ...event.payload },
|
|
122
|
+
receivedAt: progressAt(event)
|
|
123
|
+
}));
|
|
124
|
+
return {
|
|
125
|
+
roleName,
|
|
126
|
+
runId,
|
|
127
|
+
generation,
|
|
128
|
+
aggregate: {
|
|
129
|
+
taskId,
|
|
130
|
+
roleName,
|
|
131
|
+
runId,
|
|
132
|
+
generation,
|
|
133
|
+
firstAt,
|
|
134
|
+
lastAt,
|
|
135
|
+
count: ordered.length,
|
|
136
|
+
maxSequence,
|
|
137
|
+
errorCount
|
|
138
|
+
},
|
|
139
|
+
window
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function compareProgressEvents(a, b) {
|
|
143
|
+
const seqA = numericSequence(a);
|
|
144
|
+
const seqB = numericSequence(b);
|
|
145
|
+
if (seqA !== null && seqB !== null && seqA !== seqB)
|
|
146
|
+
return seqA - seqB;
|
|
147
|
+
const atA = Date.parse(progressAt(a));
|
|
148
|
+
const atB = Date.parse(progressAt(b));
|
|
149
|
+
if (atA !== atB)
|
|
150
|
+
return atA - atB;
|
|
151
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
152
|
+
}
|
|
153
|
+
function numericSequence(event) {
|
|
154
|
+
const raw = event.payload.sequence;
|
|
155
|
+
if (typeof raw !== "string")
|
|
156
|
+
return null;
|
|
157
|
+
const value = Number(raw);
|
|
158
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
159
|
+
}
|
|
160
|
+
function progressAt(event) {
|
|
161
|
+
const raw = event.payload.progressAt;
|
|
162
|
+
if (typeof raw === "string" && Number.isFinite(Date.parse(raw)))
|
|
163
|
+
return raw;
|
|
164
|
+
return event.createdAt;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Apply a plan to a staged Home. The telemetry windows/aggregates are imported
|
|
168
|
+
* first and validated, then the semantic progress events are removed inside
|
|
169
|
+
* one store transaction and validated again. Throws on any validation
|
|
170
|
+
* failure; the caller must discard the staged Home in that case. A crash
|
|
171
|
+
* between the two phases leaves the staged Home in the equivalent of
|
|
172
|
+
* dual-write state (telemetry imported, semantic progress still present),
|
|
173
|
+
* which is harmless and re-runnable.
|
|
174
|
+
*/
|
|
175
|
+
export function applyTelemetryCompaction(store, telemetry, plan, options) {
|
|
176
|
+
const now = options.now ?? new Date();
|
|
177
|
+
const keep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
|
|
178
|
+
if (!options.dryRun) {
|
|
179
|
+
// Import first and validate the sidecar before touching semantic history:
|
|
180
|
+
// a validation failure leaves the staged Home's semantic events intact.
|
|
181
|
+
for (const task of plan.tasks) {
|
|
182
|
+
for (const generation of task.generations) {
|
|
183
|
+
telemetry.importGeneration(generation.window, generation.aggregate);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
validateAggregates(telemetry, plan);
|
|
187
|
+
store.transaction(() => {
|
|
188
|
+
for (const task of plan.tasks) {
|
|
189
|
+
if (task.progressEventIds.length === 0)
|
|
190
|
+
continue;
|
|
191
|
+
store.removeEvents(task.taskId, task.progressEventIds);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
validateSemanticClean(store, plan);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
schemaVersion: 1,
|
|
198
|
+
createdAt: now.toISOString(),
|
|
199
|
+
dryRun: options.dryRun,
|
|
200
|
+
source: options.source,
|
|
201
|
+
terminalKeep: keep,
|
|
202
|
+
totals: plan.totals,
|
|
203
|
+
tasks: plan.tasks.map((task) => ({
|
|
204
|
+
taskId: task.taskId,
|
|
205
|
+
removedProgressEvents: options.dryRun ? 0 : task.progressEventIds.length,
|
|
206
|
+
generations: task.generations.map((generation) => generation.aggregate)
|
|
207
|
+
})),
|
|
208
|
+
validation: "passed"
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Verify every planned Run/generation aggregate in the sidecar matches the
|
|
213
|
+
* plan (count, first/last, max sequence) before semantic history is touched.
|
|
214
|
+
*/
|
|
215
|
+
function validateAggregates(telemetry, plan) {
|
|
216
|
+
for (const task of plan.tasks) {
|
|
217
|
+
for (const generation of task.generations) {
|
|
218
|
+
const stored = telemetry.aggregateGeneration(generation.aggregate.taskId, generation.aggregate.roleName, generation.aggregate.runId, generation.aggregate.generation);
|
|
219
|
+
if (stored === null) {
|
|
220
|
+
throw new Error(`Compaction validation failed: missing aggregate for Run ${generation.aggregate.runId}.`);
|
|
221
|
+
}
|
|
222
|
+
const expected = generation.aggregate;
|
|
223
|
+
if (stored.count !== expected.count
|
|
224
|
+
|| stored.firstAt !== expected.firstAt
|
|
225
|
+
|| stored.lastAt !== expected.lastAt
|
|
226
|
+
|| stored.maxSequence !== expected.maxSequence
|
|
227
|
+
|| stored.errorCount !== expected.errorCount) {
|
|
228
|
+
throw new Error(`Compaction validation failed: aggregate mismatch for Run ${expected.runId} `
|
|
229
|
+
+ `(expected count=${expected.count} first=${expected.firstAt} last=${expected.lastAt} `
|
|
230
|
+
+ `maxSequence=${String(expected.maxSequence)} errors=${expected.errorCount}; `
|
|
231
|
+
+ `got count=${stored.count} first=${stored.firstAt} last=${stored.lastAt} `
|
|
232
|
+
+ `maxSequence=${String(stored.maxSequence)} errors=${stored.errorCount}).`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Verify no planned progress event remains in semantic history. */
|
|
238
|
+
function validateSemanticClean(store, plan) {
|
|
239
|
+
for (const task of plan.tasks) {
|
|
240
|
+
if (task.progressEventIds.length === 0)
|
|
241
|
+
continue;
|
|
242
|
+
const remaining = new Set(store.listEvents(task.taskId)
|
|
243
|
+
.filter((event) => event.type === PROGRESS_EVENT_TYPE)
|
|
244
|
+
.map((event) => event.id));
|
|
245
|
+
for (const id of task.progressEventIds) {
|
|
246
|
+
if (remaining.has(id)) {
|
|
247
|
+
throw new Error(`Compaction validation failed: progress event ${task.taskId}/${id} still present.`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue 09 — telemetry retention & compaction configuration.
|
|
3
|
+
*
|
|
4
|
+
* Telemetry lives in the Home's authoritative `yui.db` (database-only
|
|
5
|
+
* direction). Its activation is an explicit per-Home switch so the feature
|
|
6
|
+
* can be rolled out (legacy → dual → bounded) and rolled back independently
|
|
7
|
+
* of every other Issue. Retention defaults are the schema's own constants
|
|
8
|
+
* (§4.4); the environment only overrides them.
|
|
9
|
+
*/
|
|
10
|
+
import { TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_RUN_CAP } from "../storage/sqliteSchema.js";
|
|
11
|
+
export const DEFAULT_TELEMETRY_MODE = "legacy";
|
|
12
|
+
/** Terminal Run/generation progress rows retained after prune. */
|
|
13
|
+
export const DEFAULT_TERMINAL_KEEP = TELEMETRY_KEEP_PER_GENERATION;
|
|
14
|
+
/** Hard cap of progress rows per Run while it is still active. */
|
|
15
|
+
export const DEFAULT_RUN_CAP = TELEMETRY_RUN_CAP;
|
|
16
|
+
/**
|
|
17
|
+
* Absolute ceiling for the configurable Run cap. Retention can be tuned but
|
|
18
|
+
* never disabled: every Home keeps a computable upper bound on telemetry
|
|
19
|
+
* rows (Tasks × Runs × cap).
|
|
20
|
+
*/
|
|
21
|
+
export const MAX_RUN_CAP = 10_000_000;
|
|
22
|
+
const TELEMETRY_MODES = ["legacy", "dual", "bounded"];
|
|
23
|
+
/**
|
|
24
|
+
* Resolve `YUI_TELEMETRY_MODE` (default `legacy`). Only the three exact
|
|
25
|
+
* values (case-insensitive) are accepted; anything else fails closed at
|
|
26
|
+
* startup instead of silently changing progress routing.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveTelemetryMode(env = process.env) {
|
|
29
|
+
const raw = env.YUI_TELEMETRY_MODE?.trim().toLowerCase();
|
|
30
|
+
if (raw === undefined || raw === "")
|
|
31
|
+
return DEFAULT_TELEMETRY_MODE;
|
|
32
|
+
if (!TELEMETRY_MODES.includes(raw)) {
|
|
33
|
+
throw new TypeError(`YUI_TELEMETRY_MODE must be one of ${TELEMETRY_MODES.join(", ")}; got ${JSON.stringify(raw)}.`);
|
|
34
|
+
}
|
|
35
|
+
return raw;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the terminal-Run retention window (`YUI_TELEMETRY_TERMINAL_KEEP`,
|
|
39
|
+
* default 200). Must be a positive integer.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveTerminalKeep(env = process.env) {
|
|
42
|
+
return resolvePositiveInteger(env.YUI_TELEMETRY_TERMINAL_KEEP, DEFAULT_TERMINAL_KEEP, "YUI_TELEMETRY_TERMINAL_KEEP");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the active-Run hard cap (`YUI_TELEMETRY_RUN_CAP`, default 50,000).
|
|
46
|
+
* Must be a positive integer not exceeding {@link MAX_RUN_CAP}; the cap
|
|
47
|
+
* cannot be disabled.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveRunCap(env = process.env) {
|
|
50
|
+
const cap = resolvePositiveInteger(env.YUI_TELEMETRY_RUN_CAP, DEFAULT_RUN_CAP, "YUI_TELEMETRY_RUN_CAP");
|
|
51
|
+
if (cap > MAX_RUN_CAP) {
|
|
52
|
+
throw new TypeError(`YUI_TELEMETRY_RUN_CAP must not exceed ${MAX_RUN_CAP.toLocaleString("en-US")} (retention cannot be disabled).`);
|
|
53
|
+
}
|
|
54
|
+
return cap;
|
|
55
|
+
}
|
|
56
|
+
function resolvePositiveInteger(raw, fallback, label) {
|
|
57
|
+
if (raw === undefined || raw.trim() === "")
|
|
58
|
+
return fallback;
|
|
59
|
+
const value = Number(raw);
|
|
60
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
61
|
+
throw new TypeError(`${label} must be a positive integer; got ${JSON.stringify(raw)}.`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route one provider progress observation according to `telemetry.mode`:
|
|
3
|
+
*
|
|
4
|
+
* - `legacy` — semantic event only (master behavior).
|
|
5
|
+
* - `dual` — semantic event plus best-effort sidecar upsert.
|
|
6
|
+
* - `bounded` — sidecar upsert only; semantic events stop carrying progress.
|
|
7
|
+
*
|
|
8
|
+
* The sidecar write is best-effort and isolated: a sink failure must never
|
|
9
|
+
* block or roll back the semantic lane.
|
|
10
|
+
*/
|
|
11
|
+
export function routeProviderProgress(input) {
|
|
12
|
+
const { mode, entry, semanticExists, writeSemantic, sink } = input;
|
|
13
|
+
if (mode === "bounded") {
|
|
14
|
+
safeObserve(sink, entry);
|
|
15
|
+
return "applied";
|
|
16
|
+
}
|
|
17
|
+
if (semanticExists)
|
|
18
|
+
return "applied";
|
|
19
|
+
writeSemantic();
|
|
20
|
+
if (mode === "dual")
|
|
21
|
+
safeObserve(sink, entry);
|
|
22
|
+
return "applied";
|
|
23
|
+
}
|
|
24
|
+
function safeObserve(sink, entry) {
|
|
25
|
+
try {
|
|
26
|
+
sink.observe(entry);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Telemetry is best-effort by contract; sinks must not throw, but a
|
|
30
|
+
// broken sink must never take down the semantic lane.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** No-op sink for `legacy` mode and for callers without a sidecar. */
|
|
2
|
+
export class NullTelemetrySink {
|
|
3
|
+
mode;
|
|
4
|
+
constructor(mode = "legacy") {
|
|
5
|
+
this.mode = mode;
|
|
6
|
+
}
|
|
7
|
+
observe(_entry) { }
|
|
8
|
+
health() {
|
|
9
|
+
return {
|
|
10
|
+
mode: this.mode,
|
|
11
|
+
available: false,
|
|
12
|
+
dropped: 0,
|
|
13
|
+
coalesced: 0,
|
|
14
|
+
lastError: null,
|
|
15
|
+
rows: 0
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async close() { }
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveRunCap, resolveTelemetryMode, resolveTerminalKeep } from "./telemetryConfig.js";
|
|
4
|
+
import { SqliteTelemetryStore } from "./sqliteTelemetryStore.js";
|
|
5
|
+
import { COMMITTED_DATABASE_FILENAME } from "../storage/upgrade/sqliteStateMigration.js";
|
|
6
|
+
/**
|
|
7
|
+
* Open the telemetry sidecar for a Home, resolved from the environment
|
|
8
|
+
* (`YUI_TELEMETRY_MODE`, default `legacy`). Returns null in legacy mode so
|
|
9
|
+
* callers keep the exact master behavior and never touch the database.
|
|
10
|
+
*
|
|
11
|
+
* Telemetry lives in the Home's authoritative `yui.db`. A Home without a
|
|
12
|
+
* database has not reached SQLite storage yet, so dual/bounded mode fails
|
|
13
|
+
* closed at startup with a bounded diagnosis instead of silently creating an
|
|
14
|
+
* empty database (which would corrupt Home classification). Once opened, the
|
|
15
|
+
* store fails isolated: a broken sidecar only increments its dropped counter
|
|
16
|
+
* and never blocks the semantic lane.
|
|
17
|
+
*/
|
|
18
|
+
export function openSchedulerTelemetry(home, env = process.env) {
|
|
19
|
+
const mode = resolveTelemetryMode(env);
|
|
20
|
+
if (mode === "legacy")
|
|
21
|
+
return null;
|
|
22
|
+
const dbPath = join(home, COMMITTED_DATABASE_FILENAME);
|
|
23
|
+
if (!existsSync(dbPath)) {
|
|
24
|
+
throw new Error(`YUI_TELEMETRY_MODE=${mode} requires SQLite storage, but ${dbPath} does not exist. `
|
|
25
|
+
+ "Migrate this Home to the database backend first (yui upgrade).");
|
|
26
|
+
}
|
|
27
|
+
const store = new SqliteTelemetryStore(home, {
|
|
28
|
+
mode,
|
|
29
|
+
terminalKeep: resolveTerminalKeep(env),
|
|
30
|
+
runCap: resolveRunCap(env)
|
|
31
|
+
});
|
|
32
|
+
return { mode, sink: store, reader: store };
|
|
33
|
+
}
|
package/dist/tmux/tmuxManager.js
CHANGED
|
@@ -723,6 +723,25 @@ export class TmuxManager {
|
|
|
723
723
|
const snapshot = await this.sessionWindowNamesAsync(taskId);
|
|
724
724
|
return snapshot.names.includes(roleName) ? "running" : "exited";
|
|
725
725
|
}
|
|
726
|
+
/** Exact pane process state for one Role, used by owner-identity recording. */
|
|
727
|
+
inspectRolePane(taskId, roleName) {
|
|
728
|
+
const pane = this.inspectPane(taskId, roleName);
|
|
729
|
+
return {
|
|
730
|
+
...(pane.pid === undefined ? {} : { pid: pane.pid }),
|
|
731
|
+
target: pane.target,
|
|
732
|
+
dead: pane.dead,
|
|
733
|
+
currentCommand: pane.currentCommand
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
async inspectRolePaneAsync(taskId, roleName) {
|
|
737
|
+
const pane = await this.inspectPaneAsync(taskId, roleName);
|
|
738
|
+
return {
|
|
739
|
+
...(pane.pid === undefined ? {} : { pid: pane.pid }),
|
|
740
|
+
target: pane.target,
|
|
741
|
+
dead: pane.dead,
|
|
742
|
+
currentCommand: pane.currentCommand
|
|
743
|
+
};
|
|
744
|
+
}
|
|
726
745
|
stopTask(taskId) {
|
|
727
746
|
const taskSession = this.sessionName(taskId);
|
|
728
747
|
const sessions = this.taskSessionGroupNames(taskId);
|
|
@@ -1037,7 +1056,7 @@ function launchCommand(launch) {
|
|
|
1037
1056
|
function isExplicitlyAbsentTmuxSession(error) {
|
|
1038
1057
|
if (!(error instanceof CommandExecutionError))
|
|
1039
1058
|
return false;
|
|
1040
|
-
return /can't find (?:session|window|pane)|no server running|error connecting to .+ \(No such file or directory\)/i
|
|
1059
|
+
return /can't find (?:session|window|pane)|no server running|no current target|session not found|error connecting to .+ \(No such file or directory\)/i
|
|
1041
1060
|
.test(error.stderr);
|
|
1042
1061
|
}
|
|
1043
1062
|
function isUnavailableTmuxStatus(error) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Exact same-user directory in which `tmux -L` publishes a named server.
|
|
4
|
+
* TMUX_TMPDIR is tmux's only configurable root; TMPDIR/TMP/TEMP do not apply.
|
|
5
|
+
*/
|
|
6
|
+
export function tmuxSocketDirectory(environment = process.env) {
|
|
7
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
8
|
+
return join(resolve(nonEmptyTmuxTmpdir(environment) ?? "/tmp"), `tmux-${uid}`);
|
|
9
|
+
}
|
|
10
|
+
/** Pins an invoked tmux process to the same root selected above. */
|
|
11
|
+
export function tmuxSocketEnvironment(environment = process.env) {
|
|
12
|
+
return {
|
|
13
|
+
...environment,
|
|
14
|
+
TMUX_TMPDIR: nonEmptyTmuxTmpdir(environment)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function nonEmptyTmuxTmpdir(environment) {
|
|
18
|
+
const configured = environment.TMUX_TMPDIR;
|
|
19
|
+
return configured !== undefined && configured.length > 0 ? configured : undefined;
|
|
20
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { requireIdentity, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
|
|
4
|
+
/**
|
|
5
|
+
* Issue 08: the reusable exact-SHA gate evidence record.
|
|
6
|
+
*
|
|
7
|
+
* A GateArtifact binds one gate run to its full identity tuple: Project,
|
|
8
|
+
* candidate commit, VerificationPlan digest, toolchain digest, and (for L2)
|
|
9
|
+
* the source/base/target boundary. The same tuple reuses the same successful
|
|
10
|
+
* artifact; any tuple change invalidates it. Artifacts are stored in the
|
|
11
|
+
* SQLite `gate_artifacts` / `gate_artifact_logs` tables, backend-neutral
|
|
12
|
+
* through the GateArtifactStorePort.
|
|
13
|
+
*/
|
|
14
|
+
export const GATE_ARTIFACT_SCHEMA_VERSION = 1;
|
|
15
|
+
/** The evidence-reference scheme carried in manifests and queue entries. */
|
|
16
|
+
export const GATE_ARTIFACT_REF_PREFIX = "gate-artifact:";
|
|
17
|
+
export function gateArtifactRef(key) {
|
|
18
|
+
return `${GATE_ARTIFACT_REF_PREFIX}${key}`;
|
|
19
|
+
}
|
|
20
|
+
export function parseGateArtifactRef(ref) {
|
|
21
|
+
return ref.startsWith(GATE_ARTIFACT_REF_PREFIX)
|
|
22
|
+
? ref.slice(GATE_ARTIFACT_REF_PREFIX.length)
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
/** Content-addressed key: the same identity tuple always maps to one key. */
|
|
26
|
+
export function gateArtifactKey(identity) {
|
|
27
|
+
return createHash("sha256")
|
|
28
|
+
.update(canonicalJson(identity))
|
|
29
|
+
.digest("hex");
|
|
30
|
+
}
|
|
31
|
+
export function createGateArtifact(identity, metadata, now) {
|
|
32
|
+
const timestamp = now.toISOString();
|
|
33
|
+
return validateGateArtifact({
|
|
34
|
+
schemaVersion: GATE_ARTIFACT_SCHEMA_VERSION,
|
|
35
|
+
key: gateArtifactKey(identity),
|
|
36
|
+
projectId: identity.projectId,
|
|
37
|
+
level: identity.level,
|
|
38
|
+
commit: identity.commit,
|
|
39
|
+
planId: requireIdentity(metadata.planId, "GateArtifact planId"),
|
|
40
|
+
planVersion: requireText(metadata.planVersion, "GateArtifact planVersion"),
|
|
41
|
+
planDigest: identity.planDigest,
|
|
42
|
+
toolchainDigest: identity.toolchainDigest,
|
|
43
|
+
...(identity.boundary === undefined ? {} : { boundary: identity.boundary }),
|
|
44
|
+
steps: Object.freeze([]),
|
|
45
|
+
generator: requireText(metadata.generator, "GateArtifact generator"),
|
|
46
|
+
status: "incomplete",
|
|
47
|
+
outcome: "unknown",
|
|
48
|
+
potentialReuseCount: 0,
|
|
49
|
+
reuseCount: 0,
|
|
50
|
+
createdAt: timestamp,
|
|
51
|
+
lastUsedAt: timestamp
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Terminalize an artifact with its step results. */
|
|
55
|
+
export function completeGateArtifact(artifact, steps, outcome, now) {
|
|
56
|
+
validateGateArtifact(artifact);
|
|
57
|
+
if (artifact.status !== "incomplete") {
|
|
58
|
+
throw new Error(`GateArtifact is already complete: ${artifact.key}.`);
|
|
59
|
+
}
|
|
60
|
+
const timestamp = now.toISOString();
|
|
61
|
+
return validateGateArtifact({
|
|
62
|
+
...artifact,
|
|
63
|
+
steps,
|
|
64
|
+
status: "complete",
|
|
65
|
+
outcome,
|
|
66
|
+
completedAt: timestamp,
|
|
67
|
+
lastUsedAt: timestamp
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** Record a shadow potential-reuse observation (record mode). */
|
|
71
|
+
export function recordGateArtifactPotentialReuse(artifact, now) {
|
|
72
|
+
validateGateArtifact(artifact);
|
|
73
|
+
return validateGateArtifact({
|
|
74
|
+
...artifact,
|
|
75
|
+
potentialReuseCount: artifact.potentialReuseCount + 1,
|
|
76
|
+
lastUsedAt: now.toISOString()
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Record an actual reuse (reuse/enforce mode). */
|
|
80
|
+
export function recordGateArtifactReuse(artifact, now) {
|
|
81
|
+
validateGateArtifact(artifact);
|
|
82
|
+
if (artifact.status !== "complete" || artifact.outcome !== "succeeded") {
|
|
83
|
+
throw new Error(`Only a complete successful GateArtifact can be reused: ${artifact.key}.`);
|
|
84
|
+
}
|
|
85
|
+
return validateGateArtifact({
|
|
86
|
+
...artifact,
|
|
87
|
+
reuseCount: artifact.reuseCount + 1,
|
|
88
|
+
lastUsedAt: now.toISOString()
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function validateGateArtifact(artifact) {
|
|
92
|
+
if (artifact.schemaVersion !== GATE_ARTIFACT_SCHEMA_VERSION) {
|
|
93
|
+
throw new Error(`GateArtifact must use schemaVersion ${GATE_ARTIFACT_SCHEMA_VERSION}.`);
|
|
94
|
+
}
|
|
95
|
+
requireIdentity(artifact.projectId, "GateArtifact projectId");
|
|
96
|
+
if (artifact.level !== "L1" && artifact.level !== "L2") {
|
|
97
|
+
throw new Error(`GateArtifact level is invalid: ${String(artifact.level)}.`);
|
|
98
|
+
}
|
|
99
|
+
requireCommit(artifact.commit, "GateArtifact commit");
|
|
100
|
+
requireDigest(artifact.planDigest, "GateArtifact planDigest");
|
|
101
|
+
requireDigest(artifact.toolchainDigest, "GateArtifact toolchainDigest");
|
|
102
|
+
requireDigest(artifact.key, "GateArtifact key");
|
|
103
|
+
if (artifact.key !== gateArtifactKey({
|
|
104
|
+
projectId: artifact.projectId,
|
|
105
|
+
level: artifact.level,
|
|
106
|
+
commit: artifact.commit,
|
|
107
|
+
planDigest: artifact.planDigest,
|
|
108
|
+
toolchainDigest: artifact.toolchainDigest,
|
|
109
|
+
...(artifact.boundary === undefined ? {} : { boundary: artifact.boundary })
|
|
110
|
+
})) {
|
|
111
|
+
throw new Error("GateArtifact key does not match its identity tuple.");
|
|
112
|
+
}
|
|
113
|
+
if (artifact.level === "L2" && artifact.boundary === undefined) {
|
|
114
|
+
throw new Error("An L2 GateArtifact requires a target boundary.");
|
|
115
|
+
}
|
|
116
|
+
if (artifact.boundary !== undefined) {
|
|
117
|
+
requireText(artifact.boundary.targetRef, "GateArtifact boundary targetRef");
|
|
118
|
+
requireCommit(artifact.boundary.baseHead, "GateArtifact boundary baseHead");
|
|
119
|
+
}
|
|
120
|
+
if (!["incomplete", "complete"].includes(artifact.status)) {
|
|
121
|
+
throw new Error(`GateArtifact status is invalid: ${String(artifact.status)}.`);
|
|
122
|
+
}
|
|
123
|
+
if (!["unknown", "succeeded", "failed"].includes(artifact.outcome)) {
|
|
124
|
+
throw new Error(`GateArtifact outcome is invalid: ${String(artifact.outcome)}.`);
|
|
125
|
+
}
|
|
126
|
+
if (artifact.status === "incomplete" && artifact.outcome !== "unknown") {
|
|
127
|
+
throw new Error("An incomplete GateArtifact must have outcome unknown.");
|
|
128
|
+
}
|
|
129
|
+
if (artifact.status === "complete" && artifact.outcome === "unknown") {
|
|
130
|
+
throw new Error("A complete GateArtifact must have a terminal outcome.");
|
|
131
|
+
}
|
|
132
|
+
const names = new Set();
|
|
133
|
+
for (const step of artifact.steps) {
|
|
134
|
+
requireIdentity(step.name, "GateArtifact step name");
|
|
135
|
+
if (names.has(step.name)) {
|
|
136
|
+
throw new Error(`GateArtifact step names must be unique: ${step.name}.`);
|
|
137
|
+
}
|
|
138
|
+
names.add(step.name);
|
|
139
|
+
requireText(step.command, `GateArtifact step ${step.name} command`);
|
|
140
|
+
if (!["passed", "failed", "skipped"].includes(step.outcome)) {
|
|
141
|
+
throw new Error(`GateArtifact step ${step.name} outcome is invalid.`);
|
|
142
|
+
}
|
|
143
|
+
requirePositiveInteger(step.durationMs + 1, `GateArtifact step ${step.name} durationMs`);
|
|
144
|
+
requireText(step.logPath, `GateArtifact step ${step.name} logPath`);
|
|
145
|
+
if (isAbsolute(step.logPath) || step.logPath.includes("..")) {
|
|
146
|
+
throw new Error(`GateArtifact step ${step.name} logPath must be relative.`);
|
|
147
|
+
}
|
|
148
|
+
requireDigest(step.logDigest, `GateArtifact step ${step.name} logDigest`);
|
|
149
|
+
if (!Number.isSafeInteger(step.logBytes) || step.logBytes < 0) {
|
|
150
|
+
throw new Error(`GateArtifact step ${step.name} logBytes is invalid.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
requirePositiveInteger(artifact.potentialReuseCount + 1, "GateArtifact potentialReuseCount");
|
|
154
|
+
requirePositiveInteger(artifact.reuseCount + 1, "GateArtifact reuseCount");
|
|
155
|
+
requireTimestamp(artifact.createdAt, "GateArtifact createdAt");
|
|
156
|
+
requireTimestamp(artifact.lastUsedAt, "GateArtifact lastUsedAt");
|
|
157
|
+
if (artifact.completedAt !== undefined) {
|
|
158
|
+
requireTimestamp(artifact.completedAt, "GateArtifact completedAt");
|
|
159
|
+
}
|
|
160
|
+
return artifact;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Verify that every step log is present in the log map and matches its
|
|
164
|
+
* recorded digest. A lost or corrupted log means the artifact cannot serve
|
|
165
|
+
* as complete L2 evidence: consumers must treat it as a gap.
|
|
166
|
+
*/
|
|
167
|
+
export function verifyGateArtifactLogs(artifact, logs) {
|
|
168
|
+
const missing = [];
|
|
169
|
+
const corrupted = [];
|
|
170
|
+
for (const step of artifact.steps) {
|
|
171
|
+
const content = logs.get(step.name);
|
|
172
|
+
if (content === undefined) {
|
|
173
|
+
missing.push(step.name);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const digest = createHash("sha256").update(content).digest("hex");
|
|
177
|
+
if (digest !== step.logDigest || content.length !== step.logBytes) {
|
|
178
|
+
corrupted.push(step.name);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
ok: missing.length === 0 && corrupted.length === 0,
|
|
183
|
+
missing: Object.freeze(missing),
|
|
184
|
+
corrupted: Object.freeze(corrupted)
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
export function isReusableGateArtifact(artifact) {
|
|
188
|
+
return artifact.status === "complete" && artifact.outcome === "succeeded";
|
|
189
|
+
}
|
|
190
|
+
function requireCommit(value, label) {
|
|
191
|
+
const normalized = requireText(value, label).toLowerCase();
|
|
192
|
+
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(normalized)) {
|
|
193
|
+
throw new Error(`${label} is invalid.`);
|
|
194
|
+
}
|
|
195
|
+
return normalized;
|
|
196
|
+
}
|
|
197
|
+
function requireDigest(value, label) {
|
|
198
|
+
const normalized = requireText(value, label).toLowerCase();
|
|
199
|
+
if (!/^[a-f0-9]{64}$/u.test(normalized)) {
|
|
200
|
+
throw new Error(`${label} is invalid.`);
|
|
201
|
+
}
|
|
202
|
+
return normalized;
|
|
203
|
+
}
|
|
204
|
+
function canonicalJson(value) {
|
|
205
|
+
return JSON.stringify(canonicalize(value));
|
|
206
|
+
}
|
|
207
|
+
function canonicalize(value) {
|
|
208
|
+
if (Array.isArray(value))
|
|
209
|
+
return value.map(canonicalize);
|
|
210
|
+
if (typeof value === "object" && value !== null) {
|
|
211
|
+
const entries = Object.entries(value)
|
|
212
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
213
|
+
return Object.fromEntries(entries.map(([key, entry]) => [key, canonicalize(entry)]));
|
|
214
|
+
}
|
|
215
|
+
return value;
|
|
216
|
+
}
|