@stage5/lumine 0.2.68 → 0.2.69
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/admin-featured-history.js +185 -0
- package/lib/admin-runtime-logs.js +23 -3
- package/lib/admin-workflows.js +52 -2
- package/lib/admin.js +247 -87
- package/lib/commands.js +6 -3
- package/lib/constants.js +21 -0
- package/lib/sdk.js +6 -0
- package/lib/sponsor-duty.js +15 -2
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +66 -4
- package/sdk/LUMINE_ADMIN.md +215 -12
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { readAdminJsonFile, writeAdminJsonFile } from "./admin-news.js";
|
|
6
|
+
import {
|
|
7
|
+
acquireCheckpointLock,
|
|
8
|
+
releaseCheckpointLock,
|
|
9
|
+
runAutomaticPagination,
|
|
10
|
+
combinePaginatedResults,
|
|
11
|
+
writePaginatedResultFile,
|
|
12
|
+
} from "./admin-workflows.js";
|
|
13
|
+
|
|
14
|
+
// API read bound, deliberately unrelated to the delegated addition policy.
|
|
15
|
+
export const FEATURED_HISTORY_BATCH_SIZE = 100;
|
|
16
|
+
|
|
17
|
+
function invalid(message) {
|
|
18
|
+
const error = new Error(message);
|
|
19
|
+
error.code = "CLI_ADMIN_CLI_VALIDATION";
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runBatchedFeaturedHistory(args) {
|
|
24
|
+
const { options, operation, runId } = args;
|
|
25
|
+
if (options.adminCursor) {
|
|
26
|
+
throw invalid("Use --resume with --all, not a single-batch --cursor.");
|
|
27
|
+
}
|
|
28
|
+
const subjectIds = operation.pagination.filters.subjectIds;
|
|
29
|
+
const fingerprint = createHash("sha256")
|
|
30
|
+
.update(
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
version: 1,
|
|
33
|
+
runId,
|
|
34
|
+
apiUrl: options.apiUrl,
|
|
35
|
+
path: operation.path,
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
.digest("hex");
|
|
39
|
+
const checkpointPath = path.resolve(
|
|
40
|
+
options.adminCheckpoint ||
|
|
41
|
+
path.join(
|
|
42
|
+
os.tmpdir(),
|
|
43
|
+
`lumine-admin-run-${runId}-featured-history-${fingerprint}.json`,
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
const outputPath = options.adminOutput
|
|
47
|
+
? path.resolve(options.adminOutput)
|
|
48
|
+
: null;
|
|
49
|
+
if (
|
|
50
|
+
outputPath === checkpointPath ||
|
|
51
|
+
outputPath?.startsWith(`${checkpointPath}.`)
|
|
52
|
+
) {
|
|
53
|
+
throw invalid(
|
|
54
|
+
"History output must not overwrite its checkpoint or batch files.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const lock = acquireCheckpointLock(checkpointPath, fingerprint);
|
|
58
|
+
try {
|
|
59
|
+
let generation;
|
|
60
|
+
if (options.adminResume) {
|
|
61
|
+
const saved = readAdminJsonFile(
|
|
62
|
+
checkpointPath,
|
|
63
|
+
"the Featured history batch checkpoint",
|
|
64
|
+
);
|
|
65
|
+
if (
|
|
66
|
+
saved.kind !== "featured-history-batches" ||
|
|
67
|
+
saved.fingerprint !== fingerprint ||
|
|
68
|
+
!/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(
|
|
69
|
+
saved.generation || "",
|
|
70
|
+
)
|
|
71
|
+
) {
|
|
72
|
+
throw invalid(
|
|
73
|
+
"The checkpoint does not belong to this run and exact history request.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
generation = saved.generation;
|
|
77
|
+
} else {
|
|
78
|
+
generation = randomUUID();
|
|
79
|
+
writeAdminJsonFile(
|
|
80
|
+
checkpointPath,
|
|
81
|
+
{ kind: "featured-history-batches", fingerprint, generation },
|
|
82
|
+
{ privateFile: true },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const results = [];
|
|
86
|
+
for (
|
|
87
|
+
let offset = 0;
|
|
88
|
+
offset < subjectIds.length;
|
|
89
|
+
offset += FEATURED_HISTORY_BATCH_SIZE
|
|
90
|
+
) {
|
|
91
|
+
const ids = subjectIds.slice(
|
|
92
|
+
offset,
|
|
93
|
+
offset + FEATURED_HISTORY_BATCH_SIZE,
|
|
94
|
+
);
|
|
95
|
+
const batchIndex = offset / FEATURED_HISTORY_BATCH_SIZE;
|
|
96
|
+
const batchCheckpoint = `${checkpointPath}.${generation}.batch-${batchIndex}.json`;
|
|
97
|
+
const url = new URL(operation.path, "https://lumine.invalid");
|
|
98
|
+
url.searchParams.set("subjectIds", ids.join(","));
|
|
99
|
+
const result = await runAutomaticPagination({
|
|
100
|
+
...args,
|
|
101
|
+
options: {
|
|
102
|
+
...options,
|
|
103
|
+
adminOutput: undefined,
|
|
104
|
+
adminCheckpoint: batchCheckpoint,
|
|
105
|
+
adminResume: !!options.adminResume && existsSync(batchCheckpoint),
|
|
106
|
+
},
|
|
107
|
+
operation: {
|
|
108
|
+
...operation,
|
|
109
|
+
path: `${url.pathname}${url.search}`,
|
|
110
|
+
pagination: { ...operation.pagination, filters: { subjectIds: ids } },
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
if (
|
|
114
|
+
JSON.stringify(result.data.subjects?.map((subject) => subject.id)) !==
|
|
115
|
+
JSON.stringify(ids)
|
|
116
|
+
) {
|
|
117
|
+
throw invalid(
|
|
118
|
+
"The API did not return a canonical summary for every requested subject.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
results.push(result);
|
|
122
|
+
}
|
|
123
|
+
const firstCoverage = results[0].data.coverage;
|
|
124
|
+
const uniformCoverage = results.every(
|
|
125
|
+
(result) =>
|
|
126
|
+
result.data.coverage?.complete === firstCoverage.complete &&
|
|
127
|
+
result.data.coverage?.startedAt === firstCoverage.startedAt,
|
|
128
|
+
);
|
|
129
|
+
const result = combinePaginatedResults(
|
|
130
|
+
{
|
|
131
|
+
ok: true,
|
|
132
|
+
status: "success",
|
|
133
|
+
data: {
|
|
134
|
+
coverage: uniformCoverage
|
|
135
|
+
? firstCoverage
|
|
136
|
+
: { complete: false, startedAt: null },
|
|
137
|
+
subjects: results.flatMap((result) => result.data.subjects),
|
|
138
|
+
pagination: {
|
|
139
|
+
nextCursor: null,
|
|
140
|
+
hasMore: false,
|
|
141
|
+
exhausted: true,
|
|
142
|
+
snapshotMaxId: null,
|
|
143
|
+
snapshotTimeStamp: null,
|
|
144
|
+
after: null,
|
|
145
|
+
snapshotScope: "per-batch",
|
|
146
|
+
eventOrder: "subject-batch-then-id-descending",
|
|
147
|
+
},
|
|
148
|
+
scan: {
|
|
149
|
+
checkpointPath,
|
|
150
|
+
resumed: !!options.adminResume,
|
|
151
|
+
pages: results.reduce(
|
|
152
|
+
(sum, result) => sum + result.data.scan.pages,
|
|
153
|
+
0,
|
|
154
|
+
),
|
|
155
|
+
scannedCount: results.reduce(
|
|
156
|
+
(sum, result) => sum + result.data.scan.scannedCount,
|
|
157
|
+
0,
|
|
158
|
+
),
|
|
159
|
+
candidateCount: results.reduce(
|
|
160
|
+
(sum, result) => sum + result.data.scan.candidateCount,
|
|
161
|
+
0,
|
|
162
|
+
),
|
|
163
|
+
batches: results.map((result, index) => ({
|
|
164
|
+
subjectIds: subjectIds.slice(
|
|
165
|
+
index * FEATURED_HISTORY_BATCH_SIZE,
|
|
166
|
+
(index + 1) * FEATURED_HISTORY_BATCH_SIZE,
|
|
167
|
+
),
|
|
168
|
+
coverage: result.data.coverage,
|
|
169
|
+
pagination: result.data.pagination,
|
|
170
|
+
scan: result.data.scan,
|
|
171
|
+
})),
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
results,
|
|
176
|
+
);
|
|
177
|
+
if (outputPath) {
|
|
178
|
+
result.data.scan.outputPath = outputPath;
|
|
179
|
+
await writePaginatedResultFile(outputPath, result);
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
} finally {
|
|
183
|
+
releaseCheckpointLock(lock);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -141,7 +141,7 @@ function startIntentPath(options) {
|
|
|
141
141
|
);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
function readStartIntent(intentPath, apiUrl) {
|
|
144
|
+
function readStartIntent(intentPath, apiUrl, host = "primary") {
|
|
145
145
|
if (!existsSync(intentPath)) return null;
|
|
146
146
|
const intent = readAdminJsonFile(
|
|
147
147
|
intentPath,
|
|
@@ -156,6 +156,11 @@ function readStartIntent(intentPath, apiUrl) {
|
|
|
156
156
|
) {
|
|
157
157
|
return null;
|
|
158
158
|
}
|
|
159
|
+
if ((intent.host || "primary") !== host) {
|
|
160
|
+
throw validationError(
|
|
161
|
+
`A ${intent.host || "primary"} runtime-log start has an unresolved outcome. Replay that host's start before choosing ${host}; its request key was preserved.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
159
164
|
return intent;
|
|
160
165
|
}
|
|
161
166
|
|
|
@@ -185,6 +190,7 @@ function createSession({ options, apiUrl, review }) {
|
|
|
185
190
|
kind: "lumine-admin-runtime-log-review",
|
|
186
191
|
apiUrl,
|
|
187
192
|
reviewId: review.id,
|
|
193
|
+
...(review.ownerHostId ? { ownerHostId: review.ownerHostId } : {}),
|
|
188
194
|
leaseToken: review.leaseToken,
|
|
189
195
|
status: review.status,
|
|
190
196
|
outputDirectory,
|
|
@@ -581,10 +587,14 @@ export async function runAdminRuntimeLogWorkflow({
|
|
|
581
587
|
}) {
|
|
582
588
|
const apiUrl = normalizeApiUrl(options.apiUrl);
|
|
583
589
|
if (operation.runtimeLogAction === "start") {
|
|
590
|
+
const host = operation.runtimeLogHost || "primary";
|
|
591
|
+
if (!["primary", "target"].includes(host)) {
|
|
592
|
+
throw validationError("Invalid runtime-log host.");
|
|
593
|
+
}
|
|
584
594
|
const intentPath = startIntentPath(options);
|
|
585
595
|
const intent = options.idempotencyKey
|
|
586
596
|
? null
|
|
587
|
-
: readStartIntent(intentPath, apiUrl);
|
|
597
|
+
: readStartIntent(intentPath, apiUrl, host);
|
|
588
598
|
const startRequestId = intent?.requestId || requestId;
|
|
589
599
|
if (!intent) {
|
|
590
600
|
writeAdminJsonFile(
|
|
@@ -592,6 +602,7 @@ export async function runAdminRuntimeLogWorkflow({
|
|
|
592
602
|
{
|
|
593
603
|
kind: "lumine-admin-runtime-log-review-start",
|
|
594
604
|
apiUrl,
|
|
605
|
+
host,
|
|
595
606
|
requestId: startRequestId,
|
|
596
607
|
createdAt: new Date().toISOString(),
|
|
597
608
|
},
|
|
@@ -602,7 +613,7 @@ export async function runAdminRuntimeLogWorkflow({
|
|
|
602
613
|
try {
|
|
603
614
|
result = await requestJson({
|
|
604
615
|
method: "POST",
|
|
605
|
-
url: `${apiUrl}/cli/admin/runtime-logs/reviews`,
|
|
616
|
+
url: `${apiUrl}/cli/admin/runtime-logs${operation.runtimeLogHost ? `/hosts/${host}` : ""}/reviews`,
|
|
606
617
|
authToken,
|
|
607
618
|
body: {},
|
|
608
619
|
headers: requestHeaders({ requestId: startRequestId }),
|
|
@@ -631,6 +642,15 @@ export async function runAdminRuntimeLogWorkflow({
|
|
|
631
642
|
throw error;
|
|
632
643
|
}
|
|
633
644
|
const review = validateReview(result?.data?.review);
|
|
645
|
+
if (
|
|
646
|
+
operation.runtimeLogHost &&
|
|
647
|
+
(!/^i-[a-f0-9]{17}$/.test(review.ownerHostId || "") ||
|
|
648
|
+
review.ownerHostRole !== host)
|
|
649
|
+
) {
|
|
650
|
+
throw validationError(
|
|
651
|
+
"The API did not confirm a host-owned review. Preserve the start intent and recover it; do not clear logs.",
|
|
652
|
+
);
|
|
653
|
+
}
|
|
634
654
|
const session = createSession({ options, apiUrl, review });
|
|
635
655
|
// The session now holds the lease; a later rerun must start fresh.
|
|
636
656
|
clearStartIntent(intentPath);
|
package/lib/admin-workflows.js
CHANGED
|
@@ -282,6 +282,15 @@ function operationFingerprintValue({
|
|
|
282
282
|
apiUrl: String(options.apiUrl || "").replace(/\/$/, ""),
|
|
283
283
|
name: operation.name,
|
|
284
284
|
path: pathWithCursor(operation.path, ""),
|
|
285
|
+
...(operation.pagination.summaryKeys
|
|
286
|
+
? { summaryKeys: operation.pagination.summaryKeys }
|
|
287
|
+
: {}),
|
|
288
|
+
// A completed pre-upgrade scan can still contain the old 30-day cap.
|
|
289
|
+
// Reject that checkpoint too: it would otherwise skip fetching entirely
|
|
290
|
+
// and record truncated coverage as a successful since-run scan.
|
|
291
|
+
...(operation.pagination.coverageMode === "since-run"
|
|
292
|
+
? { discoveryWindowContract: "stored-run-start-v1" }
|
|
293
|
+
: {}),
|
|
285
294
|
...(operation.name === "builds.candidates"
|
|
286
295
|
? {
|
|
287
296
|
resultTransform: {
|
|
@@ -637,6 +646,16 @@ export async function forEachPaginatedResultItem(result, visit) {
|
|
|
637
646
|
"The result does not contain spooled pagination data.",
|
|
638
647
|
);
|
|
639
648
|
}
|
|
649
|
+
if (storage.parts) {
|
|
650
|
+
let index = 0;
|
|
651
|
+
for (const part of storage.parts) {
|
|
652
|
+
await forEachPaginatedResultItem(
|
|
653
|
+
attachPaginationStorage({}, part),
|
|
654
|
+
async (item) => visit(item, index++),
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
640
659
|
verifyCandidateSpool({ ...storage, discardUnconfirmedTail: false });
|
|
641
660
|
const input = createReadStream(storage.spoolPath, { encoding: "utf8" });
|
|
642
661
|
const observedHash = createHash("sha256");
|
|
@@ -706,7 +725,7 @@ export async function writePaginatedResultJson({ result, write }) {
|
|
|
706
725
|
await write("]}}\n");
|
|
707
726
|
}
|
|
708
727
|
|
|
709
|
-
async function writePaginatedResultFile(filePath, result) {
|
|
728
|
+
export async function writePaginatedResultFile(filePath, result) {
|
|
710
729
|
const resolved = path.resolve(String(filePath || "").trim());
|
|
711
730
|
if (!String(filePath || "").trim()) {
|
|
712
731
|
throw validationError("An output file path is required.");
|
|
@@ -743,9 +762,26 @@ async function writePaginatedResultFile(filePath, result) {
|
|
|
743
762
|
return resolved;
|
|
744
763
|
}
|
|
745
764
|
|
|
765
|
+
// Keep large history event sets on disk when combining independently bounded
|
|
766
|
+
// subject batches. Each part retains its own verified spool and snapshot.
|
|
767
|
+
export function combinePaginatedResults(result, parts) {
|
|
768
|
+
const storages = parts.map(getPaginatedResultStorage);
|
|
769
|
+
if (
|
|
770
|
+
!storages.length ||
|
|
771
|
+
storages.some((entry) => !entry || entry.collectionKey !== storages[0].collectionKey)
|
|
772
|
+
) {
|
|
773
|
+
throw validationError("Cannot combine incompatible paginated results.");
|
|
774
|
+
}
|
|
775
|
+
return attachPaginationStorage(result, {
|
|
776
|
+
collectionKey: storages[0].collectionKey,
|
|
777
|
+
candidateCount: storages.reduce((sum, entry) => sum + entry.candidateCount, 0),
|
|
778
|
+
parts: storages,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
746
782
|
function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
|
|
747
783
|
const last = lastPage || { ok: true, status: "success", data: {} };
|
|
748
|
-
const lastData = { ...(last.data || {}) };
|
|
784
|
+
const lastData = { ...(last.data || {}), ...state.summaryData };
|
|
749
785
|
delete lastData[operation.pagination.collectionKey];
|
|
750
786
|
const lastPagination = { ...(last.data?.pagination || {}) };
|
|
751
787
|
const pageScannedCount = normalizeScannedCount(
|
|
@@ -809,6 +845,17 @@ function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
|
|
|
809
845
|
});
|
|
810
846
|
}
|
|
811
847
|
|
|
848
|
+
function retainedSummaryData(operation, data) {
|
|
849
|
+
return Object.fromEntries(
|
|
850
|
+
(operation.pagination.summaryKeys || []).map((key) => {
|
|
851
|
+
if (!data || !Object.hasOwn(data, key)) {
|
|
852
|
+
throw validationError(`History scan is missing ${key} summary metadata. Start a fresh scan.`);
|
|
853
|
+
}
|
|
854
|
+
return [key, data[key]];
|
|
855
|
+
}),
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
812
859
|
export async function runAutomaticPagination(args) {
|
|
813
860
|
const { options, operation, runId } = args;
|
|
814
861
|
if (!operation.pagination) {
|
|
@@ -929,6 +976,7 @@ async function runAutomaticPaginationWithLock({
|
|
|
929
976
|
filterSummariesComplete: true,
|
|
930
977
|
clientFilter: null,
|
|
931
978
|
operatorViewFilter: null,
|
|
979
|
+
summaryData: {},
|
|
932
980
|
resumed: false,
|
|
933
981
|
updatedAt: new Date().toISOString(),
|
|
934
982
|
};
|
|
@@ -1059,6 +1107,7 @@ async function runAutomaticPaginationWithLock({
|
|
|
1059
1107
|
savedPages > 0 && saved?.filterSummariesComplete === true
|
|
1060
1108
|
? normalizeOperatorViewFilterSummary(saved?.operatorViewFilter)
|
|
1061
1109
|
: null,
|
|
1110
|
+
summaryData: savedPages > 0 ? retainedSummaryData(operation, saved.summaryData) : {},
|
|
1062
1111
|
resumed: true,
|
|
1063
1112
|
};
|
|
1064
1113
|
spoolHash = verifyCandidateSpool({
|
|
@@ -1215,6 +1264,7 @@ async function runAutomaticPaginationWithLock({
|
|
|
1215
1264
|
spoolSha256: appended.spoolSha256,
|
|
1216
1265
|
clientFilter,
|
|
1217
1266
|
operatorViewFilter,
|
|
1267
|
+
summaryData: retainedSummaryData(operation, data),
|
|
1218
1268
|
nextCursor,
|
|
1219
1269
|
exhausted: pagination.exhausted,
|
|
1220
1270
|
updatedAt: new Date().toISOString(),
|