Free-text Grader โ
The free-text grader is an AI-backed extension of the validation widget. When an author marks a [VALIDATE_N] text question with ###Grading: ai-judged (or uses the regex rule type, which auto-routes to AI grading), the answer is graded server-side by an LLM with the correctAnswer kept off the wire. Local-first questions (multiple-choice, plain text equality) keep client-side grading and continue to ship correctAnswer in the public page JSON.
This grader is the production hardening of the issue #205 code-check spike โ it reuses the same prompt-redaction layer, the same per-user rate-limit pattern, and the same admin-driven feature flag wiring.
End-to-end flow โ
Author writes [VALIDATE_N] block in a -Contribution rules.vr
โ ###Grading: ai-judged directive (or rule type === 'regex')
โ scripts/parsers/rules.ts strips correctAnswer from public frontmatter
โ fetch-tutorials writes .tutorial-cache/<slug>.validate-answer.json sidecar
โ scripts/lib/publish-validate-answer.js POSTs sidecars to
/content/validate-answer-specs (bearer-auth via CONTENT_API_KEY)
โ ValidateAnswerSpecs HANA entity (correctAnswer + question + slug + step + qi)
At runtime:
Validation.vue (hugo-apps) sees the question marked aiJudged: true
โ POST /api/validate-answer { slug, stepNumber, qi, learnerAnswer }
โ /api/validate-answer Express handler (XSUAA, rate-limited)
โ dispatchValidateAnswer (srv/lib/validate-answer-tool.js)
โ defaultLoadQuestion โ pulls correctAnswer from ValidateAnswerSpecs
โ defaultCallModel โ forced tool call against the configured LLM
โ redactReferenceLeaks (reused from PR #205)
โ 3-state verdict: pass / partial / fail (+ optional model hint)
โ telemetry row in ValidateAnswerSubmissions
โ response back to Validation.vue
โ step-validated CustomEvent + data-validated="true" gateFeature flag โ
ChatSettings.validateAnswerEnabled (boolean, default false). The CAP ChatSettings singleton is admin-edited via the Joule Chat Settings tile in /admin-ui/. Per-environment rollout is the expected pattern: flip on in dev, then QA, then prod after smoke.
When the flag is false, /api/validate-answer returns HTTP 503 with { error: 'disabled' }. The widget treats 503-disabled as a fourth UI state โ "Answer checking is temporarily unavailable" โ and specifically does not mark the question wrong, so a learner whose admin has the flag off can still proceed (the Done-button gate falls through to the live-submit path on the other questions in the step).
Anti-leak guarantees โ
| Surface | Carries correctAnswer? | Notes |
|---|---|---|
<script id="tutorial-data"> (public) | No for AI-graded questions | Parser strips before frontmatter emit |
<slug>.validate-answer.json sidecar | Yes | Never copied into approuter/static/; consumed only by publish-content.ts |
ValidateAnswerSpecs HANA table | Yes | Server-side only; XSUAA-protected publish endpoint |
| LLM prompt | Yes (the model needs it to grade) | redactReferenceLeaks (reused from PR #205) scrubs the model's response before it reaches the learner |
ValidateAnswerSubmissions telemetry | Yes | Documented trade-off โ captured for explainability and admin review; covered by @PersonalData.cascade so anonymization clears it |
The local-first path (multiple-choice + plain-text equality) is unchanged and continues to ship correctAnswer in <script id="tutorial-data"> โ see the validation widget anti-leak section.
Rate limits โ
Same shape as /api/codecheck (see testing-endpoints.md for the full endpoint table):
| Scope | Window | Cap | Returns on breach |
|---|---|---|---|
| Per user | 1 hour | 30 calls | HTTP 429 |
| Per (user, slug, step) | 5 minutes | 5 calls | HTTP 429 |
Both windows are sliding and tracked in process memory; restarts reset both. This is intentional โ the AI cost ceiling is a soft guardrail around classroom abuse, not a hard quota.
Local development โ
To exercise the AI path on a hybrid run:
# In a hybrid CAP shell
cds repl --bind capdb-dev// At the prompt
await UPDATE('com.sap.developers.ims.ChatSettings').set({ validateAnswerEnabled: true });Or, equivalently, from a one-shot script:
npx cds bind --exec -- node -e "const cds = require('@sap/cds'); \
cds.connect.to('db').then(db => db.run(UPDATE('com.sap.developers.ims.ChatSettings').set({ validateAnswerEnabled: true })));"The flag is also editable via the Joule Chat Settings tile at /admin-ui/#joule-display. To disable, set back to false โ the widget will display the 503-disabled UI state on next submit.
Author flow โ
To opt a [VALIDATE_N] text question into AI grading, either:
- Explicit: add a
###Grading: ai-judgeddirective line in the block. - Implicit (auto-route): use a
regexrule type โ the parser recognizes that the question can't be exact-matched client-side and routes it to the AI path.
Multiple-choice questions can be marked ###Grading: ai-judged and the parser will accept the directive (setting aiGrading: true on the emitted question), but routing them through the LLM grader is not recommended: the prompt is structured for free-text answers and option-letter submissions produce low-quality verdicts. Author guidance: only use ###Grading: ai-judged on text-typed questions.
Two-layer guard (#238):
- Build-time: the parser emits a
console.warnfor any AI-graded MCQ, surfacing the typo duringnpm run fetch-tutorials. - Runtime:
dispatchValidateAnswerrejects AI-graded MCQs witherrorReason: 'wrong_question_type'based on the originalruleTypecaptured inValidateAnswerSpecs. The submission row is persisted (for offline analysis) but no LLM call is made โ no token spend.
See the tutorial authoring guide for the complete [VALIDATE_N] syntax. The author preview at /tutorials-qa/ is the recommended way to verify a new question type before publishing โ the QA srv has its own validateAnswerEnabled flag and its own ValidateAnswerSpecs table.
Token-spend monitoring โ
5 canonical SavedQueries are seeded into AnalyticsSavedQuery on boot (see srv/lib/ai-grading-saved-queries.js) and surface in the Analytics Builder under "shared-admins" visibility:
- AI grading โ Daily token spend (validate-answer) โ sum prompt + completion tokens per day, grouped by
modelName. Multiply by your model's ยข/1K-token rate in your runbook for USD. - AI grading โ Daily token spend (code-check) โ sibling rollup for
/api/codecheck(PR #205). - AI grading โ Verdict outcome distribution (validate-answer) โ last 7 days; useful for prompt tuning and detecting operator-mistakes (e.g. spike in
errorReason: 'wrong_question_type'means an author marked a multiple-choice questionai-judged). - AI grading โ Top tutorials by token spend, last 7 days (validate-answer) โ hot-spot detector. Surfaces tutorials with too-many AI-graded questions or too-long answers.
- AI grading โ Combined daily spend (both features) โ UNION ALL across both submissions tables. Useful for daily-spend dashboards where the per-feature split is less interesting than total burn.
All queries exclude errorReason = 'disabled' from token totals (the flag-off path short-circuits before any LLM call, but the row still counts as a submission and would inflate the count if not filtered). The verdict-distribution query intentionally INCLUDES disabled to surface "how often did this happen?".
A scheduled-aggregate cron job (Layer 2 of #240) is deferred until Layer 1 reveals real burn. If spend grows non-trivially, see #240 for the design sketch.
Reference โ
- Tool dispatcher:
srv/lib/validate-answer-tool.js - Express handler:
srv/lib/validate-answer-handler.js - Question loader:
srv/lib/validate-answer-question-loader.js - Spec publish endpoint:
srv/lib/validate-answer-spec-publish.js - Prompt + tool schema:
srv/lib/validate-answer-prompt.js - Publish-content extension:
scripts/lib/publish-validate-answer.js - Validation island:
hugo-apps/src/validation/Validation.vue - Sibling docs:
- Validation widget (PR #226 โ local-first widget this extends)
- AI code-check spike:
docs/superpowers/specs/2026-06-02-ai-code-check-spike-design.md(PR #205 โ same prompt redaction + rate-limit pattern)
- Spec:
docs/superpowers/specs/2026-06-04-209-free-text-grader-design.md - Tracking: sap-tutorials/tutorials-ims#209