tessera-learn 0.2.1 → 0.2.3
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/AGENTS.md +280 -916
- package/README.md +3 -3
- package/dist/{audit-BNrvFHq_.js → audit--fSWIOgK.js} +156 -33
- package/dist/{audit-BNrvFHq_.js.map → audit--fSWIOgK.js.map} +1 -1
- package/dist/{build-commands-BWnATKat.js → build-commands-Qyrlsp3n.js} +2 -2
- package/dist/{build-commands-BWnATKat.js.map → build-commands-Qyrlsp3n.js.map} +1 -1
- package/dist/{inline-config-Dudu5r8w.js → inline-config-DqAKsCNl.js} +2 -2
- package/dist/{inline-config-Dudu5r8w.js.map → inline-config-DqAKsCNl.js.map} +1 -1
- package/dist/plugin/cli.d.ts.map +1 -1
- package/dist/plugin/cli.js +33 -18
- package/dist/plugin/cli.js.map +1 -1
- package/dist/plugin/index.d.ts +0 -2
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2 -2
- package/dist/{plugin-diNZaDJK.js → plugin-B-aiL9-V.js} +2 -2
- package/dist/{plugin-diNZaDJK.js.map → plugin-B-aiL9-V.js.map} +1 -1
- package/package.json +11 -8
- package/src/components/FillInTheBlank.svelte +3 -27
- package/src/components/Matching.svelte +4 -26
- package/src/components/MultipleChoice.svelte +8 -27
- package/src/components/QuestionShell.svelte +35 -0
- package/src/components/Sorting.svelte +4 -26
- package/src/plugin/a11y/audit.ts +239 -39
- package/src/plugin/a11y-cli.ts +1 -4
- package/src/plugin/cli.ts +2 -3
- package/src/plugin/course-root.ts +37 -9
- package/src/plugin/validate-cli.ts +10 -4
- package/src/runtime/adapters/cmi5.ts +5 -14
- package/src/runtime/adapters/index.ts +41 -38
- package/src/runtime/adapters/scorm12.ts +1 -1
package/src/plugin/a11y-cli.ts
CHANGED
|
@@ -13,7 +13,6 @@ export type ParsedA11yArgs =
|
|
|
13
13
|
|
|
14
14
|
export function parseA11yArgs(argv: string[]): ParsedA11yArgs {
|
|
15
15
|
let threshold: ImpactLevel | undefined;
|
|
16
|
-
let rebuild = false;
|
|
17
16
|
|
|
18
17
|
for (let i = 0; i < argv.length; i++) {
|
|
19
18
|
const arg = argv[i];
|
|
@@ -26,14 +25,12 @@ export function parseA11yArgs(argv: string[]): ParsedA11yArgs {
|
|
|
26
25
|
};
|
|
27
26
|
}
|
|
28
27
|
threshold = value;
|
|
29
|
-
} else if (arg === '--build') {
|
|
30
|
-
rebuild = true;
|
|
31
28
|
} else {
|
|
32
29
|
return { ok: false, error: `Unknown argument: ${arg}` };
|
|
33
30
|
}
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
const args: AuditOptions = {
|
|
33
|
+
const args: AuditOptions = {};
|
|
37
34
|
if (threshold !== undefined) args.threshold = threshold;
|
|
38
35
|
return { ok: true, args };
|
|
39
36
|
}
|
package/src/plugin/cli.ts
CHANGED
|
@@ -19,8 +19,7 @@ Commands:
|
|
|
19
19
|
Run a command from inside a course folder, or name the course explicitly.
|
|
20
20
|
|
|
21
21
|
a11y/check options:
|
|
22
|
-
--threshold <minor|moderate|serious|critical> Failing impact (default: serious)
|
|
23
|
-
--build Force a fresh build first`;
|
|
22
|
+
--threshold <minor|moderate|serious|critical> Failing impact (default: serious)`;
|
|
24
23
|
|
|
25
24
|
// The course is a leading positional: `tessera <cmd> [course] [flags]`. Only the
|
|
26
25
|
// first token can be the course, and only when it isn't a flag — otherwise a flag
|
|
@@ -74,7 +73,7 @@ export async function main(
|
|
|
74
73
|
case 'validate':
|
|
75
74
|
return runValidate(courseRoot);
|
|
76
75
|
case 'check': {
|
|
77
|
-
const validateCode = runValidate(courseRoot);
|
|
76
|
+
const validateCode = runValidate(courseRoot, { showA11yTip: false });
|
|
78
77
|
if (validateCode !== 0) return validateCode;
|
|
79
78
|
return runA11y(courseRoot, workspaceRoot, flags);
|
|
80
79
|
}
|
|
@@ -32,29 +32,57 @@ export function findWorkspaceRoot(cwd: string): string | null {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
function scanCourses(workspaceRoot: string): {
|
|
36
|
+
courses: string[];
|
|
37
|
+
malformed: string[];
|
|
38
|
+
} {
|
|
36
39
|
const coursesDir = join(workspaceRoot, 'courses');
|
|
40
|
+
const courses: string[] = [];
|
|
41
|
+
const malformed: string[] = [];
|
|
37
42
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.
|
|
43
|
+
for (const e of readdirSync(coursesDir, { withFileTypes: true })) {
|
|
44
|
+
if (!e.isDirectory()) continue;
|
|
45
|
+
const dir = join(coursesDir, e.name);
|
|
46
|
+
if (isCourse(dir)) courses.push(e.name);
|
|
47
|
+
else if (isDir(join(dir, 'pages'))) malformed.push(e.name);
|
|
48
|
+
}
|
|
42
49
|
} catch {
|
|
43
|
-
return
|
|
50
|
+
return { courses, malformed };
|
|
44
51
|
}
|
|
52
|
+
return { courses: courses.sort(), malformed: malformed.sort() };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function listCourses(workspaceRoot: string): string[] {
|
|
56
|
+
return scanCourses(workspaceRoot).courses;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function listMalformedCourses(workspaceRoot: string): string[] {
|
|
60
|
+
return scanCourses(workspaceRoot).malformed;
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
const NOT_A_WORKSPACE =
|
|
48
64
|
'Not inside a Tessera workspace — no `courses/` directory was found at or above the current directory.';
|
|
49
65
|
|
|
66
|
+
function malformedHint(malformed: string[]): string {
|
|
67
|
+
if (malformed.length === 0) return '';
|
|
68
|
+
return (
|
|
69
|
+
`\nSkipped (missing course.config.js):\n` +
|
|
70
|
+
malformed.map((c) => ` courses/${c}`).join('\n')
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
50
74
|
function listHint(workspaceRoot: string): string {
|
|
51
|
-
const courses =
|
|
75
|
+
const { courses, malformed } = scanCourses(workspaceRoot);
|
|
52
76
|
if (courses.length === 0) {
|
|
53
|
-
return
|
|
77
|
+
return (
|
|
78
|
+
'\nNo courses found. Create one with `tessera new <name>`.' +
|
|
79
|
+
malformedHint(malformed)
|
|
80
|
+
);
|
|
54
81
|
}
|
|
55
82
|
return (
|
|
56
83
|
`\nAvailable courses:\n${courses.map((c) => ` ${c}`).join('\n')}` +
|
|
57
|
-
'\nName one (`tessera <command> <course>`) or cd into its folder.'
|
|
84
|
+
'\nName one (`tessera <command> <course>`) or cd into its folder.' +
|
|
85
|
+
malformedHint(malformed)
|
|
58
86
|
);
|
|
59
87
|
}
|
|
60
88
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
1
2
|
import { validateProject, reportValidationIssues } from './validation.js';
|
|
2
3
|
|
|
3
|
-
export function runValidate(
|
|
4
|
+
export function runValidate(
|
|
5
|
+
projectRoot: string,
|
|
6
|
+
{ showA11yTip = true }: { showA11yTip?: boolean } = {},
|
|
7
|
+
): number {
|
|
4
8
|
const { errors, warnings } = validateProject(projectRoot);
|
|
5
9
|
|
|
6
10
|
reportValidationIssues({ errors, warnings });
|
|
@@ -23,8 +27,10 @@ export function runValidate(projectRoot: string): number {
|
|
|
23
27
|
'\x1b[32m[tessera]\x1b[0m Validation passed — no issues found.',
|
|
24
28
|
);
|
|
25
29
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
if (showA11yTip) {
|
|
31
|
+
console.log(
|
|
32
|
+
`\x1b[2m[tessera] Static checks only. For a full runtime accessibility audit, run: pnpm a11y ${basename(projectRoot)}\x1b[0m`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
29
35
|
return 0;
|
|
30
36
|
}
|
|
@@ -579,10 +579,7 @@ export class CMI5Adapter implements PersistenceAdapter {
|
|
|
579
579
|
opts: { moveOn?: boolean; mastery?: boolean } = {},
|
|
580
580
|
): Record<string, unknown> {
|
|
581
581
|
const tmpl = this.#launchData?.contextTemplate ?? {};
|
|
582
|
-
const tmplActivities =
|
|
583
|
-
string,
|
|
584
|
-
unknown
|
|
585
|
-
>;
|
|
582
|
+
const tmplActivities = tmpl.contextActivities ?? {};
|
|
586
583
|
|
|
587
584
|
// Concat-dedupe category to preserve any template-supplied entries
|
|
588
585
|
// (§10.2.1 forbids overwriting them).
|
|
@@ -594,14 +591,8 @@ export class CMI5Adapter implements PersistenceAdapter {
|
|
|
594
591
|
category.push({ id, objectType: 'Activity' });
|
|
595
592
|
}
|
|
596
593
|
};
|
|
597
|
-
const templateCategory = Array.isArray(
|
|
598
|
-
|
|
599
|
-
)
|
|
600
|
-
? (
|
|
601
|
-
tmplActivities as {
|
|
602
|
-
category: Array<{ id: string; objectType?: string }>;
|
|
603
|
-
}
|
|
604
|
-
).category
|
|
594
|
+
const templateCategory = Array.isArray(tmplActivities.category)
|
|
595
|
+
? tmplActivities.category
|
|
605
596
|
: [];
|
|
606
597
|
for (const c of templateCategory) {
|
|
607
598
|
if (c && typeof c.id === 'string') push(c.id);
|
|
@@ -615,13 +606,13 @@ export class CMI5Adapter implements PersistenceAdapter {
|
|
|
615
606
|
};
|
|
616
607
|
|
|
617
608
|
const ctx: Record<string, unknown> = {
|
|
618
|
-
...
|
|
609
|
+
...tmpl,
|
|
619
610
|
contextActivities,
|
|
620
611
|
};
|
|
621
612
|
// cmi5 §9.6.3.2 — masteryScore extension is scoped to Passed/Failed.
|
|
622
613
|
if (opts.mastery && this.#masteryScore !== null) {
|
|
623
614
|
ctx.extensions = {
|
|
624
|
-
...(
|
|
615
|
+
...(tmpl.extensions ?? {}),
|
|
625
616
|
[CMI5_MASTERYSCORE_EXT]: this.#masteryScore,
|
|
626
617
|
};
|
|
627
618
|
}
|
|
@@ -19,27 +19,6 @@ export class LMSAdapterError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function missingApiError(
|
|
23
|
-
standard: 'scorm12' | 'scorm2004' | 'cmi5',
|
|
24
|
-
): LMSAdapterError {
|
|
25
|
-
const label =
|
|
26
|
-
standard === 'scorm12'
|
|
27
|
-
? 'SCORM 1.2'
|
|
28
|
-
: standard === 'scorm2004'
|
|
29
|
-
? 'SCORM 2004'
|
|
30
|
-
: 'cmi5';
|
|
31
|
-
const detail =
|
|
32
|
-
standard === 'cmi5'
|
|
33
|
-
? 'No cmi5 launch parameters (fetch / endpoint / activityId / actor) on the URL.'
|
|
34
|
-
: `No ${label} API object found in the window.parent or window.opener chain.`;
|
|
35
|
-
return new LMSAdapterError(
|
|
36
|
-
standard,
|
|
37
|
-
`Tessera: this course is configured for ${label} but ${detail} ` +
|
|
38
|
-
`The course must be launched from an LMS that provides the ${label} runtime. ` +
|
|
39
|
-
`If you are testing locally, run \`npm run dev\` instead, or set export.standard to "web".`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
22
|
export interface CreateAdapterOptions {
|
|
44
23
|
/**
|
|
45
24
|
* When true, a missing LMS API falls back to `WebAdapter` with a console
|
|
@@ -49,44 +28,68 @@ export interface CreateAdapterOptions {
|
|
|
49
28
|
allowFallback?: boolean;
|
|
50
29
|
}
|
|
51
30
|
|
|
52
|
-
/**
|
|
53
|
-
* Select the appropriate persistence adapter based on course config.
|
|
54
|
-
*
|
|
55
|
-
* In production builds, an LMS-configured course (scorm12/scorm2004/cmi5)
|
|
56
|
-
* will throw `LMSAdapterError` if the matching LMS API isn't reachable —
|
|
57
|
-
* we fail loud so a misconfigured launch is visible immediately rather
|
|
58
|
-
* than silently losing tracking to localStorage.
|
|
59
|
-
*
|
|
60
|
-
* In dev mode, missing APIs warn and fall back to `WebAdapter` so authors
|
|
61
|
-
* can still iterate locally.
|
|
62
|
-
*/
|
|
63
31
|
type LMSStandard = 'scorm12' | 'scorm2004' | 'cmi5';
|
|
64
32
|
|
|
65
|
-
/** Per-standard LMS
|
|
33
|
+
/** Per-standard LMS wiring: `detect` returns an adapter when the LMS runtime is reachable, else null. Labels are the single source for the dev warning and production error. */
|
|
66
34
|
const LMS_ADAPTERS: Record<
|
|
67
35
|
LMSStandard,
|
|
68
|
-
{
|
|
36
|
+
{
|
|
37
|
+
detect: () => PersistenceAdapter | null;
|
|
38
|
+
warnLabel: string;
|
|
39
|
+
name: string;
|
|
40
|
+
missingDetail: string;
|
|
41
|
+
}
|
|
69
42
|
> = {
|
|
70
43
|
scorm12: {
|
|
71
44
|
detect: () => {
|
|
72
45
|
const api = findSCORM12API();
|
|
73
46
|
return api ? new SCORM12Adapter(api) : null;
|
|
74
47
|
},
|
|
75
|
-
|
|
48
|
+
warnLabel: 'SCORM 1.2 API',
|
|
49
|
+
name: 'SCORM 1.2',
|
|
50
|
+
missingDetail:
|
|
51
|
+
'No SCORM 1.2 API object found in the window.parent or window.opener chain.',
|
|
76
52
|
},
|
|
77
53
|
scorm2004: {
|
|
78
54
|
detect: () => {
|
|
79
55
|
const api = findSCORM2004API();
|
|
80
56
|
return api ? new SCORM2004Adapter(api) : null;
|
|
81
57
|
},
|
|
82
|
-
|
|
58
|
+
warnLabel: 'SCORM 2004 API',
|
|
59
|
+
name: 'SCORM 2004',
|
|
60
|
+
missingDetail:
|
|
61
|
+
'No SCORM 2004 API object found in the window.parent or window.opener chain.',
|
|
83
62
|
},
|
|
84
63
|
cmi5: {
|
|
85
64
|
detect: () => (hasCMI5LaunchParams() ? new CMI5Adapter() : null),
|
|
86
|
-
|
|
65
|
+
warnLabel: 'cmi5 launch parameters',
|
|
66
|
+
name: 'cmi5',
|
|
67
|
+
missingDetail:
|
|
68
|
+
'No cmi5 launch parameters (fetch / endpoint / activityId / actor) on the URL.',
|
|
87
69
|
},
|
|
88
70
|
};
|
|
89
71
|
|
|
72
|
+
function missingApiError(standard: LMSStandard): LMSAdapterError {
|
|
73
|
+
const { name, missingDetail } = LMS_ADAPTERS[standard];
|
|
74
|
+
return new LMSAdapterError(
|
|
75
|
+
standard,
|
|
76
|
+
`Tessera: this course is configured for ${name} but ${missingDetail} ` +
|
|
77
|
+
`The course must be launched from an LMS that provides the ${name} runtime. ` +
|
|
78
|
+
`If you are testing locally, run \`npm run dev\` instead, or set export.standard to "web".`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Select the appropriate persistence adapter based on course config.
|
|
84
|
+
*
|
|
85
|
+
* In production builds, an LMS-configured course (scorm12/scorm2004/cmi5)
|
|
86
|
+
* will throw `LMSAdapterError` if the matching LMS API isn't reachable —
|
|
87
|
+
* we fail loud so a misconfigured launch is visible immediately rather
|
|
88
|
+
* than silently losing tracking to localStorage.
|
|
89
|
+
*
|
|
90
|
+
* In dev mode, missing APIs warn and fall back to `WebAdapter` so authors
|
|
91
|
+
* can still iterate locally.
|
|
92
|
+
*/
|
|
90
93
|
export function createAdapter(
|
|
91
94
|
config: CourseConfig,
|
|
92
95
|
options: CreateAdapterOptions = {},
|
|
@@ -103,7 +106,7 @@ export function createAdapter(
|
|
|
103
106
|
if (adapter) return adapter;
|
|
104
107
|
if (!allowFallback) throw missingApiError(standard);
|
|
105
108
|
console.warn(
|
|
106
|
-
`Tessera (dev): ${entry.
|
|
109
|
+
`Tessera (dev): ${entry.warnLabel} not found — falling back to localStorage`,
|
|
107
110
|
);
|
|
108
111
|
}
|
|
109
112
|
return new WebAdapter(config);
|
|
@@ -29,7 +29,7 @@ const SCORM12_DIALECT: ScormDialect<SCORM12API> = {
|
|
|
29
29
|
responseField: 'student_response',
|
|
30
30
|
timestampField: 'time',
|
|
31
31
|
timestamp: () => new Date().toTimeString().slice(0, 8),
|
|
32
|
-
typeValue:
|
|
32
|
+
typeValue: scorm12Type,
|
|
33
33
|
resultLabels: { correct: 'correct', incorrect: 'wrong' },
|
|
34
34
|
format: SCORM12_INTERACTION_FORMAT,
|
|
35
35
|
},
|