@stage5/lumine 0.2.66 → 0.2.68
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 +26 -1
- package/lib/admin-featured.js +559 -0
- package/lib/admin-runtime-logs.js +786 -0
- package/lib/admin-workflows.js +121 -46
- package/lib/admin.js +474 -40
- package/lib/commands.js +52 -8
- package/lib/sponsor-duty.js +479 -28
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +2 -2
- package/sdk/LUMINE_ADMIN.md +515 -83
package/lib/admin-workflows.js
CHANGED
|
@@ -130,7 +130,7 @@ function pathEntryExists(filePath) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
function acquireCheckpointLock(checkpointPath, operationFingerprint) {
|
|
133
|
+
export function acquireCheckpointLock(checkpointPath, operationFingerprint) {
|
|
134
134
|
const lockPath = `${checkpointPath}.lock`;
|
|
135
135
|
const reclaimPath = `${lockPath}.reclaim`;
|
|
136
136
|
mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
@@ -253,7 +253,7 @@ function removeDeadCheckpointLock(lockPath) {
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
function releaseCheckpointLock(lock) {
|
|
256
|
+
export function releaseCheckpointLock(lock) {
|
|
257
257
|
try {
|
|
258
258
|
const fileState = lstatSync(lock.lockPath);
|
|
259
259
|
if (!fileState.isFile() || fileState.isSymbolicLink()) return;
|
|
@@ -339,9 +339,13 @@ function createCandidateSpool(checkpointPath) {
|
|
|
339
339
|
function loadSupersededCandidateSpool(checkpointPath) {
|
|
340
340
|
let saved;
|
|
341
341
|
try {
|
|
342
|
-
saved = readAdminJsonFile(
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
saved = readAdminJsonFile(
|
|
343
|
+
checkpointPath,
|
|
344
|
+
"the previous pagination checkpoint",
|
|
345
|
+
{
|
|
346
|
+
maxBytes: MAX_LEGACY_ADMIN_CHECKPOINT_BYTES,
|
|
347
|
+
},
|
|
348
|
+
);
|
|
345
349
|
} catch {
|
|
346
350
|
return null;
|
|
347
351
|
}
|
|
@@ -413,8 +417,7 @@ function normalizeClientFilterSummary(value) {
|
|
|
413
417
|
if (
|
|
414
418
|
!Array.isArray(value.contentTypes) ||
|
|
415
419
|
value.contentTypes.some(
|
|
416
|
-
(contentType) =>
|
|
417
|
-
typeof contentType !== "string" || !contentType.trim(),
|
|
420
|
+
(contentType) => typeof contentType !== "string" || !contentType.trim(),
|
|
418
421
|
)
|
|
419
422
|
) {
|
|
420
423
|
throw validationError(
|
|
@@ -522,7 +525,9 @@ function verifyCandidateSpool({
|
|
|
522
525
|
try {
|
|
523
526
|
fileState = lstatSync(spoolPath);
|
|
524
527
|
} catch {
|
|
525
|
-
throw validationError(
|
|
528
|
+
throw validationError(
|
|
529
|
+
`Could not read pagination candidates at ${spoolPath}.`,
|
|
530
|
+
);
|
|
526
531
|
}
|
|
527
532
|
if (!fileState.isFile() || fileState.isSymbolicLink()) {
|
|
528
533
|
throw validationError(
|
|
@@ -628,7 +633,9 @@ function serializedObjectEntries(value, excludedKeys = new Set()) {
|
|
|
628
633
|
export async function forEachPaginatedResultItem(result, visit) {
|
|
629
634
|
const storage = paginationStorageFromResult(result);
|
|
630
635
|
if (!storage) {
|
|
631
|
-
throw validationError(
|
|
636
|
+
throw validationError(
|
|
637
|
+
"The result does not contain spooled pagination data.",
|
|
638
|
+
);
|
|
632
639
|
}
|
|
633
640
|
verifyCandidateSpool({ ...storage, discardUnconfirmedTail: false });
|
|
634
641
|
const input = createReadStream(storage.spoolPath, { encoding: "utf8" });
|
|
@@ -805,7 +812,9 @@ function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
|
|
|
805
812
|
export async function runAutomaticPagination(args) {
|
|
806
813
|
const { options, operation, runId } = args;
|
|
807
814
|
if (!operation.pagination) {
|
|
808
|
-
throw validationError(
|
|
815
|
+
throw validationError(
|
|
816
|
+
"--all is supported only by paginated admin listings.",
|
|
817
|
+
);
|
|
809
818
|
}
|
|
810
819
|
const operationFingerprint = operationFingerprintValue({
|
|
811
820
|
schemaVersion: ADMIN_PAGINATION_SCHEMA_VERSION,
|
|
@@ -820,13 +829,51 @@ export async function runAutomaticPagination(args) {
|
|
|
820
829
|
operationFingerprint,
|
|
821
830
|
});
|
|
822
831
|
const lock = acquireCheckpointLock(checkpointPath, operationFingerprint);
|
|
832
|
+
const signalSource = args.signalSource || process;
|
|
833
|
+
const controller = new AbortController();
|
|
834
|
+
let interruptedSignal = "";
|
|
835
|
+
const interrupt = (signalName) => {
|
|
836
|
+
interruptedSignal ||= signalName;
|
|
837
|
+
controller.abort(
|
|
838
|
+
new Error(`Lumine admin scan interrupted by ${signalName}.`),
|
|
839
|
+
);
|
|
840
|
+
};
|
|
841
|
+
const onSigint = () => interrupt("SIGINT");
|
|
842
|
+
const onSigterm = () => interrupt("SIGTERM");
|
|
843
|
+
signalSource.once("SIGINT", onSigint);
|
|
844
|
+
signalSource.once("SIGTERM", onSigterm);
|
|
823
845
|
try {
|
|
824
846
|
return await runAutomaticPaginationWithLock({
|
|
825
847
|
...args,
|
|
826
848
|
checkpointPath,
|
|
827
849
|
operationFingerprint,
|
|
850
|
+
signal: controller.signal,
|
|
828
851
|
});
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (interruptedSignal) {
|
|
854
|
+
const interrupted = validationError(
|
|
855
|
+
`Canonical scan cancelled by ${interruptedSignal}. Confirmed progress remains at ${checkpointPath}; rerun the same command with --resume to continue it.`,
|
|
856
|
+
);
|
|
857
|
+
interrupted.code = "LUMINE_ADMIN_SCAN_CANCELLED";
|
|
858
|
+
interrupted.data = {
|
|
859
|
+
ok: false,
|
|
860
|
+
status: "validation_error",
|
|
861
|
+
error: {
|
|
862
|
+
code: interrupted.code,
|
|
863
|
+
message: interrupted.message,
|
|
864
|
+
details: {
|
|
865
|
+
checkpointPath,
|
|
866
|
+
signal: interruptedSignal,
|
|
867
|
+
resumable: true,
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
};
|
|
871
|
+
throw interrupted;
|
|
872
|
+
}
|
|
873
|
+
throw error;
|
|
829
874
|
} finally {
|
|
875
|
+
signalSource.removeListener("SIGINT", onSigint);
|
|
876
|
+
signalSource.removeListener("SIGTERM", onSigterm);
|
|
830
877
|
releaseCheckpointLock(lock);
|
|
831
878
|
}
|
|
832
879
|
}
|
|
@@ -841,6 +888,7 @@ async function runAutomaticPaginationWithLock({
|
|
|
841
888
|
transformPage,
|
|
842
889
|
recordCoverage,
|
|
843
890
|
reportProgress = (message) => process.stderr.write(`${message}\n`),
|
|
891
|
+
signal,
|
|
844
892
|
}) {
|
|
845
893
|
const requestedOutputPath = String(options.adminOutput || "").trim();
|
|
846
894
|
if (options.adminOutput && !requestedOutputPath) {
|
|
@@ -990,15 +1038,12 @@ async function runAutomaticPaginationWithLock({
|
|
|
990
1038
|
: null,
|
|
991
1039
|
snapshotTimeStamp:
|
|
992
1040
|
savedPages > 0
|
|
993
|
-
? normalizeBoundary(
|
|
994
|
-
saved?.snapshotTimeStamp,
|
|
995
|
-
"snapshotTimeStamp",
|
|
996
|
-
)
|
|
1041
|
+
? normalizeBoundary(saved?.snapshotTimeStamp, "snapshotTimeStamp")
|
|
997
1042
|
: null,
|
|
998
1043
|
after:
|
|
999
1044
|
savedPages > 0
|
|
1000
1045
|
? normalizeBoundary(saved?.after, "after")
|
|
1001
|
-
: operation.pagination.after ?? null,
|
|
1046
|
+
: (operation.pagination.after ?? null),
|
|
1002
1047
|
boundariesConfirmed: saved?.boundariesConfirmed,
|
|
1003
1048
|
candidateCount,
|
|
1004
1049
|
spoolPath: path.resolve(String(saved?.spoolPath || "")),
|
|
@@ -1065,19 +1110,27 @@ async function runAutomaticPaginationWithLock({
|
|
|
1065
1110
|
}
|
|
1066
1111
|
let lastPage = null;
|
|
1067
1112
|
while (!state.exhausted) {
|
|
1113
|
+
signal?.throwIfAborted();
|
|
1068
1114
|
if (state.pages >= MAX_AUTOMATIC_PAGES) {
|
|
1069
1115
|
throw validationError(
|
|
1070
1116
|
`Automatic pagination stopped at the ${MAX_AUTOMATIC_PAGES}-page safety ceiling. Start a narrower request with a new checkpoint.`,
|
|
1071
1117
|
);
|
|
1072
1118
|
}
|
|
1073
1119
|
const requestPath = pathWithCursor(operation.path, state.nextCursor);
|
|
1074
|
-
const raw = await fetchPage(requestPath);
|
|
1120
|
+
const raw = await fetchPage(requestPath, signal);
|
|
1121
|
+
signal?.throwIfAborted();
|
|
1075
1122
|
const page = transformPage(raw);
|
|
1076
1123
|
lastPage = page;
|
|
1077
1124
|
const data = page?.data || {};
|
|
1078
1125
|
const pagination = data.pagination;
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1126
|
+
if (
|
|
1127
|
+
!pagination ||
|
|
1128
|
+
typeof pagination !== "object" ||
|
|
1129
|
+
Array.isArray(pagination)
|
|
1130
|
+
) {
|
|
1131
|
+
throw validationError(
|
|
1132
|
+
"The API response is missing canonical pagination metadata.",
|
|
1133
|
+
);
|
|
1081
1134
|
}
|
|
1082
1135
|
const items = Array.isArray(data[operation.pagination.collectionKey])
|
|
1083
1136
|
? data[operation.pagination.collectionKey]
|
|
@@ -1089,7 +1142,9 @@ async function runAutomaticPaginationWithLock({
|
|
|
1089
1142
|
}
|
|
1090
1143
|
const nextCursor = String(pagination.nextCursor || "");
|
|
1091
1144
|
if (nextCursor && nextCursor === state.nextCursor) {
|
|
1092
|
-
throw validationError(
|
|
1145
|
+
throw validationError(
|
|
1146
|
+
"The API returned the same pagination cursor twice.",
|
|
1147
|
+
);
|
|
1093
1148
|
}
|
|
1094
1149
|
if (!pagination.exhausted && !nextCursor) {
|
|
1095
1150
|
throw validationError(
|
|
@@ -1111,7 +1166,11 @@ async function runAutomaticPaginationWithLock({
|
|
|
1111
1166
|
);
|
|
1112
1167
|
const pageAfter = normalizeBoundary(pagination.after, "after");
|
|
1113
1168
|
if (state.boundariesConfirmed) {
|
|
1114
|
-
assertStableBoundary(
|
|
1169
|
+
assertStableBoundary(
|
|
1170
|
+
"snapshotMaxId",
|
|
1171
|
+
state.snapshotMaxId,
|
|
1172
|
+
pageSnapshotMaxId,
|
|
1173
|
+
);
|
|
1115
1174
|
assertStableBoundary(
|
|
1116
1175
|
"snapshotTimeStamp",
|
|
1117
1176
|
state.snapshotTimeStamp,
|
|
@@ -1119,7 +1178,10 @@ async function runAutomaticPaginationWithLock({
|
|
|
1119
1178
|
);
|
|
1120
1179
|
assertStableBoundary("after", state.after, pageAfter);
|
|
1121
1180
|
} else {
|
|
1122
|
-
const requestedAfter = normalizeBoundary(
|
|
1181
|
+
const requestedAfter = normalizeBoundary(
|
|
1182
|
+
operation.pagination.after,
|
|
1183
|
+
"after",
|
|
1184
|
+
);
|
|
1123
1185
|
if (requestedAfter !== null) {
|
|
1124
1186
|
assertStableBoundary("after", requestedAfter, pageAfter);
|
|
1125
1187
|
}
|
|
@@ -1147,10 +1209,7 @@ async function runAutomaticPaginationWithLock({
|
|
|
1147
1209
|
pages: state.pages + 1,
|
|
1148
1210
|
scannedCount:
|
|
1149
1211
|
state.scannedCount +
|
|
1150
|
-
normalizeScannedCount(
|
|
1151
|
-
pagination.scannedCount,
|
|
1152
|
-
items.length,
|
|
1153
|
-
),
|
|
1212
|
+
normalizeScannedCount(pagination.scannedCount, items.length),
|
|
1154
1213
|
candidateCount: state.candidateCount + items.length,
|
|
1155
1214
|
spoolBytes: state.spoolBytes + appended.addedBytes,
|
|
1156
1215
|
spoolSha256: appended.spoolSha256,
|
|
@@ -1177,19 +1236,24 @@ async function runAutomaticPaginationWithLock({
|
|
|
1177
1236
|
checkpointPath,
|
|
1178
1237
|
state,
|
|
1179
1238
|
});
|
|
1239
|
+
signal?.throwIfAborted();
|
|
1180
1240
|
if (recordCoverage && operation.pagination.coverageQueue) {
|
|
1181
|
-
await recordCoverage(
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1241
|
+
await recordCoverage(
|
|
1242
|
+
{
|
|
1243
|
+
queue: operation.pagination.coverageQueue,
|
|
1244
|
+
mode: operation.pagination.coverageMode || "all",
|
|
1245
|
+
after: state.after,
|
|
1246
|
+
pages: state.pages,
|
|
1247
|
+
scannedCount: state.scannedCount,
|
|
1248
|
+
candidateCount: state.candidateCount,
|
|
1249
|
+
snapshotMaxId: state.snapshotMaxId,
|
|
1250
|
+
snapshotTimeStamp: state.snapshotTimeStamp,
|
|
1251
|
+
exhausted: state.exhausted,
|
|
1252
|
+
filters: operation.pagination.filters || {},
|
|
1253
|
+
},
|
|
1254
|
+
signal,
|
|
1255
|
+
);
|
|
1256
|
+
signal?.throwIfAborted();
|
|
1193
1257
|
}
|
|
1194
1258
|
if (outputPath) {
|
|
1195
1259
|
result.data.scan.outputPath = outputPath;
|
|
@@ -1226,12 +1290,16 @@ export function readBatchSkipTargets({ filePath, parseTarget, defaultReason }) {
|
|
|
1226
1290
|
throw validationError("The batch target file contains no targets.");
|
|
1227
1291
|
}
|
|
1228
1292
|
if (values.length > MAX_BATCH_TARGETS) {
|
|
1229
|
-
throw validationError(
|
|
1293
|
+
throw validationError(
|
|
1294
|
+
`A batch can contain at most ${MAX_BATCH_TARGETS} targets.`,
|
|
1295
|
+
);
|
|
1230
1296
|
}
|
|
1231
1297
|
const deduped = new Map();
|
|
1232
1298
|
for (const value of values) {
|
|
1233
1299
|
const rawTarget =
|
|
1234
|
-
typeof value === "string"
|
|
1300
|
+
typeof value === "string"
|
|
1301
|
+
? value
|
|
1302
|
+
: value?.target || value?.url || value?.id;
|
|
1235
1303
|
const parsed = parseTarget({
|
|
1236
1304
|
target: rawTarget,
|
|
1237
1305
|
explicitType: typeof value === "object" ? value?.type || "" : "",
|
|
@@ -1298,7 +1366,9 @@ export async function runBatchSkips({
|
|
|
1298
1366
|
!saved.completed ||
|
|
1299
1367
|
typeof saved.completed !== "object"
|
|
1300
1368
|
) {
|
|
1301
|
-
throw validationError(
|
|
1369
|
+
throw validationError(
|
|
1370
|
+
"The checkpoint does not match this exact skip batch.",
|
|
1371
|
+
);
|
|
1302
1372
|
}
|
|
1303
1373
|
state = saved;
|
|
1304
1374
|
} else {
|
|
@@ -1339,8 +1409,9 @@ export async function runBatchSkips({
|
|
|
1339
1409
|
batch: {
|
|
1340
1410
|
targetCount: targets.length,
|
|
1341
1411
|
completedCount: Object.keys(state.completed).length,
|
|
1342
|
-
changedCount: Object.values(state.completed).filter(
|
|
1343
|
-
.
|
|
1412
|
+
changedCount: Object.values(state.completed).filter(
|
|
1413
|
+
(entry) => entry.changed,
|
|
1414
|
+
).length,
|
|
1344
1415
|
checkpointPath,
|
|
1345
1416
|
resumed: options.adminResume === true,
|
|
1346
1417
|
targets: state.completed,
|
|
@@ -1348,10 +1419,14 @@ export async function runBatchSkips({
|
|
|
1348
1419
|
},
|
|
1349
1420
|
};
|
|
1350
1421
|
if (options.adminOutput) {
|
|
1351
|
-
result.data.batch.outputPath = writeAdminJsonFile(
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1422
|
+
result.data.batch.outputPath = writeAdminJsonFile(
|
|
1423
|
+
options.adminOutput,
|
|
1424
|
+
result,
|
|
1425
|
+
{
|
|
1426
|
+
privateFile: true,
|
|
1427
|
+
maxBytes: MAX_BATCH_CHECKPOINT_BYTES,
|
|
1428
|
+
},
|
|
1429
|
+
);
|
|
1355
1430
|
}
|
|
1356
1431
|
return result;
|
|
1357
1432
|
}
|