litmus-cli 1.4.8 → 1.4.10
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/dist/commands/connect.d.ts +16 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +75 -11
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +107 -9
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/push.js +2 -2
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/submit.js +15 -0
- package/dist/commands/submit.js.map +1 -1
- package/dist/lib/ai-tracking.d.ts +6 -4
- package/dist/lib/ai-tracking.d.ts.map +1 -1
- package/dist/lib/ai-tracking.js +34 -7
- package/dist/lib/ai-tracking.js.map +1 -1
- package/dist/lib/api.d.ts +91 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +146 -2
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/extract.d.ts.map +1 -1
- package/dist/lib/extract.js +28 -1
- package/dist/lib/extract.js.map +1 -1
- package/dist/lib/python-env.d.ts +157 -0
- package/dist/lib/python-env.d.ts.map +1 -0
- package/dist/lib/python-env.js +477 -0
- package/dist/lib/python-env.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bind a Python assessment's notebook to the environment `init` just built.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS AT ALL (ENG-1824). `litmus init` has always created `venv/`
|
|
5
|
+
* and installed into it, and ENG-1417 made it prefer the grading interpreter
|
|
6
|
+
* and warn when it could not get one. None of that reaches a notebook. For an
|
|
7
|
+
* `.ipynb` deliverable the graded artifact is the SAVED CELL OUTPUT — the
|
|
8
|
+
* extractor parses the notebook as JSON and never re-executes it — so the
|
|
9
|
+
* interpreter that decides the result is the one Jupyter picked, not the one
|
|
10
|
+
* in `venv/`. A perfect venv the kernel never uses buys exactly zero, and the
|
|
11
|
+
* shipped notebooks carry the generic kernelspec `{"name": "python3"}`, which
|
|
12
|
+
* resolves to whatever `python3` kernel the machine happens to have. A
|
|
13
|
+
* candidate with their own JupyterLab and their own pandas therefore runs
|
|
14
|
+
* every cell on a foreign interpreter, and nothing anywhere notices.
|
|
15
|
+
*
|
|
16
|
+
* EVERYTHING HERE IS BEST-EFFORT AND NEVER FATAL. It runs on the path that
|
|
17
|
+
* decides whether a candidate can start working, against tooling we do not
|
|
18
|
+
* control, on three platforms. A kernel we failed to register is a candidate
|
|
19
|
+
* who picks one manually — today's behaviour. An exception thrown here would
|
|
20
|
+
* be a candidate who cannot start at all, which is far worse than the skew it
|
|
21
|
+
* was trying to prevent. Every entry point returns a result and swallows.
|
|
22
|
+
*/
|
|
23
|
+
import { spawnSync } from "child_process";
|
|
24
|
+
import { createHash } from "crypto";
|
|
25
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
26
|
+
import path from "path";
|
|
27
|
+
import fg from "fast-glob";
|
|
28
|
+
import { GRADING_PYTHON_VERSION } from "./detect-project.js";
|
|
29
|
+
/** Where the venv's interpreter lives, per platform. */
|
|
30
|
+
export function venvPython(dir) {
|
|
31
|
+
return process.platform === "win32"
|
|
32
|
+
? path.join(dir, "venv", "Scripts", "python.exe")
|
|
33
|
+
: path.join(dir, "venv", "bin", "python");
|
|
34
|
+
}
|
|
35
|
+
/** The `jupyter` the candidate should be told to run: the venv's own. */
|
|
36
|
+
export function venvJupyterCommand() {
|
|
37
|
+
return process.platform === "win32" ? "venv\\Scripts\\jupyter" : "venv/bin/jupyter";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Is `jupyter lab` actually runnable in this venv?
|
|
41
|
+
*
|
|
42
|
+
* `ipykernel` alone is not enough to answer that, and `ipykernel` is what the
|
|
43
|
+
* kernel registration proves. It brings in `jupyter-core`, so `venv/bin/jupyter`
|
|
44
|
+
* EXISTS while the `lab` subcommand — a separate `jupyterlab` distribution —
|
|
45
|
+
* does not. Telling a candidate to run the first and most prominent command we
|
|
46
|
+
* give them, and having it answer `'lab' is not a Jupyter command`, is a worse
|
|
47
|
+
* first thirty seconds than saying nothing.
|
|
48
|
+
*/
|
|
49
|
+
export function hasJupyterLab(dir) {
|
|
50
|
+
const launcher = process.platform === "win32"
|
|
51
|
+
? path.join(dir, "venv", "Scripts", "jupyter-lab.exe")
|
|
52
|
+
: path.join(dir, "venv", "bin", "jupyter-lab");
|
|
53
|
+
return existsSync(launcher);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Spawn a child, WITHOUT a shell by default.
|
|
57
|
+
*
|
|
58
|
+
* THE DEFAULT IS THE WHOLE POINT, and it is deliberately not the convention in
|
|
59
|
+
* detect-project.ts. That file spawns `shell: true` on win32 to resolve
|
|
60
|
+
* `.cmd`/`.ps1` PATH shims, which is safe there because every argument it
|
|
61
|
+
* passes is a bare token (`py -3.12 --version`). Node concatenates argv with
|
|
62
|
+
* plain spaces and NO quoting when a shell is used — it now warns about
|
|
63
|
+
* exactly this (DEP0190) — and everything here carries the opposite kind of
|
|
64
|
+
* argument: an absolute interpreter path (which contains a space on any
|
|
65
|
+
* `C:\Users\First Last\...` or OneDrive-for-Business machine), a display name
|
|
66
|
+
* that always contains a space and parentheses, and a multi-line `-c` script
|
|
67
|
+
* that cmd.exe would truncate at the first newline. Through a shell those all
|
|
68
|
+
* fail, silently, on every Windows candidate — the platform where the
|
|
69
|
+
* mis-picked kernel this change exists to fix is most likely in the first
|
|
70
|
+
* place.
|
|
71
|
+
*
|
|
72
|
+
* None of them needs a shell: they name a real `python.exe` by absolute path,
|
|
73
|
+
* which CreateProcess resolves directly. `shellForPathLookup` is opt-in for
|
|
74
|
+
* the one call that names a bare command (`uv`) and passes only bare tokens.
|
|
75
|
+
*/
|
|
76
|
+
function run(bin, args, cwd, opts = {}) {
|
|
77
|
+
try {
|
|
78
|
+
return spawnSync(bin, args, {
|
|
79
|
+
cwd,
|
|
80
|
+
encoding: "utf8",
|
|
81
|
+
shell: opts.shellForPathLookup === true && process.platform === "win32",
|
|
82
|
+
// A candidate's machine can have a wedged interpreter or a pip that sits
|
|
83
|
+
// on a dead index. Every call here is on the critical path of starting an
|
|
84
|
+
// assessment, so none of them is allowed to hang it.
|
|
85
|
+
timeout: 120000,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// spawnSync throws rather than returning a status on some ENOENT paths.
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ── uv, when the candidate already has it ───────────────────────────────────
|
|
94
|
+
//
|
|
95
|
+
// USED ONLY IF IT IS ALREADY ON PATH. We do not download it: fetching a
|
|
96
|
+
// third-party binary at init time is a supply-chain decision (a pinned hash
|
|
97
|
+
// table, a refresh policy, corporate proxies) that belongs in its own change,
|
|
98
|
+
// not smuggled into a kernel fix. But when uv IS present it is worth
|
|
99
|
+
// preferring, because it does the one thing `pip` structurally cannot —
|
|
100
|
+
// `uv venv --python 3.12` FETCHES CPython 3.12 when the machine has none, so
|
|
101
|
+
// the grading interpreter stops being something we warn about and becomes
|
|
102
|
+
// something the tool supplies. Candidates without uv get exactly today's flow.
|
|
103
|
+
export function uvVersion() {
|
|
104
|
+
// The one call that names a bare command and passes only bare tokens, so the
|
|
105
|
+
// only one that may go through a shell for PATH-shim resolution.
|
|
106
|
+
const probed = run("uv", ["--version"], undefined, { shellForPathLookup: true });
|
|
107
|
+
if (!probed || probed.status !== 0)
|
|
108
|
+
return null;
|
|
109
|
+
const match = /uv (\d+\.\d+\.\d+)/.exec(`${probed.stdout ?? ""}${probed.stderr ?? ""}`);
|
|
110
|
+
return match ? match[1] : null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build `venv/` with uv, pinned to the grading interpreter.
|
|
114
|
+
*
|
|
115
|
+
* The directory name is passed EXPLICITLY. uv's default is `.venv`, and every
|
|
116
|
+
* line of instruction we ship — the package README's `source venv/bin/activate`
|
|
117
|
+
* included — names `venv`. Taking uv's default here would leave the candidate
|
|
118
|
+
* reading true-looking instructions about a directory that does not exist.
|
|
119
|
+
*/
|
|
120
|
+
export function buildVenvWithUv(dir) {
|
|
121
|
+
// NEVER RETURN false WITH A DIRECTORY STILL ON DISK. When this returns false
|
|
122
|
+
// `init` runs the fallback — `<python> -m venv venv && venv/bin/pip install
|
|
123
|
+
// …` — over the same path, and CPython's venv module SKIPS `bin/python` and
|
|
124
|
+
// `bin/python3` when they already exist. The result is a venv whose
|
|
125
|
+
// interpreter is uv's 3.12 while its `pyvenv.cfg`, its pip and its
|
|
126
|
+
// `site-packages` belong to the host's Python: the fallback exits 0, init
|
|
127
|
+
// reports success, and every package installs somewhere the kernel cannot
|
|
128
|
+
// import from. Reproduced end to end during review. Clearing first costs a
|
|
129
|
+
// directory we were about to abandon anyway.
|
|
130
|
+
// ONLY EVER REMOVE WHAT THIS FUNCTION CREATED. A recursive delete inside a
|
|
131
|
+
// candidate's assessment folder is the most destructive thing in this file,
|
|
132
|
+
// so it is gated on the directory not having been there when we arrived.
|
|
133
|
+
const venvDir = path.join(dir, "venv");
|
|
134
|
+
const preExisting = existsSync(venvDir);
|
|
135
|
+
const abandon = () => {
|
|
136
|
+
if (preExisting)
|
|
137
|
+
return false;
|
|
138
|
+
try {
|
|
139
|
+
rmSync(venvDir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Best-effort. A venv we cannot remove is one the fallback will layer
|
|
143
|
+
// over — bad, but not worth failing the whole init for.
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
};
|
|
147
|
+
// `--seed` installs pip into the new venv. `uv venv` does not by default, and
|
|
148
|
+
// on this path the `python -m venv` that would have run ensurepip is skipped
|
|
149
|
+
// entirely — so without it a candidate who follows the package README's
|
|
150
|
+
// `source venv/bin/activate` and runs `pip install …` mid-assessment gets
|
|
151
|
+
// either "command not found" or, worse, the system pip silently installing
|
|
152
|
+
// into a Python their kernel is not running.
|
|
153
|
+
const created = run("uv", ["venv", "--seed", "--python", GRADING_PYTHON_VERSION, "venv"], dir);
|
|
154
|
+
if (!created || created.status !== 0)
|
|
155
|
+
return abandon();
|
|
156
|
+
const manifest = path.join(dir, "requirements.txt");
|
|
157
|
+
const install = existsSync(manifest)
|
|
158
|
+
? run("uv", ["pip", "install", "--python", venvPython(dir), "-r", "requirements.txt"], dir)
|
|
159
|
+
: run("uv", ["pip", "install", "--python", venvPython(dir), "-e", "."], dir);
|
|
160
|
+
if (!install || install.status !== 0)
|
|
161
|
+
return abandon();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
// ── What the environment actually has ───────────────────────────────────────
|
|
165
|
+
/**
|
|
166
|
+
* The distributions `requirements.txt` declares, as installable names.
|
|
167
|
+
*
|
|
168
|
+
* Deliberately conservative: anything that is not a plain pinned-or-floored
|
|
169
|
+
* requirement (a flag, an `-r` include, a URL, a path) is dropped rather than
|
|
170
|
+
* guessed at, because the only consumer is a probe whose false alarms would be
|
|
171
|
+
* worse than its silence.
|
|
172
|
+
*/
|
|
173
|
+
export function declaredRequirements(dir) {
|
|
174
|
+
const manifest = path.join(dir, "requirements.txt");
|
|
175
|
+
if (!existsSync(manifest))
|
|
176
|
+
return [];
|
|
177
|
+
const names = [];
|
|
178
|
+
for (const raw of readFileSync(manifest, "utf8").split(/\r?\n/)) {
|
|
179
|
+
const line = raw.split("#")[0].trim();
|
|
180
|
+
if (!line || line.startsWith("-") || line.includes("://") || line.startsWith("."))
|
|
181
|
+
continue;
|
|
182
|
+
// A PEP 508 marker means the requirement is conditional — `pywin32;
|
|
183
|
+
// sys_platform == "win32"` is correctly absent on a healthy mac. Evaluating
|
|
184
|
+
// markers here would mean reimplementing them; declining to guess keeps the
|
|
185
|
+
// probe's promise, which is that anything it names as missing really is
|
|
186
|
+
// missing. A false alarm on a healthy environment costs the candidate an
|
|
187
|
+
// install they do not need and teaches them to ignore the one warning that
|
|
188
|
+
// will matter.
|
|
189
|
+
if (line.includes(";"))
|
|
190
|
+
continue;
|
|
191
|
+
const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(line);
|
|
192
|
+
if (match)
|
|
193
|
+
names.push(match[1]);
|
|
194
|
+
}
|
|
195
|
+
return names;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Ask the venv's own interpreter what it has.
|
|
199
|
+
*
|
|
200
|
+
* PRESENCE IS CHECKED THROUGH `importlib.metadata`, NOT BY IMPORTING THE
|
|
201
|
+
* REQUIREMENT NAME. Distribution names and module names disagree often enough
|
|
202
|
+
* (`beautifulsoup4`/`bs4`, `scikit-learn`/`sklearn`, `pillow`/`PIL`) that a
|
|
203
|
+
* hand-maintained alias table would eventually tell a candidate their working
|
|
204
|
+
* environment is broken. The metadata lookup is exact. The import check is
|
|
205
|
+
* then driven off the metadata's own top-level modules, so it stays exact too
|
|
206
|
+
* while still catching the case presence cannot — a wheel that installs and
|
|
207
|
+
* then fails to load.
|
|
208
|
+
*/
|
|
209
|
+
export function probeEnvironment(dir) {
|
|
210
|
+
const python = venvPython(dir);
|
|
211
|
+
if (!existsSync(python))
|
|
212
|
+
return { version: null, missing: [], broken: [] };
|
|
213
|
+
const script = `
|
|
214
|
+
import json, sys
|
|
215
|
+
from importlib import metadata
|
|
216
|
+
|
|
217
|
+
declared = json.loads(sys.argv[1])
|
|
218
|
+
missing, modules = [], []
|
|
219
|
+
for name in declared:
|
|
220
|
+
try:
|
|
221
|
+
metadata.distribution(name)
|
|
222
|
+
except Exception:
|
|
223
|
+
missing.append(name)
|
|
224
|
+
continue
|
|
225
|
+
try:
|
|
226
|
+
top = (metadata.distribution(name).read_text("top_level.txt") or "").split()
|
|
227
|
+
except Exception:
|
|
228
|
+
top = []
|
|
229
|
+
modules.extend(m for m in top if m and not m.startswith("_"))
|
|
230
|
+
|
|
231
|
+
broken = []
|
|
232
|
+
for module in dict.fromkeys(modules):
|
|
233
|
+
try:
|
|
234
|
+
__import__(module)
|
|
235
|
+
except Exception:
|
|
236
|
+
broken.append(module)
|
|
237
|
+
|
|
238
|
+
print(json.dumps({
|
|
239
|
+
"version": "%s.%s.%s" % sys.version_info[:3],
|
|
240
|
+
"missing": missing,
|
|
241
|
+
"broken": broken,
|
|
242
|
+
}))
|
|
243
|
+
`.trim();
|
|
244
|
+
const probed = run(python, ["-c", script, JSON.stringify(declaredRequirements(dir))]);
|
|
245
|
+
if (!probed || probed.status !== 0)
|
|
246
|
+
return { version: null, missing: [], broken: [] };
|
|
247
|
+
try {
|
|
248
|
+
const parsed = JSON.parse((probed.stdout ?? "").trim().split("\n").pop() || "{}");
|
|
249
|
+
return {
|
|
250
|
+
version: typeof parsed.version === "string" ? parsed.version : null,
|
|
251
|
+
missing: Array.isArray(parsed.missing) ? parsed.missing : [],
|
|
252
|
+
broken: Array.isArray(parsed.broken) ? parsed.broken : [],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return { version: null, missing: [], broken: [] };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ── The kernel ──────────────────────────────────────────────────────────────
|
|
260
|
+
/**
|
|
261
|
+
* A kernelspec name identifying ONE CHECKOUT, not one assessment.
|
|
262
|
+
*
|
|
263
|
+
* Jupyter lowercases kernelspec names on install and matches them literally
|
|
264
|
+
* afterwards, so the name is normalised HERE rather than being compared
|
|
265
|
+
* case-insensitively later — otherwise the notebook we rewrite points at
|
|
266
|
+
* `litmus-Foo` while the registered kernel is `litmus-foo`, and the candidate
|
|
267
|
+
* gets a picker prompt about a missing kernel, which is worse than the generic
|
|
268
|
+
* name we started with.
|
|
269
|
+
*
|
|
270
|
+
* THE PATH FINGERPRINT IS WHAT KEEPS TWO CHECKOUTS APART, and the name alone
|
|
271
|
+
* cannot: kernelspecs are user-scope and `ipykernel install` OVERWRITES a spec
|
|
272
|
+
* of the same name. Two folders that normalise alike — a practice run beside a
|
|
273
|
+
* real attempt, the same assessment re-initialised under a different parent,
|
|
274
|
+
* or two companies who both called theirs "Take Home" — would otherwise share
|
|
275
|
+
* one spec. The second init would silently redirect the first notebook at the
|
|
276
|
+
* second checkout's interpreter, and submitting either would delete the spec
|
|
277
|
+
* the other one is bound to. The fingerprint is not shown to anyone: the
|
|
278
|
+
* picker displays `display_name`, and the notebook is bound by name so the
|
|
279
|
+
* candidate never has to read either.
|
|
280
|
+
*/
|
|
281
|
+
export function kernelNameFor(folderName, projectRoot) {
|
|
282
|
+
const slug = folderName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
283
|
+
// Of the absolute path, so it is stable across re-runs in place and distinct
|
|
284
|
+
// between checkouts. `init` records the result in .litmus/config.json, so a
|
|
285
|
+
// folder the candidate later moves is still cleaned up by the name it was
|
|
286
|
+
// registered under rather than by one re-derived from its new location.
|
|
287
|
+
const fingerprint = createHash("sha256")
|
|
288
|
+
.update(path.resolve(projectRoot))
|
|
289
|
+
.digest("hex")
|
|
290
|
+
.slice(0, 8);
|
|
291
|
+
return `litmus-${slug || "assessment"}-${fingerprint}`;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Register the venv's interpreter as a Jupyter kernel.
|
|
295
|
+
*
|
|
296
|
+
* `--user`, NOT `--sys-prefix`. A sys-prefix kernel is visible only to a
|
|
297
|
+
* JupyterLab launched from inside that venv, which excludes exactly the
|
|
298
|
+
* candidate this whole change is for: the one who already has a global
|
|
299
|
+
* JupyterLab, opens the notebook with it, and cannot even SEE the correct
|
|
300
|
+
* kernel. A user-scope kernelspec is discoverable by any Jupyter on the
|
|
301
|
+
* machine, which is the property that makes the notebook rewrite below mean
|
|
302
|
+
* something.
|
|
303
|
+
*
|
|
304
|
+
* Requires `ipykernel` in the venv. Assessments that ship jupyterlab get it
|
|
305
|
+
* transitively; ones that do not simply return false and the flow degrades to
|
|
306
|
+
* today's behaviour.
|
|
307
|
+
*/
|
|
308
|
+
export function registerKernel(dir, name, displayName) {
|
|
309
|
+
const python = venvPython(dir);
|
|
310
|
+
if (!existsSync(python))
|
|
311
|
+
return false;
|
|
312
|
+
const installed = run(python, [
|
|
313
|
+
"-m", "ipykernel", "install", "--user", "--name", name, "--display-name", displayName,
|
|
314
|
+
], dir);
|
|
315
|
+
return !!installed && installed.status === 0;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Remove the kernel we registered, before the assessment folder goes away.
|
|
319
|
+
*
|
|
320
|
+
* A user-scope kernelspec OUTLIVES THE FOLDER, and `litmus submit` deletes the
|
|
321
|
+
* folder. Left behind, it is a permanent entry in the candidate's own Jupyter
|
|
322
|
+
* pointing at an interpreter that no longer exists — our litter on their
|
|
323
|
+
* machine, and a broken kernel in every notebook they open afterwards. We made
|
|
324
|
+
* the global change, so we undo it.
|
|
325
|
+
*
|
|
326
|
+
* Called while the venv still exists, because that is what provides
|
|
327
|
+
* `jupyter_client` — asking it to remove the spec is how this stays correct on
|
|
328
|
+
* all three platforms without hardcoding three kernel directories.
|
|
329
|
+
*/
|
|
330
|
+
export function unregisterKernel(dir, name) {
|
|
331
|
+
const python = venvPython(dir);
|
|
332
|
+
if (!existsSync(python))
|
|
333
|
+
return false;
|
|
334
|
+
const removed = run(python, [
|
|
335
|
+
"-c",
|
|
336
|
+
"import sys; from jupyter_client.kernelspec import KernelSpecManager; " +
|
|
337
|
+
"KernelSpecManager().remove_kernel_spec(sys.argv[1])",
|
|
338
|
+
name,
|
|
339
|
+
], dir);
|
|
340
|
+
return !!removed && removed.status === 0;
|
|
341
|
+
}
|
|
342
|
+
// ── Pointing the notebook at it ─────────────────────────────────────────────
|
|
343
|
+
/**
|
|
344
|
+
* Rewrite a notebook's kernelspec name/display_name, changing nothing else.
|
|
345
|
+
*
|
|
346
|
+
* A SURGICAL STRING EDIT, NOT A PARSE-AND-SERIALISE. The obvious
|
|
347
|
+
* implementation — `JSON.parse`, mutate, `JSON.stringify` — reformats the
|
|
348
|
+
* whole file: a 900-line notebook collapses to one line with the default
|
|
349
|
+
* serialiser and still differs byte-for-byte under any indent setting. That
|
|
350
|
+
* matters here more than it usually would, because `init` git-commits the
|
|
351
|
+
* extracted package as the candidate's baseline and the notebook diff against
|
|
352
|
+
* that baseline is grading evidence. Reflowing the deliverable before the
|
|
353
|
+
* candidate has opened it turns every later diff into one line of churn on the
|
|
354
|
+
* one file that carries their work.
|
|
355
|
+
*
|
|
356
|
+
* So: find the `kernelspec` object, replace the two string values inside it,
|
|
357
|
+
* leave every other byte — including `language`, the trailing whitespace and
|
|
358
|
+
* the file's own indentation — exactly as shipped. Returns null when there is
|
|
359
|
+
* nothing to edit, which is a report, not a failure: a notebook without a
|
|
360
|
+
* kernelspec is one we should leave alone rather than restructure.
|
|
361
|
+
*/
|
|
362
|
+
export function rewriteKernelspec(source, name, displayName) {
|
|
363
|
+
const key = /"kernelspec"\s*:\s*\{/.exec(source);
|
|
364
|
+
if (!key)
|
|
365
|
+
return null;
|
|
366
|
+
// Walk to the matching brace, tracking string state so a `}` inside a value
|
|
367
|
+
// cannot end the object early.
|
|
368
|
+
let depth = 0;
|
|
369
|
+
let inString = false;
|
|
370
|
+
let escaped = false;
|
|
371
|
+
let end = -1;
|
|
372
|
+
for (let i = key.index + key[0].length - 1; i < source.length; i++) {
|
|
373
|
+
const ch = source[i];
|
|
374
|
+
if (inString) {
|
|
375
|
+
if (escaped)
|
|
376
|
+
escaped = false;
|
|
377
|
+
else if (ch === "\\")
|
|
378
|
+
escaped = true;
|
|
379
|
+
else if (ch === '"')
|
|
380
|
+
inString = false;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (ch === '"')
|
|
384
|
+
inString = true;
|
|
385
|
+
else if (ch === "{")
|
|
386
|
+
depth++;
|
|
387
|
+
else if (ch === "}") {
|
|
388
|
+
depth--;
|
|
389
|
+
if (depth === 0) {
|
|
390
|
+
end = i + 1;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (end === -1)
|
|
396
|
+
return null;
|
|
397
|
+
const block = source.slice(key.index, end);
|
|
398
|
+
let rewritten = block;
|
|
399
|
+
let changed = false;
|
|
400
|
+
for (const [field, value] of [["name", name], ["display_name", displayName]]) {
|
|
401
|
+
// Anchored to the field's own key so a value containing the word "name"
|
|
402
|
+
// cannot be hit, and applied once so a nested object cannot be either.
|
|
403
|
+
const pattern = new RegExp(`("${field}"\\s*:\\s*)"(?:[^"\\\\]|\\\\.)*"`);
|
|
404
|
+
if (!pattern.test(rewritten))
|
|
405
|
+
continue;
|
|
406
|
+
// A FUNCTION REPLACEMENT, NEVER A STRING ONE. String.replace expands `$1`,
|
|
407
|
+
// `$&`, `` $` `` and `$'` inside the REPLACEMENT, and the display name is
|
|
408
|
+
// built from the assessment's name, which is unvalidated server text. An
|
|
409
|
+
// assessment called "Q$1 2026" would splice the captured group into the
|
|
410
|
+
// value and write structurally broken JSON over the candidate's
|
|
411
|
+
// deliverable. A function replacement performs no expansion at all.
|
|
412
|
+
rewritten = rewritten.replace(pattern, (_match, prefix) => prefix + JSON.stringify(value));
|
|
413
|
+
changed = true;
|
|
414
|
+
}
|
|
415
|
+
if (!changed || rewritten === block)
|
|
416
|
+
return null;
|
|
417
|
+
const rewrittenSource = source.slice(0, key.index) + rewritten + source.slice(end);
|
|
418
|
+
// LAST GATE BEFORE WE HAND BACK AN EDIT TO THE GRADED ARTIFACT. Everything
|
|
419
|
+
// above is careful, and none of that matters as much as never returning a
|
|
420
|
+
// notebook that Jupyter cannot open: the caller writes this to disk and
|
|
421
|
+
// `init` commits it as the candidate's baseline. If the result does not
|
|
422
|
+
// parse, the edit is abandoned and the shipped notebook stands — a candidate
|
|
423
|
+
// picking a kernel by hand is a small cost, a corrupt deliverable is not.
|
|
424
|
+
try {
|
|
425
|
+
JSON.parse(rewrittenSource);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
return rewrittenSource;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Point every notebook the package shipped at the registered kernel.
|
|
434
|
+
*
|
|
435
|
+
* Scoped to notebooks that exist at init time, i.e. ones we shipped. The
|
|
436
|
+
* candidate's own later notebooks are theirs; the deliverable is the one that
|
|
437
|
+
* has to execute on the environment we built.
|
|
438
|
+
*/
|
|
439
|
+
export function bindNotebooks(dir, name, displayName) {
|
|
440
|
+
let notebooks;
|
|
441
|
+
try {
|
|
442
|
+
notebooks = fg.sync("**/*.ipynb", {
|
|
443
|
+
cwd: dir,
|
|
444
|
+
onlyFiles: true,
|
|
445
|
+
followSymbolicLinks: false,
|
|
446
|
+
ignore: ["**/venv/**", "**/.venv/**", "**/node_modules/**", "**/.git/**", "**/.ipynb_checkpoints/**"],
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
// Shallowest first, then alphabetical. `init` names the first of these as the
|
|
453
|
+
// command to run, and fast-glob's traversal order is not a promise about
|
|
454
|
+
// which notebook is the deliverable — the one at the package root is.
|
|
455
|
+
notebooks.sort((a, b) => {
|
|
456
|
+
const depth = a.split("/").length - b.split("/").length;
|
|
457
|
+
return depth !== 0 ? depth : a.localeCompare(b);
|
|
458
|
+
});
|
|
459
|
+
const bound = [];
|
|
460
|
+
for (const relative of notebooks) {
|
|
461
|
+
const file = path.join(dir, relative);
|
|
462
|
+
try {
|
|
463
|
+
const source = readFileSync(file, "utf8");
|
|
464
|
+
const rewritten = rewriteKernelspec(source, name, displayName);
|
|
465
|
+
if (rewritten === null)
|
|
466
|
+
continue;
|
|
467
|
+
writeFileSync(file, rewritten);
|
|
468
|
+
bound.push(relative);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// A notebook we cannot read or write is one the candidate picks a kernel
|
|
472
|
+
// for by hand. Not worth failing an assessment start over.
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return bound;
|
|
476
|
+
}
|
|
477
|
+
//# sourceMappingURL=python-env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"python-env.js","sourceRoot":"","sources":["../../src/lib/python-env.ts"],"names":[],"mappings":"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"}
|