@stage5/lumine 0.3.82 → 0.3.84

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.
@@ -20,6 +20,46 @@ function invalid(message) {
20
20
  return error;
21
21
  }
22
22
 
23
+ // Resolve once before pagination so every page uses the same canonical board.
24
+ export function resolveCurrentFeaturedHistory({ operation, receipt, all }) {
25
+ const subjects = receipt?.data?.subjects;
26
+ if (
27
+ receipt?.ok !== true || receipt.status !== "success" ||
28
+ !Array.isArray(subjects) || receipt.data.count !== subjects.length ||
29
+ subjects.some((subject) => !Number.isSafeInteger(subject?.id) || subject.id < 1)
30
+ ) {
31
+ throw invalid("Could not read the canonical current Featured board; history was not requested.");
32
+ }
33
+ const subjectIds = subjects.map((subject) => subject.id);
34
+ if (new Set(subjectIds).size !== subjectIds.length) {
35
+ throw invalid("The current Featured board contains duplicate subject IDs; history was not requested.");
36
+ }
37
+ if (subjectIds.length > 20_000) {
38
+ throw invalid("Featured history accepts at most 20000 subject IDs per CLI scan.");
39
+ }
40
+ if (subjectIds.length > FEATURED_HISTORY_BATCH_SIZE && !all) {
41
+ throw invalid("The current Featured board has more than 100 subjects; pass --all to batch history reads.");
42
+ }
43
+ const url = new URL(operation.path, "https://lumine.invalid");
44
+ url.searchParams.set("subjectIds", subjectIds.join(","));
45
+ return {
46
+ ...operation,
47
+ path: `${url.pathname}${url.search}`,
48
+ pagination: {
49
+ ...operation.pagination,
50
+ summaryKeys: [...operation.pagination.summaryKeys, "subjectSelection"],
51
+ filters: { subjectIds },
52
+ },
53
+ featuredHistorySelection: {
54
+ source: "current-featured-board",
55
+ subjectIds,
56
+ message: subjectIds.length
57
+ ? `Defaulted to the current Featured board (${subjectIds.length} subjects).`
58
+ : "Defaulted to the current Featured board, which is empty; no history was requested.",
59
+ },
60
+ };
61
+ }
62
+
23
63
  export async function runBatchedFeaturedHistory(args) {
24
64
  const { options, operation, runId } = args;
25
65
  if (options.adminCursor) {
@@ -135,6 +175,9 @@ export async function runBatchedFeaturedHistory(args) {
135
175
  ? firstCoverage
136
176
  : { complete: false, startedAt: null },
137
177
  subjects: results.flatMap((result) => result.data.subjects),
178
+ ...(operation.featuredHistorySelection
179
+ ? { subjectSelection: operation.featuredHistorySelection }
180
+ : {}),
138
181
  pagination: {
139
182
  nextCursor: null,
140
183
  hasMore: false,
package/lib/admin.js CHANGED
@@ -86,6 +86,7 @@ import {
86
86
  } from "./admin-featured.js";
87
87
  import {
88
88
  FEATURED_HISTORY_BATCH_SIZE,
89
+ resolveCurrentFeaturedHistory,
89
90
  runBatchedFeaturedHistory,
90
91
  } from "./admin-featured-history.js";
91
92
 
@@ -1379,7 +1380,7 @@ export async function adminCommand(options) {
1379
1380
  if (result && result.ok === false) process.exitCode = 1;
1380
1381
  return result;
1381
1382
  }
1382
- const operation = parseAdminOperation(options);
1383
+ let operation = parseAdminOperation(options);
1383
1384
  if (operation.name === "news.validate") {
1384
1385
  const validation = validateNewsEditorial({
1385
1386
  claim: operation.claim,
@@ -1607,15 +1608,54 @@ export async function adminCommand(options) {
1607
1608
  }
1608
1609
  return receipt;
1609
1610
  };
1610
- const transformResult = (rawResult) =>
1611
- transformAdminResult({
1611
+ if (operation.featuredHistoryCurrentBoard) {
1612
+ // The empty-board path skips pagination, but must preserve its files too.
1613
+ if (options.adminAll && options.adminCheckpoint && options.adminOutput &&
1614
+ path.resolve(options.adminCheckpoint) === path.resolve(options.adminOutput)) {
1615
+ throw cliValidationError("The pagination checkpoint and result output must use different files.");
1616
+ }
1617
+ if (options.adminAll && options.adminCursor) {
1618
+ throw cliValidationError("Use --resume with the scan checkpoint instead of combining --all with --cursor.");
1619
+ }
1620
+ if (options.adminResume && !options.adminAll) {
1621
+ throw cliValidationError("--resume requires --all or post skip-batch.");
1622
+ }
1623
+ operation = resolveCurrentFeaturedHistory({
1624
+ operation,
1625
+ receipt: await fetchOperation("/cli/admin/subjects/featured"),
1626
+ all: options.adminAll,
1627
+ });
1628
+ if (!operation.featuredHistorySelection.subjectIds.length && (options.adminResume || options.adminCursor)) {
1629
+ throw cliValidationError("The current Featured board is empty; it cannot supply the subject IDs for a previous history scan. Use explicit --subject-ids for a fresh scan or cursor continuation.");
1630
+ }
1631
+ }
1632
+ const transformResult = (rawResult) => {
1633
+ const transformed = transformAdminResult({
1612
1634
  operation,
1613
1635
  result: rawResult,
1614
1636
  options,
1615
1637
  recommendationContentTypes,
1616
1638
  viewFilter,
1617
1639
  });
1618
- if (operation.name === "spending.report") {
1640
+ return operation.featuredHistorySelection && transformed.ok
1641
+ ? { ...transformed, data: { ...transformed.data, subjectSelection: operation.featuredHistorySelection } }
1642
+ : transformed;
1643
+ };
1644
+ if (operation.featuredHistorySelection?.subjectIds.length === 0) {
1645
+ // An empty board is canonical; there is no valid nonempty history query.
1646
+ // Do not invent audit coverage or a history snapshot for this case.
1647
+ result = {
1648
+ ok: true,
1649
+ status: "success",
1650
+ data: {
1651
+ subjectSelection: operation.featuredHistorySelection,
1652
+ subjects: [],
1653
+ events: [],
1654
+ coverage: null,
1655
+ pagination: { hasMore: false, exhausted: true, nextCursor: null },
1656
+ },
1657
+ };
1658
+ } else if (operation.name === "spending.report") {
1619
1659
  result = await completeSpendingReport(await fetchOperation(), options);
1620
1660
  if (result.status === "partial") process.exitCode = 1;
1621
1661
  } else if (operation.featuredWorkflow) {
@@ -4395,10 +4435,10 @@ export function parseAdminOperation(options) {
4395
4435
  return readOperation("featured.list", "/cli/admin/subjects/featured");
4396
4436
  }
4397
4437
  if (action === "history") {
4398
- const subjectIds = parseFeaturedSubjectIds(
4399
- options.adminIds,
4400
- "--subject-ids",
4401
- );
4438
+ const currentBoard = !String(options.adminIds || "").trim();
4439
+ const subjectIds = currentBoard
4440
+ ? []
4441
+ : parseFeaturedSubjectIds(options.adminIds, "--subject-ids");
4402
4442
  if (subjectIds.length > 20_000) {
4403
4443
  throw cliValidationError(
4404
4444
  "Featured history accepts at most 20000 subject IDs per CLI scan.",
@@ -4420,6 +4460,7 @@ export function parseAdminOperation(options) {
4420
4460
  limit: options.limit,
4421
4461
  }),
4422
4462
  {
4463
+ ...(currentBoard ? { featuredHistoryCurrentBoard: true } : {}),
4423
4464
  pagination: {
4424
4465
  collectionKey: "events",
4425
4466
  summaryKeys: ["coverage", "subjects"],
@@ -6983,6 +7024,9 @@ function printArenaEvidence(arena) {
6983
7024
  console.log(`Paper #${arena.submissionId}: ${[t.school, t.gradeLevel, t.year, t.term, t.subject].filter(Boolean).join(" ")} (${t.examKind}) · ${arena.status}`);
6984
7025
  for (const file of arena.files || []) console.log(` page ${file.page} (${file.kind}${file.redacted ? ", blurred" : ""}, 1-hour link): ${file.url}`);
6985
7026
  console.log(arena.duplicate ? `Duplicate: ${arena.duplicate.reason} of paper #${arena.duplicate.duplicateOf}` : "Duplicate: none found");
7027
+ for (const other of arena.matchingTagPapers || []) {
7028
+ console.log(` Same tags, not a confirmed duplicate: paper #${other.submissionId}${other.approvalItemId ? ` (approvals show ${other.approvalItemId})` : ""}. Compare the actual questions before deciding.`);
7029
+ }
6986
7030
  console.log(`Rarity: ${arena.rarity || "-"} · reward ${arena.reward?.xp?.toLocaleString?.("en-US") ?? 0} XP + ${arena.reward?.coins ?? 0} coins${arena.reward?.onApprove ? " on approval" : arena.reward?.paid ? " (paid)" : ""}`);
6987
7031
  if (arena.reviewNote) console.log(`Review note: ${arena.reviewNote}`);
6988
7032
  printPaperPrescreen(arena.prescreen);
@@ -8442,6 +8486,10 @@ function printAdminResult({ operation, result }) {
8442
8486
  }
8443
8487
 
8444
8488
  function printFeaturedHistorySummary(data) {
8489
+ if (data.subjectSelection) {
8490
+ console.log(data.subjectSelection.message);
8491
+ if (data.subjectSelection.subjectIds.length === 0) return;
8492
+ }
8445
8493
  const coverage = data.coverage || {};
8446
8494
  console.log(
8447
8495
  coverage.complete
package/lib/commands.js CHANGED
@@ -3389,7 +3389,7 @@ export function printHelp() {
3389
3389
  lumine admin subject creator set-made-by-poster <subject-id> [--json]
3390
3390
  lumine admin subject feature|unfeature <subject-id> [--json]
3391
3391
  lumine admin featured list [--unviewed|--viewed] [--json]
3392
- lumine admin featured history --subject-ids <id,id,...> [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
3392
+ lumine admin featured history [--subject-ids <id,id,...>] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
3393
3393
  lumine admin featured add --subject-ids <id,id,...> --posted-after <ISO-8601-or-Unix-time> [--json]
3394
3394
  lumine admin featured reorder --subject-ids <id,id,...> [--json]
3395
3395
  lumine admin featured rotate --remove-subject-ids <id,id,...> --add-subject-ids <id,id,...> [--posted-after <timestamp>] [--json]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.82",
3
+ "version": "0.3.84",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2671,6 +2671,7 @@ lumine admin subject creator set-made-by-poster 123 --json
2671
2671
  lumine admin subject feature 123 --json
2672
2672
  lumine admin subject unfeature 123 --json
2673
2673
  lumine admin featured list --json
2674
+ lumine admin featured history --all --json # defaults to current Featured board
2674
2675
  lumine admin featured history --subject-ids 50,40 --all --json
2675
2676
  lumine admin featured add --subject-ids 50,40 \
2676
2677
  --posted-after 2026-08-27T00:00:00+07:00 --json
@@ -2789,6 +2790,8 @@ unknown/deleted IDs, missing current members, non-subject rows, and more than
2789
2790
  100 subjects. Permanent pins and editorial ordering policy are deliberately not
2790
2791
  hardcoded.
2791
2792
 
2793
+ When `lumine admin featured history` omits `--subject-ids`, the CLI reads the current Featured board once and requests history for those IDs in board order. Human output explicitly says that it defaulted to the current board; JSON and `--output` include `data.subjectSelection` with `source: "current-featured-board"`, `subjectIds`, and the same message. An empty board succeeds with empty subjects/events and `coverage: null`, explicitly stating that no history was requested. A failed or malformed board read fails rather than being treated as empty. Explicit `--subject-ids` keeps its existing behavior and skips the board read. Pagination retains the resolved IDs for the entire scan. A no-ID `--all --resume` requires the same board IDs in the same order; a changed board is rejected by checkpoint scope validation. If the board changes, use the previous result's explicit `subjectIds` for `--cursor`, or start a fresh `--all` scan with those IDs and a new checkpoint. An empty board cannot resume a prior scan or supply IDs for its cursor.
2794
+
2792
2795
  `featured history` is the compact canonical evidence path. It returns only
2793
2796
  coverage metadata, per-subject lifetime summaries, and paginated mutation
2794
2797
  events; it does not repeat full audit before/after board snapshots. An event of