@stage5/lumine 0.3.24 → 0.3.25
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 +11 -0
- package/lib/admin.js +18 -0
- package/lib/commands.js +64 -6
- package/lib/math-lab-build-sheet.js +133 -0
- package/lib/math-lab-preservation.js +105 -0
- package/lib/workspace.js +2 -0
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +29 -0
package/README.md
CHANGED
|
@@ -81,6 +81,17 @@ pulling the owner's main project creates or reuses your contribution branch and
|
|
|
81
81
|
checks out that branch locally. Saves go to your branch, so the project owner
|
|
82
82
|
can merge or replace main from Twinkle.
|
|
83
83
|
|
|
84
|
+
For an exact source review of the live app, use `lumine pull <build-id>
|
|
85
|
+
--published` in a persistent review folder. This checks out the requested
|
|
86
|
+
build's published artifact read-only; `--main` explicitly selects its root
|
|
87
|
+
project. It never substitutes the latest draft or creates a contribution
|
|
88
|
+
branch. The CLI checks the publication pointer again before writing and
|
|
89
|
+
refuses a concurrent publication; modified local files are stashed before a
|
|
90
|
+
successful checkout. It requires the API's published-version selector to be
|
|
91
|
+
deployed first. `--published` and `--version <n>` are mutually exclusive.
|
|
92
|
+
`lumine admin builds review` already reviews the published app; unsupported
|
|
93
|
+
selectors such as `--mode` or `--review-mode` fail instead of being ignored.
|
|
94
|
+
|
|
84
95
|
Use `lumine branches <build-url-or-id>` to list the contribution branches you
|
|
85
96
|
can review, including each contributor, branch number, status, and URL. Then use
|
|
86
97
|
`lumine diff <branch-url>` to inspect one branch.
|
package/lib/admin.js
CHANGED
|
@@ -3264,6 +3264,16 @@ export function parseAdminOperation(options) {
|
|
|
3264
3264
|
{
|
|
3265
3265
|
status: parseTodoStatus(options.adminStatus),
|
|
3266
3266
|
note,
|
|
3267
|
+
...(options.todoWorkflowFile
|
|
3268
|
+
? {
|
|
3269
|
+
workflow: readAdminJsonFile(options.todoWorkflowFile),
|
|
3270
|
+
expectedRevision: parseRequiredInteger(
|
|
3271
|
+
options.followupRevision,
|
|
3272
|
+
"--revision",
|
|
3273
|
+
1,
|
|
3274
|
+
),
|
|
3275
|
+
}
|
|
3276
|
+
: {}),
|
|
3267
3277
|
},
|
|
3268
3278
|
);
|
|
3269
3279
|
}
|
|
@@ -7249,6 +7259,14 @@ function printTodoItems(items, heading) {
|
|
|
7249
7259
|
` #${todo.id} ${String(todo.status || "open").toUpperCase()} ${todo.kind || "task"} — ${todo.title || "(untitled)"}`,
|
|
7250
7260
|
);
|
|
7251
7261
|
if (todo.details) console.log(` ${todo.details}`);
|
|
7262
|
+
if (todo.workflow) {
|
|
7263
|
+
const w = todo.workflow;
|
|
7264
|
+
console.log(` Implementation: ${w.implementation}; release: ${w.release}; verification: ${w.verification}`);
|
|
7265
|
+
if (w.fixRef) console.log(` Fix: ${w.fixRef}`);
|
|
7266
|
+
if (w.releaseRef) console.log(` Release: ${w.releaseRef}`);
|
|
7267
|
+
if (w.evidence) console.log(` Evidence: ${w.evidence}`);
|
|
7268
|
+
if (w.nextAction) console.log(` Next: ${w.nextAction}`);
|
|
7269
|
+
}
|
|
7252
7270
|
if (todo.lastProgressNote) {
|
|
7253
7271
|
console.log(` Latest progress: ${todo.lastProgressNote}`);
|
|
7254
7272
|
}
|
package/lib/commands.js
CHANGED
|
@@ -943,6 +943,8 @@ export async function selectProject(options) {
|
|
|
943
943
|
}
|
|
944
944
|
|
|
945
945
|
export async function pull(options) {
|
|
946
|
+
if (options.pullPublished && options.versionProvided)
|
|
947
|
+
throw new Error("Choose --published or --version, not both.");
|
|
946
948
|
if (options.versionProvided && !(options.pullVersion > 0)) {
|
|
947
949
|
throw new Error(
|
|
948
950
|
"Pass a save number: `lumine pull --version <n>` (run `lumine versions` to see save numbers).",
|
|
@@ -962,7 +964,7 @@ export async function pull(options) {
|
|
|
962
964
|
auth,
|
|
963
965
|
buildId: requestedBuildId,
|
|
964
966
|
});
|
|
965
|
-
if (options.pullVersion > 0) {
|
|
967
|
+
if (options.pullPublished || options.pullVersion > 0) {
|
|
966
968
|
await pullVersionBuildFiles({ options, auth, build: selectedBuild });
|
|
967
969
|
return;
|
|
968
970
|
}
|
|
@@ -1830,10 +1832,10 @@ export async function pullMainBuildFiles({ options, auth, build }) {
|
|
|
1830
1832
|
}
|
|
1831
1833
|
|
|
1832
1834
|
// Read-only checkout of ONE previous save (`lumine pull --version <n>`).
|
|
1833
|
-
//
|
|
1834
|
-
//
|
|
1835
|
+
// Numeric saves target the editable workspace branch unless --main is given.
|
|
1836
|
+
// --published reads the named Build directly; --main selects the project root.
|
|
1835
1837
|
export async function pullVersionBuildFiles({ options, auth, build }) {
|
|
1836
|
-
const versionNumber = options.pullVersion;
|
|
1838
|
+
const versionNumber = options.pullPublished ? "published" : options.pullVersion;
|
|
1837
1839
|
let targetBuild = build;
|
|
1838
1840
|
if (options.pullMain) {
|
|
1839
1841
|
const rootBuildId =
|
|
@@ -1842,7 +1844,7 @@ export async function pullVersionBuildFiles({ options, auth, build }) {
|
|
|
1842
1844
|
rootBuildId === Number(build?.id || 0)
|
|
1843
1845
|
? build
|
|
1844
1846
|
: await loadBuildMetadata({ options, auth, buildId: rootBuildId });
|
|
1845
|
-
} else {
|
|
1847
|
+
} else if (!options.pullPublished) {
|
|
1846
1848
|
targetBuild = await resolveEditableWorkspaceBuild({
|
|
1847
1849
|
options,
|
|
1848
1850
|
auth,
|
|
@@ -1856,6 +1858,27 @@ export async function pullVersionBuildFiles({ options, auth, build }) {
|
|
|
1856
1858
|
buildId,
|
|
1857
1859
|
versionNumber,
|
|
1858
1860
|
});
|
|
1861
|
+
if (options.pullPublished) {
|
|
1862
|
+
const expected = Number(result.build?.publishedArtifactVersionId || 0);
|
|
1863
|
+
if (
|
|
1864
|
+
!expected ||
|
|
1865
|
+
Number(result.version?.versionId) !== expected ||
|
|
1866
|
+
!result.build?.isPublic
|
|
1867
|
+
) {
|
|
1868
|
+
throw new Error(
|
|
1869
|
+
"The API did not return the exact published artifact; update the API before using --published.",
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
const current = await loadBuildMetadata({ options, auth, buildId });
|
|
1873
|
+
if (
|
|
1874
|
+
!current.isPublic ||
|
|
1875
|
+
Number(current.publishedArtifactVersionId) !== expected
|
|
1876
|
+
) {
|
|
1877
|
+
throw new Error(
|
|
1878
|
+
"The published version changed during checkout. Retry; no local files were changed.",
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1859
1882
|
const resolvedBuild = result.build || targetBuild;
|
|
1860
1883
|
const version = result.version || { version: versionNumber };
|
|
1861
1884
|
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
@@ -1879,6 +1902,7 @@ export async function pullVersionBuildFiles({ options, auth, build }) {
|
|
|
1879
1902
|
? path.join(path.dirname(enclosing.rootDir), dirName)
|
|
1880
1903
|
: dirName),
|
|
1881
1904
|
);
|
|
1905
|
+
await stashLocalProjectFilesBeforePull({ dir, files });
|
|
1882
1906
|
await writeProjectFiles({ dir, files });
|
|
1883
1907
|
if (files.some((file) => isIndexHtmlPath(String(file.path)))) {
|
|
1884
1908
|
await removeLocalProjectFilesNotIn({ dir, files });
|
|
@@ -2422,6 +2446,7 @@ export function parseArgs(args) {
|
|
|
2422
2446
|
"open",
|
|
2423
2447
|
"noDescription",
|
|
2424
2448
|
"publish",
|
|
2449
|
+
"published",
|
|
2425
2450
|
"save",
|
|
2426
2451
|
"force",
|
|
2427
2452
|
"noUpdateCheck",
|
|
@@ -2491,6 +2516,35 @@ export function parseArgs(args) {
|
|
|
2491
2516
|
}
|
|
2492
2517
|
}
|
|
2493
2518
|
|
|
2519
|
+
if (
|
|
2520
|
+
command === "admin" &&
|
|
2521
|
+
positional[0] === "builds" &&
|
|
2522
|
+
positional[1] === "review"
|
|
2523
|
+
) {
|
|
2524
|
+
const allowed = new Set([
|
|
2525
|
+
"help",
|
|
2526
|
+
"output",
|
|
2527
|
+
"outputDir",
|
|
2528
|
+
"dir",
|
|
2529
|
+
"waitMs",
|
|
2530
|
+
"interact",
|
|
2531
|
+
"browserPath",
|
|
2532
|
+
"json",
|
|
2533
|
+
"apiUrl",
|
|
2534
|
+
"siteUrl",
|
|
2535
|
+
"authFile",
|
|
2536
|
+
"authToken",
|
|
2537
|
+
"clientName",
|
|
2538
|
+
"timeoutMs",
|
|
2539
|
+
"noUpdateCheck",
|
|
2540
|
+
]);
|
|
2541
|
+
for (const key of Object.keys(raw)) {
|
|
2542
|
+
if (!allowed.has(key))
|
|
2543
|
+
throw new Error(
|
|
2544
|
+
`Unsupported Build review option --${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}. Runtime review already uses the published app; use pull --published for its source.`,
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2494
2548
|
const suggestionInboxActions = new Set([
|
|
2495
2549
|
"list",
|
|
2496
2550
|
"merge",
|
|
@@ -2593,6 +2647,7 @@ export function parseArgs(args) {
|
|
|
2593
2647
|
adminSeverity: raw.severity ? String(raw.severity) : "",
|
|
2594
2648
|
adminStatus: raw.status ? String(raw.status) : "",
|
|
2595
2649
|
followupRevision: raw.revision ? String(raw.revision) : "",
|
|
2650
|
+
todoWorkflowFile: raw.workflow ? String(raw.workflow) : "",
|
|
2596
2651
|
adminDecision: raw.decision ? String(raw.decision) : "",
|
|
2597
2652
|
adminWaitMs: raw.waitMs ? String(raw.waitMs) : "",
|
|
2598
2653
|
adminBrowserPath: raw.browserPath ? String(raw.browserPath) : "",
|
|
@@ -2808,6 +2863,7 @@ export function parseArgs(args) {
|
|
|
2808
2863
|
clientName: String(raw.clientName || "Lumine CLI").slice(0, 120),
|
|
2809
2864
|
dir: raw.dir ? String(raw.dir) : "",
|
|
2810
2865
|
pullMain: parseBoolean(raw.main, false),
|
|
2866
|
+
pullPublished: parseBoolean(raw.published, false),
|
|
2811
2867
|
// Strict: only a plain positive integer counts. Anything else (missing
|
|
2812
2868
|
// value, "foo", a swallowed flag like "--main", "2.5") stays 0 and the
|
|
2813
2869
|
// consuming command must reject it via versionProvided instead of
|
|
@@ -3067,6 +3123,7 @@ export function printHelp() {
|
|
|
3067
3123
|
lumine pull [twinkle-build-url]
|
|
3068
3124
|
lumine pull [twinkle-build-url] --main
|
|
3069
3125
|
lumine pull [twinkle-build-url] --version <n> [--main]
|
|
3126
|
+
lumine pull [twinkle-build-url] --published [--main]
|
|
3070
3127
|
lumine versions [twinkle-build-url] [--main] [--limit <n>]
|
|
3071
3128
|
lumine restore <n> [--main]
|
|
3072
3129
|
lumine reference <twinkle-build-url>
|
|
@@ -3117,7 +3174,7 @@ export function printHelp() {
|
|
|
3117
3174
|
lumine admin escalation set <audit-id> --status open|acknowledged|resolved --note <decision> [--json]
|
|
3118
3175
|
lumine admin todo list [--status pending|open|in_progress|blocked|completed|cancelled|all] [--limit <number>] [--json]
|
|
3119
3176
|
lumine admin todo add --title <title> --note <handoff-and-acceptance-criteria> [--kind task|experiment] [--status open|in_progress|blocked] [--json]
|
|
3120
|
-
lumine admin todo update <todo-id> --status open|in_progress|blocked|completed|cancelled --note <progress-or-evidence> [--json]
|
|
3177
|
+
lumine admin todo update <todo-id> --status open|in_progress|blocked|completed|cancelled --note <progress-or-evidence> [--workflow <file.json> --revision <n>] [--json]
|
|
3121
3178
|
lumine admin sponsor applications list [--status pending|approved|rejected|withdrawn] [--json]
|
|
3122
3179
|
lumine admin sponsor applications review <application-id> --decision approve|reject [--note <text>] [--json]
|
|
3123
3180
|
lumine admin sponsor status set <user-id> --status probationary|trusted|suspended|revoked [--note <text>] [--json]
|
|
@@ -3280,6 +3337,7 @@ Options:
|
|
|
3280
3337
|
--no-review-loop Skip the evidence-based post-run loop retrospective
|
|
3281
3338
|
--target <build> Explicit Build URL or ID for rename/describe/upgrade
|
|
3282
3339
|
--main With pull/versions/restore: target the team project's main
|
|
3340
|
+
--published With pull: exact currently published artifact, read-only
|
|
3283
3341
|
--version <n> With pull: read-only checkout of previous save v<n>
|
|
3284
3342
|
--title <text> Build title for new/rename, or private todo title
|
|
3285
3343
|
--description <text> Build description for new/describe
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Merges parts/<level>.json into the private question sheet the Math Lab
|
|
2
|
+
// uploads with `lumine rewards sheet`, then validates the sheet against the
|
|
3
|
+
// workspace's rewards.json exactly the way the server will (same code).
|
|
4
|
+
// Usage: node --import tsx build-sheet.mjs --approved-review <live review id>
|
|
5
|
+
// Reads that review afresh and preserves its questions before writing output.
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
+
import {
|
|
12
|
+
LEVELS,
|
|
13
|
+
verifyApprovedPrefix,
|
|
14
|
+
verifyRewardEconomy,
|
|
15
|
+
} from "./math-lab-preservation.js";
|
|
16
|
+
|
|
17
|
+
export async function buildMathLabSheet({
|
|
18
|
+
contentDir,
|
|
19
|
+
args = process.argv.slice(2),
|
|
20
|
+
}) {
|
|
21
|
+
const here = path.resolve(contentDir);
|
|
22
|
+
const parts = path.join(here, "parts");
|
|
23
|
+
if (
|
|
24
|
+
args.length !== 2 ||
|
|
25
|
+
args[0] !== "--approved-review" ||
|
|
26
|
+
!/^[1-9]\d*$/.test(args[1])
|
|
27
|
+
) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Usage: node --import tsx build-sheet.mjs --approved-review <current live review id>. No sheet was written.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const cli = fileURLToPath(new URL("../bin/lumine.js", import.meta.url));
|
|
33
|
+
const approved = JSON.parse(
|
|
34
|
+
execFileSync(
|
|
35
|
+
process.execPath,
|
|
36
|
+
[
|
|
37
|
+
cli,
|
|
38
|
+
"admin",
|
|
39
|
+
"reward-review",
|
|
40
|
+
"show",
|
|
41
|
+
args[1],
|
|
42
|
+
"--json",
|
|
43
|
+
"--no-update-check",
|
|
44
|
+
],
|
|
45
|
+
{
|
|
46
|
+
encoding: "utf8",
|
|
47
|
+
timeout: 60000,
|
|
48
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
49
|
+
},
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
const sheet = { rules: {} };
|
|
53
|
+
const missing = [];
|
|
54
|
+
for (const level of LEVELS) {
|
|
55
|
+
const file = path.join(parts, `${level}.json`);
|
|
56
|
+
if (!fs.existsSync(file)) {
|
|
57
|
+
missing.push(level);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
61
|
+
sheet.rules[`${level}-daily`] = {
|
|
62
|
+
sets: data.sets.map((set) => ({
|
|
63
|
+
key: set.key,
|
|
64
|
+
questions: set.questions.map((q) => ({
|
|
65
|
+
prompt: q.prompt,
|
|
66
|
+
answer: q.answer,
|
|
67
|
+
...(q.hint ? { hint: q.hint } : {}),
|
|
68
|
+
...(q.guide ? { guide: q.guide } : {}),
|
|
69
|
+
})),
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const out = path.resolve(here, "..", "math-lab-sheet.json");
|
|
74
|
+
if (missing.length)
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Missing grade parts: ${missing.join(", ")}. No sheet was written.`,
|
|
77
|
+
);
|
|
78
|
+
const preservation = verifyApprovedPrefix(sheet, approved);
|
|
79
|
+
|
|
80
|
+
// Server-identical validation: rewards.json + sheet → approved-shape config.
|
|
81
|
+
const policy = await import(
|
|
82
|
+
pathToFileURL(
|
|
83
|
+
path.resolve(
|
|
84
|
+
here,
|
|
85
|
+
"..",
|
|
86
|
+
"..",
|
|
87
|
+
"twinkle-api",
|
|
88
|
+
"helpers",
|
|
89
|
+
"build",
|
|
90
|
+
"rewardsPolicy.ts",
|
|
91
|
+
),
|
|
92
|
+
).href
|
|
93
|
+
);
|
|
94
|
+
const declared = JSON.parse(
|
|
95
|
+
fs.readFileSync(
|
|
96
|
+
path.resolve(here, "..", "math-lab", "rewards.json"),
|
|
97
|
+
"utf8",
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
const composed = policy.composeRewardConfig({ declared, sheet });
|
|
101
|
+
if (composed.errors.length) {
|
|
102
|
+
for (const error of composed.errors) console.log(`- ${error}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
verifyRewardEconomy(composed.config, approved);
|
|
106
|
+
const total = composed.config.rules.reduce(
|
|
107
|
+
(sum, rule) => sum + (rule.sets || []).length,
|
|
108
|
+
0,
|
|
109
|
+
);
|
|
110
|
+
// Nothing replaces the prior usable sheet until every check succeeds.
|
|
111
|
+
const output = JSON.stringify(sheet, null, 1) + "\n";
|
|
112
|
+
fs.writeFileSync(`${out}.next`, output, { mode: 0o600 });
|
|
113
|
+
fs.renameSync(`${out}.next`, out);
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
`${out}.preservation.json`,
|
|
116
|
+
JSON.stringify(
|
|
117
|
+
{
|
|
118
|
+
...preservation,
|
|
119
|
+
checkedAt: new Date().toISOString(),
|
|
120
|
+
sha256: createHash("sha256").update(output).digest("hex"),
|
|
121
|
+
},
|
|
122
|
+
null,
|
|
123
|
+
2,
|
|
124
|
+
) + "\n",
|
|
125
|
+
{ mode: 0o600 },
|
|
126
|
+
);
|
|
127
|
+
console.log(
|
|
128
|
+
`wrote ${out}; preserved ${preservation.rules.reduce((n, r) => n + r.preserved, 0)} published questions from review ${preservation.liveReview}`,
|
|
129
|
+
);
|
|
130
|
+
console.log(
|
|
131
|
+
`composed OK: ${composed.config.rules.length} rules, ${total} sets, ${Buffer.byteLength(JSON.stringify(sheet))} bytes`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
|
|
3
|
+
export const LEVELS = [
|
|
4
|
+
"e1",
|
|
5
|
+
"e2",
|
|
6
|
+
"e3",
|
|
7
|
+
"e4",
|
|
8
|
+
"e5",
|
|
9
|
+
"e6",
|
|
10
|
+
"m1",
|
|
11
|
+
"m2",
|
|
12
|
+
"m3",
|
|
13
|
+
"h1",
|
|
14
|
+
"h2",
|
|
15
|
+
"h3",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function verifyRewardEconomy(config, response) {
|
|
19
|
+
const withoutRefillSets = (value) => ({
|
|
20
|
+
...value,
|
|
21
|
+
rules: value.rules.map((rule) => {
|
|
22
|
+
if (!LEVELS.some((level) => rule.id === `${level}-daily`)) return rule;
|
|
23
|
+
const { sets: _sets, ...economy } = rule;
|
|
24
|
+
return economy;
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
if (
|
|
28
|
+
!isDeepStrictEqual(
|
|
29
|
+
withoutRefillSets(config),
|
|
30
|
+
withoutRefillSets(response.data.review.config),
|
|
31
|
+
)
|
|
32
|
+
) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"A refill must preserve the approved reward rules and budgets. Review any economy change separately.",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Refill-only: edits/removals to existing questions need a separate content review.
|
|
40
|
+
export function verifyApprovedPrefix(sheet, response) {
|
|
41
|
+
const review = response?.data?.review;
|
|
42
|
+
const artifact = Number(review?.publishedArtifactVersionId);
|
|
43
|
+
if (
|
|
44
|
+
response?.ok !== true ||
|
|
45
|
+
Number(review?.buildId) !== 2460 ||
|
|
46
|
+
review?.status !== "approved" ||
|
|
47
|
+
review?.isLive !== true ||
|
|
48
|
+
!Number.isSafeInteger(artifact) ||
|
|
49
|
+
artifact <= 0 ||
|
|
50
|
+
Number(review?.publishedVersionId) !== artifact
|
|
51
|
+
) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"A fresh, live, approved Math Lab review is required. Re-read the current reward review.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const rules = review.config?.rules;
|
|
57
|
+
if (!Array.isArray(rules))
|
|
58
|
+
throw new Error("The approved review has no question rules.");
|
|
59
|
+
const result = [];
|
|
60
|
+
for (const level of LEVELS) {
|
|
61
|
+
const id = `${level}-daily`;
|
|
62
|
+
const matches = rules.filter((rule) => rule.id === id);
|
|
63
|
+
const before = matches[0]?.sets;
|
|
64
|
+
const after = sheet?.rules?.[id]?.sets;
|
|
65
|
+
if (
|
|
66
|
+
matches.length !== 1 ||
|
|
67
|
+
!Array.isArray(before) ||
|
|
68
|
+
!before.length ||
|
|
69
|
+
!Array.isArray(after)
|
|
70
|
+
) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`${id}: missing or ambiguous approved questions or candidate grade.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (after.length < before.length)
|
|
76
|
+
throw new Error(
|
|
77
|
+
`${id}: candidate loses ${before.length - after.length} approved questions.`,
|
|
78
|
+
);
|
|
79
|
+
for (let i = 0; i < before.length; i++) {
|
|
80
|
+
if (!isDeepStrictEqual(after[i], before[i])) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${id}: approved question ${before[i].key || i + 1} changed or moved. Sync the canonical sheet before refilling.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const keys = after.map((set) => set.key);
|
|
87
|
+
if (
|
|
88
|
+
keys.some((key) => typeof key !== "string" || !key.trim()) ||
|
|
89
|
+
new Set(keys).size !== keys.length
|
|
90
|
+
) {
|
|
91
|
+
throw new Error(`${id}: question keys must be present and unique.`);
|
|
92
|
+
}
|
|
93
|
+
result.push({
|
|
94
|
+
rule: id,
|
|
95
|
+
preserved: before.length,
|
|
96
|
+
added: after.length - before.length,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
liveReview: Number(review.id),
|
|
101
|
+
artifactVersionId: artifact,
|
|
102
|
+
preservedExact: true,
|
|
103
|
+
rules: result,
|
|
104
|
+
};
|
|
105
|
+
}
|
package/lib/workspace.js
CHANGED
|
@@ -738,6 +738,8 @@ export async function writeVersionCheckoutMetadata({
|
|
|
738
738
|
readOnly: true,
|
|
739
739
|
versionCheckout: true,
|
|
740
740
|
checkoutVersion: Number(version?.version || 0) || null,
|
|
741
|
+
checkoutArtifactVersionId: Number(version?.versionId || 0) || null,
|
|
742
|
+
publishedCheckout: options.pullPublished === true,
|
|
741
743
|
checkoutVersionSummary: version?.summary || null,
|
|
742
744
|
checkoutVersionCreatedAt: Number(version?.createdAt || 0) || null,
|
|
743
745
|
build: {
|
package/package.json
CHANGED
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -1737,6 +1737,35 @@ audit path: canonical todo state and its private `todo.create` / `todo.update`
|
|
|
1737
1737
|
audit response commit together, and no public bot, public mutation count, or
|
|
1738
1738
|
rotation signal is involved.
|
|
1739
1739
|
|
|
1740
|
+
Track implementation, release and verification separately when a carryover
|
|
1741
|
+
involves a fix. After `add-admin-todo-workflow.sql` and the matching API are
|
|
1742
|
+
deployed, save a local JSON file such as:
|
|
1743
|
+
|
|
1744
|
+
```json
|
|
1745
|
+
{
|
|
1746
|
+
"implementation": "ready",
|
|
1747
|
+
"release": "pending",
|
|
1748
|
+
"verification": "pending",
|
|
1749
|
+
"fixRef": "work/topic/release-review.md",
|
|
1750
|
+
"releaseRef": "",
|
|
1751
|
+
"evidence": "Focused regression passed; production is unchanged.",
|
|
1752
|
+
"nextAction": "Deployer releases the reviewed fix, then check the live result."
|
|
1753
|
+
}
|
|
1754
|
+
```
|
|
1755
|
+
|
|
1756
|
+
Use `lumine admin todo update 12 --revision <current-revision> --workflow
|
|
1757
|
+
work/topic/workflow.json --status in_progress --note "Ready for the deployer."
|
|
1758
|
+
--json`. The revision comes from the canonical list/update response; stale
|
|
1759
|
+
updates are rejected. Implementation values are `needed`, `in_progress`,
|
|
1760
|
+
`ready`, `not_needed`; release is `pending`, `live`, `not_needed`; verification
|
|
1761
|
+
is `pending` or `passed`. Any unfinished phase needs a concrete `nextAction`;
|
|
1762
|
+
passed verification needs `evidence`. Completion requires implementation ready
|
|
1763
|
+
(or unnecessary), release live (or unnecessary), and verification passed.
|
|
1764
|
+
Keep physical-device checks and observation windows pending until they happen.
|
|
1765
|
+
Legacy todos have `workflow: null`, which means unknown, never verified. An
|
|
1766
|
+
update without `--workflow` preserves an existing workflow and cannot bypass
|
|
1767
|
+
its completion requirements.
|
|
1768
|
+
|
|
1740
1769
|
Every successful full `daily-run start` response automatically includes all
|
|
1741
1770
|
unfinished items under `data.carryoverTodos`. The same run ID increments an
|
|
1742
1771
|
item's surfacing telemetry at most once, even when start is retried. This is the
|