Authoring and support
A teaching workflow, followed by the complete authoring reference
Start with the teaching choices below. The developer reference collects all question-block options, supported notation and integration details. For matrix questions, see matrix input; for a basis, a matrix condition or positive definiteness, see Python answer tests.
For teachers
Start with a native export
CTM STACK is a part of CTM Assessment. Install CTM Assessment in your Quarto project and SymPy in the Python environment that Quarto uses. CTM Assessment brings the STACK engine (ctm-checkers) and the guided hints (AI feedback):
quarto add https://erasmus-ctm.github.io/ctm-assessment/ctm-assessment.tar.gz
python -m pip install 'sympy==1.13.3'Add the question filter to _quarto.yml:
filters:
- ctm-assessmentExport a supported Moodle STACK question as XML and keep it in your project. Use its filename and exact question name in a Quarto block:
Expand and collect like terms: \((x+{3})(x+{4})\).
Give your answer as a polynomial in \(x\).
Show source
```{.stack-question}
#| label: authoring-expand-a-product-form-sensitive
#| file: ../questions/polynomial-expansion.xml
#| question: Expand a product (form-sensitive)
#| variants: 3
#| review: true
#| feedback: true
```Show STACK source
<?xml version="1.0" encoding="UTF-8"?>
<!-- Example question authored by the CTM team. Licensed under CC BY-SA 4.0. -->
<quiz>
<question type="stack">
<name>
<text>Expand a product (form-sensitive)</text>
</name>
<questiontext format="html">
<text><p>Expand and collect like terms: \((x+{@a@})(x+{@b@})\).</p><p>Give your answer as a polynomial in \(x\). [[input:ans]] [[validation:ans]] [[feedback:prt1]]</p></text>
</questiontext>
<generalfeedback format="html">
<text><p>Multiply each term and collect: \(x^2+({@a@}+{@b@})x+{@a@}\cdot{@b@}={@ta@}\).</p><p>A factored expression has the same value, but this task asks for expanded form.</p></text>
</generalfeedback>
<defaultgrade>1</defaultgrade>
<penalty>0.1</penalty>
<hidden>0</hidden>
<idnumber/>
<stackversion>
<text>2021072200</text>
</stackversion>
<questionvariables>
<text>a:rand(4)+2;
b:rand(3)+2;
ta:x^2+(a+b)*x+a*b;</text>
</questionvariables>
<specificfeedback format="html">
<text><p>[[feedback:prt1]]</p></text>
</specificfeedback>
<questionnote format="moodle_auto_format">
<text>Expansion: (x+a)(x+b) = ta</text>
</questionnote>
<questiondescription format="moodle_auto_format">
<text/>
</questiondescription>
<questionsimplify>1</questionsimplify>
<assumepositive>0</assumepositive>
<assumereal>0</assumereal>
<scientificnotation>*10</scientificnotation>
<multiplicationsign>none</multiplicationsign>
<sqrtsign>1</sqrtsign>
<complexno>i</complexno>
<inversetrig>cos-1</inversetrig>
<logicsymbol>lang</logicsymbol>
<matrixparens>[</matrixparens>
<isbroken>0</isbroken>
<variantsselectionseed/>
<input>
<name>ans</name>
<type>algebraic</type>
<tans>ta</tans>
<boxsize>18</boxsize>
<strictsyntax>1</strictsyntax>
<insertstars>1</insertstars>
<syntaxhint/>
<syntaxattribute>0</syntaxattribute>
<forbidwords/>
<allowwords/>
<forbidfloat>1</forbidfloat>
<requirelowestterms>0</requirelowestterms>
<checkanswertype>1</checkanswertype>
<mustverify>1</mustverify>
<showvalidation>1</showvalidation>
<options/>
</input>
<prt>
<name>prt1</name>
<value>1.0000000</value>
<autosimplify>0</autosimplify>
<feedbackstyle>1</feedbackstyle>
<feedbackvariables>
<text/>
</feedbackvariables>
<node>
<name>0</name>
<description/>
<answertest>EqualComAss</answertest>
<sans>ans</sans>
<tans>ta</tans>
<testoptions/>
<quiet>0</quiet>
<truescoremode>=</truescoremode>
<truescore>1.0000000</truescore>
<truepenalty/>
<truenextnode>-1</truenextnode>
<trueanswernote>prt1-0-T</trueanswernote>
<truefeedback format="html">
<text><p>Correct: the expression is expanded and collected.</p></text>
</truefeedback>
<falsescoremode>=</falsescoremode>
<falsescore>0.0000000</falsescore>
<falsepenalty/>
<falsenextnode>1</falsenextnode>
<falseanswernote>prt1-0-F</falseanswernote>
<falsefeedback format="html">
<text/>
</falsefeedback>
</node>
<node>
<name>1</name>
<description/>
<answertest>AlgEquiv</answertest>
<sans>ans</sans>
<tans>ta</tans>
<testoptions/>
<quiet>0</quiet>
<truescoremode>=</truescoremode>
<truescore>0.5000000</truescore>
<truepenalty/>
<truenextnode>-1</truenextnode>
<trueanswernote>prt1-1-T</trueanswernote>
<truefeedback format="html">
<text><p>This is equivalent, but expand the product and collect like terms for full credit.</p></text>
</truefeedback>
<falsescoremode>=</falsescoremode>
<falsescore>0.0000000</falsescore>
<falsepenalty/>
<falsenextnode>-1</falsenextnode>
<falseanswernote>prt1-1-F</falseanswernote>
<falsefeedback format="html">
<text><p>Check the middle term and constant term when you multiply the binomials.</p></text>
</falsefeedback>
</node>
</prt>
<deployedseed>17</deployedseed>
<deployedseed>29</deployedseed>
<deployedseed>47</deployedseed>
<qtest>
<testcase>1</testcase>
<description>Expanded polynomial</description>
<testinput>
<name>ans</name>
<value>ta</value>
</testinput>
<expected>
<name>prt1</name>
<expectedscore>1.0000000</expectedscore>
<expectedpenalty>0.0000000</expectedpenalty>
<expectedanswernote>prt1-0-T</expectedanswernote>
</expected>
</qtest>
<qtest>
<testcase>2</testcase>
<description>Equivalent but still factored</description>
<testinput>
<name>ans</name>
<value>(x+a)*(x+b)</value>
</testinput>
<expected>
<name>prt1</name>
<expectedscore>0.5000000</expectedscore>
<expectedpenalty>0.1000000</expectedpenalty>
<expectedanswernote>prt1-0-F | prt1-1-T</expectedanswernote>
</expected>
</qtest>
<qtest>
<testcase>3</testcase>
<description>Incorrect middle coefficient</description>
<testinput>
<name>ans</name>
<value>x^2+(a+b+1)*x+a*b</value>
</testinput>
<expected>
<name>prt1</name>
<expectedscore>0.0000000</expectedscore>
<expectedpenalty>0.1000000</expectedpenalty>
<expectedanswernote>prt1-0-F | prt1-1-F</expectedanswernote>
</expected>
</qtest>
</question>
</quiz>
This page sits in docs/, so its filename starts with ../questions/. A page at the project root would use questions/. The stable label identifies the card and its review history. variants is a proposal batch size, default 25. SymPy generates its own seeds starting at 1; Moodle’s deployed seeds do not carry over. Equal question notes produce one proposal. Maxima owns its own seed generation. Select cas: maxima for the native producer described in the runtime-v3 authoring guide; it uses the same review and approval workflow.
Generate candidates from the current source, then inspect their live student cards and actual XML question-test results before approving any for publication:
python _extensions/ctm-stack/question-variants.py --project . --file docs/authoring.qmd --label authoring-expand-a-product-form-sensitive generate25more
python _extensions/ctm-stack/variants-review.py --project . --file docs/authoring.qmd --label authoring-expand-a-product-form-sensitiveThe shared manifest is _variants/<label>.yml. Approval is required by default; a publication without a current approved variant fails. CTM_VARIANTS=review builds an explicit teacher preview with unapproved proposals visibly marked. Decisions use the current source and manifest lease; stale decisions require a fresh review. Content hashes preserve approval and stable numbers when a seed’s locator moves to equivalent mathematical content. Changed grading requires review.
Learners receive a stable choice from the shared variant selector. New variant asks before clearing a nonempty answer and avoids repeats until all deployed choices have been seen. A saved answer is restored only for the same mathematical content. Answers to changed or withdrawn variants remain readable and require accepting a current variant. #| seed: and the learner’s old selection menu retire.
A learner first presses Validate answers to review the interpreted answer, then Check answer to receive a mark. Editing an answer requires validation again. Run question tests appears when review: true is selected. Below each card, Show source and Show STACK source provide the Quarto block and question XML for inspection and copying.
Choose what counts as correct
| Teaching task | Answer test or feature | Where to start |
|---|---|---|
| Equivalent expressions | AlgEquiv |
Rational expressions; state domain restrictions in the prompt. |
| Expanded, factorized or fractional form | EqualComAss, FacForm, SingleFrac, LowestTerms |
Form-sensitive practice and the form reference. LowestTerms checks form only; pair it with a value check when value matters. |
| Approximate measurements or written precision | NumAbsolute, NumRelative, NumDecPlaces |
Numerical checks. |
| Lists, sets and nested collections | AlgEquiv, Sets, SameType, EqualComAss |
Collection examples; decide whether order and repetitions matter. |
| Boolean expressions and polynomial equations | AlgEquiv |
Boolean equivalence and zero-product equations. |
| A particular matrix | Matrix input with AlgEquiv, SameType or EqualComAss |
Matrix input and grading. |
| Any basis of a matrix’s null, column or row space | PythonChecker with the supplied basis function |
Basis questions. Supply the original matrix and select nullspace, columnspace or rowspace in the JSON space option. |
| A matrix with stated properties | PythonChecker with the supplied matrix_property function or an author-written function |
Matrix conditions, including the positive-definiteness walkthrough. |
| Another mathematical rule | PythonChecker |
Custom checker API. |
| Partial credit and targeted feedback | Weighted response trees (PRTs) | Three numerical checks and determinant-one conditions. |
| Guided hints | Optional AI Feedback | Choose a teaching policy. |
The question corpus shows unchanged imports and the features that still need adaptation. The detailed semantics for STACK authors explain important differences from Moodle. This is a defined subset of STACK; unsupported authoring features stop the build instead of receiving an invented grading rule.
Choose a teaching policy
AI Feedback gives guided questions, concepts, procedures and a worked solution. It supports copy-prompt mode and a learner-configured provider. Deterministic answer tests still award the marks; AI Feedback does not grade the response.
Choose the material that should accompany a request for help:
| Context option | What the feedback sees |
|---|---|
context: auto |
Visible teaching prose in the preceding section; this is the default. |
context: expansion-hint |
The teaching block tagged with that ID. Use comma-separated IDs for several blocks. |
context: none |
The question and current response, without nearby teaching prose. |
For example, tag a short explanation with .ai-context and reference it from the question:
::: {#expansion-hint .ai-context}
Distributivity gives $a(b+c)=ab+ac$.
:::
```{.stack-question}
#| label: authoring-expand-a-product-form-sensitive-670eab
#| file: questions/polynomial-expansion.xml
#| question: Expand a product (form-sensitive)
#| feedback: true
#| context: expansion-hint
```feedback-language chooses the explanation language; otherwise the document’s language is used. A named feedback-policy can change the sequence or word limit. The rational-expression example uses a guiding question followed by a worked explanation. The policy configuration reference explains how to define and select these policies.
Feedback uses the current answer, visible question and selected context. A matching answer check may supply its status and score. Teacher answers, source panels, question tests, Python code and detailed grader messages are excluded. Provider keys are not embedded in the page. Editing removes stale feedback and check evidence; choosing another variant restarts the hints.
Validate and publish
Keep tests for alternate correct answers, common mistakes and the intended partial credit. Run the question’s author tests, render the page, and review its variants before publishing:
export PYTHONPATH=$(ls _extensions/ctm-checkers/*.whl | paste -sd: -) # ctm-checkers' two wheels
python -m ctm_checkers.stack.cli test --file questions/polynomial-expansion.xml
quarto render
python -m http.server --directory _site 8000Serve the page over HTTP, including locally. Opening an HTML file directly is not a reliable way to run browser workers. The first answer check downloads Pyodide and SymPy, so learners need network access. Grading then runs in the browser. The page does not provide Moodle’s attempt history or persistence.
While adapting a question, diagnostics: true displays a build problem in the page. Remove that option before publication so an unsupported question stops the build. Compare representative responses in Moodle when native STACK compatibility matters for your assessment.
Questions with many correct answers
An input model is one valid response, not a list of everything a learner may submit. Select the supplied basis or matrix_property function with a choice in its JSON options object, or register your own function for another condition. The normal response-tree branches still decide the credit. The Python answer-test guide contains the teacher workflow and a complete developer walkthrough for positive definiteness.
For developers
Question-block options
All options below are written as #| key: value inside a .stack-question block. File paths are relative to the containing .qmd page.
| Option | Default or requirement | Purpose |
|---|---|---|
file |
Required | XML filename. |
question |
Required | Exact XML question name. |
variants |
25; from 1 to 100 |
Requested proposal batch; only approved current variants publish. |
review |
false |
Show Run question tests for the selected variant. |
diagnostics |
false |
Display build errors while authoring. |
feedback |
auto |
Enable CTM Assessment’s AI feedback for the card. |
context |
auto |
Nearby prose, none, or comma-separated context IDs. |
feedback-language |
Document lang |
Language for AI explanations. |
feedback-policy |
Inherited policy | Name of a policy loaded by CTM Assessment’s AI feedback. |
label |
Required, unique on the page | Stable exercise key for variant history, saved answers and policy overrides. |
checker |
Omitted | Trusted check(response, symbols) source; see the checker API. |
checker-value |
1 |
Positive weight of the implicit checker response tree. |
tests |
Omitted | Declarative answers and expected verdicts, run for every variant at build time. |
The extension requires CTM Assessment (with ctm-checkers 0.2.0 or later; 0.3.0 or later forbids learners the names of question and feedback variables, 0.6.0 or later applies STACK’s rules for the text of an answer, below; 0.10.0 or later stops on question options it does not read, applies numerical tests and checkanswertype to answers that are not numbers or not of the teacher’s type, and treats an answer a test cannot decide as false, below), Python 3.9+, SymPy 1.13.3, Quarto 1.8.27+ and HTML output with JavaScript. Set STACK_SYMPY_PYTHON to the Python interpreter to use when Quarto’s default interpreter is not the required environment.
Input and XML reference
The source is a Moodle STACK <quiz> containing named <question type="stack"> entries. Category and description entries are skipped. Retain inputs, PRTs, deployed seeds and <qtest> cases. Unsupported fields and HTML/CASText fail with a question-specific location.
| XML input setting | Handling |
|---|---|
type |
algebraic, scalar numerical, or rectangular matrix. |
tans |
The input’s model answer. A PRT node’s tans is a separate test argument. |
strictsyntax, insertstars |
Strict syntax is required. Insertion mode 0 requires explicit *; mode 1 also accepts adjacent products. |
forbidwords, forbidfloat |
Reject configured words or decimal literals. |
requirelowestterms |
Reject reducible written integer fractions during validation. |
checkanswertype |
Refuse an answer of another type than the teacher’s answer (expression, equation, logical expression, list, set or matrix) with a message that names the expected type; a list of the teacher’s length also needs entries of the teacher’s types. This is not a check of the shape or of equality. |
mustverify, showvalidation |
Verification metadata and preview visibility are retained; this UI always requires validation before grading. Supported visibility values are 0, 1, 3. |
syntaxhint, boxsize |
Parse the authored hint and size text fields within the supported limits. |
matrix |
Optional fixed/resizable dimension settings; see matrix input. |
allowwords and input options must be empty; nondefault syntax attributes and other insertion modes are unsupported. A question that sets a question option the engine does not read stops the build, with the question’s name, the option and its value: complexno other than i, decimals other than ., scientificnotation other than *10, inversetrig other than cos-1, logicsymbol other than lang, sqrtsign other than 1, multiplicationsign other than dot or none, and matrixparens other than [ or ( (the last two only change how a question writes products and matrices). An absent option is its default. Question-level assumereal and assumepositive affect symbolic assumptions. questionsimplify=0 preserves supported written forms when they survive variant preparation; unsupported held expressions fail clearly.
CASText supports {@expression@} for scalars, collections and matrices, and {@left=right@} for a displayed scalar equality. The placeholders [[input:name]], [[validation:name]] and [[feedback:name]] connect the question text to inputs and response trees. HTML is sanitized. This does not provide arbitrary Maxima evaluation or all STACK text directives.
Answer entry and form
Supported scalar notation includes arithmetic + - * / ^ (also **), parentheses, symbols and STACK’s constants e, pi and Pi, i and %e, %pi, %i; E and I are variables (a current, an energy), as in STACK. PI is a constant in an author’s expressions, but a student writes pi or Pi: PI is rejected as the wrong case (see below). Functions are sqrt, sin, cos, tan, sec, csc, cot, sinh, cosh, tanh, sech, csch, coth, the inverse functions asin, acos, atan, asec, acsc, acot and their hyperbolic forms (asinh, …), exp, log, abs, rational-polynomial gcd and real scalar mod. Aliases are ln, cosec, cosech and STACK’s arcsin, arctan, arsinh, …. mod(x,0)=x follows Maxima’s convention. With insertion mode 1, 2x, 2(x+1) and (x+1)(x-1) are products; xy remains one symbol and spaces alone never imply multiplication.
An answer follows STACK’s rules for its text, with STACK’s messages: a space between two terms is an error (2 x: “Illegal spaces found in expression 2_x.”), and so is a comma outside a list or set (3,5). A name of three letters or more is forbidden (sinx, sin*x: “Forbidden variable or constant: sinx.”) unless STACK knows it (alpha, theta, pi) or it ends in a digit (x1); names are case sensitive (Sin(x): “Input is case sensitive: Sin is an unknown function. Did you mean sin?”; PI: “Input is case sensitive: PI is an unknown variable. Did you mean Pi, pi?”). Unicode letters and symbols that STACK replaces are replaced (π is pi, α is alpha, − is -); other characters are errors (x²). 0^0 is Maxima’s error “expt: undefined: 0^0”. A question test’s inputs are computed as STACK computes them (an input’s name stands for its teacher’s answer, ans1+1 is the teacher’s answer plus 1), and the computed value is then a student’s answer: these rules apply to it, and a test whose input is invalid fails.
Form tests EqualComAss, FacForm, SingleFrac, LowestTerms require a direct input name in sans. EqualComAss preserves structure up to commutative and associative reordering. FacForm requires one polynomial variable in testoptions and handles univariate rational polynomials. SingleFrac checks an equivalent single written fraction or atomic answer. LowestTerms checks written integer fractions and supported denominator restrictions, without comparing the teacher value or cancelling symbolic polynomial factors.
NumAbsolute and NumRelative use the configured tolerance; omitted or syntactically malformed options default to 0.05. NumDecPlaces requires an explicit count from 1 to 50 and checks trailing zeros as written precision. These tests compare finite real numbers. An answer that is not one (a symbol, a complex number, inf, a list, a set, a matrix, an equation) makes the test false, with the message “Your answer should be a floating point number, but is not.” (NumDecPlaces: “Your answer must be a floating point number, but is not.”), and the response tree takes the false branch; NumDecPlaces also needs a decimal literal (1/2 and sqrt(2) are not). A tolerance or a count that is not a number, and a teacher’s answer that is a collection or an equation, are errors of the question. Use a numerical answer test for an approximate measurement rather than treating algebraic equivalence as a tolerance test.
Lists, sets and equations
Lists use [...]; sets use {...}. Both may nest and be empty. Limits are 32 entries per container, six collection levels and 128 total collection nodes, with scalar expression leaves. List comparison is ordered. Set comparison normalizes members without a general proof of every possible scalar identity; see set conventions. Sets explains outer duplicates and missing/extra members. EqualComAss retains list order, nested boundaries and repeated set entries. SameType follows applicability: two sets have the same type, and different-length lists can have the same type. An equation, a logical expression and an expression are three types, so SameType is false for x=1 against x+1. It does not establish equality.
A valid answer that a test does not compare or cannot decide does not stop the grading: the test is false and the response tree takes its false branch. EqualComAss, FacForm and SingleFrac give a message that the answer should be an expression (an equation) for an answer that is not one. For everything else (an answer outside a bound of the test, LowestTerms of a list, an equation or a matrix, a number too large to evaluate) the message is “This answer could not be checked automatically.” and the answer note is the test’s name followed by _undecided. A test that does not exist, an option that is wrong and a teacher’s answer that the test cannot compare remain errors of the question.
Use exact fractions in sets and prepared author collections. Direct decimal list entries have narrower support: literal constant arithmetic with integer powers works; decimal-bearing functions, symbolic terms, transformed collections and transitive author aliases do not. Collection tests require a direct learner input, preserving the literal arithmetic used by the comparison.
AlgEquiv also supports Boolean constants and and, or, not, implies, xor, xnor, with nounand, nounor, nounnot aliases and at most eight Boolean variables. Supported polynomial equations and their disjunctions use a product-of-residuals rule: a=0 or b=0 matches a*b=0. Root multiplicity is retained. Conjunctions/negations of equations, variable denominators and mixtures of equations with Boolean propositions are unsupported. Principal complex roots and logarithms require attention to their domains; the conventions reference records the known differences and proof limits.
Policy configuration
Loading definitions with policy-files does not select a policy. Select one with policy; STACK cards use their own integration, stack. This site’s configuration is:
ai-feedback:
policy-files: feedback/course.yml
policy:
stack: course-mathematicsA named policy can inherit settings and replace its hint sequence:
ai-feedback:
policies:
course-mathematics:
max-words: 120
reset-on-run: true
brief-mathematics:
extends: course-mathematics
max-words: 90
two-step-practice:
extends: brief-mathematics
steps:
- step-prompt: Ask one guiding question without giving a formula or answer.
- step-prompt: Give a concise worked explanation of this task.
allow-full-solution: truePlace this definition in a policy YAML file and select feedback-policy: two-step-practice on a card. A stable label targets exercises.stack overrides. The policy resolver applies defaults, project/page selections, exercise overrides, the selected policy and its ancestry, then the current step. steps replaces the inherited list; base-prompt supplies ongoing instructions and step-prompt one step. The retired prompt, inline integrations and inline defaults settings are rejected. See the shared policy reference.
Edits cancel stale responses and clear grading evidence while retaining hint progress. Check resets hints by default; reset-on-run: false preserves progress. Changing variants always resets hints. Only a displayed reply or a generated copy prompt advances the step. .ai-feedback-context and .math-exercise-context are aliases of .ai-context. Explicit jsxgraph:diagram-id references can include visual context through the shared extension; diagrams are not collected implicitly.
Tests, runtime and contribution
PRTs are acyclic response-tree graphs with =, +, - branch score operations, feedback, notes and weighted aggregation. Branch-specific penalties are not supported. Native <qtest> cases check scores, initial-attempt penalty and ordered answer notes, including anchored notes and NULL. The browser has no Moodle attempt state. For custom predicates, also select the checker filename on CLI tests; see the Python guide.
The same Python engine (ctm_checkers.stack) runs locally and in a Pyodide worker. Quarto prepares variants at build time; the worker validates, grades and replays tests. A worker timeout can stop runaway grading. The native comparison and project regression report provide separate evidence, not a blanket compatibility claim. These reports are generated by CI; a normal quarto render does not rerun their mathematical comparisons.
For a source checkout, install requirements-dev.txt and CTM Assessment’s parts at the commits in .github/pins.json, run CTM_COMPONENT_DEV=1 python -m pytest -q tests, and consult the development guide for browser/native checks. Include a minimal XML question and its qtests when reporting a grading issue. General matrix arithmetic, inequalities, units, arbitrary Maxima scripts and Moodle’s full attempt workflow remain unsupported.