Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Zero and withheld grades stay distinct. See the notes' teacher contract for poli
- `src/core/src/runner/orchestrator.rs` — Runs prepared bundles per student, tokio parallel.
- `src/core/src/models/spec.rs` — TOML test specification types.
- `src/core/src/discovery.rs` — File discovery and archive extraction with size/count limits.
- `src/core/src/input/local.rs` and `input/table.rs` — Assignment-relative local sources, explicit student/file manifests and CSV/XLSX roster mappings. Spreadsheet student IDs must be text; preserve row/sheet diagnostics.
- `src/core/src/record.rs` — Versioned grading evidence and score revisions, including reuse checks.
- `src/core/src/grading.rs` — GradingPolicy dispatch (templates + Rhai formulas).
- `src/cli/src/main.rs` — CLI command handlers and application orchestration.
Expand All @@ -126,6 +127,7 @@ Zero and withheld grades stay distinct. See the notes' teacher contract for poli
- Integration tests spawn real Python processes — need `python3` available
- `scriptmark-core` exposes grading models and operations; CLI adapters import it directly, without compatibility re-exports.
- Core's `test-support` feature exposes shared graded/withheld report fixtures for adapter tests; it is enabled only by the CLI's dev-dependency.
- `[input]` in `assignment.toml` configures either submission paths plus a mapped CSV/XLSX roster, or `[[input.students]]` with exact files/archives. `grade`, `run` and `match` resolve the same input; rescore reconstructs it from saved CLI overrides and the recorded assignment file. `summarize --roster` and `db import-roster` read tables through `Config::roster_table` with `--assignment`'s layout. DB saves import the identities frozen in the record (`Record::roster`). File and function matching remains owned by `matching`.
- Publish both Rust packages with `cargo publish --workspace`, which orders workspace dependencies before their consumers.
- Platform-specific code uses `#[cfg(target_os = "macos")]` / `#[cfg(target_os = "linux")]` for rlimit types

Expand Down
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,71 @@ scriptmark tui grades.db

## TOML Test Specs

### Local input configuration

`grade`, `run` and `match` can read submission paths and a CSV/XLSX roster from
`assignment.toml`. Configured paths are relative to that file. See the
[runnable local-import example](examples/bundles/local_import/README.md).

```toml
[input]
submissions = ["submissions"]

[input.roster]
path = "roster.xlsx"
sheet = "学生"
header_row = 2

[input.roster.columns]
student_id = "学号"
name = "姓名"
# canvas_user_id = "Canvas ID" # optional; distinct from the student number
```

`.xlsx` and `.xlsm` are read as workbooks; `.xls`/`.ods` must be saved as one of
those first, and any other file is read as CSV (omit `sheet`). Header rows and numeric
column selectors start at 1; a column selector can also be an exact heading. Without a
mapping, the default layout is `name,student_id` or
`name,unused,student_id[,canvas_user_id]`. XLSX student IDs must be text cells; numeric
cells are rejected because their original leading zeros cannot be established. Workbook
files with several sheets need `sheet`. Blank rows are skipped; a table with no usable
student row below the header is refused rather than graded as an empty class.

For an explicit class and file list, replace `[input]`/`[input.roster]` with:

```toml
[[input.students]]
student_id = "00123"
name = "张三"
files = ["handins/answer.py"] # individual files or archives; [] means no submission
```

An explicit list cannot be combined with a roster table or submission directory.
It uses the same ownership and item matching component. A file named explicitly, in
the list or as a CLI path, is never skipped as noise (`.*`, `__*`), and student rules
see only its file name, never the directories above it. Invalid identities/paths
stop grading and remain visible in `match` diagnostics. An identical repeated roster
row is reported at each repeat and merged; conflicting rows are refused. CLI submission
paths and `--roster` override configured paths in table mode. To use only configured
sources:

```fish
scriptmark match -t tests/ --assignment assignment.toml
scriptmark grade -t tests/ --assignment assignment.toml
scriptmark export output/results.json -o grades.xlsx
```

`summarize --roster` and `db import-roster` read a table with the default layout, or
with the `[input.roster]` layout of `--assignment`, which also supplies the path when
no roster file is given. A roster with errors is refused there as in grading:

```fish
scriptmark summarize output/results.json --assignment assignment.toml
scriptmark db import-roster --assignment assignment.toml --db grades.db
```

### Test definitions

Teachers can configure student ownership, item file patterns, function aliases and
per-student overrides in `assignment.toml`. Preview decisions and candidates with
`scriptmark match`; unresolved conflicts withhold grades. See
Expand Down
43 changes: 43 additions & 0 deletions examples/bundles/local_import/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Local roster or explicit submission list

From the repository root:

```fish
cargo run -p scriptmark -- match -t examples/bundles/local_import/tests -o output/local-matches.json
cargo run -p scriptmark -- grade -t examples/bundles/local_import/tests -o output/local.json
cargo run -p scriptmark -- export output/local.json -o output/local-grades.xlsx

# The same class, with every submitted file assigned explicitly.
cargo run -p scriptmark -- grade -t examples/bundles/local_import/tests --assignment examples/bundles/local_import/manifest.toml -o output/manifest.json
```

`001` earns full points, `002` earns zero, and `003` remains ungraded because no work
was submitted. Student numbers remain text, including their leading zeros.

`assignment.toml` selects row 2 as the CSV header and maps its Chinese headings.
To use an Excel roster, save the same table as `roster.xlsx`, keep the student ID
column as **text**, change `input.roster.path`, and set `input.roster.sheet` to the
worksheet name. Multiple-sheet workbooks require an explicit sheet. Columns accept
either exact header names or one-based column numbers.

Paths in either configuration are relative to that configuration file. CLI paths
are relative to the working directory; positional submissions and `--roster` override
the corresponding configured paths. The explicit `input.students` list is a separate
input mode and cannot be combined with a roster table or submission directories.
It accepts individual files and archives; `files = []` records a non-submitter.
A listed file is graded whatever its name, even one a directory scan would skip as
noise (`.*`, `__*`).
File-to-item and function matching still use the normal `[matching]` rules.

Run `match` to see import diagnostics without executing student code. Invalid rows,
numeric Excel student IDs, a roster with no usable student row and invalid/duplicate
submission paths stop grading, with the source file, worksheet and row where available.
Blank rows are skipped. Each repeat of an identical roster ID is reported and merged;
conflicting identities stop grading.

The same mapping names students outside grading:

```fish
cargo run -p scriptmark -- summarize output/local.json --assignment examples/bundles/local_import/assignment.toml
cargo run -p scriptmark -- db import-roster --assignment examples/bundles/local_import/assignment.toml --db output/grades.db
```
18 changes: 18 additions & 0 deletions examples/bundles/local_import/assignment.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[assignment]
name = "Local import example"

[input]
submissions = ["submissions"]

[input.roster]
path = "roster.csv"
header_row = 2

[input.roster.columns]
student_id = "学号"
name = "姓名"

[[items]]
id = "double"
points = 10
aggregation = "proportional"
22 changes: 22 additions & 0 deletions examples/bundles/local_import/manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[assignment]
name = "Local import example"

[[input.students]]
student_id = "001"
name = "张三"
files = ["submissions/001_work.py"]

[[input.students]]
student_id = "002"
name = "李四"
files = ["submissions/002_work.py"]

[[input.students]]
student_id = "003"
name = "王五"
files = []

[[items]]
id = "double"
points = 10
aggregation = "proportional"
5 changes: 5 additions & 0 deletions examples/bundles/local_import/roster.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
示例班级
姓名,学号
张三,001
李四,002
王五,003
2 changes: 2 additions & 0 deletions examples/bundles/local_import/submissions/001_work.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def double(x: int) -> int:
return x * 2
2 changes: 2 additions & 0 deletions examples/bundles/local_import/submissions/002_work.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def double(x: int) -> int:
return x
10 changes: 10 additions & 0 deletions examples/bundles/local_import/tests/double.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[meta]
name = "double"
file = "work.py"
function = "double"
language = "python"

[[cases]]
name = "positive"
args = [3]
expect = 6
2 changes: 1 addition & 1 deletion notes
Submodule notes updated from 08d9bb to 785624
1 change: 1 addition & 0 deletions src/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ ratatui = { workspace = true }
crossterm = { workspace = true }

[dev-dependencies]
rust_xlsxwriter = { workspace = true }
scriptmark-core = { workspace = true, features = ["test-support"] }
tempfile = "3"
zip = { workspace = true }
Expand Down
7 changes: 4 additions & 3 deletions src/cli/src/db/roster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ impl Database {
let mut stmt = self.conn.prepare(
"INSERT INTO students (id, name, canvas_id) VALUES (?1, ?2, ?3)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
-- A CSV roster carries no Canvas id, so writing its NULL would erase one
-- an earlier Canvas import had stored, and grade push would lose it.
-- A roster that does not know a value must not erase one stored earlier:
-- a CSV roster carries no Canvas id, which grade push needs, and a record
-- graded without names would blank those an import had stored.
name = COALESCE(excluded.name, students.name),
canvas_id = COALESCE(excluded.canvas_id, students.canvas_id)",
)?;
let mut stored = std::collections::BTreeSet::new();
Expand Down
Loading
Loading