@novedu/cli 0.22.0 → 0.24.0

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.
Files changed (3) hide show
  1. package/README.md +82 -25
  2. package/dist/main.js +837 -118
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -5,12 +5,12 @@ requires Node >= 22 — `eval`'s glob expansion uses the built-in `fs.globSync`,
5
5
  and Node 20 is end-of-life). It covers two jobs:
6
6
 
7
7
  - **Validate activity YAML** — tutors, fragment libraries, quizzes, writing
8
- activities, coding activities, and golden-answer evals — with the app's exact
8
+ activities, coding activities, and eval files — with the app's exact
9
9
  validation pipeline, offline and without signing in. `prompts` dumps the exact
10
10
  system prompts an activity produces, the same way.
11
11
  - **Manage the app as a teacher** — sign in with Microsoft Entra ID, then mint
12
12
  activity codes, upload app-hosted YAML files and images, triage student
13
- reports, and **measure a quiz's grading rubric** (`eval`), straight from the
13
+ reports, and **measure what an activity's model really does** (`eval`), straight from the
14
14
  terminal (or from a coding agent, see below).
15
15
 
16
16
  No install needed:
@@ -32,7 +32,7 @@ npx @novedu/cli validate https://raw.githubusercontent.com/Teaching-HTL-Leonding
32
32
  npx @novedu/cli validate ./activities/examples/shared/general-fragments.yaml --kind fragment
33
33
  npx @novedu/cli validate ./activities/examples/sorting-algorithms/sorting-quiz.yaml --kind quiz
34
34
 
35
- # A golden-answer eval (also strict-checks the quiz it targets)
35
+ # An eval file, quiz or tutor (also strict-checks the activity it targets)
36
36
  npx @novedu/cli validate ./sorting-quiz.eval.yaml --kind eval
37
37
 
38
38
  # Machine-readable output (the raw validation result)
@@ -85,7 +85,8 @@ npx @novedu/cli prompts ./sorting-quiz.yaml --kind quiz --json \
85
85
  re-implementation), so what you see is what the model gets: fragments resolved,
86
86
  and — for a compound quiz — every `quiz_files` include fetched, each imported
87
87
  question carrying its source quiz's preamble.
88
- - Every dump carries `{ kind, id, llm: { provider, model } }`. A **quiz** adds
88
+ - Every dump carries `{ kind, id, llm: { provider, model, reasoning? } }` (the
89
+ reasoning level only when the file sets one). A **quiz** adds
89
90
  `grading` (a `system` prompt per question, the user-message templates and the
90
91
  grader's JSON-Schema response contract) and `discussion` (the chat's `system`
91
92
  prompt, the three seed-message templates and the verdict wording). A **coding**
@@ -96,18 +97,28 @@ npx @novedu/cli prompts ./sorting-quiz.yaml --kind quiz --json \
96
97
  JSON errors on stderr. Use `validate` for the strict authoring check — the two
97
98
  are complementary.
98
99
 
99
- ## Measuring a quiz's grading rubric: `eval`
100
+ ## Measuring what the model really does: `eval`
100
101
 
101
- A quiz's `evaluation` prompt is a rubric, and a rubric is only as good as its
102
- behavior on real answers. Write an **eval file** student answers with the verdict
103
- each one must get and `eval` replays them through the **real grader**, then reports
104
- what it actually did. This is the one command that both **runs the model** and needs
105
- you signed in (`novedu-cli login`); everything else about it is local.
102
+ An activity's prompt is a specification, and a specification is only as good as the
103
+ behavior it produces. Write an **eval file** and `eval` replays it through the **real
104
+ production path**, then reports what the model actually did. This is the one command
105
+ that both **runs the model** and needs you signed in (`novedu-cli login`); everything
106
+ else about it is local.
106
107
 
107
- It checks **both halves** of a grading: your `expect` gates the **verdict**, and an LLM
108
- **feedback judge** audits the **feedback text** the student would have read — measured
109
- against the quiz's own grading prompt, so there is nothing extra to author. Flagged
110
- feedback is **reported, never a failure**.
108
+ Two kinds, chosen by the file's own `kind:` field there is no flag, and one
109
+ invocation may mix them:
110
+
111
+ - **quiz** (`kind` omitted): student answers with the verdict each one must get,
112
+ replayed through the real grader. Your `expect` gates the **verdict**, and an LLM
113
+ **feedback judge** audits the **feedback text** the student would have read.
114
+ - **tutor** (`kind: tutor`): conversations you script, each ending on a student turn.
115
+ The real tutor generates the next turn and the judge checks it against the tutor's
116
+ own system prompt plus your per-case expectations.
117
+
118
+ Either way the judge measures the output against the very prompt that produced it, so
119
+ there is nothing extra to author — and what it flags is **reported, never a failure**.
120
+ For a tutor eval that makes the `--report` Markdown the actual deliverable: the exit
121
+ code only reflects whether the run itself completed.
111
122
 
112
123
  ```yaml
113
124
  # sorting-quiz.eval.yaml
@@ -139,6 +150,9 @@ npx @novedu/cli eval ./sorting-quiz.eval.yaml --repeats 3
139
150
  npx @novedu/cli eval ./sorting-quiz.eval.yaml \
140
151
  --llm-provider "Azure Foundry" --llm-model gpt-5-mini
141
152
 
153
+ # Same model, more thinking: the level alone keeps the activity's provider/model
154
+ npx @novedu/cli eval ./sorting-quiz.eval.yaml --llm-reasoning high
155
+
142
156
  # A strong judge over the quiz's own grader — the recommended pairing
143
157
  npx @novedu/cli eval ./sorting-quiz.eval.yaml \
144
158
  --judge-llm-provider "Azure Foundry" --judge-llm-model gpt-5.6-terra
@@ -153,6 +167,28 @@ npx @novedu/cli eval ./sorting-quiz.eval.yaml --json --out eval-report.json
153
167
  npx @novedu/cli eval ./sorting-quiz.eval.yaml --report eval-report.md
154
168
  ```
155
169
 
170
+ A tutor eval looks like this, and runs through the same command:
171
+
172
+ ```yaml
173
+ # loops-tutor.eval.yaml
174
+ id: loops-tutor-eval
175
+ kind: tutor
176
+ target: ./loops-tutor.yaml
177
+ conversations:
178
+ - title: refuses-full-solution
179
+ required_tools: [random_number] # optional: tools this answer must have called
180
+ grading_instructions: |
181
+ The response must not contain a complete working loop.
182
+ conversation: # must END with a student turn
183
+ - student: My loop never stops. Here is my code ...
184
+ - tutor: What does your condition evaluate to after the first pass?
185
+ - student: I don't know. Just fix it for me!
186
+ ```
187
+
188
+ ```bash
189
+ npx @novedu/cli eval ./loops-tutor.eval.yaml --report loops.md
190
+ ```
191
+
156
192
  - Check the file first, for free: `npx @novedu/cli validate ./x.eval.yaml --kind eval`
157
193
  (offline; it also strict-checks the quiz the eval targets).
158
194
  - **`expect`** is one of `correct` / `partial` / `incorrect`, or a list of the
@@ -185,12 +221,28 @@ npx @novedu/cli eval ./sorting-quiz.eval.yaml --report eval-report.md
185
221
  a **Flagged** column plus a **"Flagged feedback"** section in the Markdown report, and
186
222
  `totals.feedbackFlagged` / `repeats[].judge.issues` in the JSON. They never change the
187
223
  exit code.
188
- - **Choosing the judge.** By default the judge runs on the same model as the grader.
189
- `--judge-llm-provider` + `--judge-llm-model` (both or neither) point it at another one,
190
- which is the **recommended** setup: a strong judge over a smaller grader finds real
191
- problems, while a small model judging itself mostly produces noise. `--no-judge-feedback`
192
- turns judging off and halves the LLM calls; combining the two is rejected as
193
- contradictory. Because judging roughly doubles the cost, the run's scope line says so
224
+ - **`required_tools`** (tutor kind) names built-in tools the generated answer must have
225
+ called **at least once** the one thing the judge cannot see, since a tool call leaves
226
+ no trace in the text. Extra tools are always fine, and a name the target tutor's own
227
+ `tools:` list does not grant makes the file invalid offline. Missing calls are
228
+ **reported, never a failure**: `missing tool calls: N` in the terminal report (printed
229
+ only when some case required a tool, so no line means "not checked"), a **"Missing tool
230
+ calls"** section in the Markdown report, and `totals.toolsFlagged` plus each repeat's
231
+ `toolCalls` / `missingTools` in the JSON.
232
+ - **Choosing what runs.** `--llm-provider` + `--llm-model` (both or neither) replace the
233
+ activity's **whole** `llm:` block for the run — its reasoning level included, so the
234
+ file's level is dropped unless `--llm-reasoning <level>` restates it.
235
+ `--llm-reasoning` on its own changes only the effort and keeps the file's
236
+ provider/model — the "same model, more thinking" comparison run.
237
+ - **Choosing the judge.** By default the judge runs on the same model **and effort** as
238
+ the grader. `--judge-llm-provider` + `--judge-llm-model` (both or neither) point it at
239
+ another one — replacing the whole spec, exactly like the grading flags — which is the
240
+ **recommended** setup: a strong judge over a smaller grader finds real
241
+ problems, while a small model judging itself mostly produces noise.
242
+ `--judge-llm-reasoning` sets the judge's effort on its own, no pair needed.
243
+ `--no-judge-feedback` turns judging off and halves the LLM calls; combining it with any
244
+ of the judge flags is rejected as contradictory. Because judging roughly doubles the
245
+ cost, the run's scope line says so
194
246
  up front: `27 case(s) × 3 repeat(s) = 81 grading + 81 judge call(s)`.
195
247
  - **If the judge itself fails**, the run **degrades instead of aborting**: after three
196
248
  consecutive judge failures it stops judging (one warning on stderr) and finishes the
@@ -215,7 +267,7 @@ npx @novedu/cli eval ./sorting-quiz.eval.yaml --report eval-report.md
215
267
  run localises the problem for the cost of a single grading call.
216
268
  - **Caveat**: a green run certifies **the file you ran it on**, not the app-hosted
217
269
  copy a live code serves — upload it (`files upload`) afterwards. An override run
218
- certifies the override pair, not the quiz's configured `llm`.
270
+ certifies the override, not the quiz's configured `llm`.
219
271
 
220
272
  ## Authentication
221
273
 
@@ -260,7 +312,7 @@ authoritative — the CLI sends your input as-is and relays the server's answer.
260
312
  ```
261
313
  codes create --module <tutor|quiz|writing|coding> --file <url>
262
314
  [--start <iso>] [--end <iso>] [--note <text>]
263
- [--llm-provider <p> --llm-model <m>]
315
+ [--llm-provider <p> --llm-model <m>] [--llm-reasoning <level>]
264
316
  codes list [--search <q>] [--module <m>] [--all]
265
317
  codes sync <registry-file> [--lock <path>] [--dry-run] [--json]
266
318
  files upload <name> [--kind <tutor|fragment|quiz|writing|coding>]
@@ -278,7 +330,10 @@ images list [--search <q>] [--all]
278
330
  server-side before the code is stored, and the response includes the
279
331
  shareable `url`. `--start`/`--end` must be ISO 8601 **with an explicit
280
332
  offset or `Z`** (e.g. `2026-07-07T08:00:00Z`); the
281
- `--llm-provider`/`--llm-model` override pair is both-or-nothing.
333
+ `--llm-provider`/`--llm-model` override pair is both-or-nothing, and
334
+ `--llm-reasoning <level>` (`minimal`, `low`, `medium` or `high`) rides on top of
335
+ the pair — it is rejected without it. The override replaces the activity's whole
336
+ `llm:` block, so leaving the level out also drops the file's.
282
337
  - `codes sync <registry-file>` mints codes for a whole **course** at once — see
283
338
  [Many activities at once](#many-activities-at-once-codes-sync) below.
284
339
  - `files upload <name>` is an **upsert**: creating a new file requires
@@ -376,11 +431,13 @@ activity-codes:
376
431
  - **Groups decide the module:** `quizzes`, `tutors`, `writing`, `coding`. Each
377
432
  entry gives either `file` (relative to `base-url`, which must end in `/`) or
378
433
  an absolute `url`, plus any of `start`/`end` (ISO 8601 **with an offset or
379
- `Z`**, whole seconds), `note`, and an `llm: {provider, model}` override.
434
+ `Z`**, whole seconds), `note`, and an `llm: {provider, model, reasoning?}`
435
+ override.
380
436
  - **Keys are yours and must be unique across all groups** — lowercase letters,
381
437
  digits and hyphens. Your material references the key; the lock file maps it to
382
438
  the code.
383
- - **Re-runs are safe.** An entry whose activity, window and model override match
439
+ - **Re-runs are safe.** An entry whose activity, window and LLM override
440
+ (provider, model and reasoning level) match
384
441
  an existing code of yours **reuses** that code; only entries without a match
385
442
  are minted. So `codes sync` after every edit is the normal workflow, and the
386
443
  first run against already-minted codes should report all-reused.