@intellectif/lk-core 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +30 -1
  3. package/dist/{activity-f0bgZSTP.d.cts → activity-CYc7SD3d.d.cts} +157 -11
  4. package/dist/{activity-f0bgZSTP.d.ts → activity-CYc7SD3d.d.ts} +157 -11
  5. package/dist/ai-check.cjs +103 -15
  6. package/dist/ai-check.cjs.map +1 -1
  7. package/dist/ai-check.d.cts +9 -5
  8. package/dist/ai-check.d.ts +9 -5
  9. package/dist/ai-check.js +102 -14
  10. package/dist/ai-check.js.map +1 -1
  11. package/dist/{chunk-BIJCO2HM.js → chunk-5UCR4UNR.js} +301 -20
  12. package/dist/chunk-5UCR4UNR.js.map +1 -0
  13. package/dist/{chunk-FVZJ4X3Y.cjs → chunk-OPMY2MNS.cjs} +302 -21
  14. package/dist/chunk-OPMY2MNS.cjs.map +1 -0
  15. package/dist/{chunk-MMEQKFFQ.js → chunk-Q7MV3IXJ.js} +196 -8
  16. package/dist/chunk-Q7MV3IXJ.js.map +1 -0
  17. package/dist/{chunk-COBK2RUX.cjs → chunk-WQVZPIN7.cjs} +197 -9
  18. package/dist/chunk-WQVZPIN7.cjs.map +1 -0
  19. package/dist/{index-BA9f2ydg.d.ts → index-0Z3-nnAP.d.ts} +1 -1
  20. package/dist/{index-7S3-TLjT.d.cts → index-CxxiNSSH.d.cts} +1 -1
  21. package/dist/index.cjs +56 -9
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +93 -10
  24. package/dist/index.d.ts +93 -10
  25. package/dist/index.js +52 -5
  26. package/dist/index.js.map +1 -1
  27. package/dist/schemas.d.cts +2 -2
  28. package/dist/schemas.d.ts +2 -2
  29. package/dist/scoring.cjs +14 -2
  30. package/dist/scoring.cjs.map +1 -1
  31. package/dist/scoring.d.cts +240 -4
  32. package/dist/scoring.d.ts +240 -4
  33. package/dist/scoring.js +13 -1
  34. package/dist/xapi.d.cts +1 -1
  35. package/dist/xapi.d.ts +1 -1
  36. package/package.json +1 -1
  37. package/vectors/README.md +10 -0
  38. package/vectors/scoring.json +2127 -1
  39. package/dist/chunk-BIJCO2HM.js.map +0 -1
  40. package/dist/chunk-COBK2RUX.cjs.map +0 -1
  41. package/dist/chunk-FVZJ4X3Y.cjs.map +0 -1
  42. package/dist/chunk-MMEQKFFQ.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @intellectif/lk-core
2
2
 
3
+ ## 0.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#80](https://github.com/intellectif/learning-kit/pull/80) [`782b01b`](https://github.com/intellectif/learning-kit/commit/782b01ba697a90583b472ab1a7a2be102fd888c4) Thanks [@diego28e](https://github.com/diego28e)! - **Feedback on writing**: "Get feedback on my draft" on a written response in practice. The learner revises and asks again, and every correction must quote what they wrote.
8
+
9
+ **Action required:** none unless your code switches exhaustively over `AiFeature` or `AiRefusal` (new members `'writing-feedback'` and `'misquotes-answer'`), reads `AiCheckCase.request` without narrowing on `feature`, or implements a complete `LkStrings` (eight new keys). Nothing changes until you pass a `writingFeedback` port. Otherwise this is a major only because the peer range moves to `@intellectif/lk-core@^0.22.0`.
10
+
11
+ - **lk-core:** `aiWritingFeedbackRequest` builds what a model is given: the task, the draft verbatim, its word count as the grader counts it, the rubric, and the feedback on earlier drafts. `checkAiWritingFeedback` checks the reply before a learner sees it. It finds each correction's quote in the draft itself (typographic quotes and runs of whitespace folded, case kept) and anchors it there. A correction of words the draft does not contain refuses the whole reply as `misquotes-answer`. Criteria must be the rubric's; the author's weights are attached, and a model's weight is ignored. The indicative score is `gradeFromRubric`'s total, computed only when every criterion was judged. It is never a grade. Limits: 20 corrections, 500 characters a field.
12
+ - **lk-react:** a `writingFeedback` port and `maxWritingFeedback` (3 by default, at most 10) on `LearnerAi`. `<WrittenResponse>` shows the feedback with its corrections, rubric comments, the indicative score marked "Not a grade." and the AI notice, and says so once the draft has changed. It is never in `exam` or `review`, and it follows the author's and the paper's `ai.explanations`, `feedback` and `solutions`. `useAiWritingFeedback` gives a written response you draw yourself the same rules.
13
+ - **Records:** `ai-writing-feedback-shown` (the draft number, how many corrections, the indicative score, provenance and usage; never the text), and `ai-help-refused` with `feature: 'writing-feedback'`.
14
+ - **ai-check:** a `writingFeedback` port runs four writing cases: a draft with mistakes, one without, a revision, and feedback asked for in Spanish.
15
+ - Eight new strings: `aiWritingFeedback`, `aiWritingFeedbackLoading`, `aiWritingFeedbackHeading`, `aiWritingFeedbackUnavailable`, `aiNoMoreWritingFeedback`, `aiWritingFeedbackOutdated`, `aiCorrections`, `aiIndicativeScore`.
16
+
17
+ Guide: https://github.com/intellectif/learning-kit/blob/main/docs/ai.md#feedback-on-writing
18
+
19
+ ## 0.21.0
20
+
21
+ ### Minor Changes
22
+
23
+ - [#78](https://github.com/intellectif/learning-kit/pull/78) [`30a9b3d`](https://github.com/intellectif/learning-kit/commit/30a9b3da9be60589d63516752a02cc2f44bdf73b) Thanks [@diego28e](https://github.com/diego28e)! - **Scoring policies**: "Try again" in practice, which try counts, and what tries and hints cost — set per paper, recorded in the plan, and no grade moves without one.
24
+
25
+ **Action required:** only if you build an `InteractiveVideoQuestion` by hand (add `scoring`, e.g. `DEFAULT_ITEM_SCORING_POLICY`) or implement a complete `LkStrings` of your own (nine new keys). Everyone else upgrades with no change.
26
+
27
+ - **lk-core:** `ItemScoringPolicy` — `retries` (0–10), `counts` (`'first'` by default, `'best'`, `'last'`), `retryPenalty` and `hintPenalty` (fractions of the marks). `scoreTries` does the arithmetic: a cost is subtracted, never below zero, with floating-point residue under a pass line removed. `evaluate(item, response, { scoring })` charges the response's hints; `evaluateTries(item, responses, { scoring })` picks the try that counts. `validateItemScoringPolicy` and `resolveItemScoringPolicy` refuse a policy that cannot be applied — a `RangeError`, never a guess. `planAttempt(…, { scoring })` records it in the plan and `planHash`, and `verifyAttemptPlan` reports `scoringChanged`. With no policy every number is what it was; 43 new grade vectors pin the arithmetic.
28
+ - **Responses:** multiple-choice, fill-in-the-blanks and gap-select responses gain an optional `hintsRevealed`, as dictation's already had: in `practice`, the hints a learner was shown, counted from the start of the question.
29
+ - **lk-react:** `scoring` on every activity, `<ActivitySequence>` and `<InteractiveVideo>`; the paper's policy wins over a question's. "Try again" after an answer short of full marks keeps the answer and clears the marks; "Show answer" (or "Keep this answer") ends the tries. While a try is on offer the right answer and "Explain my answer" wait. `onComplete` fires after every try with the question's score so far. A set waits for any question being tried again, and for the question on screen while it offers a try, and closes every question's tries once it reports; the video closes them at Finish.
30
+ - **Fixed:** a dictation under `delivery={{ solutions: false }}` hid "Show solution" but its marks still named every word they corrected. They now say a word is wrong or missing without naming it, and draw no letter diff.
31
+ - Nine new strings: `tryAgain`, `showAnswer`, `keepAnswer`, `triesLeft`, `countedTry`, `scoreBeforeCosts`, `hintCost`, `dictationWordWrongUnnamed`, `dictationWordMissingUnnamed`.
32
+
33
+ Guide: https://github.com/intellectif/learning-kit/blob/main/docs/scoring.md
34
+
3
35
  ## 0.20.0
4
36
 
5
37
  ### Minor Changes
package/README.md CHANGED
@@ -27,6 +27,8 @@ npm install @intellectif/lk-core
27
27
  - **Assessment composition** — `composeAssessmentScore()` for weighted, sectioned totals, with a `provisional` status so an unmarked essay never deflates a grade — and a grade out of contract (`NaN`, 85 "out of 1") never finalises one — and `RoundingPolicy` / `roundGrade` / `gte` / `classifyBand` keeping grade rounding and band classification as the two distinct operations they are.
28
28
  - **Item groups** — `Stimulus` + `ItemGroup` (one passage or recording serving several questions), `flattenSequence()` for seeded, shuffle-atomic presentation order with stable slot identity. Slot ids are positional by default (`"3"`, `"1.0"`); author a `slotKey` on an entry and its id survives an insert, so a stored response cannot slide onto a different question.
29
29
  - **Delivery policies** — `DeliveryPolicy` says what a school lets a learner see around a question: feedback, solutions, hints and AI help. `resolveDeliveryPolicy` spells one out (an unreadable setting restricts, never allows), `validateDeliveryPolicy` checks one before it is stored, and `combineDeliveryPolicies` keeps a setting on only where every policy leaves it on.
30
+ - **Scoring policies** — `ItemScoringPolicy` says how a paper's questions are scored when a learner can try again or ask for hints: `retries`, which try `counts`, and what a try and a hint cost. `scoreTries` does the arithmetic, `evaluate(…, { scoring })` and `evaluateTries` reach a question's outcome from its answers, and `validateItemScoringPolicy` / `resolveItemScoringPolicy` refuse a policy that cannot be applied rather than guess — every setting moves a grade. No policy changes nothing, and the arithmetic is pinned by the grade vectors.
31
+ - **AI help contracts** — the SDK calls no model; it builds what yours is given and checks what comes back. `aiExplanationRequest`, `aiHintRequest` and `aiWritingFeedbackRequest` build the requests a component makes, so a server can rebuild them from its own copy of an item; `checkAiExplanation`, `checkAiHint` and `checkAiWritingFeedback` refuse an explanation that contradicts the grade, a hint that contains the answer, and writing feedback that corrects words the learner never wrote, and `checkAiWritingFeedback` computes the rubric's indicative score itself.
30
32
  - **Attempt plans** — `planAttempt(entries, { seed, shuffleEntries, points, delivery })` freezes the exact paper a learner sat, and the delivery policy it was sat under: slot order, per-slot points, a content hash of each item, and a `planHash` over the lot. `verifyAttemptPlan(plan, current)` reports the drift when that content is later edited (`changedSlotIds`, `changedStimulusSlotIds`, `changedPointsSlotIds`, `missingSlotIds`, …) — the thing you need during a remark or an appeal. `scoredItemsFromPlan(plan, outcomes)` turns collected outcomes into the `ScoredItem[]` composition wants, and defaults a slot with **no** outcome to `deferred` rather than silently shrinking the denominator.
31
33
  - **Attempt state** — `serializeAttemptState(plan, progress)` / `restoreAttemptState(plan, snapshot)` round-trip an interrupted attempt (responses, submitted slots, position) and are **bound to the plan**: restoring answers onto a paper the learner never sat throws instead of succeeding quietly. `diffResponses(before, after)` reports what moved between two snapshots, including an answer the learner *cleared*.
32
34
  - **Media playback policy** — `media.playback` on an audio recording: `maxPlays`, `seek`, `rate`, and advisory `nativeControlHints`. `resolvePlaybackPolicy()` derives the defaults once so the schema, the plan and the renderer cannot drift. Budgets are frozen into the plan by `planAttempt`, so editing `maxPlays` mid-window cannot change what a past learner was held to, and `planAttempt` refuses a paper where one recording is budgeted under two keys.
@@ -125,12 +127,39 @@ and content hashing — those have no subpath of their own.
125
127
  All exports ship as ESM + CJS with `.d.ts` types. Tree-shakeable; `sideEffects: false`. Node >= 20. In browsers, the SDK's own code needs `Object.hasOwn` — Chrome and Edge 93, Firefox 92, Safari 15.4 — because output targets ES2022 and nothing is polyfilled.
126
128
  Zod is the single runtime dependency: the package depends on `zod@^3.25` and imports the **Zod 4 API** from its `zod/v4` subpath, so it coexists with an app still on Zod 3.
127
129
 
130
+ ## Releases & upgrading
131
+
132
+ - **What is supported, and since when** — [Features](https://github.com/intellectif/learning-kit/blob/main/docs/features.md): every activity and capability, the `lk-core` / `lk-react` versions it arrived in, and its guide.
133
+ - **What changed** — [GitHub Releases](https://github.com/intellectif/learning-kit/releases), one per package version, and the same notes in each package's `CHANGELOG.md` (it ships in the npm package, so it is in your `node_modules` too). Notes written since release notes gained a template — every release after lk-core 0.20.0 / lk-react 20.0.0 — start with a one-line summary and **Action required**: `none`, or what to do. Earlier notes lead with the detail; the version map in the upgrading guide says what each one asks of you.
134
+ - **How to upgrade** — [Upgrading](https://github.com/intellectif/learning-kit/blob/main/docs/upgrading.md): which `lk-core` goes with which `lk-react`, and a section per release saying what, if anything, you need to change. A `lk-react` major is often only the `lk-core` peer bump; the map says so.
135
+ - **Hearing about a release** — on GitHub, **Watch → Custom → Releases**, or subscribe to [the releases feed](https://github.com/intellectif/learning-kit/releases.atom). Better still, let your dependency bot bring each release to you as a pull request, with its notes — and keep the two packages together in one, since they move together:
136
+
137
+ ```yaml
138
+ # .github/dependabot.yml — one entry per directory with a package.json
139
+ version: 2
140
+ updates:
141
+ - package-ecosystem: npm
142
+ directory: /
143
+ schedule: { interval: weekly }
144
+ groups:
145
+ learning-kit:
146
+ patterns: ["@intellectif/lk-*"]
147
+ ```
148
+
149
+ With Renovate: `"packageRules": [{ "matchPackageNames": ["@intellectif/lk-core", "@intellectif/lk-react"], "groupName": "learning-kit" }]`.
150
+
151
+ `lk-core` is still `0.x`, and a caret range on a `0.x` version matches only that minor: `^0.18.0` never installs `0.19.0`. A bot is how you find out there is one.
152
+
128
153
  ## Documentation
129
154
 
130
- - [Upgrading](https://github.com/intellectif/learning-kit/blob/main/docs/upgrading.md) — start here on any upgrade from 0.3.x, 0.4.x or 0.5.x.
155
+ - [Features](https://github.com/intellectif/learning-kit/blob/main/docs/features.md) — everything supported, and the versions each arrived in.
156
+ - [Upgrading](https://github.com/intellectif/learning-kit/blob/main/docs/upgrading.md) — which `lk-core` goes with which `lk-react`, and what each release asks of you.
131
157
  - [Changelog](https://github.com/intellectif/learning-kit/blob/main/packages/lk-core/CHANGELOG.md) — every release, with the reasoning.
132
158
  - [Grade-stability vectors](https://github.com/intellectif/learning-kit/blob/main/packages/lk-core/vectors/README.md) — the package's grading frozen as data; replay it against the build you install.
133
159
  - [Authoring & content storage](https://github.com/intellectif/learning-kit/blob/main/docs/authoring.md) — data model, validation, fetch → validate → render flow, and building an editor (the draft issue codes).
160
+ - [Delivery policies](https://github.com/intellectif/learning-kit/blob/main/docs/delivery.md) — `DeliveryPolicy`, and recording it with an attempt.
161
+ - [Scoring policies](https://github.com/intellectif/learning-kit/blob/main/docs/scoring.md) — `ItemScoringPolicy`: tries, which counts, what hints cost, and scoring them on a server.
162
+ - [AI help for learners](https://github.com/intellectif/learning-kit/blob/main/docs/ai.md) — the AI contract, the checks, and the `ai-check` kit for your prompts.
134
163
  - [Project README](https://github.com/intellectif/learning-kit#readme) — the full picture, including the React renderers.
135
164
  - [Contributing](https://github.com/intellectif/learning-kit/blob/main/CONTRIBUTING.md) — adding a new activity type.
136
165
 
@@ -224,7 +224,11 @@ interface GradeRecord {
224
224
  * one-word vocabulary item, say.
225
225
  */
226
226
  interface ActivityAiPermissions {
227
- /** An explanation of the learner's graded answer. */
227
+ /**
228
+ * An explanation of the learner's graded answer — and, on a written
229
+ * response, feedback on a draft: both are a model's words about the
230
+ * learner's own answer.
231
+ */
228
232
  explanations?: boolean;
229
233
  /** Hints before the learner submits. */
230
234
  hints?: boolean;
@@ -243,8 +247,8 @@ interface AiActivityInput {
243
247
  readonly redacted?: unknown;
244
248
  readonly ai?: unknown;
245
249
  }
246
- /** The two kinds of help a learner can be given. */
247
- type AiFeature = 'explanation' | 'hint';
250
+ /** The kinds of help a learner can be given. */
251
+ type AiFeature = 'explanation' | 'hint' | 'writing-feedback';
248
252
  /** The activity types the SDK builds facts for. */
249
253
  type AiSupportedActivityType = 'multiple-choice' | 'fill-in-the-blanks' | 'gap-select' | 'dictation';
250
254
  /**
@@ -404,8 +408,117 @@ interface AiTextResult {
404
408
  */
405
409
  usage?: GraderUsage;
406
410
  }
407
- /** Why a result was not shown. */
408
- type AiRefusal = 'malformed' | 'empty' | 'too-long' | 'contradicts-grade' | 'reveals-answer';
411
+ /**
412
+ * Why a result was not shown. `misquotes-answer` is writing feedback that
413
+ * corrects words the learner did not write: a correction whose quote is not in
414
+ * the draft, or not where it claims to be.
415
+ */
416
+ type AiRefusal = 'malformed' | 'empty' | 'too-long' | 'contradicts-grade' | 'reveals-answer' | 'misquotes-answer';
417
+ /** One criterion of a written response's rubric, as the author wrote it. */
418
+ interface AiRubricCriterionFact {
419
+ name: string;
420
+ description?: string;
421
+ weight: number;
422
+ }
423
+ /**
424
+ * What a model is told about a draft of a written response: the task as the
425
+ * learner saw it, the draft verbatim, and the rubric it will be graded on.
426
+ */
427
+ interface AiWritingFacts {
428
+ activityType: 'written-response';
429
+ activityId: string;
430
+ title: string;
431
+ /** The language the item is written in, when the author gave one. */
432
+ locale?: string;
433
+ prompt: string;
434
+ /** The draft, exactly as the learner wrote it. Every correction quotes it. */
435
+ text: string;
436
+ /** Recomputed with `countWords`, as the grader will count it. */
437
+ wordCount: number;
438
+ minWords: number;
439
+ maxWords: number;
440
+ withinWordBounds: boolean;
441
+ /** The rubric the answer will be graded on; `null` when the item has none. */
442
+ rubric: AiRubricCriterionFact[] | null;
443
+ /** The target language and level, when the author gave one (e.g. `"en-A2"`). */
444
+ languageTarget?: string;
445
+ }
446
+ /**
447
+ * Asks for feedback on a draft of a written response, before the learner
448
+ * submits it. Practice only.
449
+ */
450
+ interface AiWritingFeedbackRequest {
451
+ feature: 'writing-feedback';
452
+ facts: AiWritingFacts;
453
+ /** 1-based: which draft this is. */
454
+ draftNumber: number;
455
+ /** The text of the feedback already shown on earlier drafts, oldest first. */
456
+ previousFeedback: string[];
457
+ /** The language to write in: the learner's interface language, when known. */
458
+ learnerLocale?: string;
459
+ }
460
+ /**
461
+ * One correction a model proposes. `original` must be words the learner
462
+ * wrote; the SDK finds them in the draft itself, so a model need not count
463
+ * characters — leave `range` out unless you are sure of it, because a range
464
+ * that does not hold the quote is refused.
465
+ */
466
+ interface AiWritingCorrection {
467
+ original: string;
468
+ corrected: string;
469
+ explanation?: string;
470
+ /** An error-type tag, e.g. `"tense"`, `"article"`. */
471
+ category?: string;
472
+ range?: {
473
+ start: number;
474
+ end: number;
475
+ };
476
+ }
477
+ /**
478
+ * A model's judgement of one rubric criterion. The SDK attaches the author's
479
+ * weight; a weight sent here is ignored.
480
+ */
481
+ type AiCriterionJudgement = Omit<CriterionScore, 'weight'> & {
482
+ weight?: number;
483
+ };
484
+ /**
485
+ * What a host's `writingFeedback` port returns: the overall feedback as plain
486
+ * text, and optionally corrections and a judgement per rubric criterion.
487
+ */
488
+ interface AiWritingFeedbackResult {
489
+ /** The overall feedback, addressed to the learner. Plain text, never HTML. */
490
+ text: string;
491
+ corrections?: AiWritingCorrection[];
492
+ criteria?: AiCriterionJudgement[];
493
+ provenance?: AiProvenance;
494
+ usage?: GraderUsage;
495
+ }
496
+ /**
497
+ * Feedback on a draft, as the SDK accepted it: what a learner is shown.
498
+ */
499
+ interface AiWritingFeedback {
500
+ text: string;
501
+ /**
502
+ * Every correction, anchored in the draft: `range` is where the SDK found
503
+ * the quote, and `original` is the draft's own text at that range.
504
+ */
505
+ corrections: InlineCorrection[];
506
+ /** The model's judgements, with the rubric's weights. */
507
+ criteria: CriterionScore[];
508
+ /**
509
+ * The rubric's weighted total of the judgements, from 0 to 1, computed by
510
+ * the SDK as `gradeFromRubric` computes a grade — when every criterion of
511
+ * the rubric was judged; `null` otherwise, and on an item with no rubric.
512
+ *
513
+ * **An indication, never a grade:** it is a model's reading of a draft the
514
+ * learner has not submitted, and nothing in the SDK scores an answer with
515
+ * it. `ai-writing-feedback-shown` reports it, so a record can say what the
516
+ * learner was shown.
517
+ */
518
+ indicativeScore: number | null;
519
+ provenance?: AiProvenance;
520
+ usage?: GraderUsage;
521
+ }
409
522
 
410
523
  /**
411
524
  * An xAPI Activity object — the thing a statement is about.
@@ -1253,12 +1366,28 @@ interface MultipleChoiceLearnerResponse {
1253
1366
  type: 'multiple-choice';
1254
1367
  /** IDs of the options the learner selected. */
1255
1368
  selectedOptionIds: string[];
1369
+ /**
1370
+ * Hints the learner was shown before submitting this answer, counted from
1371
+ * the start of the question — the author's and AI hints alike. Written by
1372
+ * the SDK's components in `practice`; absent or 0 means none. A scoring
1373
+ * policy's `hintPenalty` charges for them. Client-reported, like any count
1374
+ * a browser makes.
1375
+ */
1376
+ hintsRevealed?: number;
1256
1377
  }
1257
1378
  /** Learner response for a Fill-in-the-Blanks activity. */
1258
1379
  interface FillInTheBlanksLearnerResponse {
1259
1380
  type: 'fill-in-the-blanks';
1260
1381
  /** Map of blank ID to the learner's typed answer. */
1261
1382
  answers: Record<string, string>;
1383
+ /**
1384
+ * Hints the learner was shown before submitting this answer, counted from
1385
+ * the start of the question — the author's and AI hints alike. Written by
1386
+ * the SDK's components in `practice`; absent or 0 means none. A scoring
1387
+ * policy's `hintPenalty` charges for them. Client-reported, like any count
1388
+ * a browser makes.
1389
+ */
1390
+ hintsRevealed?: number;
1262
1391
  }
1263
1392
  /**
1264
1393
  * Learner response for a Gap Select activity.
@@ -1273,6 +1402,14 @@ interface GapSelectLearnerResponse {
1273
1402
  type: 'gap-select';
1274
1403
  /** Map of gap ID to the ID of the choice the learner selected. */
1275
1404
  selections: Record<string, string>;
1405
+ /**
1406
+ * Hints the learner was shown before submitting this answer, counted from
1407
+ * the start of the question — the author's and AI hints alike. Written by
1408
+ * the SDK's components in `practice`; absent or 0 means none. A scoring
1409
+ * policy's `hintPenalty` charges for them. Client-reported, like any count
1410
+ * a browser makes.
1411
+ */
1412
+ hintsRevealed?: number;
1276
1413
  }
1277
1414
  /** Learner response for a Written Response activity. */
1278
1415
  interface WrittenResponseLearnerResponse {
@@ -1286,15 +1423,18 @@ interface WrittenResponseLearnerResponse {
1286
1423
  * Learner response for a Dictation activity.
1287
1424
  *
1288
1425
  * `text` is exactly what the learner typed; every normalisation happens in the
1289
- * scorer. `hintsRevealed` is client-reported telemetry — how many hint words
1290
- * were shown before submitting — that the scorer ignores and a consumer may
1291
- * log or penalise, knowing it cannot be verified.
1426
+ * scorer. `hintsRevealed` is client-reported — how many hint words were shown
1427
+ * before submitting. The scorer ignores it; a scoring policy's `hintPenalty`
1428
+ * charges for it, knowing it cannot be verified.
1292
1429
  */
1293
1430
  interface DictationLearnerResponse {
1294
1431
  type: 'dictation';
1295
1432
  /** The learner's text, as typed. */
1296
1433
  text: string;
1297
- /** Hint words revealed before submitting (practice only). Absent or 0 means none. */
1434
+ /**
1435
+ * Hint words shown before submitting (practice only), counted from the start
1436
+ * of the question. Absent or 0 means none.
1437
+ */
1298
1438
  hintsRevealed?: number;
1299
1439
  }
1300
1440
  /**
@@ -1572,7 +1712,13 @@ type InteractionKind = 'option-selected' | 'option-deselected' | 'blank-filled'
1572
1712
  * watches its own model in production, where the development console warning
1573
1713
  * it accompanies is silent.
1574
1714
  */
1575
- | 'ai-help-refused' | (string & {});
1715
+ | 'ai-help-refused'
1716
+ /**
1717
+ * Feedback on a draft of a written response was shown (`draftNumber`, the
1718
+ * number of `corrections`, the `indicativeScore` when there was one, and the
1719
+ * port's `provenance` and `usage` when it sent them). Never the text.
1720
+ */
1721
+ | 'ai-writing-feedback-shown' | (string & {});
1576
1722
  /** Fired by activity components on every discrete learner interaction. */
1577
1723
  interface InteractionEvent {
1578
1724
  /** The kind of interaction that occurred. */
@@ -1585,4 +1731,4 @@ interface InteractionEvent {
1585
1731
  payload: Record<string, unknown>;
1586
1732
  }
1587
1733
 
1588
- export { type MediaPlaybackPolicy as $, type ActivityAiPermissions as A, type BlankConfig as B, type CriterionScore as C, type DeferredReason as D, type DictationData as E, type DictationEquivalence as F, type DictationLearnerResponse as G, type DictationSlowMedia as H, type DictationTolerance as I, type FillInTheBlanksData as J, type FillInTheBlanksLearnerResponse as K, type GapSelectBank as L, type GapSelectChoice as M, type GapSelectData as N, type GapSelectGap as O, type GapSelectLearnerResponse as P, type GradeRecord as Q, type Grader as R, type GraderKind as S, type GraderUsage as T, type GradingState as U, type InlineCorrection as V, type InteractionEvent as W, type InteractionKind as X, type ItemOutcome as Y, type LearnerResponse as Z, type LearnerResponseMap as _, type ActivityData as a, type MediaTrack as a0, type MultipleChoiceData as a1, type MultipleChoiceLearnerResponse as a2, type MultipleChoiceOption as a3, type MultipleChoiceOptionMedia as a4, type NativeControlHint as a5, type ReadAloudData as a6, type ReadAloudDimension as a7, type ReadAloudDimensionWeight as a8, type ReadAloudLearnerResponse as a9, type ReadAloudSlowMedia as aa, type RecordingBounds as ab, type RecordingRef as ac, type ScoringDetail as ad, type ScoringOutcome as ae, type ScoringResult as af, type TextMatchPolicy as ag, type TextMatchResult as ah, type ValidationError as ai, type ValidationResult as aj, type WrittenResponseData as ak, type WrittenResponseLearnerResponse as al, type WrittenResponseRubric as am, type WrittenResponseRubricCriterion as an, type XAPIActor as ao, type XAPIConfig as ap, type XAPIContext as aq, type XAPIContextActivities as ar, type XAPIError as as, type XAPIObject as at, type XAPIResult as au, type XAPIScore as av, type XAPIStatement as aw, type XAPIVerbObject as ax, levenshteinDistance as ay, matchText as az, type ActivityDataMap as b, type ActivityFeedback as c, type ActivityMedia as d, type ActivityResult as e, type ActivityType as f, type AiActivityInput as g, type AiBlankFact as h, type AiDictationFacts as i, type AiExplanationRequest as j, type AiFeature as k, type AiFillInTheBlanksFacts as l, type AiGapFact as m, type AiGapSelectFacts as n, type AiGrade as o, type AiHintRequest as p, type AiItemFacts as q, type AiMultipleChoiceFacts as r, type AiOptionFact as s, type AiProvenance as t, type AiRefusal as u, type AiSupportedActivityType as v, type AiTextResult as w, type AiVerdict as x, type AiWordFact as y, type DeferredScoringPartial as z };
1734
+ export { type GradingState as $, type ActivityAiPermissions as A, type AiWordFact as B, type AiWritingCorrection as C, type AiWritingFacts as D, type AiWritingFeedback as E, type AiWritingFeedbackRequest as F, type AiWritingFeedbackResult as G, type BlankConfig as H, type CriterionScore as I, type DeferredReason as J, type DeferredScoringPartial as K, type DictationData as L, type DictationEquivalence as M, type DictationLearnerResponse as N, type DictationSlowMedia as O, type DictationTolerance as P, type FillInTheBlanksData as Q, type FillInTheBlanksLearnerResponse as R, type GapSelectBank as S, type GapSelectChoice as T, type GapSelectData as U, type GapSelectGap as V, type GapSelectLearnerResponse as W, type GradeRecord as X, type Grader as Y, type GraderKind as Z, type GraderUsage as _, type ActivityData as a, type InlineCorrection as a0, type InteractionEvent as a1, type InteractionKind as a2, type ItemOutcome as a3, type LearnerResponse as a4, type LearnerResponseMap as a5, type MediaPlaybackPolicy as a6, type MediaTrack as a7, type MultipleChoiceData as a8, type MultipleChoiceLearnerResponse as a9, type XAPIObject as aA, type XAPIResult as aB, type XAPIScore as aC, type XAPIStatement as aD, type XAPIVerbObject as aE, levenshteinDistance as aF, matchText as aG, type MultipleChoiceOption as aa, type MultipleChoiceOptionMedia as ab, type NativeControlHint as ac, type ReadAloudData as ad, type ReadAloudDimension as ae, type ReadAloudDimensionWeight as af, type ReadAloudLearnerResponse as ag, type ReadAloudSlowMedia as ah, type RecordingBounds as ai, type RecordingRef as aj, type ScoringDetail as ak, type ScoringOutcome as al, type ScoringResult as am, type TextMatchPolicy as an, type TextMatchResult as ao, type ValidationError as ap, type ValidationResult as aq, type WrittenResponseData as ar, type WrittenResponseLearnerResponse as as, type WrittenResponseRubric as at, type WrittenResponseRubricCriterion as au, type XAPIActor as av, type XAPIConfig as aw, type XAPIContext as ax, type XAPIContextActivities as ay, type XAPIError as az, type ActivityDataMap as b, type ActivityFeedback as c, type ActivityMedia as d, type ActivityResult as e, type ActivityType as f, type AiActivityInput as g, type AiBlankFact as h, type AiCriterionJudgement as i, type AiDictationFacts as j, type AiExplanationRequest as k, type AiFeature as l, type AiFillInTheBlanksFacts as m, type AiGapFact as n, type AiGapSelectFacts as o, type AiGrade as p, type AiHintRequest as q, type AiItemFacts as r, type AiMultipleChoiceFacts as s, type AiOptionFact as t, type AiProvenance as u, type AiRefusal as v, type AiRubricCriterionFact as w, type AiSupportedActivityType as x, type AiTextResult as y, type AiVerdict as z };
@@ -224,7 +224,11 @@ interface GradeRecord {
224
224
  * one-word vocabulary item, say.
225
225
  */
226
226
  interface ActivityAiPermissions {
227
- /** An explanation of the learner's graded answer. */
227
+ /**
228
+ * An explanation of the learner's graded answer — and, on a written
229
+ * response, feedback on a draft: both are a model's words about the
230
+ * learner's own answer.
231
+ */
228
232
  explanations?: boolean;
229
233
  /** Hints before the learner submits. */
230
234
  hints?: boolean;
@@ -243,8 +247,8 @@ interface AiActivityInput {
243
247
  readonly redacted?: unknown;
244
248
  readonly ai?: unknown;
245
249
  }
246
- /** The two kinds of help a learner can be given. */
247
- type AiFeature = 'explanation' | 'hint';
250
+ /** The kinds of help a learner can be given. */
251
+ type AiFeature = 'explanation' | 'hint' | 'writing-feedback';
248
252
  /** The activity types the SDK builds facts for. */
249
253
  type AiSupportedActivityType = 'multiple-choice' | 'fill-in-the-blanks' | 'gap-select' | 'dictation';
250
254
  /**
@@ -404,8 +408,117 @@ interface AiTextResult {
404
408
  */
405
409
  usage?: GraderUsage;
406
410
  }
407
- /** Why a result was not shown. */
408
- type AiRefusal = 'malformed' | 'empty' | 'too-long' | 'contradicts-grade' | 'reveals-answer';
411
+ /**
412
+ * Why a result was not shown. `misquotes-answer` is writing feedback that
413
+ * corrects words the learner did not write: a correction whose quote is not in
414
+ * the draft, or not where it claims to be.
415
+ */
416
+ type AiRefusal = 'malformed' | 'empty' | 'too-long' | 'contradicts-grade' | 'reveals-answer' | 'misquotes-answer';
417
+ /** One criterion of a written response's rubric, as the author wrote it. */
418
+ interface AiRubricCriterionFact {
419
+ name: string;
420
+ description?: string;
421
+ weight: number;
422
+ }
423
+ /**
424
+ * What a model is told about a draft of a written response: the task as the
425
+ * learner saw it, the draft verbatim, and the rubric it will be graded on.
426
+ */
427
+ interface AiWritingFacts {
428
+ activityType: 'written-response';
429
+ activityId: string;
430
+ title: string;
431
+ /** The language the item is written in, when the author gave one. */
432
+ locale?: string;
433
+ prompt: string;
434
+ /** The draft, exactly as the learner wrote it. Every correction quotes it. */
435
+ text: string;
436
+ /** Recomputed with `countWords`, as the grader will count it. */
437
+ wordCount: number;
438
+ minWords: number;
439
+ maxWords: number;
440
+ withinWordBounds: boolean;
441
+ /** The rubric the answer will be graded on; `null` when the item has none. */
442
+ rubric: AiRubricCriterionFact[] | null;
443
+ /** The target language and level, when the author gave one (e.g. `"en-A2"`). */
444
+ languageTarget?: string;
445
+ }
446
+ /**
447
+ * Asks for feedback on a draft of a written response, before the learner
448
+ * submits it. Practice only.
449
+ */
450
+ interface AiWritingFeedbackRequest {
451
+ feature: 'writing-feedback';
452
+ facts: AiWritingFacts;
453
+ /** 1-based: which draft this is. */
454
+ draftNumber: number;
455
+ /** The text of the feedback already shown on earlier drafts, oldest first. */
456
+ previousFeedback: string[];
457
+ /** The language to write in: the learner's interface language, when known. */
458
+ learnerLocale?: string;
459
+ }
460
+ /**
461
+ * One correction a model proposes. `original` must be words the learner
462
+ * wrote; the SDK finds them in the draft itself, so a model need not count
463
+ * characters — leave `range` out unless you are sure of it, because a range
464
+ * that does not hold the quote is refused.
465
+ */
466
+ interface AiWritingCorrection {
467
+ original: string;
468
+ corrected: string;
469
+ explanation?: string;
470
+ /** An error-type tag, e.g. `"tense"`, `"article"`. */
471
+ category?: string;
472
+ range?: {
473
+ start: number;
474
+ end: number;
475
+ };
476
+ }
477
+ /**
478
+ * A model's judgement of one rubric criterion. The SDK attaches the author's
479
+ * weight; a weight sent here is ignored.
480
+ */
481
+ type AiCriterionJudgement = Omit<CriterionScore, 'weight'> & {
482
+ weight?: number;
483
+ };
484
+ /**
485
+ * What a host's `writingFeedback` port returns: the overall feedback as plain
486
+ * text, and optionally corrections and a judgement per rubric criterion.
487
+ */
488
+ interface AiWritingFeedbackResult {
489
+ /** The overall feedback, addressed to the learner. Plain text, never HTML. */
490
+ text: string;
491
+ corrections?: AiWritingCorrection[];
492
+ criteria?: AiCriterionJudgement[];
493
+ provenance?: AiProvenance;
494
+ usage?: GraderUsage;
495
+ }
496
+ /**
497
+ * Feedback on a draft, as the SDK accepted it: what a learner is shown.
498
+ */
499
+ interface AiWritingFeedback {
500
+ text: string;
501
+ /**
502
+ * Every correction, anchored in the draft: `range` is where the SDK found
503
+ * the quote, and `original` is the draft's own text at that range.
504
+ */
505
+ corrections: InlineCorrection[];
506
+ /** The model's judgements, with the rubric's weights. */
507
+ criteria: CriterionScore[];
508
+ /**
509
+ * The rubric's weighted total of the judgements, from 0 to 1, computed by
510
+ * the SDK as `gradeFromRubric` computes a grade — when every criterion of
511
+ * the rubric was judged; `null` otherwise, and on an item with no rubric.
512
+ *
513
+ * **An indication, never a grade:** it is a model's reading of a draft the
514
+ * learner has not submitted, and nothing in the SDK scores an answer with
515
+ * it. `ai-writing-feedback-shown` reports it, so a record can say what the
516
+ * learner was shown.
517
+ */
518
+ indicativeScore: number | null;
519
+ provenance?: AiProvenance;
520
+ usage?: GraderUsage;
521
+ }
409
522
 
410
523
  /**
411
524
  * An xAPI Activity object — the thing a statement is about.
@@ -1253,12 +1366,28 @@ interface MultipleChoiceLearnerResponse {
1253
1366
  type: 'multiple-choice';
1254
1367
  /** IDs of the options the learner selected. */
1255
1368
  selectedOptionIds: string[];
1369
+ /**
1370
+ * Hints the learner was shown before submitting this answer, counted from
1371
+ * the start of the question — the author's and AI hints alike. Written by
1372
+ * the SDK's components in `practice`; absent or 0 means none. A scoring
1373
+ * policy's `hintPenalty` charges for them. Client-reported, like any count
1374
+ * a browser makes.
1375
+ */
1376
+ hintsRevealed?: number;
1256
1377
  }
1257
1378
  /** Learner response for a Fill-in-the-Blanks activity. */
1258
1379
  interface FillInTheBlanksLearnerResponse {
1259
1380
  type: 'fill-in-the-blanks';
1260
1381
  /** Map of blank ID to the learner's typed answer. */
1261
1382
  answers: Record<string, string>;
1383
+ /**
1384
+ * Hints the learner was shown before submitting this answer, counted from
1385
+ * the start of the question — the author's and AI hints alike. Written by
1386
+ * the SDK's components in `practice`; absent or 0 means none. A scoring
1387
+ * policy's `hintPenalty` charges for them. Client-reported, like any count
1388
+ * a browser makes.
1389
+ */
1390
+ hintsRevealed?: number;
1262
1391
  }
1263
1392
  /**
1264
1393
  * Learner response for a Gap Select activity.
@@ -1273,6 +1402,14 @@ interface GapSelectLearnerResponse {
1273
1402
  type: 'gap-select';
1274
1403
  /** Map of gap ID to the ID of the choice the learner selected. */
1275
1404
  selections: Record<string, string>;
1405
+ /**
1406
+ * Hints the learner was shown before submitting this answer, counted from
1407
+ * the start of the question — the author's and AI hints alike. Written by
1408
+ * the SDK's components in `practice`; absent or 0 means none. A scoring
1409
+ * policy's `hintPenalty` charges for them. Client-reported, like any count
1410
+ * a browser makes.
1411
+ */
1412
+ hintsRevealed?: number;
1276
1413
  }
1277
1414
  /** Learner response for a Written Response activity. */
1278
1415
  interface WrittenResponseLearnerResponse {
@@ -1286,15 +1423,18 @@ interface WrittenResponseLearnerResponse {
1286
1423
  * Learner response for a Dictation activity.
1287
1424
  *
1288
1425
  * `text` is exactly what the learner typed; every normalisation happens in the
1289
- * scorer. `hintsRevealed` is client-reported telemetry — how many hint words
1290
- * were shown before submitting — that the scorer ignores and a consumer may
1291
- * log or penalise, knowing it cannot be verified.
1426
+ * scorer. `hintsRevealed` is client-reported — how many hint words were shown
1427
+ * before submitting. The scorer ignores it; a scoring policy's `hintPenalty`
1428
+ * charges for it, knowing it cannot be verified.
1292
1429
  */
1293
1430
  interface DictationLearnerResponse {
1294
1431
  type: 'dictation';
1295
1432
  /** The learner's text, as typed. */
1296
1433
  text: string;
1297
- /** Hint words revealed before submitting (practice only). Absent or 0 means none. */
1434
+ /**
1435
+ * Hint words shown before submitting (practice only), counted from the start
1436
+ * of the question. Absent or 0 means none.
1437
+ */
1298
1438
  hintsRevealed?: number;
1299
1439
  }
1300
1440
  /**
@@ -1572,7 +1712,13 @@ type InteractionKind = 'option-selected' | 'option-deselected' | 'blank-filled'
1572
1712
  * watches its own model in production, where the development console warning
1573
1713
  * it accompanies is silent.
1574
1714
  */
1575
- | 'ai-help-refused' | (string & {});
1715
+ | 'ai-help-refused'
1716
+ /**
1717
+ * Feedback on a draft of a written response was shown (`draftNumber`, the
1718
+ * number of `corrections`, the `indicativeScore` when there was one, and the
1719
+ * port's `provenance` and `usage` when it sent them). Never the text.
1720
+ */
1721
+ | 'ai-writing-feedback-shown' | (string & {});
1576
1722
  /** Fired by activity components on every discrete learner interaction. */
1577
1723
  interface InteractionEvent {
1578
1724
  /** The kind of interaction that occurred. */
@@ -1585,4 +1731,4 @@ interface InteractionEvent {
1585
1731
  payload: Record<string, unknown>;
1586
1732
  }
1587
1733
 
1588
- export { type MediaPlaybackPolicy as $, type ActivityAiPermissions as A, type BlankConfig as B, type CriterionScore as C, type DeferredReason as D, type DictationData as E, type DictationEquivalence as F, type DictationLearnerResponse as G, type DictationSlowMedia as H, type DictationTolerance as I, type FillInTheBlanksData as J, type FillInTheBlanksLearnerResponse as K, type GapSelectBank as L, type GapSelectChoice as M, type GapSelectData as N, type GapSelectGap as O, type GapSelectLearnerResponse as P, type GradeRecord as Q, type Grader as R, type GraderKind as S, type GraderUsage as T, type GradingState as U, type InlineCorrection as V, type InteractionEvent as W, type InteractionKind as X, type ItemOutcome as Y, type LearnerResponse as Z, type LearnerResponseMap as _, type ActivityData as a, type MediaTrack as a0, type MultipleChoiceData as a1, type MultipleChoiceLearnerResponse as a2, type MultipleChoiceOption as a3, type MultipleChoiceOptionMedia as a4, type NativeControlHint as a5, type ReadAloudData as a6, type ReadAloudDimension as a7, type ReadAloudDimensionWeight as a8, type ReadAloudLearnerResponse as a9, type ReadAloudSlowMedia as aa, type RecordingBounds as ab, type RecordingRef as ac, type ScoringDetail as ad, type ScoringOutcome as ae, type ScoringResult as af, type TextMatchPolicy as ag, type TextMatchResult as ah, type ValidationError as ai, type ValidationResult as aj, type WrittenResponseData as ak, type WrittenResponseLearnerResponse as al, type WrittenResponseRubric as am, type WrittenResponseRubricCriterion as an, type XAPIActor as ao, type XAPIConfig as ap, type XAPIContext as aq, type XAPIContextActivities as ar, type XAPIError as as, type XAPIObject as at, type XAPIResult as au, type XAPIScore as av, type XAPIStatement as aw, type XAPIVerbObject as ax, levenshteinDistance as ay, matchText as az, type ActivityDataMap as b, type ActivityFeedback as c, type ActivityMedia as d, type ActivityResult as e, type ActivityType as f, type AiActivityInput as g, type AiBlankFact as h, type AiDictationFacts as i, type AiExplanationRequest as j, type AiFeature as k, type AiFillInTheBlanksFacts as l, type AiGapFact as m, type AiGapSelectFacts as n, type AiGrade as o, type AiHintRequest as p, type AiItemFacts as q, type AiMultipleChoiceFacts as r, type AiOptionFact as s, type AiProvenance as t, type AiRefusal as u, type AiSupportedActivityType as v, type AiTextResult as w, type AiVerdict as x, type AiWordFact as y, type DeferredScoringPartial as z };
1734
+ export { type GradingState as $, type ActivityAiPermissions as A, type AiWordFact as B, type AiWritingCorrection as C, type AiWritingFacts as D, type AiWritingFeedback as E, type AiWritingFeedbackRequest as F, type AiWritingFeedbackResult as G, type BlankConfig as H, type CriterionScore as I, type DeferredReason as J, type DeferredScoringPartial as K, type DictationData as L, type DictationEquivalence as M, type DictationLearnerResponse as N, type DictationSlowMedia as O, type DictationTolerance as P, type FillInTheBlanksData as Q, type FillInTheBlanksLearnerResponse as R, type GapSelectBank as S, type GapSelectChoice as T, type GapSelectData as U, type GapSelectGap as V, type GapSelectLearnerResponse as W, type GradeRecord as X, type Grader as Y, type GraderKind as Z, type GraderUsage as _, type ActivityData as a, type InlineCorrection as a0, type InteractionEvent as a1, type InteractionKind as a2, type ItemOutcome as a3, type LearnerResponse as a4, type LearnerResponseMap as a5, type MediaPlaybackPolicy as a6, type MediaTrack as a7, type MultipleChoiceData as a8, type MultipleChoiceLearnerResponse as a9, type XAPIObject as aA, type XAPIResult as aB, type XAPIScore as aC, type XAPIStatement as aD, type XAPIVerbObject as aE, levenshteinDistance as aF, matchText as aG, type MultipleChoiceOption as aa, type MultipleChoiceOptionMedia as ab, type NativeControlHint as ac, type ReadAloudData as ad, type ReadAloudDimension as ae, type ReadAloudDimensionWeight as af, type ReadAloudLearnerResponse as ag, type ReadAloudSlowMedia as ah, type RecordingBounds as ai, type RecordingRef as aj, type ScoringDetail as ak, type ScoringOutcome as al, type ScoringResult as am, type TextMatchPolicy as an, type TextMatchResult as ao, type ValidationError as ap, type ValidationResult as aq, type WrittenResponseData as ar, type WrittenResponseLearnerResponse as as, type WrittenResponseRubric as at, type WrittenResponseRubricCriterion as au, type XAPIActor as av, type XAPIConfig as aw, type XAPIContext as ax, type XAPIContextActivities as ay, type XAPIError as az, type ActivityDataMap as b, type ActivityFeedback as c, type ActivityMedia as d, type ActivityResult as e, type ActivityType as f, type AiActivityInput as g, type AiBlankFact as h, type AiCriterionJudgement as i, type AiDictationFacts as j, type AiExplanationRequest as k, type AiFeature as l, type AiFillInTheBlanksFacts as m, type AiGapFact as n, type AiGapSelectFacts as o, type AiGrade as p, type AiHintRequest as q, type AiItemFacts as r, type AiMultipleChoiceFacts as s, type AiOptionFact as t, type AiProvenance as u, type AiRefusal as v, type AiRubricCriterionFact as w, type AiSupportedActivityType as x, type AiTextResult as y, type AiVerdict as z };