@stage5/lumine 0.2.76 → 0.2.79
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 +4 -0
- package/lib/admin.js +404 -87
- package/lib/commands.js +24 -4
- package/lib/constants.js +13 -4
- package/lib/rewards.js +46 -13
- package/lib/sdk.js +10 -2
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +17 -11
- package/sdk/LUMINE_ADMIN.md +81 -21
package/lib/admin.js
CHANGED
|
@@ -31,8 +31,14 @@ import {
|
|
|
31
31
|
runManagedBuildReview,
|
|
32
32
|
} from "./build-review.js";
|
|
33
33
|
import { runAdminRuntimeLogWorkflow } from "./admin-runtime-logs.js";
|
|
34
|
-
import {
|
|
35
|
-
|
|
34
|
+
import {
|
|
35
|
+
readApprovedFeaturedPlan,
|
|
36
|
+
runFeaturedWorkflow,
|
|
37
|
+
} from "./admin-featured.js";
|
|
38
|
+
import {
|
|
39
|
+
FEATURED_HISTORY_BATCH_SIZE,
|
|
40
|
+
runBatchedFeaturedHistory,
|
|
41
|
+
} from "./admin-featured-history.js";
|
|
36
42
|
|
|
37
43
|
const MAX_EDITORIAL_FILE_BYTES = 256 * 1024;
|
|
38
44
|
const MAX_COMPOSED_TEXT_FILE_BYTES = 64 * 1024;
|
|
@@ -133,6 +139,80 @@ function readBuildReviewContextFile(filePath) {
|
|
|
133
139
|
const MAX_REWARD_CONFIG_FILE_BYTES = 256 * 1024;
|
|
134
140
|
const REWARD_REVIEW_STATUSES = ["pending", "approved", "all"];
|
|
135
141
|
const REWARD_REVIEW_DECISIONS = ["approve", "reject", "revoke"];
|
|
142
|
+
const REWARD_PROPOSAL_MAX_FILES = 500;
|
|
143
|
+
const REWARD_PROPOSAL_MAX_BYTES = 5 * 1024 * 1024;
|
|
144
|
+
const REWARD_PROPOSAL_SKIPPED_DIRS = new Set([
|
|
145
|
+
".git",
|
|
146
|
+
"node_modules",
|
|
147
|
+
".lumine",
|
|
148
|
+
".twinkle",
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
// Reads a reviewer's edited copy of a reward-review snapshot (a directory
|
|
152
|
+
// written by `show --dir`, then edited) back into project files for
|
|
153
|
+
// `reward-review propose`. Text files only; dotfiles and tool directories
|
|
154
|
+
// are skipped; paths are confined to the directory.
|
|
155
|
+
export function readRewardProposalDirectory(directory) {
|
|
156
|
+
const requested = String(directory || "").trim();
|
|
157
|
+
if (!requested) {
|
|
158
|
+
throw cliValidationError(
|
|
159
|
+
"Pass the edited snapshot directory with --dir <path>.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const root = path.resolve(requested);
|
|
163
|
+
let rootStat;
|
|
164
|
+
try {
|
|
165
|
+
rootStat = lstatSync(root);
|
|
166
|
+
} catch {
|
|
167
|
+
throw cliValidationError(`--dir ${root} does not exist.`);
|
|
168
|
+
}
|
|
169
|
+
if (!rootStat.isDirectory()) {
|
|
170
|
+
throw cliValidationError(`--dir ${root} must be a directory.`);
|
|
171
|
+
}
|
|
172
|
+
const realRoot = realpathSync(root);
|
|
173
|
+
const files = [];
|
|
174
|
+
let bytes = 0;
|
|
175
|
+
const walk = (dir) => {
|
|
176
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
177
|
+
if (entry.name.startsWith(".") || REWARD_PROPOSAL_SKIPPED_DIRS.has(entry.name))
|
|
178
|
+
continue;
|
|
179
|
+
const fullPath = path.join(dir, entry.name);
|
|
180
|
+
if (entry.isSymbolicLink()) {
|
|
181
|
+
throw cliValidationError(
|
|
182
|
+
`Refusing to read symlink inside the proposal directory: ${fullPath}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (entry.isDirectory()) {
|
|
186
|
+
walk(fullPath);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!entry.isFile()) continue;
|
|
190
|
+
const buffer = readFileSync(fullPath);
|
|
191
|
+
if (buffer.includes(0)) {
|
|
192
|
+
throw cliValidationError(
|
|
193
|
+
`${fullPath} is not a text file. Twinkle project files must be UTF-8 text; media belongs in build assets.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
bytes += buffer.length;
|
|
197
|
+
const relative = path.relative(realRoot, fullPath).split(path.sep).join("/");
|
|
198
|
+
files.push({ path: `/${relative}`, content: buffer.toString("utf8") });
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
walk(realRoot);
|
|
202
|
+
if (files.length === 0) {
|
|
203
|
+
throw cliValidationError(`--dir ${root} holds no project files.`);
|
|
204
|
+
}
|
|
205
|
+
if (files.length > REWARD_PROPOSAL_MAX_FILES) {
|
|
206
|
+
throw cliValidationError(
|
|
207
|
+
`A proposal may carry at most ${REWARD_PROPOSAL_MAX_FILES} files.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (bytes > REWARD_PROPOSAL_MAX_BYTES) {
|
|
211
|
+
throw cliValidationError("A proposal may carry at most 5 MB of files.");
|
|
212
|
+
}
|
|
213
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
214
|
+
return files;
|
|
215
|
+
}
|
|
136
216
|
|
|
137
217
|
// The reviewer's earning rules for one Build reward approval: budgets plus
|
|
138
218
|
// server-verified numeric-quiz rules keyed by the rule IDs the app source
|
|
@@ -166,7 +246,7 @@ export function readRewardConfigFile(filePath) {
|
|
|
166
246
|
}
|
|
167
247
|
if (!Array.isArray(parsed.rules) || parsed.rules.length === 0) {
|
|
168
248
|
throw cliValidationError(
|
|
169
|
-
|
|
249
|
+
'Approval needs at least one earning rule in rules[] (id, title, xp, coins, verifier: "numeric-quiz", questions[{prompt, answer}] and/or sets[{from, to?, questions}], optional maxAttempts (null = unlimited) and retry {xpPercent, coinsPercent}).',
|
|
170
250
|
);
|
|
171
251
|
}
|
|
172
252
|
return parsed;
|
|
@@ -178,18 +258,26 @@ export function readRewardConfigFile(filePath) {
|
|
|
178
258
|
export function writeRewardReviewSnapshot({ directory, files }) {
|
|
179
259
|
const requested = String(directory || "").trim();
|
|
180
260
|
if (!requested) {
|
|
181
|
-
throw cliValidationError(
|
|
261
|
+
throw cliValidationError(
|
|
262
|
+
"Pass a new or empty directory with --dir <path>.",
|
|
263
|
+
);
|
|
182
264
|
}
|
|
183
265
|
const root = path.resolve(requested);
|
|
184
266
|
if (root === path.resolve("/")) {
|
|
185
|
-
throw cliValidationError(
|
|
267
|
+
throw cliValidationError(
|
|
268
|
+
"Pass a new or empty directory with --dir <path>.",
|
|
269
|
+
);
|
|
186
270
|
}
|
|
187
271
|
// The snapshot must land in a directory that holds nothing else, so the
|
|
188
272
|
// reviewer never reads stale files from another review or clobbers a real
|
|
189
273
|
// workspace, and so no pre-existing symlink can redirect a write.
|
|
190
274
|
if (existsSync(root)) {
|
|
191
275
|
const stat = lstatSync(root);
|
|
192
|
-
if (
|
|
276
|
+
if (
|
|
277
|
+
stat.isSymbolicLink() ||
|
|
278
|
+
!stat.isDirectory() ||
|
|
279
|
+
readdirSync(root).length
|
|
280
|
+
) {
|
|
193
281
|
throw cliValidationError(
|
|
194
282
|
`--dir ${root} must be a new or empty directory (not a symlink, file, or populated folder).`,
|
|
195
283
|
);
|
|
@@ -382,7 +470,12 @@ export async function adminCommand(options) {
|
|
|
382
470
|
viewFilter,
|
|
383
471
|
});
|
|
384
472
|
if (operation.featuredWorkflow) {
|
|
385
|
-
result = await runFeaturedWorkflow({
|
|
473
|
+
result = await runFeaturedWorkflow({
|
|
474
|
+
options,
|
|
475
|
+
operation,
|
|
476
|
+
authToken: auth.token,
|
|
477
|
+
runId,
|
|
478
|
+
});
|
|
386
479
|
} else if (operation.name === "build.review") {
|
|
387
480
|
result = await runManagedBuildReview({
|
|
388
481
|
options,
|
|
@@ -409,9 +502,12 @@ export async function adminCommand(options) {
|
|
|
409
502
|
"Use --resume with the scan checkpoint instead of combining --all with --cursor.",
|
|
410
503
|
);
|
|
411
504
|
}
|
|
412
|
-
const paginate =
|
|
413
|
-
operation.
|
|
414
|
-
|
|
505
|
+
const paginate =
|
|
506
|
+
operation.name === "featured.history" &&
|
|
507
|
+
operation.pagination.filters.subjectIds.length >
|
|
508
|
+
FEATURED_HISTORY_BATCH_SIZE
|
|
509
|
+
? runBatchedFeaturedHistory
|
|
510
|
+
: runAutomaticPagination;
|
|
415
511
|
result = await paginate({
|
|
416
512
|
options,
|
|
417
513
|
operation,
|
|
@@ -443,11 +539,16 @@ export async function adminCommand(options) {
|
|
|
443
539
|
} catch (error) {
|
|
444
540
|
if (operation.name === "runtime.evidence" && error.status === 404) {
|
|
445
541
|
error.code = "CLI_ADMIN_RUNTIME_EVIDENCE_NOT_DEPLOYED";
|
|
446
|
-
error.message =
|
|
542
|
+
error.message =
|
|
543
|
+
"The runtime evidence route is not deployed on the requested host. Evidence is unknown; deploy the matching API and activate the collector in an authorized primary-generation release. No restart or host substitution was attempted.";
|
|
447
544
|
error.data = {
|
|
448
545
|
ok: false,
|
|
449
546
|
status: "unavailable",
|
|
450
|
-
error: {
|
|
547
|
+
error: {
|
|
548
|
+
code: error.code,
|
|
549
|
+
message: error.message,
|
|
550
|
+
details: { httpStatus: 404 },
|
|
551
|
+
},
|
|
451
552
|
};
|
|
452
553
|
}
|
|
453
554
|
if (operation.featuredWorkflow && error.featuredProgress) {
|
|
@@ -456,12 +557,18 @@ export async function adminCommand(options) {
|
|
|
456
557
|
ok: false,
|
|
457
558
|
status: "partial_failure",
|
|
458
559
|
error: {
|
|
459
|
-
code:
|
|
560
|
+
code:
|
|
561
|
+
serverError?.code ||
|
|
562
|
+
error.code ||
|
|
563
|
+
"LUMINE_ADMIN_FEATURED_WORKFLOW_FAILED",
|
|
460
564
|
message: error.message,
|
|
461
565
|
details: {
|
|
462
|
-
...(serverError && typeof serverError === "object"
|
|
566
|
+
...(serverError && typeof serverError === "object"
|
|
567
|
+
? serverError.details
|
|
568
|
+
: {}),
|
|
463
569
|
...error.featuredProgress,
|
|
464
|
-
retryInstruction:
|
|
570
|
+
retryInstruction:
|
|
571
|
+
"Resume the exact command with --resume; confirmed items are not replayed.",
|
|
465
572
|
},
|
|
466
573
|
},
|
|
467
574
|
};
|
|
@@ -649,7 +756,11 @@ async function finishAdminOutput({ options, operation, result }) {
|
|
|
649
756
|
operation.name !== "news.claim" &&
|
|
650
757
|
operation.name !== "post.skip-batch"
|
|
651
758
|
) {
|
|
652
|
-
writeAdminResultOutput({
|
|
759
|
+
writeAdminResultOutput({
|
|
760
|
+
filePath: options.adminOutput,
|
|
761
|
+
result,
|
|
762
|
+
operation,
|
|
763
|
+
});
|
|
653
764
|
}
|
|
654
765
|
if (options.json) {
|
|
655
766
|
if (paginationStorage) {
|
|
@@ -1098,8 +1209,16 @@ export function assertAdminOperationAllowedForRunScope({
|
|
|
1098
1209
|
}) {
|
|
1099
1210
|
if (runScope === "newspaper") {
|
|
1100
1211
|
if (
|
|
1101
|
-
[
|
|
1102
|
-
|
|
1212
|
+
[
|
|
1213
|
+
"news.status",
|
|
1214
|
+
"news.claim",
|
|
1215
|
+
"news.submit",
|
|
1216
|
+
"news.print",
|
|
1217
|
+
"daily-run.complete",
|
|
1218
|
+
"daily-run.fail",
|
|
1219
|
+
].includes(operation.name)
|
|
1220
|
+
)
|
|
1221
|
+
return;
|
|
1103
1222
|
throw cliValidationError(
|
|
1104
1223
|
`A newspaper-only run does not authorize ${operation.name}.`,
|
|
1105
1224
|
);
|
|
@@ -1137,6 +1256,7 @@ function adminOperationRequiresRun(operation) {
|
|
|
1137
1256
|
"escalation.list",
|
|
1138
1257
|
"escalation.set",
|
|
1139
1258
|
"notable.add",
|
|
1259
|
+
"notable.remove",
|
|
1140
1260
|
"notable.status",
|
|
1141
1261
|
"runtime-logs.start",
|
|
1142
1262
|
"runtime-logs.status",
|
|
@@ -1393,7 +1513,9 @@ export function parseAdminOperation(options) {
|
|
|
1393
1513
|
if (action === "show" || action === "get") {
|
|
1394
1514
|
const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
|
|
1395
1515
|
if (options.dir && !snapshotDir) {
|
|
1396
|
-
throw cliValidationError(
|
|
1516
|
+
throw cliValidationError(
|
|
1517
|
+
"Pass a new or empty directory with --dir <path>.",
|
|
1518
|
+
);
|
|
1397
1519
|
}
|
|
1398
1520
|
return readOperation(
|
|
1399
1521
|
"reward-review.show",
|
|
@@ -1405,6 +1527,32 @@ export function parseAdminOperation(options) {
|
|
|
1405
1527
|
{ requiresRun: false, reviewId, snapshotDir },
|
|
1406
1528
|
);
|
|
1407
1529
|
}
|
|
1530
|
+
if (action === "propose") {
|
|
1531
|
+
// Offer an edited copy of the snapshot as the condition of approval:
|
|
1532
|
+
// the creator accepts (which publishes it) or declines (which rejects).
|
|
1533
|
+
const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
|
|
1534
|
+
const reason = String(options.adminReason || "").trim();
|
|
1535
|
+
if (reason.length > 1000) {
|
|
1536
|
+
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
1537
|
+
}
|
|
1538
|
+
if (!options.adminConfigFile) {
|
|
1539
|
+
throw cliValidationError(
|
|
1540
|
+
"lumine admin reward-review propose <id> needs --config <rules.json> (the rules the creator's acceptance publishes) and --dir <edited snapshot>.",
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
const files = readRewardProposalDirectory(options.dir);
|
|
1544
|
+
return writeOperation(
|
|
1545
|
+
"reward-review.propose",
|
|
1546
|
+
"POST",
|
|
1547
|
+
`/cli/admin/reward-reviews/${reviewId}/propose`,
|
|
1548
|
+
{
|
|
1549
|
+
files,
|
|
1550
|
+
config: readRewardConfigFile(options.adminConfigFile),
|
|
1551
|
+
reason,
|
|
1552
|
+
},
|
|
1553
|
+
{ requiresRun: false, reviewId, fileCount: files.length },
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1408
1556
|
if (REWARD_REVIEW_DECISIONS.includes(action)) {
|
|
1409
1557
|
const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
|
|
1410
1558
|
const reason = String(options.adminReason || "").trim();
|
|
@@ -1420,7 +1568,8 @@ export function parseAdminOperation(options) {
|
|
|
1420
1568
|
if (action === "approve") {
|
|
1421
1569
|
// Without --config the app's own proposal (rewards.json + sheet, frozen
|
|
1422
1570
|
// in the request) is approved as it stands; --config replaces it.
|
|
1423
|
-
if (options.adminConfigFile)
|
|
1571
|
+
if (options.adminConfigFile)
|
|
1572
|
+
body.config = readRewardConfigFile(options.adminConfigFile);
|
|
1424
1573
|
} else if (options.adminConfigFile) {
|
|
1425
1574
|
throw cliValidationError(
|
|
1426
1575
|
"--config is only used with approve; rejections and revocations take --reason.",
|
|
@@ -1435,19 +1584,29 @@ export function parseAdminOperation(options) {
|
|
|
1435
1584
|
);
|
|
1436
1585
|
}
|
|
1437
1586
|
throw cliValidationError(
|
|
1438
|
-
"Usage: lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] | show <id> [--dir <path>] | approve <id> [--config <rules.json>] [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
|
|
1587
|
+
"Usage: lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] | show <id> [--dir <path>] | approve <id> [--config <rules.json>] [--reason <text>] | propose <id> --dir <edited-snapshot> --config <rules.json> [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
|
|
1439
1588
|
);
|
|
1440
1589
|
}
|
|
1441
1590
|
|
|
1442
1591
|
if (namespace === "reward-activity" || namespace === "reward-telemetry") {
|
|
1443
1592
|
// Read-only claim telemetry for the daily run: no run lease, no mutation.
|
|
1444
|
-
const
|
|
1593
|
+
const date = options.adminDate
|
|
1594
|
+
? parseUtcDayKey(options.adminDate)
|
|
1595
|
+
: undefined;
|
|
1596
|
+
const days = options.adminDays
|
|
1597
|
+
? parseRequiredInteger(options.adminDays, "--days", 1)
|
|
1598
|
+
: date
|
|
1599
|
+
? 1
|
|
1600
|
+
: 7;
|
|
1445
1601
|
if (days > 31) throw cliValidationError("--days must be at most 31.");
|
|
1446
1602
|
return readOperation(
|
|
1447
1603
|
"reward-activity.report",
|
|
1448
1604
|
withQuery("/cli/admin/reward-activity", {
|
|
1449
1605
|
days: String(days),
|
|
1450
|
-
|
|
1606
|
+
date,
|
|
1607
|
+
buildId: options.buildIdFlag
|
|
1608
|
+
? String(parseRequiredInteger(options.buildIdFlag, "--build", 1))
|
|
1609
|
+
: "",
|
|
1451
1610
|
}),
|
|
1452
1611
|
{ requiresRun: false },
|
|
1453
1612
|
);
|
|
@@ -1623,9 +1782,7 @@ export function parseAdminOperation(options) {
|
|
|
1623
1782
|
const runScope = parseDailyRunScope(options.adminScope || "full");
|
|
1624
1783
|
const commentMode = parseCommentMode(options.commentMode || "off");
|
|
1625
1784
|
if (runScope !== "full" && commentMode !== "off") {
|
|
1626
|
-
throw cliValidationError(
|
|
1627
|
-
"A scoped run requires --comment-mode off.",
|
|
1628
|
-
);
|
|
1785
|
+
throw cliValidationError("A scoped run requires --comment-mode off.");
|
|
1629
1786
|
}
|
|
1630
1787
|
return writeOperation(
|
|
1631
1788
|
"daily-run.start",
|
|
@@ -1899,27 +2056,68 @@ export function parseAdminOperation(options) {
|
|
|
1899
2056
|
if (namespace === "featured") {
|
|
1900
2057
|
if (action === "comments") {
|
|
1901
2058
|
if (!["scan", "acknowledge", "recommend", "report"].includes(target)) {
|
|
1902
|
-
throw cliValidationError(
|
|
2059
|
+
throw cliValidationError(
|
|
2060
|
+
"Usage: featured comments scan|acknowledge|recommend|report --checkpoint <file>.",
|
|
2061
|
+
);
|
|
1903
2062
|
}
|
|
1904
|
-
if (
|
|
1905
|
-
|
|
2063
|
+
if (
|
|
2064
|
+
options.adminAll ||
|
|
2065
|
+
options.adminCursor ||
|
|
2066
|
+
options.adminUnviewed ||
|
|
2067
|
+
options.adminViewed ||
|
|
2068
|
+
options.adminAfter
|
|
2069
|
+
) {
|
|
2070
|
+
throw cliValidationError(
|
|
2071
|
+
"The Featured review covers every comment; filtered or caller-supplied cursors are not supported.",
|
|
2072
|
+
);
|
|
1906
2073
|
}
|
|
1907
|
-
return {
|
|
1908
|
-
|
|
2074
|
+
return {
|
|
2075
|
+
...writeOperation(
|
|
2076
|
+
`featured.comments.${target}`,
|
|
2077
|
+
"POST",
|
|
2078
|
+
"/cli/admin/subjects/featured/reviews",
|
|
2079
|
+
{},
|
|
2080
|
+
),
|
|
2081
|
+
mutates: target !== "report",
|
|
2082
|
+
featuredWorkflow: target,
|
|
2083
|
+
};
|
|
1909
2084
|
}
|
|
1910
2085
|
if (action === "plan") {
|
|
1911
|
-
if (!options.adminPostedAfter)
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2086
|
+
if (!options.adminPostedAfter)
|
|
2087
|
+
throw cliValidationError(
|
|
2088
|
+
"A Featured plan requires --posted-after <timestamp>.",
|
|
2089
|
+
);
|
|
2090
|
+
return writeOperation(
|
|
2091
|
+
"featured.plan",
|
|
2092
|
+
"POST",
|
|
2093
|
+
"/cli/admin/subjects/featured/plan",
|
|
2094
|
+
{
|
|
2095
|
+
removeIds: parseFeaturedSubjectIds(
|
|
2096
|
+
options.adminRemoveIds,
|
|
2097
|
+
"--remove-subject-ids",
|
|
2098
|
+
),
|
|
2099
|
+
addIds: parseFeaturedSubjectIds(
|
|
2100
|
+
options.adminAddIds,
|
|
2101
|
+
"--add-subject-ids",
|
|
2102
|
+
),
|
|
2103
|
+
...(options.adminIds
|
|
2104
|
+
? { finalIds: parseOrderedIds(options.adminIds) }
|
|
2105
|
+
: {}),
|
|
2106
|
+
postedAfter: options.adminPostedAfter,
|
|
2107
|
+
},
|
|
2108
|
+
);
|
|
1918
2109
|
}
|
|
1919
2110
|
if (action === "apply") {
|
|
1920
2111
|
readApprovedFeaturedPlan(options.adminFile, options.adminApprove);
|
|
1921
|
-
return {
|
|
1922
|
-
|
|
2112
|
+
return {
|
|
2113
|
+
...writeOperation(
|
|
2114
|
+
"featured.apply",
|
|
2115
|
+
"POST",
|
|
2116
|
+
"/cli/admin/subjects/featured/plan/apply",
|
|
2117
|
+
{},
|
|
2118
|
+
),
|
|
2119
|
+
featuredWorkflow: "apply",
|
|
2120
|
+
};
|
|
1923
2121
|
}
|
|
1924
2122
|
if (action === "list") {
|
|
1925
2123
|
return readOperation("featured.list", "/cli/admin/subjects/featured");
|
|
@@ -1930,10 +2128,17 @@ export function parseAdminOperation(options) {
|
|
|
1930
2128
|
"--subject-ids",
|
|
1931
2129
|
);
|
|
1932
2130
|
if (subjectIds.length > 20_000) {
|
|
1933
|
-
throw cliValidationError(
|
|
2131
|
+
throw cliValidationError(
|
|
2132
|
+
"Featured history accepts at most 20000 subject IDs per CLI scan.",
|
|
2133
|
+
);
|
|
1934
2134
|
}
|
|
1935
|
-
if (
|
|
1936
|
-
|
|
2135
|
+
if (
|
|
2136
|
+
subjectIds.length > FEATURED_HISTORY_BATCH_SIZE &&
|
|
2137
|
+
!options.adminAll
|
|
2138
|
+
) {
|
|
2139
|
+
throw cliValidationError(
|
|
2140
|
+
"Pass --all to automatically batch history reads larger than 100 subjects.",
|
|
2141
|
+
);
|
|
1937
2142
|
}
|
|
1938
2143
|
return readOperation(
|
|
1939
2144
|
"featured.history",
|
|
@@ -2118,11 +2323,11 @@ export function parseAdminOperation(options) {
|
|
|
2118
2323
|
);
|
|
2119
2324
|
}
|
|
2120
2325
|
|
|
2121
|
-
if (namespace === "notable" &&
|
|
2326
|
+
if (namespace === "notable" && ["add", "remove"].includes(action) && !extra) {
|
|
2122
2327
|
const rawTarget = String(target || "").trim();
|
|
2123
2328
|
if (!rawTarget) {
|
|
2124
2329
|
throw cliValidationError(
|
|
2125
|
-
|
|
2330
|
+
`Usage: lumine admin notable ${action} <userId|username> --note <text>.`,
|
|
2126
2331
|
);
|
|
2127
2332
|
}
|
|
2128
2333
|
const body = /^\d+$/.test(rawTarget)
|
|
@@ -2133,7 +2338,9 @@ export function parseAdminOperation(options) {
|
|
|
2133
2338
|
const note = String(options.note || "").trim();
|
|
2134
2339
|
if (!note) {
|
|
2135
2340
|
throw cliValidationError(
|
|
2136
|
-
|
|
2341
|
+
action === "remove"
|
|
2342
|
+
? "Explain why this user should be removed with --note <text>."
|
|
2343
|
+
: "Pass what made this user notable with --note <text>.",
|
|
2137
2344
|
);
|
|
2138
2345
|
}
|
|
2139
2346
|
if (note.length > MAX_NOTABLE_NOTE_LENGTH) {
|
|
@@ -2143,8 +2350,8 @@ export function parseAdminOperation(options) {
|
|
|
2143
2350
|
}
|
|
2144
2351
|
body.note = note;
|
|
2145
2352
|
return writeOperation(
|
|
2146
|
-
|
|
2147
|
-
"POST",
|
|
2353
|
+
`notable.${action}`,
|
|
2354
|
+
action === "remove" ? "DELETE" : "POST",
|
|
2148
2355
|
"/cli/admin/notable-users",
|
|
2149
2356
|
body,
|
|
2150
2357
|
);
|
|
@@ -2218,8 +2425,14 @@ export function parseAdminOperation(options) {
|
|
|
2218
2425
|
}
|
|
2219
2426
|
|
|
2220
2427
|
if (namespace === "runtime") {
|
|
2221
|
-
if (
|
|
2222
|
-
|
|
2428
|
+
if (
|
|
2429
|
+
action !== "evidence" ||
|
|
2430
|
+
!["primary", "target"].includes(target) ||
|
|
2431
|
+
extra
|
|
2432
|
+
) {
|
|
2433
|
+
throw cliValidationError(
|
|
2434
|
+
"Usage: lumine admin runtime evidence primary|target [--days 1..7].",
|
|
2435
|
+
);
|
|
2223
2436
|
}
|
|
2224
2437
|
return readOperation(
|
|
2225
2438
|
"runtime.evidence",
|
|
@@ -2233,7 +2446,8 @@ export function parseAdminOperation(options) {
|
|
|
2233
2446
|
if (namespace === "runtime-logs") {
|
|
2234
2447
|
if (
|
|
2235
2448
|
(action === "start" || action === "status" || action === "read") &&
|
|
2236
|
-
(!target ||
|
|
2449
|
+
(!target ||
|
|
2450
|
+
(action === "start" && ["primary", "target"].includes(target))) &&
|
|
2237
2451
|
!extra
|
|
2238
2452
|
) {
|
|
2239
2453
|
if (
|
|
@@ -2679,7 +2893,9 @@ function featuredRotateOperation(options) {
|
|
|
2679
2893
|
{
|
|
2680
2894
|
removeIds,
|
|
2681
2895
|
addIds,
|
|
2682
|
-
...(options.adminPostedAfter
|
|
2896
|
+
...(options.adminPostedAfter
|
|
2897
|
+
? { postedAfter: options.adminPostedAfter }
|
|
2898
|
+
: {}),
|
|
2683
2899
|
},
|
|
2684
2900
|
);
|
|
2685
2901
|
}
|
|
@@ -3398,7 +3614,9 @@ function printAdminEnergyBudget(energyBudget) {
|
|
|
3398
3614
|
`AI Energy budget health, last completed UTC day ${headline.dayKey}: ${formatAdminUsd(headline.chargedUsd)} charged, ${formatAdminUsd(headline.overflowUsd)} overflow, ${headline.users} user(s), ${headline.recharges} recharge(s), ${headline.runs.total.runs} run(s), ${headline.telemetry.metrics.busy_refusal.count} busy refusal(s).`,
|
|
3399
3615
|
);
|
|
3400
3616
|
} else {
|
|
3401
|
-
console.log(
|
|
3617
|
+
console.log(
|
|
3618
|
+
"AI Energy budget health: no completed UTC day in the requested window.",
|
|
3619
|
+
);
|
|
3402
3620
|
}
|
|
3403
3621
|
console.log(
|
|
3404
3622
|
"day | charged | overflow | users | rechg | runs | calls/run avg/p90 | $/run avg/p90 | stops chg/unchg | busy | telemetry rows",
|
|
@@ -3416,7 +3634,9 @@ function printAdminEnergyBudget(energyBudget) {
|
|
|
3416
3634
|
console.log("Flags: none.");
|
|
3417
3635
|
return;
|
|
3418
3636
|
}
|
|
3419
|
-
console.log(
|
|
3637
|
+
console.log(
|
|
3638
|
+
`Flags (${flags.length}) — record each as a carry-over todo and escalate; never auto-enforce:`,
|
|
3639
|
+
);
|
|
3420
3640
|
for (const flag of flags) {
|
|
3421
3641
|
console.log(` ${flag.dayKey} ${flag.code}: ${flag.message}`);
|
|
3422
3642
|
}
|
|
@@ -3616,25 +3836,41 @@ function formatRewardReviewLine(review) {
|
|
|
3616
3836
|
}
|
|
3617
3837
|
|
|
3618
3838
|
// One reviewer-facing line per earning rule: amounts, what a later try pays,
|
|
3619
|
-
// how many tries, and which
|
|
3839
|
+
// how many tries, and which site days (UTC) the dated sets cover.
|
|
3620
3840
|
export function formatRewardRuleLine(rule) {
|
|
3621
|
-
const parts = [
|
|
3841
|
+
const parts = [
|
|
3842
|
+
`rule ${rule.id}: ${rule.title} · ${rule.xp} XP + ${rule.coins} Coins`,
|
|
3843
|
+
];
|
|
3622
3844
|
if (rule.verifier === "completion") {
|
|
3623
|
-
parts.push(
|
|
3845
|
+
parts.push(
|
|
3846
|
+
`completion · pays when the app reports it finished at least ${rule.minSeconds || 0}s after start · once per learner per day`,
|
|
3847
|
+
);
|
|
3624
3848
|
return parts.join(" · ");
|
|
3625
3849
|
}
|
|
3626
3850
|
if (rule.retry) {
|
|
3627
|
-
parts.push(
|
|
3851
|
+
parts.push(
|
|
3852
|
+
`retry pays ${Math.floor((rule.xp * rule.retry.xpPercent) / 100)} XP + ${Math.floor((rule.coins * rule.retry.coinsPercent) / 100)} Coins${rule.retry.paidAttempts ? ` through try ${rule.retry.paidAttempts} (later solves pay nothing)` : ""}`,
|
|
3853
|
+
);
|
|
3628
3854
|
}
|
|
3629
|
-
parts.push(
|
|
3855
|
+
parts.push(
|
|
3856
|
+
rule.maxAttempts === null
|
|
3857
|
+
? "unlimited tries"
|
|
3858
|
+
: `${rule.maxAttempts ?? 3} tries`,
|
|
3859
|
+
);
|
|
3630
3860
|
const standing = Array.isArray(rule.questions) ? rule.questions.length : 0;
|
|
3631
3861
|
const sets = Array.isArray(rule.sets) ? rule.sets : [];
|
|
3632
3862
|
if (sets.length && rule.progression === "until-earned") {
|
|
3633
3863
|
const keys = sets.map((set, index) => set.key || `set-${index + 1}`);
|
|
3634
|
-
parts.push(
|
|
3864
|
+
parts.push(
|
|
3865
|
+
`${sets.length} until-earned set(s) in order: ${keys.join(", ")}${standing ? ` · ${standing} standing question(s)` : ""}`,
|
|
3866
|
+
);
|
|
3635
3867
|
} else if (sets.length) {
|
|
3636
|
-
const days = sets.map((set) =>
|
|
3637
|
-
|
|
3868
|
+
const days = sets.map((set) =>
|
|
3869
|
+
set.to && set.to !== set.from ? `${set.from}..${set.to}` : set.from,
|
|
3870
|
+
);
|
|
3871
|
+
parts.push(
|
|
3872
|
+
`${sets.length} dated set(s): ${days.join(", ")}${standing ? ` · ${standing} standing question(s)` : " · no standing questions"}`,
|
|
3873
|
+
);
|
|
3638
3874
|
} else {
|
|
3639
3875
|
parts.push(`${standing} question(s)`);
|
|
3640
3876
|
}
|
|
@@ -3644,32 +3880,79 @@ export function formatRewardRuleLine(rule) {
|
|
|
3644
3880
|
function printRewardReviewResult({ operation, data }) {
|
|
3645
3881
|
if (operation.name === "reward-review.list") {
|
|
3646
3882
|
const reviews = Array.isArray(data.reviews) ? data.reviews : [];
|
|
3647
|
-
console.log(
|
|
3648
|
-
|
|
3883
|
+
console.log(
|
|
3884
|
+
`${reviews.length} reward review(s) (${data.filter || "pending"}):`,
|
|
3885
|
+
);
|
|
3886
|
+
for (const review of reviews)
|
|
3887
|
+
console.log(` ${formatRewardReviewLine(review)}`);
|
|
3649
3888
|
if (data.nextCursor) console.log(`More: --cursor ${data.nextCursor}`);
|
|
3650
3889
|
return;
|
|
3651
3890
|
}
|
|
3652
3891
|
const review = data.review || {};
|
|
3653
3892
|
console.log(formatRewardReviewLine(review));
|
|
3654
3893
|
if (operation.name === "reward-review.decide") {
|
|
3655
|
-
console.log(
|
|
3894
|
+
console.log(
|
|
3895
|
+
`Decision recorded: ${operation.decision}. ${review.reason ? `Reason: ${review.reason}` : ""}`.trim(),
|
|
3896
|
+
);
|
|
3897
|
+
if (review.published) {
|
|
3898
|
+
console.log(
|
|
3899
|
+
`Published on approval: version ${review.published.version} (artifact ${review.published.artifactVersionId}, ${review.published.transition}). The app is live now.`,
|
|
3900
|
+
);
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
if (operation.name === "reward-review.propose") {
|
|
3904
|
+
console.log(
|
|
3905
|
+
`Proposal offered to the creator (${operation.fileCount} file(s) sent). They can accept (which publishes your version with these rules) or decline (which rejects the request).`,
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
if (review.proposal) {
|
|
3909
|
+
const summary = review.proposal.diffSummary || {};
|
|
3910
|
+
console.log(
|
|
3911
|
+
`Proposal: ${summary.total ?? 0} file(s) changed (${summary.added ?? 0} added, ${summary.updated ?? 0} updated, ${summary.deleted ?? 0} deleted)${review.proposal.note ? ` · note: ${review.proposal.note}` : ""}${review.status === "changes_offered" ? " · waiting for the creator" : ""}`,
|
|
3912
|
+
);
|
|
3913
|
+
for (const file of review.proposal.changedFiles || [])
|
|
3914
|
+
console.log(` ${file.status}: ${file.path}`);
|
|
3915
|
+
}
|
|
3916
|
+
if (review.status === "rejected" && review.declinedByCreator) {
|
|
3917
|
+
console.log("The creator declined the proposed changes; the request is closed.");
|
|
3918
|
+
}
|
|
3919
|
+
if (review.status === "approved" && review.publishedArtifactVersionId) {
|
|
3920
|
+
console.log(
|
|
3921
|
+
`Approved and published (artifact version ${review.publishedArtifactVersionId}).`,
|
|
3922
|
+
);
|
|
3656
3923
|
}
|
|
3657
3924
|
if (Array.isArray(review.detectedRuleIds)) {
|
|
3658
|
-
console.log(
|
|
3925
|
+
console.log(
|
|
3926
|
+
`Rule IDs found in source (heuristic): ${review.detectedRuleIds.join(", ") || "none"}`,
|
|
3927
|
+
);
|
|
3659
3928
|
}
|
|
3660
|
-
if (review.isLatest === false)
|
|
3661
|
-
|
|
3929
|
+
if (review.isLatest === false)
|
|
3930
|
+
console.log(
|
|
3931
|
+
"WARNING: a newer request exists for this app; decide on the latest one.",
|
|
3932
|
+
);
|
|
3933
|
+
if (review.isLive)
|
|
3934
|
+
console.log("This review is the live approval currently paying out.");
|
|
3662
3935
|
if (review.awarded) {
|
|
3663
|
-
console.log(
|
|
3936
|
+
console.log(
|
|
3937
|
+
`Paid by this review: ${review.awarded.awards} awards to ${review.awarded.earners} people · ${review.awarded.xp} XP · ${review.awarded.coins} Coins (app lifetime ${review.appLifetime?.xp ?? 0} XP / ${review.appLifetime?.coins ?? 0} Coins)`,
|
|
3938
|
+
);
|
|
3664
3939
|
}
|
|
3665
3940
|
const config = review.config || {};
|
|
3666
3941
|
if (Array.isArray(config.rules)) {
|
|
3667
|
-
console.log(
|
|
3668
|
-
|
|
3942
|
+
console.log(
|
|
3943
|
+
`Budgets: day ${config.dailyXP} XP/${config.dailyCoins} Coins · per user/day ${config.userDailyXP} XP/${config.userDailyCoins} Coins${config.userDailyClaims ? ` · ${config.userDailyClaims} claim(s)` : ""} · lifetime ${config.lifetimeXP} XP/${config.lifetimeCoins} Coins`,
|
|
3944
|
+
);
|
|
3945
|
+
for (const rule of config.rules)
|
|
3946
|
+
console.log(` ${formatRewardRuleLine(rule)}`);
|
|
3669
3947
|
}
|
|
3670
3948
|
if (Array.isArray(review.files)) {
|
|
3671
|
-
console.log(
|
|
3672
|
-
|
|
3949
|
+
console.log(
|
|
3950
|
+
`Source: ${review.files.length} file(s)${data.snapshotDirectory ? ` written to ${data.snapshotDirectory}` : " (pass --dir <path> to write the snapshot)"}`,
|
|
3951
|
+
);
|
|
3952
|
+
for (const file of review.files)
|
|
3953
|
+
console.log(
|
|
3954
|
+
` ${file.path} (${file.bytes ?? Buffer.byteLength(String(file.content || ""), "utf8")} bytes)`,
|
|
3955
|
+
);
|
|
3673
3956
|
}
|
|
3674
3957
|
console.log("Use --json for the full record.");
|
|
3675
3958
|
}
|
|
@@ -3677,22 +3960,42 @@ function printRewardReviewResult({ operation, data }) {
|
|
|
3677
3960
|
function printRewardActivity(data) {
|
|
3678
3961
|
const apps = Array.isArray(data.apps) ? data.apps : [];
|
|
3679
3962
|
const suspects = Array.isArray(data.suspects) ? data.suspects : [];
|
|
3680
|
-
console.log(
|
|
3963
|
+
console.log(
|
|
3964
|
+
`Reward activity ${data.from} → ${data.to} (${data.days} day(s)): ${apps.length} app(s) paid, ${suspects.length} flagged player-day(s).`,
|
|
3965
|
+
);
|
|
3681
3966
|
for (const app of apps) {
|
|
3682
|
-
console.log(
|
|
3967
|
+
console.log(
|
|
3968
|
+
` app ${app.buildId} ${app.title}: ${app.claims} claim(s) · ${app.earners} earner(s) · ${app.xp} XP · ${app.coins} Coins · ${app.flagged} flagged`,
|
|
3969
|
+
);
|
|
3683
3970
|
}
|
|
3684
3971
|
if (!suspects.length) {
|
|
3685
|
-
console.log(
|
|
3972
|
+
console.log(
|
|
3973
|
+
"Nothing unusual: no claim on the minimum time, no bursts, no sweeps, no repeated cap days, no guessing.",
|
|
3974
|
+
);
|
|
3686
3975
|
return;
|
|
3687
3976
|
}
|
|
3688
3977
|
console.log("Flagged (worst first):");
|
|
3689
3978
|
for (const s of suspects) {
|
|
3690
|
-
const parts = [
|
|
3691
|
-
|
|
3692
|
-
|
|
3979
|
+
const parts = [
|
|
3980
|
+
`${s.flags.join("+")}`,
|
|
3981
|
+
`user ${s.userId}${s.username ? ` ${s.username}` : ""}`,
|
|
3982
|
+
`app ${s.buildId} ${s.title}`,
|
|
3983
|
+
s.dayKey,
|
|
3984
|
+
`${s.claims} claim(s) · ${s.xp} XP`,
|
|
3985
|
+
];
|
|
3986
|
+
if (s.fastClaims)
|
|
3987
|
+
parts.push(
|
|
3988
|
+
`${s.fastClaims} on the minimum (fastest ${s.minElapsedSeconds}s)`,
|
|
3989
|
+
);
|
|
3990
|
+
if (s.guessing)
|
|
3991
|
+
parts.push(
|
|
3992
|
+
`${s.guessing} challenge(s) with ${data.limits?.guessingAttempts ?? 15}+ wrong answers`,
|
|
3993
|
+
);
|
|
3693
3994
|
console.log(` ${parts.join(" · ")}`);
|
|
3694
3995
|
}
|
|
3695
|
-
console.log(
|
|
3996
|
+
console.log(
|
|
3997
|
+
"Flags: fast = claimed within 5 s of the rule's minimum; burst = 3+ claims in 10 min; sweep = 75%+ of an app's completion rules within 30 min; cap = at the per-learner day cap; daily-max = at the cap on 3+ days; guessing = 15+ wrong answers on one quiz challenge. None is proof: read the player before acting.",
|
|
3998
|
+
);
|
|
3696
3999
|
}
|
|
3697
4000
|
|
|
3698
4001
|
function printAdminResult({ operation, result }) {
|
|
@@ -3707,20 +4010,34 @@ function printAdminResult({ operation, result }) {
|
|
|
3707
4010
|
}
|
|
3708
4011
|
if (operation.name === "runtime.evidence") {
|
|
3709
4012
|
const evidence = data.evidence || {};
|
|
3710
|
-
console.log(
|
|
3711
|
-
|
|
4013
|
+
console.log(
|
|
4014
|
+
`Runtime evidence (${data.host?.requested || "unknown"}): ${evidence.status || "unknown"}.`,
|
|
4015
|
+
);
|
|
4016
|
+
console.log(
|
|
4017
|
+
`Samples: ${evidence.coverage?.samples ?? "unknown"}; latest: ${evidence.coverage?.lastAtMs ?? "unknown"}; age ms: ${evidence.coverage?.ageMs ?? "unknown"}.`,
|
|
4018
|
+
);
|
|
3712
4019
|
for (const recycle of evidence.recycles || []) {
|
|
3713
|
-
console.log(
|
|
4020
|
+
console.log(
|
|
4021
|
+
` ${recycle.id}: ${recycle.outcome}; under load: ${recycle.observedUnderLoad ?? "unknown"}.`,
|
|
4022
|
+
);
|
|
3714
4023
|
}
|
|
3715
|
-
console.log(
|
|
4024
|
+
console.log(
|
|
4025
|
+
"Missing evidence is unknown, not healthy. A recovered topology does not verify interrupted user work. Use --json for full evidence.",
|
|
4026
|
+
);
|
|
3716
4027
|
return;
|
|
3717
4028
|
}
|
|
3718
4029
|
if (operation.name === "featured.plan") {
|
|
3719
4030
|
for (const pair of data.plan?.replacements || []) {
|
|
3720
|
-
console.log(
|
|
4031
|
+
console.log(
|
|
4032
|
+
`${pair.remove.id} ${pair.remove.title} -> ${pair.add.id} ${pair.add.title}`,
|
|
4033
|
+
);
|
|
3721
4034
|
}
|
|
3722
|
-
console.log(
|
|
3723
|
-
|
|
4035
|
+
console.log(
|
|
4036
|
+
`Proposed final order: ${(data.plan?.finalIds || []).join(",")}`,
|
|
4037
|
+
);
|
|
4038
|
+
console.log(
|
|
4039
|
+
`Plan hash: ${data.planHash}. Obtain Mikey's approval before using featured apply --file <plan.json> --approve ${data.planHash}.`,
|
|
4040
|
+
);
|
|
3724
4041
|
return;
|
|
3725
4042
|
}
|
|
3726
4043
|
if (operation.featuredWorkflow) {
|