Native STACK preparation and cards

Select cas: maxima in a STACK block, or set stack.cas: maxima in the project or page settings. Native questions use the owned runtime-v3, with source-loaded STACK at 33383047af39c204bf4442802a058f8b918b2b1b, for both author preparation and learner grading. The SymPy backend remains available.

The author path uses Python for the PRT compiler and the common variant review workflow, Node for the pinned JavaScript grammar frontend and Wasm runner, and Maxima for mathematical evaluation. PHP is used only in adversarial comparison tests. Neither PHP nor a browser is required for native preparation. Ordinary native questions need no SymPy. PythonCheckers use the shared checker library (and SymPy for checker value decoding/execution); mathematical question preparation remains in Maxima.

Stage runtime-v3

Use Node 22 or 24 and Python with the installed CTM checker wheels and native authoring distribution. Obtain maxima-runtime-v3.json and maxima-runtime-v3.tar.gz from the owned runtime-v3 GitHub Release, and check out STACK at the exact revision above. From the component source:

python scripts/stage-native.py --release /path/to/release-files \
  --stack-source /path/to/pinned-stack --output /path/to/new-native-package
export CTM_STACK_NATIVE_PACKAGE=/path/to/new-native-package

Staging verifies the release record, archive, manifest and every runtime/STACK file. It does not rebuild or modify the runtime, change the release record format, or change the release archive layout. A missing package is a build error. Set CTM_STACK_NODE if Node is not available as node.

The usual question-variants.py generate/review/apply commands now use the selected backend. Maxima owns its seed sequence; these are not imported Moodle deployments. Approval is required by default. Preview proposals are visibly unapproved. Publication replays retained candidates, requires current approval, and checks source/history leases using the same producer as SymPy. A prose edit refreshes the display while preserving approval; changed mathematics or grading requires review. Cached native results bind the runtime, STACK pin, compiler, checker admission wheel, question, seed and Node version; caches do not approve variants.

Current capability boundary

The native frontend supports the structural XML importer’s bounded algebraic, numerical and matrix inputs; scoped question/feedback assignments; native answer tests; weighted PRTs; and basic CASText text, mathematics, input, validation, feedback, conditionals and comments. Learner expressions use the same pinned JavaScript admission implementation as the checker payload, with an additional native serialization allowlist before Maxima evaluation.

Author execution uses an explicit function allowlist independent of STACK’s security-map completeness. map and custom sort callbacks must be literal allowlisted first-order functions or inline lambdas. Function names obtained through variables, lists, strings, concat or sconcat do not grant executable authority. Stored lambdas, syntax forms passed as callbacks, apply and at remain outside this subset. Strings such as "none", "red" and "real" are ordinary data, including in lambda collections, substitutions and answers.

subst(new, old, expression) checks actual native operator replacements before substituting: a new operator must be an allowlisted first-order function (non-random in feedback). Rewriting lambda or evaluator syntax is unsupported. substinpart accepts positive literal indices only, and ev accepts the supported first-order function flags or simp, numer, nouns, noeval. Author-definition failures are checked before producing candidate evidence.

Unsupported features stop the author build with a diagnostic. These include arbitrary author function definitions or execution, JSXGraph, external includes, general CASText blocks such as foreach, CASText question-test inputs (CT:), and variable/input names that collide with protected native functions. An explicitly rejected feature is not a fallback to SymPy. This is an initial native question subset, not complete Moodle/STACK compatibility.

Every prepared seed must pass its XML tests and a full-mark teacher-vector check on every PRT. Wrong-answer probes are retained as author evidence. Question-test support is bounded by the importer and is not a claim to every Moodle test sentinel or expectation mode.

Learners validate with the explicit button before grading. Instant validation is not enabled by this change. One question/variant owns a fresh worker heap; switching variants retires it. This proves state separation at the cost of runtime startup on a switch. Loading is outside the calculation clock. The page needs neither cross-origin isolation nor SharedArrayBuffer. Ordinary native questions do not download or start Pyodide. Questions using PythonCheckers or declarative tests warm a private Pyodide worker before the calculation clock. It never uses mainPyodide; Stop and variant changes retire both workers. Pages containing only SymPy cards do not load the native runtime package.

Compiler and audit

The PRT compiler is a clean Python implementation with typed IR and a single active-node transition. The emitted program runs in Maxima; Python and JavaScript do not interpret the response tree at grading time. The independent test reference walks paths with Decimal arithmetic. Both our compiler and the pinned upstream PHP compiler passed the same 120 graphs and 2,572 verdict assignments against that reference. This tests routing and score arithmetic, not the mathematical correctness of each answer test.

The adversarial audit provides two reproduced upstream rendering defects and one score-precision concern, with raw evidence and standalone reproduction. Our backend preserves Maxima’s three-decimal PRT score rather than applying upstream PHP’s additional continued-fraction approximation. This compatibility difference is explicit.

The native question CI also exercises actual installed Quarto cards, mixed backends, full/partial/zero grades, invalid input, XML tests, fresh variant heaps, shared approvals and stale leases. Six additional authored families (rational, numerical, matrix, Boolean, nested collections and hyperbolic) pass 20 XML tests and their full-mark teacher vectors. Literal test decimals retain trailing zeros, and expected note sequences use upstream’s anchoring rules. Transport replay remains in the separate native foundation workflow. The full GC stress campaign runs for changes to the call boundary, lifetime, runtime pin/staging or GC stress test itself, and for manual qualification. Compiler and card changes use the normal replay, lifecycle and browser gates. JSXGraph, broader input/feedback conformance, locales and offline distribution remain tracked work.

PythonChecker continuations (M4d)

XML PythonChecker nodes and block checker: declarations use the same H1 checker contract as SymPy. A compiled native tree suspends with exact written state and its evaluated teacher value, calls the fixed text-only stack.check API, then resumes its generated Maxima function. No native pointers or active ECL frames survive the suspension. Maxima still owns routing, scores, penalties and branch feedback. A partial Python score takes an XML node’s false branch; a block checker contributes its numeric weighted score. Invalid results remain invalid and clear matrix assessments. Checker feedback is plain text. Each invocation has a fresh Python namespace.

Native author builds use one supervised Python checker process per question, with fresh namespaces and a 10-second per-call bound; browser checks use the card’s calculation deadline. Custom checkers receive native variables, teachers, assumptions and the final assigned {n, id, hash}. Approval identity includes checker code and checker runtime identities. Assigned identities are replayed before publication.

Declarative tests: answers are evaluated in Maxima, including question variables; raw("literal") bypasses that evaluation for invalid-input tests. Expectations use the common stack.test-results verifier, which never resolves mathematics. XML and declarative tests must pass before approval. The native continuation audit covers 32 mixed-tree verdict cases, sequential Python nodes, branch skipping and an exact 39-digit state value. Installed browser acceptance covers custom/library checkers, Stop, errors, variant changes and matrix axes.

Revision and exact replay

_extensions/ctm-stack/native/stack-pin.json is the one editable STACK revision. The workflows, Python tools and grammar generator read it. Run node tools/compiler/generate.mjs /path/to/STACK to regenerate the browser pin, grammars, metadata and provenance. Refresh the checked source inventories with their recording tools when upgrading; generated inventories report the pin but do not configure it. Runtime upgrades still require the owned release gate.

Native operations recreate question variables from the seed on every call, including teacher validation. They never restore native state from the printed env evidence. This preserves binary float precision and unevaluated structure even when an author sets fpprintprec:12.

Build-time PythonCheckers are trusted, unsandboxed code in host CPython, following the H1 author trust model. Process isolation, output bounds and deadlines limit accidental failures; they do not sandbox filesystem, network or host access. Only trusted authored checker source may be built. Browser checkers run in the card’s private Pyodide worker. The author Python identity records its actual interpreter and SymPy versions and the checker wheel hash. The Python identity also records browserSympyVersion; this backend requires host and browser SymPy versions to match. After installing its packages, each private Pyodide worker measures sympy.__version__ and compares it to the compiled identity before grading or authored-test execution. Missing or changed versions retire both heaps. A browser SymPy upgrade needs a matching host rebuild and fresh variant approval; the identity is part of the question/cache hash.

Host SymPy pin

The native build host’s SymPy is pinned to the SymPy that the ctm-libs Pyodide runtime bundles, not chosen here. ctm-libs owns the Pyodide pin and records its SymPy in _extensions/ctm-libs/runtime/pyodide-sympy.json, generated from Pyodide’s own pyodide-lock.json by ctm-libs’ scripts/sync-pyodide-sympy.py (Pyodide 0.28.3 bundles SymPy 1.13.3). requirements.txt pins exactly that version (sympy==1.13.3), and so do the install commands of the README and the authoring guide. The pin changes only together with the Pyodide pin: a ctm-libs Pyodide upgrade that brings another SymPy is adopted here by repinning ctm-libs (.github/pins.json) and moving requirements.txt in the same change.

Install exactly the pinned SymPy in the build environment. Overriding SymPy locally (another pip install sympy, an unpinned environment) would build PythonChecker questions whose recorded browserSympyVersion the learner’s browser does not have, and grading there fails closed with python-checker-identity. The native build therefore stops such a question: before preparing any seed of a question with PythonCheckers or declarative tests, it compares the host SymPy with the record of the ctm-libs installed beside ctm-stack (CTM_LIBS_EXTENSION in component development builds) and names both versions and the pip install command for the build’s Python. Questions without Python are not affected, and a ctm-libs that predates the record leaves the browser check as the only one. python scripts/check-host-sympy.py --libs <ctm-libs checkout or installed _extensions/ctm-libs> compares the pins with the ctm-libs record without a browser; CI runs it (host-sympy.yml) at the ctm-libs commit of .github/pins.json on every pull request that touches the requirements, the pins or native code. The browser card test (tests/native/python-card-browser.py) remains the end-to-end check of the identity.

Continuation feedback variables are re-evaluated from the same native inputs and seeded question variables on every resume. Only routing/score/feedback control data is serialized, with full float printing precision. Printed mathematical values are never used to restore native question/feedback state. Authored-test observations carry checked test names and the actual candidate id/hash before validation, including the final numbered-identity replay.