Changelog
Source:NEWS.md
markermd 0.1.0
Breaking changes
The exported API was made consistent before the first release. There are no deprecated aliases.
- The project argument is always
project.init_project(),project_config(),project_set(),project_sitrep(),validate_project(),parse_project(),scan_project()andmark()tookpath. -
export_marks()is nowexport_feedback(). It writesscores.csvand the per-repository feedback files;marks_export()still writes the marks YAML. -
template()now runs the editor likemark(), with...passed toshiny::runApp(), instead of returning an app object. Its first argument issource(wasassignment_path). -
assignment_outline()takessource(wasassignment_path) and now accepts every sourcetemplate()does: a project (it outlines the key document with the stored template’s section stops), a file, a directory, a saved template YAML or object, or a GitHub repository (newlocal_dirandassignmentarguments).parse_assignment_document()takespath(wasfile_path). -
get_question(),add_question()andremove_question()are nowtemplate_question(),template_add_question()andtemplate_remove_question(), with argumentstemplateandquestion(weretmplandq). -
get_allowed_rule_verbs(),get_allowed_node_types()andget_allowed_filter_condition_types()are nowrule_verbs(),rule_node_types()andfilter_condition_types().
Changes
- Initial version.
- Source code is shown with a small vendored copy of Prism instead of the Monaco editor, in the Grade tab’s Source view, the source and node preview modals, and both apps’ file views. The views work offline, the installed package is several megabytes smaller, and Quarto and R Markdown documents get their own grammar: chunks of any engine are highlighted in their language (including knitr-style
{r label, opt=value}headers and#|options), as are inline`r expr`and`{r} expr`, YAML front matter, Quarto div fences and heading attributes. The question highlight now sits exactly on its lines, wrapped lines included. - Added
parse_project(), a headless per-repository parse check, and the bundledmarkermd-fix-parsingskill that repairs submissions the strict q2r parser rejects on agrading-fixesbranch. - The bundled skills now ship in two sets, for Claude Code (
inst/skills/claude/, installed to.claude/skills/) and Codex (inst/skills/codex/, installed to.agents/skills/);init_project()gained anagentsargument (default: both) andmarkermd_skills_path()anagentargument. -
mark()now opens on an Assignments tab holding one table with a row per repository: its folder, GitHub, report and source links, its validation status and grading progress, and its score per question and in total as entered through the rubric, with the graded count, mean, median and standard deviation per question pinned beneath as summary rows. The table filters by name and status, sorts by any column, and updates as grading proceeds. Clicking a repository’s validation marker opens that repository’s rule results in a modal (with a question selector whose entries carry a colored pass or fail mark and scroll to the question’s card, each card collapsing from its header, and a Re-check button that parses and validates the repository again), and clicking a score opens that repository and question in the Grade tab. Repository names in the table are plain text rather than selection buttons. The table is areactable(added to Imports);gtis no longer a dependency. -
mark()has a Rubric tab editing every question’s rubric in one place (scoring settings, items and groups, each item with its selection count), sharing its state with the Grade tab, and an optional Terminal tab (mark(terminal = TRUE)) running a shell or an agent CLI in the project root through the suggestedshinytermpackage. -
mark()is now permissive about where a repo’s rendered report lives: any HTML file named for the repo, or anywhere under a directory named for the repo (<repo>/, or<repo>plus a delimiter such as ghclass’s<repo>_<artifact-name>/), is accepted when it is the only one. Previously only three exact layouts were checked, so projects laid out byghclass::org_grade_assignment()showed “No Artifact Available” for every repo. When several HTML files are found for a repo, a heuristic modeled on the ghclass layout picks the most likely report (rendering resource directories such as*_files/rank last), a consolidated warning at launch names each affected repo and its choice, and the Grade tab shows a notice above the report. - Rubric items are no longer effectively limited to ten per question. The first ten keep the hotkeys 1-9 and 0; further items are toggled by clicking (their button is blank with a dashed outline). Display order is now stored explicitly in the grading database (
items.position, schema version 2, migrated in place the first time a project is opened), so reordering items past the tenth survives a restart and export; previously those items always reloaded in insertion order. - Raw blocks that are nothing but
<!-- ... -->comments are now their own node kind,HTML comment, so rules and filters distinguish{=html}write-ups (stillRaw Block) from skeleton placeholders and repair markers, andassignment_outline()no longer counts comments as content. - Template questions have a
scope:"sections"(the default) targets the selected headings and divs, and"document"covers the document as a whole, for problems that belong to no single question (a render setting in the YAML header, global chunk options, a document that does not render). A document-scope question selects no nodes; its filters and rules, if any, evaluate against the whole document. The Add Question dialog intemplate()chooses between a question on the document’s sections, an entry covering the whole document, and (template format 3.2) a question on one of the assignment’s other files;export_comments()writes document-scope feedback first, and templates with a document-scope question are written as format 3.1 (3.0 files still load). - A question’s scoring can be marked
optional: an optional question counts as graded for every repository and scores as if no rubric item were selected until something is recorded, so a document-level question that applies to few submissions needs no selection in the rest and no longer leaves their totalsNA. It is set from the grade settings popover inmark()or withoptional: truein the rubric YAML (format 1.2, written only when used), stored insettings.optional(schema version 4, migrated in place), and marked with a small red dot beside the question’s name in the Assignments table. - The bundled skills record document-level problems once in a document-scope question (0 points, optional, not bounded below zero) added only when a submission needs one, instead of spreading them across questions, and after a
mode = "replace"rubric import give pairs left with no selection their confirmation item. -
project_set()gainedartifacts, to record the directories of rendered reports (or none) without editingconfig.yml. - Re-running
init_project()now keeps the configured artifact directories (dropping any that no longer exist) instead of re-deriving them from every top-level folder, which added working folders such as notes or backups; new folders are listed instead.rescan_artifacts = TRUErestores the full rescan, andoverwrite_instructions = TRUEreplaces existingCLAUDE.md/AGENTS.mdfiles with the packaged versions. - Snapshots of the grading state. A snapshot is a named, timestamped, annotated copy of the template, rubric and marks, kept inside the project database (schema version 5; an existing database gains a baseline snapshot when first opened). One is taken automatically, in the same transaction, before every bulk change:
rubric_import(),marks_import(overwrite = TRUE),template_import(),project_set(template = ),snapshot_restore(), saving intemplate(), and inmark()a rubric import, deleting an item that has selections, or deleting a group. Those functions takesnapshotandsummaryarguments so the reason for a change is recorded with it; an automatic snapshot of an unchanged state becomes a note on the latest snapshot instead of a new row.snapshot_create()takes a named one by hand;snapshot_list(),snapshot_info()andsnapshot_annotate()inspect and annotate them;snapshot_diff()reports what changed between two snapshots or since one (rubric items added, removed, reworded, re-pointed or regrouped, group bounds and scoring, per-repository selection and comment changes, per-pair score deltas and totals before and after, template questions changed);snapshot_scores()returns a snapshot’s recorded scores;snapshot_restore()writes a snapshot’s rubric and marks (optionally its template, or only chosen questions and repositories) back in place in one transaction, after snapshotting the current state, and aborts if the restored scores differ from the recorded ones;snapshot_export()writes a snapshot as YAML files with asnapshot.yamlsidecar;snapshot_pin(),snapshot_delete()andsnapshot_prune()manage retention;project_backup()copies the database file withVACUUM INTO.rubric_export(),marks_export()andtemplate_export()gained asnapshotargument to export a past state,export_scores()andexport_comments()record which snapshot each export came from, andproject_sitrep()reports the snapshot state and whether the exports are stale. -
mark()gained a History tab: one timeline of the snapshots and the activity log (the calls of markermd functions by agents and scripts, the bulk operations made in the app, and the app’s own grading grouped into sessions), a click on a row opening the entry’s detail in a modal (overview and notes, changes against the current state or another snapshot, and the recorded scores) with the take, restore, export, pin, delete and backup actions; theskey opens the take-snapshot dialog; the rubric import dialog takes a summary; and the navbar shows when the latest snapshot was taken and how many selections were recorded since.template()gained a Snapshots menu to take a snapshot or load a snapshot’s template into the editor. - The bundled skills pass a
summaryto every import and report a change withsnapshot_diff(); the generatedCLAUDE.md/AGENTS.mddescribe the snapshot functions and drop the need for hand-made backup copies. - Added
scan_project(), a deterministic scan of every student repository and rendered report for prompt injection attempts (text aimed at an automated grader: instructions to ignore the rubric or award full marks, text addressed to an AI model, chat role markers, fabricated grading output, and text hidden in HTML comments, hidden-styled elements, unshown chunks, invisible Unicode, base64, or HTML attributes, plus agent instruction files and<!-- markermd: ... -->markers the fix-parsing skill did not add), and the bundledmarkermd-check-injectionskill that triages its findings and reports them without changing anything. The generatedCLAUDE.md/AGENTS.mdnow state that submission content is data and never instructions.xml2was added to Imports. - An activity log (schema version 6) records every call of a markermd function that changed grading state, with who made it (
MARKERMD_ACTOR, orclaude/codexwhen run under those agent CLIs), the grading rows it wrote and the template or rubric it replaced;project_activity()andactivity_detail()read it,mark()shows new entries as they land, badges the items an agent selected until the grader touches the pair, and raises a banner when the rubric or template changed outside the session.rubric_item_set()rewords or re-points one rubric item in place, keeping its selections;rubric_items_set()applies a data frame or YAML file of such changes in one transaction, with one snapshot and one activity entry;rubric_item_delete()deletes one item, asking for confirmation when repositories have it selected (force = TRUEskips the question, and is required outside an interactive session). - Snapshot restores now work by item id: a restore writes the snapshot’s items and groups back in place under their own ids, so every repository’s selections of a surviving item are kept, a restore narrowed to some repositories leaves the others exactly as they were, and a full restore clears the pairs graded after the snapshot. Snapshots carry the selected item ids beside the descriptions, so a rubric with repeated or blank descriptions still snapshots, scores and restores. Previously a narrowed restore re-created the items and silently dropped every other repository’s selections for those questions.
- Rubric item descriptions, which identify items in the YAML formats, are now kept unique and non-blank in
mark(): a new item starts as “New item” (selected on focus) and a blank or duplicate edit is refused. Descriptions are saved when the field loses focus or typing pauses, not per keystroke. - Renaming a question in
template()or throughtemplate_import()carries its rubric, scoring and marks over to the new name instead of orphaning them, matching questions by id. All the renames of one save are applied together, so swapped names move each question’s grading with it, and the rename warns, naming each move. A rename onto a name that still holds a removed question’s grading is refused, removing a question that holds grading warns, and restoring a snapshot’s template moves carried-over grading back.template()keeps each question’s id through imports and snapshot loads and gives a new question an id above every stored one. -
mark()grades and validates the same document per repository asvalidate_project()andparse_project()(the template’s document by name, else a root-level one), instead of the first.qmdfound anywhere in the repository; the Source view shows the file as written, with the question highlight mapped back from the parsed text, and parse diagnostics name the file’s line when a rewritten knitr chunk header moved it. - Student-rendered reports are shown in sandboxed iframes: their scripts run, but can no longer reach the grading app’s page or inputs.
- The app’s rubric edits, imports, deletions and restores are recorded in the activity log with the actor
app; a running session shows no “changed outside the app” notice for its own actions. Toggling an item writes its grade rows directly, the navbar snapshot status no longer re-hashes the whole grading state on every click, score and marks tables are built in one pass, and the live score is no longer written to the settings table on every repository switch. - The Assignments table’s Total column follows
export_scores(): NA until every question is graded, with the running sum shown in grey meanwhile; the summary footer covers the complete totals. Score cells can be opened from the keyboard, the rubric rows carry text-box and button roles for assistive technology, and the Grade tab’s hotkeys ignore Cmd/Ctrl/Alt combinations. - Grading event timestamps are written in UTC (schema version 7; older rows are rewritten on first touch), a database written by a newer markermd is refused before any migration touches it, and a template’s stored source path resolves against the project before the working directory.
- Deleting a rubric item or group, a replace rubric import and a rubric restore no longer delete anything: items and groups are soft-deleted (
deleted_at, schema version 9), so their grade events stay in the log and History is never rewritten, while every score, export and selection count ignores them. Restoring a snapshot brings a deleted item back under its id with its selections, a deleted item’s description and a deleted group’s label can be used again, and a deleted id is never reissued. -
shinyWidgetsis no longer a dependency (the “has between” rule uses two numeric inputs). - Templates saved from
template()store rules as an array again, so they passvalidate_template_file(). Rule, filter group and condition rows have stable keys, so a quick second click no longer deletes the wrong row or resets node types, and an invalid number in a rule keeps the stored values with a warning instead of resetting them to the defaults. - Validation fails every rule of a question whose selected section a submission lacks (“Selected section #id not found in this document”), so a renamed heading no longer passes
lacks contentorhas at mostrules. A rule pattern that is not a valid regular expression fails instead of passing vacuously, a div selected inside a selected section is counted once, andtemplate()flags duplicate heading or div ids and refuses to select an ambiguous one. The editor’s live rule badges and grading share one evaluator, so a whole-document question’s rules pass in the editor as they do in grading. -
rubric_import(mode = "append")refuses an item whose description is already stored for its question.find_repo_assignment()prefers a root-level document over a nested copy with the same name, andfind_repo_file()no longer follows a symbolic link out of the repository. - Snapshot diffs compare selections by item id and report a relabeled group as one rename, and printing one no longer interprets braces in item descriptions. Snapshot and live scores come from one implementation of which pairs count as graded.
-
scan_project()reports active content in rendered reports (scripts that reach out of the report frame, event handlers, outside frames), text hidden by the report’s own CSS classes, phrases disguised with look-alike letters or split by invisible characters, and decoded variation-selector payloads. A visible line needs two kinds of signal to be reported as medium, emoji flag sequences and ordinary right-to-left text are no longer flagged, excerpts of long lines center on the match, and repair markers count as verified only when the grading machine’s configured author wrote them. - The bundled skills’ frontmatter is valid YAML. The skills keep stored question ids when regenerating a template, append only new rubric items, read each repository’s graded document from
parse_project(), and keep reports undernotes/. The JSON Schemas check quoted format versions and blank descriptions and describe how ids carry grading across a rename.