@intellectif/lk-core 0.13.0 → 0.14.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.
- package/CHANGELOG.md +35 -0
- package/README.md +1 -1
- package/dist/{activity-DfAmJ1sl.d.cts → activity-BVkg9VDo.d.cts} +153 -4
- package/dist/{activity-DfAmJ1sl.d.ts → activity-BVkg9VDo.d.ts} +153 -4
- package/dist/{chunk-DQKJUMUW.cjs → chunk-2NIALQEH.cjs} +24 -19
- package/dist/chunk-2NIALQEH.cjs.map +1 -0
- package/dist/{chunk-XKKKWKRH.js → chunk-6RND47QE.js} +694 -126
- package/dist/chunk-6RND47QE.js.map +1 -0
- package/dist/{chunk-FT7SAC3X.cjs → chunk-CZRUMHKW.cjs} +4 -4
- package/dist/{chunk-FT7SAC3X.cjs.map → chunk-CZRUMHKW.cjs.map} +1 -1
- package/dist/chunk-J5OIRIOW.cjs +1034 -0
- package/dist/chunk-J5OIRIOW.cjs.map +1 -0
- package/dist/{chunk-Y36S3X3L.js → chunk-NBADQ3JU.js} +7 -2
- package/dist/chunk-NBADQ3JU.js.map +1 -0
- package/dist/{chunk-PYP3HCRQ.js → chunk-R7PV3XIC.js} +2 -2
- package/dist/chunk-RTIR6R2U.js +1034 -0
- package/dist/chunk-RTIR6R2U.js.map +1 -0
- package/dist/{chunk-XONKWL6D.cjs → chunk-VCXIPLEW.cjs} +585 -17
- package/dist/chunk-VCXIPLEW.cjs.map +1 -0
- package/dist/{index-Bz9wUigA.d.cts → index-BE1bM2Wi.d.cts} +225 -3
- package/dist/{index-CJbbVE9o.d.ts → index-v2_-Fyn3.d.ts} +225 -3
- package/dist/index.cjs +113 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -17
- package/dist/index.d.ts +50 -17
- package/dist/index.js +39 -6
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +11 -3
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +10 -2
- package/dist/scoring.cjs +23 -3
- package/dist/scoring.cjs.map +1 -1
- package/dist/scoring.d.cts +471 -16
- package/dist/scoring.d.ts +471 -16
- package/dist/scoring.js +24 -4
- package/dist/xapi.cjs +3 -3
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +2 -2
- package/package.json +1 -1
- package/vectors/README.md +63 -3
- package/vectors/replay.d.mts +7 -2
- package/vectors/replay.mjs +83 -1
- package/vectors/scoring.json +10875 -2
- package/dist/chunk-DQKJUMUW.cjs.map +0 -1
- package/dist/chunk-FGN4LLSP.js +0 -387
- package/dist/chunk-FGN4LLSP.js.map +0 -1
- package/dist/chunk-UHQVIC3P.cjs +0 -387
- package/dist/chunk-UHQVIC3P.cjs.map +0 -1
- package/dist/chunk-XKKKWKRH.js.map +0 -1
- package/dist/chunk-XONKWL6D.cjs.map +0 -1
- package/dist/chunk-Y36S3X3L.js.map +0 -1
- /package/dist/{chunk-PYP3HCRQ.js.map → chunk-R7PV3XIC.js.map} +0 -0
package/vectors/README.md
CHANGED
|
@@ -70,7 +70,31 @@ a known hazard carry a `note` saying why they exist.
|
|
|
70
70
|
edges, negative zero, half-even ties and near-ties, and bands that share a
|
|
71
71
|
minimum.
|
|
72
72
|
- **`countWords`**, **`seededShuffle`** in both versions, **`gradeFromRubric`**
|
|
73
|
-
including its unscorable paths,
|
|
73
|
+
including its unscorable paths, its opt-in `rounding` option on each of the
|
|
74
|
+
three ways a pass is decided, and **`outcomeFromGrade`**.
|
|
75
|
+
- **`gradeReadAloud`**: every reason a pronunciation assessment is refused
|
|
76
|
+
rather than scored — each `unscorable` code — the weighted total over the
|
|
77
|
+
authored dimensions, a blank take, a dimension scored `0` against one that was
|
|
78
|
+
not scored at all, the pass line with and without rounding, and the per-word
|
|
79
|
+
marks in `details`, which are for review and never feed the score.
|
|
80
|
+
- **`alignReadAloud`**, which decides the reference word each mark lands on:
|
|
81
|
+
omissions, insertions and mispronunciations, a repeated word, an assessor word
|
|
82
|
+
that normalises to no token or to two, punctuation that merges two words into
|
|
83
|
+
one, and both `miscue` settings. **`validateSpeechAssessment`** with the paths
|
|
84
|
+
and codes an adapter branches on — including the two totals that bound the
|
|
85
|
+
text of an assessment, at the bound and one character past it — and
|
|
86
|
+
**`outcomeFromUnscorable`**, which keeps a refusal's code in the stored
|
|
87
|
+
outcome.
|
|
88
|
+
- **`validateActivity`**, for the one rule that decides whether an item can be
|
|
89
|
+
stored at all rather than how an answer scores: a read-aloud `referenceText`
|
|
90
|
+
that survives normalisation as nothing. An item that marks no word grades a
|
|
91
|
+
silent take at 0 with no omission recorded and a perfect reading as a run of
|
|
92
|
+
insertions, so loosening that refusal would move grades.
|
|
93
|
+
- **`inspectWav`** and the **`READ_ALOUD_*`** and `SPEECH_ASSESSMENT_MAX_WORDS`
|
|
94
|
+
limits: what a recording's duration, peak level and voiced time are measured
|
|
95
|
+
as, across silence, a tone, two channels, two sample rates, a partial last
|
|
96
|
+
window, an odd chunk's pad byte and WAVE_FORMAT_EXTENSIBLE, and which files
|
|
97
|
+
are reported unread instead of guessed at.
|
|
74
98
|
- **`scoredItemsFromPlan`**, which decides what a stored paper's denominator is
|
|
75
99
|
when an outcome is missing, and **`composeAssessmentScore`** across weights,
|
|
76
100
|
points, provisional and unscorable items, and section thresholds.
|
|
@@ -101,5 +125,41 @@ tagged: `{ "$number": "NaN" | "Infinity" | "-Infinity" | "-0" }` and
|
|
|
101
125
|
`\uXXXX` escape, so a no-break space can never pass for an ordinary one.
|
|
102
126
|
Comparison ignores object key order.
|
|
103
127
|
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
A recording is bytes, which JSON cannot express either, so a `Uint8Array` is
|
|
129
|
+
tagged `{ "$bytes": "<base64>" }` and comes back from `decode` as a fresh one.
|
|
130
|
+
Any other view of an `ArrayBuffer` is refused rather than tagged: JSON would
|
|
131
|
+
store it as `{"0":82,"1":73}`, which replays as an ordinary object, and the call
|
|
132
|
+
under test would be handed something that is not a byte array at all. The base64
|
|
133
|
+
is computed in `replay.mjs` itself, so the file needs no `Buffer` and still runs
|
|
134
|
+
wherever your tests run.
|
|
135
|
+
|
|
136
|
+
`corpusVersion` changes only if this encoding does — it is **2** since the
|
|
137
|
+
`$bytes` tag was added, and no expectation changed with it. Adding vectors does
|
|
138
|
+
not change it, so always load the `replay.mjs` that sits beside the corpus: an
|
|
139
|
+
older one throws rather than replaying a corpus it cannot read.
|
|
140
|
+
|
|
141
|
+
## `frozenFrom`
|
|
142
|
+
|
|
143
|
+
`frozenFrom` is **the version of `@intellectif/lk-core` the corpus was last
|
|
144
|
+
regenerated at** — the package version in `package.json` when someone ran
|
|
145
|
+
`node scripts/generate-vectors.mjs`. That is all it is.
|
|
146
|
+
|
|
147
|
+
**It is not the version this corpus ships in, and it is not expected to match
|
|
148
|
+
it.** `changeset version` bumps the package without regenerating the corpus, so
|
|
149
|
+
a release almost always publishes a corpus whose `frozenFrom` names the previous
|
|
150
|
+
version. That is correct and wanted: the string records when these expectations
|
|
151
|
+
were last recomputed, and a release that changes no expectation should not
|
|
152
|
+
rewrite them.
|
|
153
|
+
|
|
154
|
+
It is deliberately not asserted anywhere. `replay()` never reads it — it walks
|
|
155
|
+
`vectors` alone, comparing each frozen call with what your build returns — and
|
|
156
|
+
the generator's own grade-change check compares only each vector's call and
|
|
157
|
+
expectation, so a `frozenFrom` one version behind can never fail a grade gate.
|
|
158
|
+
(`corpusVersion` is different: `replay()` does check that one, and throws when
|
|
159
|
+
it does not recognise the encoding.) Read `frozenFrom` as a date stamp, not as a
|
|
160
|
+
package label. The version that matters for "which build produced this grade" is
|
|
161
|
+
the one you installed; record that yourself beside any grade you store.
|
|
162
|
+
|
|
163
|
+
Adding a regenerate-after-version step to the release was considered and
|
|
164
|
+
rejected: it would put a step that can fail *after* the version bump, which is
|
|
165
|
+
strictly worse than a string that reads one version behind.
|
package/vectors/replay.d.mts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/** Types for `replay.mjs`. See that file for the contract and the encoding. */
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The encoding version this replayer reads. Version 2 added the `$bytes` tag:
|
|
5
|
+
* a `Uint8Array` is stored as `{ "$bytes": "<base64>" }` and decoded back into
|
|
6
|
+
* a fresh one, and any other view of an `ArrayBuffer` is refused rather than
|
|
7
|
+
* frozen as an object of indices.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CORPUS_VERSION: 2;
|
|
5
10
|
|
|
6
11
|
/** One frozen call. Exactly one of `fn` / `const` is present. */
|
|
7
12
|
export interface ScoringVector {
|
package/vectors/replay.mjs
CHANGED
|
@@ -35,10 +35,83 @@
|
|
|
35
35
|
* deliberately returns `0`, not `-0`, for a negative value that rounds to
|
|
36
36
|
* zero, and JSON would erase the difference. A call that throws is recorded as
|
|
37
37
|
* `{ "$throws": "<error.name>" }`.
|
|
38
|
+
*
|
|
39
|
+
* A recording is bytes, which JSON cannot express either, so a `Uint8Array` is
|
|
40
|
+
* tagged `{ "$bytes": "<base64>" }` and comes back as a fresh one. Any other
|
|
41
|
+
* view of an `ArrayBuffer` is refused rather than tagged: JSON would store it
|
|
42
|
+
* as `{"0":82,"1":73}`, which replays as an ordinary object, and a function
|
|
43
|
+
* that reads bytes would be handed something that is not a byte array at all —
|
|
44
|
+
* silently, and with a frozen expectation to match. The base64 is computed
|
|
45
|
+
* here, in plain JavaScript: `Buffer` is Node's alone, and this file runs
|
|
46
|
+
* wherever a consumer's tests run.
|
|
38
47
|
*/
|
|
39
48
|
|
|
40
49
|
/** Bump only when the ENCODING changes. Adding vectors does not change it. */
|
|
41
|
-
export const CORPUS_VERSION =
|
|
50
|
+
export const CORPUS_VERSION = 2;
|
|
51
|
+
|
|
52
|
+
/** The base64 alphabet, RFC 4648 section 4, padded with `=`. */
|
|
53
|
+
const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
54
|
+
|
|
55
|
+
/** Base64 of `bytes`, three bytes at a time, the last group padded. */
|
|
56
|
+
function toBase64(bytes) {
|
|
57
|
+
let text = '';
|
|
58
|
+
for (let at = 0; at < bytes.length; at += 3) {
|
|
59
|
+
const first = bytes[at];
|
|
60
|
+
const second = bytes[at + 1];
|
|
61
|
+
const third = bytes[at + 2];
|
|
62
|
+
text += BASE64[first >> 2];
|
|
63
|
+
text += BASE64[((first & 0x03) << 4) | ((second ?? 0) >> 4)];
|
|
64
|
+
text += second === undefined ? '=' : BASE64[((second & 0x0f) << 2) | ((third ?? 0) >> 6)];
|
|
65
|
+
text += third === undefined ? '=' : BASE64[third & 0x3f];
|
|
66
|
+
}
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The bytes of a base64 string, as a fresh `Uint8Array`. */
|
|
71
|
+
function fromBase64(text) {
|
|
72
|
+
// Every refusal below is a plain `Error`, which no vector expects: a corpus
|
|
73
|
+
// whose bytes cannot be read must fail the gate, and a `TypeError` here would
|
|
74
|
+
// be indistinguishable from the one `inspectWav` throws for bytes that are not
|
|
75
|
+
// a `Uint8Array` — which seven vectors do expect.
|
|
76
|
+
if (typeof text !== 'string') {
|
|
77
|
+
// A tag that carries no string carried no bytes either. Substituting an
|
|
78
|
+
// empty array would replay green against every vector that expects a
|
|
79
|
+
// refusal, because an empty recording is refused too, with the bytes the
|
|
80
|
+
// vector froze gone.
|
|
81
|
+
throw new Error(
|
|
82
|
+
`A $bytes tag carries a base64 string, and this one is of type ${typeof text}.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const body = text.replace(/=+$/, '');
|
|
86
|
+
// Base64 spends four characters on three bytes, so a trailing group of one
|
|
87
|
+
// character encodes nothing: the string was cut short, and the bytes before
|
|
88
|
+
// the cut are not the bytes anybody froze.
|
|
89
|
+
if (body.length % 4 === 1) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Invalid base64 in a $bytes tag: a body of ${body.length} characters ends in a group of one, which encodes no byte.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const bytes = new Uint8Array((body.length * 3) >> 2);
|
|
95
|
+
let held = 0;
|
|
96
|
+
let bits = 0;
|
|
97
|
+
let at = 0;
|
|
98
|
+
for (const character of body) {
|
|
99
|
+
const value = BASE64.indexOf(character);
|
|
100
|
+
if (value === -1) {
|
|
101
|
+
// A hand-edited corpus, or a file mangled in transit. Decoding it as far
|
|
102
|
+
// as it goes would hand the call under test bytes nobody froze.
|
|
103
|
+
throw new Error(`Invalid base64 in a $bytes tag: ${JSON.stringify(character)}.`);
|
|
104
|
+
}
|
|
105
|
+
held = (held << 6) | value;
|
|
106
|
+
bits += 6;
|
|
107
|
+
if (bits >= 8) {
|
|
108
|
+
bits -= 8;
|
|
109
|
+
bytes[at] = (held >> bits) & 0xff;
|
|
110
|
+
at += 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return bytes;
|
|
114
|
+
}
|
|
42
115
|
|
|
43
116
|
/** Turns a runtime value into the corpus's JSON-safe form. */
|
|
44
117
|
export function encode(value) {
|
|
@@ -50,6 +123,14 @@ export function encode(value) {
|
|
|
50
123
|
return value;
|
|
51
124
|
}
|
|
52
125
|
if (value === undefined) return { $undefined: true };
|
|
126
|
+
if (ArrayBuffer.isView(value)) {
|
|
127
|
+
if (value instanceof Uint8Array) return { $bytes: toBase64(value) };
|
|
128
|
+
// A DataView or a wider typed array has no agreed byte order in JSON, and
|
|
129
|
+
// the object branch below would freeze a `Float32Array` as its indices.
|
|
130
|
+
throw new TypeError(
|
|
131
|
+
'A vector can carry bytes as a Uint8Array only; convert any other view of an ArrayBuffer first.',
|
|
132
|
+
);
|
|
133
|
+
}
|
|
53
134
|
if (Array.isArray(value)) return value.map(encode);
|
|
54
135
|
if (value !== null && typeof value === 'object') {
|
|
55
136
|
const out = {};
|
|
@@ -77,6 +158,7 @@ export function decode(value) {
|
|
|
77
158
|
}[value.$number];
|
|
78
159
|
}
|
|
79
160
|
if (keys.length === 1 && keys[0] === '$undefined') return undefined;
|
|
161
|
+
if (keys.length === 1 && keys[0] === '$bytes') return fromBase64(value.$bytes);
|
|
80
162
|
const out = {};
|
|
81
163
|
for (const [key, entry] of Object.entries(value)) out[key] = decode(entry);
|
|
82
164
|
return out;
|