tessera-learn 0.0.6 → 0.0.7

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation-B4UhCY5y.js","names":[],"sources":["../src/plugin/manifest.ts","../src/runtime/xapi/agent-rules.ts","../src/plugin/validation.ts"],"sourcesContent":["import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';\nimport { resolve, basename, extname } from 'node:path';\nimport JSON5 from 'json5';\nimport type { QuizConfig } from '../runtime/types.js';\n\n// ---------- Types ----------\n\nexport type { QuizConfig };\n\nexport interface ManifestPage {\n index: number;\n title: string;\n slug: string;\n importPath: string;\n quiz: QuizConfig | null;\n completesOn?: 'view';\n}\n\nexport interface ManifestLesson {\n title: string;\n slug: string;\n pages: ManifestPage[];\n}\n\nexport interface ManifestSection {\n title: string;\n slug: string;\n lessons: ManifestLesson[];\n}\n\nexport interface Manifest {\n sections: ManifestSection[];\n pages: ManifestPage[];\n totalPages: number;\n}\n\n/** Append `.svelte` if not already present. Both bare and suffixed names are accepted in author config. */\nexport function ensureSvelteSuffix(name: string): string {\n return name.endsWith('.svelte') ? name : `${name}.svelte`;\n}\n\n// ---------- File read cache ----------\n\n/**\n * Module-level cache of source file contents keyed by absolute path with\n * mtime invalidation. Both `validateProject` and `generateManifest` read the\n * same .svelte / _meta.js / course.config.js files during a single build;\n * sharing the read avoids the second disk hit (and matters most on cold-cache\n * CI runs and large courses).\n */\nconst fileContentCache = new Map<string, { mtimeMs: number; content: string }>();\n\nexport function readSourceFileCached(filePath: string): string {\n const stat = statSync(filePath);\n const cached = fileContentCache.get(filePath);\n if (cached && cached.mtimeMs === stat.mtimeMs) return cached.content;\n const content = readFileSync(filePath, 'utf-8');\n fileContentCache.set(filePath, { mtimeMs: stat.mtimeMs, content });\n return content;\n}\n\n// ---------- Helpers ----------\n\n/** Strip numeric prefix and hyphen: \"01-introduction\" → \"introduction\" */\nexport function stripPrefix(name: string): string {\n return name.replace(/^\\d+-/, '');\n}\n\n/** Title-case a slug: \"getting-started\" → \"Getting Started\" */\nexport function titleCase(slug: string): string {\n return slug\n .split('-')\n .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\n/** Derive slug from folder/file name */\nexport function deriveSlug(name: string, isFile = false): string {\n if (isFile) {\n return basename(name, extname(name));\n }\n return stripPrefix(name);\n}\n\n/** Matches both Svelte 5 `<script module>` and legacy `<script context=\"module\">`. */\nexport const MODULE_SCRIPT_RE =\n /<script\\s+(?:context\\s*=\\s*[\"']module[\"']|module)[^>]*>([\\s\\S]*?)<\\/script>/;\n\n/** Matches `export const pageConfig =` (RHS is read separately). */\nexport const PAGE_CONFIG_EXPORT_RE = /export\\s+const\\s+pageConfig\\s*=\\s*/;\n\n/** Matches `export default ` (RHS is read separately). */\nconst DEFAULT_EXPORT_RE = /export\\s+default\\s*/;\n\n/**\n * Locate `export default { ... }` and return the object literal substring,\n * or null if no balanced object literal follows the `export default` keyword.\n * Used by both manifest extraction and project validation.\n */\nexport function extractDefaultExportObjectLiteral(source: string): string | null {\n const match = source.match(DEFAULT_EXPORT_RE);\n if (!match || match.index === undefined) return null;\n const startIndex = source.indexOf('{', match.index);\n if (startIndex < 0) return null;\n return extractObjectLiteral(source, startIndex);\n}\n\n/**\n * Read a _meta.js file and extract its default export object.\n * Uses the same JSON5 approach as pageConfig extraction — find the object literal\n * after `export default` and parse it.\n */\nexport function readMetaFile(metaPath: string): { title?: string; pages?: string[] } {\n if (!existsSync(metaPath)) return {};\n\n const objectStr = extractDefaultExportObjectLiteral(readSourceFileCached(metaPath));\n if (!objectStr) return {};\n\n try {\n return JSON5.parse(objectStr);\n } catch {\n return {};\n }\n}\n\n/** Result of parsing a `.svelte` source for its `pageConfig` module-script export. */\nexport type PageConfigParseResult =\n /** No module script, or no `pageConfig =` export. Treat as \"no config\". */\n | { kind: 'none' }\n /** Found and successfully parsed. */\n | { kind: 'ok'; value: { title?: string; quiz?: QuizConfig; completesOn?: 'view' } }\n /** Found but couldn't parse as a static object literal — non-literal RHS or JSON5 failure. */\n | { kind: 'invalid' };\n\n/** Source-level pageConfig extraction shared by manifest generation and build-time validation. */\nexport function parsePageConfigFromSource(content: string): PageConfigParseResult {\n const moduleScriptMatch = content.match(MODULE_SCRIPT_RE);\n if (!moduleScriptMatch) return { kind: 'none' };\n\n const scriptContent = moduleScriptMatch[1];\n\n const configMatch = scriptContent.match(PAGE_CONFIG_EXPORT_RE);\n if (!configMatch || configMatch.index === undefined) return { kind: 'none' };\n\n const afterExport = scriptContent\n .slice(configMatch.index + configMatch[0].length)\n .trimStart();\n // pageConfig assigned to something other than an object literal — flag as invalid.\n if (!afterExport.startsWith('{')) return { kind: 'invalid' };\n\n const startIndex = scriptContent.indexOf('{', configMatch.index + configMatch[0].length);\n if (startIndex < 0) return { kind: 'invalid' };\n const objectStr = extractObjectLiteral(scriptContent, startIndex);\n if (!objectStr) return { kind: 'invalid' };\n\n try {\n return { kind: 'ok', value: JSON5.parse(objectStr) };\n } catch {\n return { kind: 'invalid' };\n }\n}\n\n/** Extract pageConfig from a .svelte file. Throws on parse failure. */\nexport function extractPageConfig(filePath: string): { title?: string; quiz?: QuizConfig; completesOn?: 'view' } {\n const result = parsePageConfigFromSource(readSourceFileCached(filePath));\n if (result.kind === 'ok') return result.value;\n if (result.kind === 'invalid') {\n throw new Error(\n `${filePath}: pageConfig must be a static object literal (no variables, function calls, or computed values)`\n );\n }\n return {};\n}\n\n/**\n * Extract an object literal from source starting at the opening brace.\n * Tracks brace depth to find the matching closing brace.\n */\nexport function extractObjectLiteral(source: string, startIndex: number): string | null {\n if (source[startIndex] !== '{') return null;\n\n let depth = 0;\n let inString: string | null = null;\n let escaped = false;\n\n for (let i = startIndex; i < source.length; i++) {\n const char = source[i];\n\n if (escaped) {\n escaped = false;\n continue;\n }\n\n if (char === '\\\\' && inString) {\n escaped = true;\n continue;\n }\n\n if (inString) {\n if (char === inString) {\n inString = null;\n }\n continue;\n }\n\n if (char === '\"' || char === \"'\" || char === '`') {\n inString = char;\n continue;\n }\n\n // Skip single-line comments\n if (char === '/' && i + 1 < source.length && source[i + 1] === '/') {\n const newline = source.indexOf('\\n', i);\n i = newline === -1 ? source.length : newline;\n continue;\n }\n\n // Skip multi-line comments\n if (char === '/' && i + 1 < source.length && source[i + 1] === '*') {\n const end = source.indexOf('*/', i + 2);\n i = end === -1 ? source.length : end + 1;\n continue;\n }\n\n if (char === '{') depth++;\n if (char === '}') {\n depth--;\n if (depth === 0) {\n return source.slice(startIndex, i + 1);\n }\n }\n }\n\n return null;\n}\n\n/**\n * Get sorted subdirectories of a given path.\n */\nfunction getSortedDirs(dirPath: string): string[] {\n if (!existsSync(dirPath)) return [];\n return readdirSync(dirPath)\n .filter(name => {\n const full = resolve(dirPath, name);\n return statSync(full).isDirectory() && !name.startsWith('.');\n })\n .sort();\n}\n\n/**\n * Get .svelte files in a directory.\n */\nfunction getSvelteFiles(dirPath: string): string[] {\n if (!existsSync(dirPath)) return [];\n return readdirSync(dirPath)\n .filter(name => name.endsWith('.svelte'))\n .sort();\n}\n\n// ---------- Main ----------\n\n/**\n * Generate a course manifest by scanning the pages/ directory.\n */\nexport function generateManifest(pagesDir: string): Manifest {\n const sections: ManifestSection[] = [];\n const flatPages: ManifestPage[] = [];\n let pageIndex = 0;\n\n const sectionDirs = getSortedDirs(pagesDir);\n\n for (const sectionName of sectionDirs) {\n const sectionPath = resolve(pagesDir, sectionName);\n const sectionMeta = readMetaFile(resolve(sectionPath, '_meta.js'));\n const sectionSlug = deriveSlug(sectionName);\n\n const section: ManifestSection = {\n title: sectionMeta.title || titleCase(sectionSlug),\n slug: sectionSlug,\n lessons: [],\n };\n\n const lessonDirs = getSortedDirs(sectionPath);\n\n for (const lessonName of lessonDirs) {\n const lessonPath = resolve(sectionPath, lessonName);\n const lessonMeta = readMetaFile(resolve(lessonPath, '_meta.js'));\n const lessonSlug = deriveSlug(lessonName);\n\n const lesson: ManifestLesson = {\n title: lessonMeta.title || titleCase(lessonSlug),\n slug: lessonSlug,\n pages: [],\n };\n\n // Determine page order\n const allSvelteFiles = getSvelteFiles(lessonPath);\n const orderedFiles = orderPageFiles(allSvelteFiles, lessonMeta.pages);\n\n for (const fileName of orderedFiles) {\n const filePath = resolve(lessonPath, fileName);\n const pageSlug = deriveSlug(fileName, true);\n\n let pageConfig: { title?: string; quiz?: QuizConfig; completesOn?: 'view' } = {};\n try {\n pageConfig = extractPageConfig(filePath);\n } catch (e) {\n // Validation errors will be handled by the validation plugin (Step 11).\n // For now, log and continue with defaults.\n console.warn(`[tessera warning] ${(e as Error).message}`);\n }\n\n const relativePath = `/pages/${sectionName}/${lessonName}/${fileName}`;\n\n const page: ManifestPage = {\n index: pageIndex,\n title: pageConfig.title || titleCase(pageSlug),\n slug: pageSlug,\n importPath: relativePath,\n quiz: pageConfig.quiz || null,\n ...(pageConfig.completesOn === 'view' ? { completesOn: 'view' as const } : {}),\n };\n\n lesson.pages.push(page);\n flatPages.push(page);\n pageIndex++;\n }\n\n section.lessons.push(lesson);\n }\n\n sections.push(section);\n }\n\n return {\n sections,\n pages: flatPages,\n totalPages: flatPages.length,\n };\n}\n\n/**\n * Order .svelte files: listed in `pages` array first (in order), then unlisted appended alphabetically.\n */\nexport function orderPageFiles(allFiles: string[], pagesArray?: string[]): string[] {\n if (!pagesArray || pagesArray.length === 0) {\n return allFiles;\n }\n\n const listed = pagesArray.map(ensureSvelteSuffix);\n const listedSet = new Set(listed);\n const unlisted = allFiles.filter(f => !listedSet.has(f)).sort();\n\n // Only include listed files that actually exist\n const validListed = listed.filter(f => allFiles.includes(f));\n\n return [...validListed, ...unlisted];\n}\n","/**\n * xAPI Identified Agent and Basic-auth credential validation rules.\n *\n * Pure logic — no Svelte/runtime imports. Imported by both `publisher.ts`\n * (runtime validation of resolved actor / auth) and `plugin/validation.ts`\n * (build-time validation of static `course.config.js` actor / auth).\n * Keeping the rules in one place prevents the two callsites from drifting.\n */\n\n/**\n * Validate that a candidate is an Identified Agent per xAPI 1.0.3.\n * Returns null on success or a human-readable error suffix on failure.\n *\n * Suffixes are prefix-friendly: callers concatenate their own label\n * (`xapi.actor`, `xapi[0].actor`, etc.) with a single space — no \"actor\"\n * appears in the suffix to avoid doubling.\n */\nexport function validateAgent(actor: unknown): string | null {\n if (!actor || typeof actor !== 'object') {\n return 'must be an object';\n }\n const a = actor as Record<string, unknown>;\n if (Array.isArray(a.member) && a.member.length > 0) {\n return 'is a Group (has `member`); v1 supports Identified Agents only';\n }\n let count = 0;\n if (a.mbox !== undefined) count++;\n if (a.mbox_sha1sum !== undefined) count++;\n if (a.openid !== undefined) count++;\n if (a.account !== undefined) count++;\n if (count === 0) {\n return 'must have one of mbox, mbox_sha1sum, openid, or account (Identified Agent rule)';\n }\n if (count > 1) {\n return 'must have exactly one IFI (mbox / mbox_sha1sum / openid / account), not multiple';\n }\n if (a.mbox !== undefined) {\n if (typeof a.mbox !== 'string' || !a.mbox.startsWith('mailto:')) {\n return '.mbox must be a string starting with \"mailto:\"';\n }\n }\n if (a.mbox_sha1sum !== undefined) {\n if (typeof a.mbox_sha1sum !== 'string' || !/^[0-9a-f]{40}$/i.test(a.mbox_sha1sum)) {\n return '.mbox_sha1sum must be a 40-character hex string';\n }\n }\n if (a.openid !== undefined) {\n if (typeof a.openid !== 'string' || !a.openid) {\n return '.openid must be a non-empty string';\n }\n try {\n new URL(a.openid);\n } catch {\n return '.openid must be an absolute URI';\n }\n }\n if (a.account !== undefined) {\n const acc = a.account as Record<string, unknown>;\n if (!acc || typeof acc !== 'object') {\n return '.account must be an object with homePage and name';\n }\n if (typeof acc.homePage !== 'string' || !acc.homePage) {\n return '.account.homePage must be a non-empty string';\n }\n try {\n new URL(acc.homePage);\n } catch {\n return '.account.homePage must be an absolute URL';\n }\n if (typeof acc.name !== 'string' || !acc.name) {\n return '.account.name must be a non-empty string';\n }\n }\n return null;\n}\n\n/**\n * Validate a Basic-auth credential string (the value after \"Basic \").\n * v1 supports Basic only. Bearer is a hard error so OAuth users see the\n * non-goal explicitly.\n */\nexport function validateAuthCredential(auth: string): string | null {\n if (typeof auth !== 'string' || !auth) {\n return 'auth must be a non-empty string';\n }\n if (/^basic\\s/i.test(auth)) {\n return \"auth must be the Basic credential value only, not the full header. Drop the 'Basic ' prefix.\";\n }\n if (/^bearer\\s/i.test(auth)) {\n return 'Bearer/OAuth credentials are not supported in v1. Use Basic auth, or wrap your token-exchange in an auth function that returns a Basic credential.';\n }\n return null;\n}\n","import { existsSync, readdirSync, statSync } from 'node:fs';\nimport { resolve, relative } from 'node:path';\nimport JSON5 from 'json5';\nimport {\n extractDefaultExportObjectLiteral,\n parsePageConfigFromSource,\n readSourceFileCached,\n ensureSvelteSuffix,\n} from './manifest.js';\nimport { validateAgent } from '../runtime/xapi/agent-rules.js';\n\n// ---------- Types ----------\n\nexport interface ValidationResult {\n errors: string[];\n warnings: string[];\n}\n\n// Known top-level config fields\nconst KNOWN_CONFIG_FIELDS = new Set([\n 'title',\n 'description',\n 'author',\n 'version',\n 'branding',\n 'navigation',\n 'completion',\n 'scoring',\n 'export',\n 'chrome',\n 'xapi',\n]);\n\nconst VALID_NAV_MODES = ['free', 'sequential'];\nconst VALID_COMPLETION_MODES = ['quiz', 'percentage', 'manual'];\nconst VALID_EXPORT_STANDARDS = ['web', 'scorm12', 'scorm2004', 'cmi5'];\nconst VALID_MANUAL_TRIGGERS = ['page'];\nconst VALID_REQUIRE_SUCCESS_STATUS = ['passed', 'failed'];\n\n// ---------- Main ----------\n\n/**\n * Validate a Tessera project at the given root.\n * Returns errors (block build) and warnings (informational).\n */\nexport function validateProject(projectRoot: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n // 1. Check course.config.js exists\n const configPath = resolve(projectRoot, 'course.config.js');\n if (!existsSync(configPath)) {\n errors.push('course.config.js not found in project root');\n return { errors, warnings };\n }\n\n // 2. Parse and validate config\n const config = parseConfig(configPath, errors, warnings);\n\n // 3. Validate pages directory\n const pagesDir = resolve(projectRoot, 'pages');\n const assetsDir = resolve(projectRoot, 'assets');\n const pageResults = validatePages(pagesDir, assetsDir, projectRoot);\n errors.push(...pageResults.errors);\n warnings.push(...pageResults.warnings);\n\n // 4. Contract-bypass checks on project-root shell files\n for (const shellFile of ['layout.svelte', 'quiz.svelte']) {\n const shellPath = resolve(projectRoot, shellFile);\n if (existsSync(shellPath)) {\n validateContractBypass(readSourceFileCached(shellPath), shellFile, errors);\n }\n }\n\n // 5. Cross-cutting validations\n if (config) {\n crossValidate(config, pageResults, errors, warnings);\n }\n\n return { errors, warnings };\n}\n\n// ---------- Config Validation ----------\n\ninterface ParsedConfig {\n title?: string;\n navigation?: { mode?: string };\n completion?: {\n mode?: string;\n percentageThreshold?: number;\n trigger?: string;\n requireSuccessStatus?: string;\n };\n scoring?: { passingScore?: number };\n export?: { standard?: string };\n [key: string]: unknown;\n}\n\nfunction parseConfig(\n configPath: string,\n errors: string[],\n warnings: string[]\n): ParsedConfig | null {\n const objectStr = extractDefaultExportObjectLiteral(readSourceFileCached(configPath));\n if (!objectStr) {\n errors.push(\n 'course.config.js: could not parse — must use `export default { ... }` syntax'\n );\n return null;\n }\n\n let config: ParsedConfig;\n try {\n config = JSON5.parse(objectStr);\n } catch {\n errors.push(\n 'course.config.js: syntax error — must export a static object literal'\n );\n return null;\n }\n\n // Check for unknown fields\n for (const key of Object.keys(config)) {\n if (!KNOWN_CONFIG_FIELDS.has(key)) {\n warnings.push(\n `course.config.js: unknown field \"${key}\" — will be ignored`\n );\n }\n }\n\n // Validate navigation.mode\n if (config.navigation?.mode !== undefined) {\n if (!VALID_NAV_MODES.includes(config.navigation.mode)) {\n errors.push(\n `course.config.js: \"navigation.mode\" must be \"free\" or \"sequential\", got \"${config.navigation.mode}\"`\n );\n }\n }\n\n // Validate completion.mode\n if (config.completion?.mode !== undefined) {\n if (!VALID_COMPLETION_MODES.includes(config.completion.mode)) {\n errors.push(\n `course.config.js: \"completion.mode\" must be \"quiz\", \"percentage\", or \"manual\", got \"${config.completion.mode}\"`\n );\n }\n }\n\n if (config.completion?.trigger !== undefined) {\n if (config.completion.mode !== 'manual') {\n warnings.push(\n `course.config.js: \"completion.trigger\" is ignored unless completion.mode is \"manual\"`\n );\n } else if (!VALID_MANUAL_TRIGGERS.includes(config.completion.trigger)) {\n errors.push(\n `course.config.js: \"completion.trigger\" must be \"page\" or omitted, got \"${config.completion.trigger}\"`\n );\n }\n }\n\n if (config.completion?.requireSuccessStatus !== undefined) {\n if (config.completion.mode !== 'manual') {\n warnings.push(\n `course.config.js: \"completion.requireSuccessStatus\" is ignored unless completion.mode is \"manual\"`\n );\n } else if (!VALID_REQUIRE_SUCCESS_STATUS.includes(config.completion.requireSuccessStatus)) {\n errors.push(\n `course.config.js: \"completion.requireSuccessStatus\" must be \"passed\" or \"failed\" (omit for \"unknown\"), got \"${config.completion.requireSuccessStatus}\"`\n );\n }\n }\n\n // Validate export.standard\n if (config.export?.standard !== undefined) {\n if (!VALID_EXPORT_STANDARDS.includes(config.export.standard)) {\n errors.push(\n `course.config.js: \"export.standard\" must be \"web\", \"scorm12\", \"scorm2004\", or \"cmi5\", got \"${config.export.standard}\"`\n );\n }\n }\n\n // Validate scoring.passingScore\n if (config.scoring?.passingScore !== undefined) {\n const score = config.scoring.passingScore;\n if (typeof score !== 'number' || score < 0 || score > 100) {\n errors.push(\n `course.config.js: \"scoring.passingScore\" must be 0–100, got ${score}`\n );\n }\n }\n\n // Validate completion.percentageThreshold\n if (config.completion?.percentageThreshold !== undefined) {\n const threshold = config.completion.percentageThreshold;\n if (typeof threshold !== 'number' || threshold < 0 || threshold > 100) {\n errors.push(\n `course.config.js: \"completion.percentageThreshold\" must be 0–100, got ${threshold}`\n );\n }\n }\n\n // Validate xapi (publisher destinations)\n if (config.xapi !== undefined) {\n validateXAPIConfig(\n config.xapi,\n config.export?.standard ?? 'web',\n errors,\n warnings\n );\n }\n\n return config;\n}\n\n// ---------- xAPI Config Validation ----------\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nconst SHA1_RE = /^[0-9a-f]{40}$/i;\n\nfunction validateXAPIConfig(\n raw: unknown,\n standard: string,\n errors: string[],\n warnings: string[]\n): void {\n if (raw === undefined || raw === null) return;\n\n // Normalize to array form. The single-object case is shorthand for a\n // one-element array — same machinery, no special case in the runtime.\n const entries: unknown[] = Array.isArray(raw) ? raw : [raw];\n\n if (Array.isArray(raw)) {\n if (entries.length === 0) {\n errors.push(\n 'course.config.js: xapi must contain at least one destination, or be omitted'\n );\n return;\n }\n // At most one 'lms' entry — more than one is never legitimate.\n const lmsCount = entries.filter(\n (e) =>\n e &&\n typeof e === 'object' &&\n (e as { endpoint?: unknown }).endpoint === 'lms'\n ).length;\n if (lmsCount > 1) {\n errors.push(\n \"course.config.js: xapi has multiple entries with endpoint: 'lms' — only one cmi5 launch-inherited destination is allowed\"\n );\n }\n // Warn on duplicate explicit endpoints.\n const seen = new Map<string, number>();\n for (const e of entries) {\n if (e && typeof e === 'object') {\n const ep = (e as { endpoint?: unknown }).endpoint;\n if (typeof ep === 'string' && ep !== 'lms') {\n seen.set(ep, (seen.get(ep) ?? 0) + 1);\n }\n }\n }\n for (const [ep, count] of seen) {\n if (count > 1) {\n warnings.push(\n `course.config.js: xapi has ${count} entries with endpoint \"${ep}\" — usually a copy-paste mistake; ` +\n 'fan-out to the same LRS with different actors/activityIds is supported but uncommon.'\n );\n }\n }\n } else if (typeof raw !== 'object') {\n errors.push(\n 'course.config.js: xapi must be an object or an array of objects'\n );\n return;\n }\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const label = Array.isArray(raw) ? `xapi[${i}]` : 'xapi';\n if (!entry || typeof entry !== 'object') {\n errors.push(`course.config.js: ${label} must be an object`);\n continue;\n }\n validateSingleXAPIEntry(\n entry as Record<string, unknown>,\n label,\n standard,\n errors,\n warnings\n );\n }\n}\n\nfunction validateSingleXAPIEntry(\n entry: Record<string, unknown>,\n label: string,\n standard: string,\n errors: string[],\n warnings: string[]\n): void {\n const endpoint = entry.endpoint;\n if (endpoint === undefined) {\n errors.push(`course.config.js: ${label}.endpoint is required`);\n return;\n }\n if (typeof endpoint !== 'string') {\n errors.push(`course.config.js: ${label}.endpoint must be a string`);\n return;\n }\n\n if (endpoint === 'lms') {\n // Forbid under non-cmi5 export.\n if (standard !== 'cmi5') {\n errors.push(\n `course.config.js: ${label}.endpoint: 'lms' requires export.standard: 'cmi5' (you have \"${standard}\"). ` +\n 'Either change the export standard or specify an explicit LRS endpoint.'\n );\n }\n // Forbid extra fields — everything is inherited from the cmi5 launch.\n const forbidden = ['auth', 'actor', 'activityId', 'registration', 'actorAccountHomePage'];\n for (const f of forbidden) {\n if (entry[f] !== undefined) {\n errors.push(\n `course.config.js: ${label}.${f} must be omitted when ${label}.endpoint is 'lms' — it is inherited from the cmi5 launch.`\n );\n }\n }\n return;\n }\n\n // Explicit endpoint — must be an absolute http(s) URL.\n let url: URL;\n try {\n url = new URL(endpoint);\n } catch {\n errors.push(\n `course.config.js: ${label}.endpoint must be an absolute http(s) URL, got \"${endpoint}\"`\n );\n return;\n }\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n errors.push(\n `course.config.js: ${label}.endpoint must use http: or https:, got \"${url.protocol}\"`\n );\n return;\n }\n if (url.protocol === 'http:' && process.env.NODE_ENV === 'production') {\n warnings.push(\n `course.config.js: ${label}.endpoint uses http:; LRS credentials will travel in cleartext. Use https in production.`\n );\n }\n if (!endpoint.endsWith('/')) {\n warnings.push(\n `course.config.js: ${label}.endpoint should end with a slash to avoid concatenation surprises ` +\n `(e.g. 'https://lrs.example.com/xapi/' not 'https://lrs.example.com/xapi'). Runtime normalizes regardless.`\n );\n }\n\n // auth — required for explicit endpoints.\n const auth = entry.auth;\n if (auth === undefined) {\n errors.push(`course.config.js: ${label}.auth is required`);\n } else if (typeof auth === 'string') {\n if (!auth) {\n errors.push(`course.config.js: ${label}.auth must be a non-empty string`);\n } else if (/^basic\\s/i.test(auth)) {\n errors.push(\n `course.config.js: ${label}.auth must be the Basic credential value only, not the full header. Drop the 'Basic ' prefix.`\n );\n } else if (/^bearer\\s/i.test(auth)) {\n errors.push(\n `course.config.js: ${label}.auth: Bearer/OAuth credentials are not supported in v1. Use Basic auth, or wrap your token-exchange in an auth function that returns a Basic credential.`\n );\n } else {\n warnings.push(\n `course.config.js: ${label}.auth is a static string and will be embedded in the bundle. ` +\n 'For production, pass a function that fetches a short-lived token from a server endpoint.'\n );\n }\n } else if (typeof auth !== 'function') {\n errors.push(\n `course.config.js: ${label}.auth must be a string or a function, got ${typeof auth}`\n );\n }\n\n // activityId — required IRI.\n const activityId = entry.activityId;\n if (activityId === undefined || activityId === '') {\n errors.push(`course.config.js: ${label}.activityId is required`);\n } else if (typeof activityId !== 'string') {\n errors.push(`course.config.js: ${label}.activityId must be a string`);\n } else {\n try {\n // Any absolute IRI — the URL constructor accepts uncommon schemes.\n new URL(activityId);\n } catch {\n errors.push(\n `course.config.js: ${label}.activityId must be an absolute IRI, got \"${activityId}\"`\n );\n }\n }\n\n // actor — required under web; optional otherwise.\n const actor = entry.actor;\n if (actor === undefined) {\n if (standard === 'web') {\n errors.push(\n `course.config.js: ${label}.actor is required for web export — there is no LMS to derive a learner identity from. ` +\n 'Provide either a static actor object or a function that resolves one (e.g. from your auth system).'\n );\n }\n } else if (typeof actor === 'object' && actor !== null) {\n const err = validateAgent(actor);\n if (err) {\n const joined = err.startsWith('.')\n ? `${label}.actor${err}`\n : `${label}.actor ${err}`;\n errors.push(`course.config.js: ${joined}`);\n }\n } else if (typeof actor !== 'function') {\n errors.push(\n `course.config.js: ${label}.actor must be an object or function, got ${typeof actor}`\n );\n }\n\n // actorAccountHomePage — optional, only meaningful under SCORM with no\n // explicit actor.\n const aahp = entry.actorAccountHomePage;\n if (aahp !== undefined) {\n if (typeof aahp !== 'string') {\n errors.push(\n `course.config.js: ${label}.actorAccountHomePage must be a string`\n );\n } else {\n try {\n new URL(aahp);\n } catch {\n errors.push(\n `course.config.js: ${label}.actorAccountHomePage must be an absolute URL`\n );\n }\n }\n if (actor !== undefined) {\n warnings.push(\n `course.config.js: ${label}.actorAccountHomePage is ignored when ${label}.actor is supplied explicitly.`\n );\n }\n if (standard === 'cmi5' || standard === 'web') {\n warnings.push(\n `course.config.js: ${label}.actorAccountHomePage is only used under scorm12/scorm2004 actor synthesis; ignored under \"${standard}\".`\n );\n }\n }\n\n // SCORM with auto-derived actor and a non-http(s) activityId:\n // actorAccountHomePage becomes required.\n if (\n actor === undefined &&\n (standard === 'scorm12' || standard === 'scorm2004') &&\n typeof activityId === 'string'\n ) {\n let isHttp = false;\n try {\n const u = new URL(activityId);\n isHttp = u.protocol === 'http:' || u.protocol === 'https:';\n } catch {\n isHttp = false;\n }\n if (!isHttp && aahp === undefined) {\n errors.push(\n `course.config.js: ${label}.activityId is not an http(s) URL, so its origin can't be used as the SCORM actor's account.homePage. ` +\n `Provide ${label}.actorAccountHomePage explicitly.`\n );\n }\n }\n\n // registration — optional UUID v4.\n const registration = entry.registration;\n if (registration !== undefined) {\n if (typeof registration !== 'string' || !UUID_RE.test(registration)) {\n errors.push(\n `course.config.js: ${label}.registration must be a UUID v4, got \"${String(registration)}\"`\n );\n }\n if (standard !== 'cmi5') {\n warnings.push(\n `course.config.js: ${label}.registration is a cmi5 concept; the LRS will accept it under \"${standard}\" but most analytics tools won't know what to do with it.`\n );\n }\n }\n}\n\n// ---------- Pages Validation ----------\n\ninterface PageInfo {\n fileRel: string;\n navIndex: number;\n hasGradedQuiz: boolean;\n hasQuiz: boolean;\n completesOnView: boolean;\n}\n\ninterface PagesValidationResult extends ValidationResult {\n totalPages: number;\n totalQuizzes: number;\n hasGradedQuiz: boolean;\n pages: PageInfo[];\n}\n\n/**\n * Validate a single page .svelte file. Used for both section-level (flat) and\n * lesson-level pages — the validation is identical, only the containing\n * directory differs.\n */\nfunction validatePageFile(\n filePath: string,\n projectRoot: string,\n assetsDir: string,\n navIndex: number,\n errors: string[],\n warnings: string[],\n assetExistsCache: Map<string, boolean>\n): { page: PageInfo; isQuiz: boolean; isGradedQuiz: boolean } {\n const fileRel = relative(projectRoot, filePath);\n const content = readSourceFileCached(filePath);\n\n const pageConfig = validatePageConfig(content, fileRel, errors);\n\n const isQuiz = !!pageConfig?.quiz;\n let isGradedQuiz = false;\n if (pageConfig?.quiz) {\n validateQuizConfig(pageConfig.quiz, fileRel, errors);\n if ((pageConfig.quiz as { graded?: unknown }).graded === true) {\n isGradedQuiz = true;\n }\n }\n\n const completesOnView = validateCompletesOn(pageConfig, fileRel, errors);\n\n validateAssetRefs(content, fileRel, assetsDir, warnings, assetExistsCache);\n validateQuestionComponents(content, fileRel, errors);\n validateContractBypass(content, fileRel, errors);\n if (\n pageConfig?.quiz &&\n !HAS_USE_QUESTION_RE.test(content) &&\n !HAS_QUESTION_TAG_RE.test(content)\n ) {\n warnings.push(\n `${fileRel}: quiz page has no question components or useQuestion() calls — ` +\n `the quiz will have nothing to score`\n );\n }\n\n return {\n page: { fileRel, navIndex, hasGradedQuiz: isGradedQuiz, hasQuiz: isQuiz, completesOnView },\n isQuiz,\n isGradedQuiz,\n };\n}\n\nfunction validatePages(\n pagesDir: string,\n assetsDir: string,\n projectRoot: string\n): PagesValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n const pages: PageInfo[] = [];\n let totalPages = 0;\n let totalQuizzes = 0;\n let hasGradedQuiz = false;\n // One existsSync per unique asset for the whole pass.\n const assetExistsCache = new Map<string, boolean>();\n\n if (!existsSync(pagesDir)) {\n errors.push(\n 'No pages found. Create at least one section with a lesson and page in pages/'\n );\n return { errors, warnings, totalPages: 0, totalQuizzes: 0, hasGradedQuiz: false, pages };\n }\n\n const topLevelEntries = readdirSync(pagesDir);\n\n // Check for stray .svelte files at pages/ root\n for (const entry of topLevelEntries) {\n const fullPath = resolve(pagesDir, entry);\n if (entry.endsWith('.svelte') && statSync(fullPath).isFile()) {\n const relPath = relative(projectRoot, fullPath);\n warnings.push(\n `${relPath}: this file is outside the section/lesson structure and will be ignored`\n );\n }\n }\n\n // Get section directories\n const sectionDirs = topLevelEntries\n .filter((name) => {\n const full = resolve(pagesDir, name);\n return statSync(full).isDirectory() && !name.startsWith('.');\n })\n .sort();\n\n if (sectionDirs.length === 0) {\n errors.push(\n 'No pages found. Create at least one section with a lesson and page in pages/'\n );\n return { errors, warnings, totalPages: 0, totalQuizzes: 0, hasGradedQuiz: false, pages };\n }\n\n for (const sectionName of sectionDirs) {\n const sectionPath = resolve(pagesDir, sectionName);\n const sectionRel = relative(projectRoot, sectionPath);\n\n // Validate section _meta.js\n const sectionMeta = validateMetaFile(\n resolve(sectionPath, '_meta.js'),\n sectionRel,\n errors\n );\n\n // Flat mode: .svelte files directly at section level are pages of an\n // implicit single lesson. Validate them just like lesson-level pages.\n const sectionEntries = readdirSync(sectionPath);\n const sectionSvelteFiles = sectionEntries\n .filter((name) => {\n const full = resolve(sectionPath, name);\n return name.endsWith('.svelte') && statSync(full).isFile();\n })\n .sort();\n\n if (sectionMeta?.pages) {\n for (const pageName of sectionMeta.pages) {\n const fileName = ensureSvelteSuffix(pageName);\n if (!sectionSvelteFiles.includes(fileName)) {\n const metaRel = relative(projectRoot, resolve(sectionPath, '_meta.js'));\n errors.push(\n `${metaRel}: pages array lists \"${pageName}\" but ${fileName} not found in this directory`\n );\n }\n }\n }\n\n for (const fileName of sectionSvelteFiles) {\n const result = validatePageFile(\n resolve(sectionPath, fileName),\n projectRoot,\n assetsDir,\n totalPages,\n errors,\n warnings,\n assetExistsCache\n );\n totalPages++;\n if (result.isQuiz) totalQuizzes++;\n if (result.isGradedQuiz) hasGradedQuiz = true;\n pages.push(result.page);\n }\n\n // Get lesson directories\n const lessonDirs = sectionEntries\n .filter((name) => {\n const full = resolve(sectionPath, name);\n return statSync(full).isDirectory() && !name.startsWith('.');\n })\n .sort();\n\n for (const lessonName of lessonDirs) {\n const lessonPath = resolve(sectionPath, lessonName);\n const lessonRel = relative(projectRoot, lessonPath);\n\n // Validate lesson _meta.js\n const meta = validateMetaFile(\n resolve(lessonPath, '_meta.js'),\n lessonRel,\n errors\n );\n\n // Get .svelte files\n const svelteFiles = readdirSync(lessonPath)\n .filter((name) => name.endsWith('.svelte'))\n .sort();\n\n // Check pages array references\n if (meta?.pages) {\n for (const pageName of meta.pages) {\n const fileName = ensureSvelteSuffix(pageName);\n if (!svelteFiles.includes(fileName)) {\n const metaRel = relative(projectRoot, resolve(lessonPath, '_meta.js'));\n errors.push(\n `${metaRel}: pages array lists \"${pageName}\" but ${fileName} not found in this directory`\n );\n }\n }\n }\n\n // Check for unlisted .svelte files\n if (meta?.pages && meta.pages.length > 0) {\n const listedSet = new Set(meta.pages.map(ensureSvelteSuffix));\n for (const file of svelteFiles) {\n if (!listedSet.has(file)) {\n const relPath = relative(projectRoot, resolve(lessonPath, file));\n warnings.push(\n `${relPath}: not listed in _meta.js pages array — will be appended at end`\n );\n }\n }\n }\n\n // Validate each .svelte file\n for (const fileName of svelteFiles) {\n const result = validatePageFile(\n resolve(lessonPath, fileName),\n projectRoot,\n assetsDir,\n totalPages,\n errors,\n warnings,\n assetExistsCache\n );\n totalPages++;\n if (result.isQuiz) totalQuizzes++;\n if (result.isGradedQuiz) hasGradedQuiz = true;\n pages.push(result.page);\n }\n }\n }\n\n if (totalPages === 0) {\n errors.push(\n 'No pages found. Create at least one section with a lesson and page in pages/'\n );\n }\n\n return { errors, warnings, totalPages, totalQuizzes, hasGradedQuiz, pages };\n}\n\n// ---------- _meta.js Validation ----------\n\nfunction validateMetaFile(\n metaPath: string,\n parentRel: string,\n errors: string[]\n): { title?: string; pages?: string[] } | null {\n if (!existsSync(metaPath)) return null;\n\n const metaRel = `${parentRel}/_meta.js`;\n const objectStr = extractDefaultExportObjectLiteral(readSourceFileCached(metaPath));\n\n if (!objectStr) {\n errors.push(`${metaRel}: syntax error — must export default { title: \"...\" }`);\n return null;\n }\n\n let meta: { title?: string; pages?: string[] };\n try {\n meta = JSON5.parse(objectStr);\n } catch {\n errors.push(`${metaRel}: syntax error — must export default { title: \"...\" }`);\n return null;\n }\n\n if (!meta.title) {\n errors.push(`${metaRel}: missing required \"title\" field`);\n }\n\n return meta;\n}\n\n// ---------- pageConfig Validation ----------\n\nfunction validatePageConfig(\n content: string,\n fileRel: string,\n errors: string[]\n): { title?: string; quiz?: unknown; completesOn?: unknown } | null {\n const result = parsePageConfigFromSource(content);\n if (result.kind === 'ok') return result.value;\n if (result.kind === 'invalid') {\n errors.push(\n `${fileRel}: pageConfig must be a static object literal (no variables, function calls, or computed values)`\n );\n }\n return null;\n}\n\nfunction validateCompletesOn(\n pageConfig: { completesOn?: unknown } | null,\n fileRel: string,\n errors: string[]\n): boolean {\n if (!pageConfig || pageConfig.completesOn === undefined) return false;\n if (pageConfig.completesOn === 'view') return true;\n errors.push(\n `${fileRel}: pageConfig.completesOn must be \"view\", got ${JSON.stringify(pageConfig.completesOn)}`\n );\n return false;\n}\n\n// ---------- Quiz Config Validation ----------\n\nfunction validateQuizConfig(quiz: unknown, fileRel: string, errors: string[]): void {\n if (!quiz || typeof quiz !== 'object') return;\n const cfg = quiz as Record<string, unknown>;\n\n if (cfg.maxAttempts !== undefined) {\n const val = cfg.maxAttempts;\n if (val !== Infinity && (typeof val !== 'number' || val <= 0 || !Number.isFinite(val))) {\n errors.push(\n `${fileRel}: quiz.maxAttempts must be a positive number or Infinity, got ${String(val)}`\n );\n }\n }\n\n for (const field of ['graded', 'gatesProgress', 'showFeedback']) {\n if (cfg[field] !== undefined && typeof cfg[field] !== 'boolean') {\n errors.push(\n `${fileRel}: quiz.${field} must be a boolean, got ${typeof cfg[field]}`\n );\n }\n }\n}\n\n// ---------- Question Component Validation ----------\n\nconst QUESTION_COMPONENT_REQUIRED: Record<string, string[]> = {\n MultipleChoice: ['question', 'options', 'correct'],\n FillInTheBlank: ['question', 'answers'],\n Matching: ['question', 'pairs'],\n Sorting: ['question', 'items', 'targets', 'correct'],\n};\n\ntype PropValue =\n | { kind: 'string'; value: string }\n | { kind: 'expr'; raw: string }\n | { kind: 'bool' };\n\n/** Extract a balanced {...} or [...] span starting at startIndex, or null. */\nfunction extractBalanced(source: string, startIndex: number): string | null {\n const open = source[startIndex];\n if (open !== '{' && open !== '[') return null;\n let depth = 0;\n let inString: string | null = null;\n let escaped = false;\n for (let i = startIndex; i < source.length; i++) {\n const char = source[i];\n if (escaped) {\n escaped = false;\n continue;\n }\n if (char === '\\\\' && inString) {\n escaped = true;\n continue;\n }\n if (inString) {\n if (char === inString) inString = null;\n continue;\n }\n if (char === '\"' || char === \"'\" || char === '`') {\n inString = char;\n continue;\n }\n if (char === '{' || char === '[') depth++;\n if (char === '}' || char === ']') {\n depth--;\n if (depth === 0) return source.slice(startIndex, i + 1);\n }\n }\n return null;\n}\n\n/**\n * Parse the props of an opening tag starting just after the component name.\n * Returns null if the tag can't be parsed cleanly — callers then skip it\n * rather than risk a false positive.\n */\nfunction parseTagProps(content: string, start: number): Map<string, PropValue> | null {\n const props = new Map<string, PropValue>();\n let i = start;\n while (i < content.length) {\n while (i < content.length && /\\s/.test(content[i])) i++;\n if (i >= content.length) return null;\n const c = content[i];\n if (c === '>') return props;\n if (c === '/' && content[i + 1] === '>') return props;\n // Spread / shorthand expression — skip the whole {...} block.\n if (c === '{') {\n const block = extractBalanced(content, i);\n if (!block) return null;\n i += block.length;\n continue;\n }\n const nameMatch = /^[A-Za-z_][\\w-]*/.exec(content.slice(i));\n if (!nameMatch) return null;\n const propName = nameMatch[0];\n i += propName.length;\n while (i < content.length && /\\s/.test(content[i])) i++;\n if (content[i] !== '=') {\n props.set(propName, { kind: 'bool' });\n continue;\n }\n i++;\n while (i < content.length && /\\s/.test(content[i])) i++;\n const v = content[i];\n if (v === '\"' || v === \"'\") {\n const end = content.indexOf(v, i + 1);\n if (end === -1) return null;\n props.set(propName, { kind: 'string', value: content.slice(i + 1, end) });\n i = end + 1;\n } else if (v === '{') {\n const block = extractBalanced(content, i);\n if (!block) return null;\n props.set(propName, { kind: 'expr', raw: block.slice(1, -1).trim() });\n i += block.length;\n } else {\n return null;\n }\n }\n return null;\n}\n\nfunction staticArray(prop: PropValue | undefined): unknown[] | null {\n if (prop?.kind !== 'expr' || !prop.raw.startsWith('[')) return null;\n try {\n const parsed = JSON5.parse(prop.raw);\n return Array.isArray(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\nfunction staticNumber(prop: PropValue | undefined): number | null {\n if (prop?.kind !== 'expr') return null;\n try {\n const parsed = JSON5.parse(prop.raw);\n return typeof parsed === 'number' ? parsed : null;\n } catch {\n return null;\n }\n}\n\nfunction validateQuestionComponents(\n content: string,\n fileRel: string,\n errors: string[]\n): void {\n const names = Object.keys(QUESTION_COMPONENT_REQUIRED).join('|');\n const tagStartRe = new RegExp(`<(${names})(?=[\\\\s/>])`, 'g');\n const seenIds = new Set<string>();\n let m: RegExpExecArray | null;\n while ((m = tagStartRe.exec(content)) !== null) {\n const name = m[1];\n const props = parseTagProps(content, m.index + m[0].length);\n if (!props) continue;\n\n for (const req of QUESTION_COMPONENT_REQUIRED[name]) {\n if (!props.has(req)) {\n errors.push(`${fileRel}: <${name}> is missing required prop \"${req}\"`);\n }\n }\n\n const idProp = props.get('id');\n if (idProp?.kind === 'string') {\n if (seenIds.has(idProp.value)) {\n errors.push(\n `${fileRel}: duplicate question id \"${idProp.value}\" — each question on a page needs a unique id`\n );\n }\n seenIds.add(idProp.value);\n }\n\n if (name === 'MultipleChoice') {\n const options = staticArray(props.get('options'));\n const correct = staticNumber(props.get('correct'));\n if (options && correct !== null) {\n if (!Number.isInteger(correct) || correct < 0 || correct >= options.length) {\n errors.push(\n `${fileRel}: <MultipleChoice> correct={${correct}} is out of range for ${options.length} options (valid: 0–${options.length - 1})`\n );\n }\n }\n } else if (name === 'Sorting') {\n const items = staticArray(props.get('items'));\n const targets = staticArray(props.get('targets'));\n const correct = staticArray(props.get('correct'));\n if (items && correct && correct.length !== items.length) {\n errors.push(\n `${fileRel}: <Sorting> correct has ${correct.length} entries but items has ${items.length} — they must be parallel arrays`\n );\n }\n if (targets && correct) {\n for (const idx of correct) {\n if (\n typeof idx !== 'number' ||\n !Number.isInteger(idx) ||\n idx < 0 ||\n idx >= targets.length\n ) {\n errors.push(\n `${fileRel}: <Sorting> correct contains ${JSON.stringify(idx)}, out of range for ${targets.length} targets (valid: 0–${targets.length - 1})`\n );\n break;\n }\n }\n }\n } else if (name === 'Matching') {\n const pairs = staticArray(props.get('pairs'));\n if (pairs) {\n const bad = pairs.some(\n (p) =>\n typeof p !== 'object' ||\n p === null ||\n typeof (p as { left?: unknown }).left !== 'string' ||\n typeof (p as { right?: unknown }).right !== 'string'\n );\n if (bad) {\n errors.push(\n `${fileRel}: <Matching> pairs must be an array of { left: string, right: string } objects`\n );\n }\n }\n } else if (name === 'FillInTheBlank') {\n const answers = staticArray(props.get('answers'));\n if (answers) {\n if (answers.length === 0) {\n errors.push(`${fileRel}: <FillInTheBlank> answers must not be empty`);\n } else if (answers.some((a) => typeof a !== 'string')) {\n errors.push(\n `${fileRel}: <FillInTheBlank> answers must be an array of strings`\n );\n }\n }\n }\n }\n}\n\n// ---------- Contract Bypass Detection ----------\n\nconst QUIZ_COMPLETE_DISPATCH_RE =\n /(?:new\\s+CustomEvent\\s*\\(\\s*['\"]tessera-quiz-complete['\"]|dispatchEvent\\s*\\([\\s\\S]{0,120}tessera-quiz-complete)/;\nconst RUNTIME_INTERNAL_IMPORT_RE = /from\\s+['\"]tessera-learn\\/runtime\\//;\nconst HAS_USE_QUESTION_RE = /\\buseQuestion\\s*\\(/;\nconst HAS_QUESTION_TAG_RE = new RegExp(\n `<(${Object.keys(QUESTION_COMPONENT_REQUIRED).join('|')})(?=[\\\\s/>])`\n);\n\n/**\n * Detect ways an author file can bypass the LMS data contract. These check\n * source text for known escape hatches — they never inspect course content,\n * so they constrain how you wire things up, not what you build.\n */\nfunction validateContractBypass(\n content: string,\n fileRel: string,\n errors: string[]\n): void {\n if (QUIZ_COMPLETE_DISPATCH_RE.test(content)) {\n errors.push(\n `${fileRel}: dispatches \"tessera-quiz-complete\" directly — submit through ` +\n `useQuiz().submit() so the result reaches the LMS`\n );\n }\n if (RUNTIME_INTERNAL_IMPORT_RE.test(content)) {\n errors.push(\n `${fileRel}: imports from tessera-learn/runtime/* — use the public hooks ` +\n `(useQuiz, useQuestion, useNavigation, …) instead`\n );\n }\n}\n\n// ---------- Asset Reference Validation ----------\n\nconst ASSET_REF_RE = /\\$assets\\/([^\\s\"'`)]+)/g;\n\n/** Match $assets/... refs in any context (src attrs, import statements, url() etc) and dedupe. */\nfunction collectAssetRefs(content: string): string[] {\n const seen = new Set<string>();\n let match: RegExpExecArray | null;\n ASSET_REF_RE.lastIndex = 0;\n while ((match = ASSET_REF_RE.exec(content)) !== null) {\n seen.add(match[1]);\n }\n return [...seen];\n}\n\nfunction validateAssetRefs(\n content: string,\n fileRel: string,\n assetsDir: string,\n warnings: string[],\n existsCache: Map<string, boolean>\n): void {\n for (const assetPath of collectAssetRefs(content)) {\n const fullAssetPath = resolve(assetsDir, assetPath);\n let exists = existsCache.get(fullAssetPath);\n if (exists === undefined) {\n exists = existsSync(fullAssetPath);\n existsCache.set(fullAssetPath, exists);\n }\n if (!exists) {\n warnings.push(\n `${fileRel}: \"$assets/${assetPath}\" not found in assets/ directory`\n );\n }\n }\n}\n\n// ---------- Cross-Cutting Validations ----------\n\nfunction crossValidate(\n config: ParsedConfig,\n pageResults: PagesValidationResult,\n errors: string[],\n warnings: string[]\n): void {\n // completion.mode \"quiz\" but no graded quizzes\n if (config.completion?.mode === 'quiz' && !pageResults.hasGradedQuiz) {\n errors.push(\n 'completion.mode is \"quiz\" but no pages have quiz config with graded: true'\n );\n }\n\n const isManual = config.completion?.mode === 'manual';\n const completesOnPages = pageResults.pages.filter((p) => p.completesOnView);\n\n if (isManual && config.completion?.trigger === 'page' && completesOnPages.length === 0) {\n errors.push(\n 'completion.mode is \"manual\" with trigger: \"page\", but no page declares pageConfig.completesOn: \"view\". ' +\n 'Either add a completesOn page or remove the trigger field to drop the static check.'\n );\n }\n\n if (isManual) {\n for (const page of pageResults.pages) {\n if (page.hasGradedQuiz) {\n warnings.push(\n `${page.fileRel}: quiz.graded is true under completion.mode: \"manual\". ` +\n 'The score will be reported to the LMS for transcripts, but it will not drive ' +\n 'completion or success status — `markComplete()` / completesOn does. If that\\'s ' +\n 'not what you want, set graded: false or change completion.mode.'\n );\n }\n }\n }\n\n if (isManual && config.completion?.percentageThreshold !== undefined) {\n warnings.push(\n 'course.config.js: \"completion.percentageThreshold\" is ignored under completion.mode: \"manual\"'\n );\n }\n if (!isManual) {\n for (const page of completesOnPages) {\n warnings.push(\n `${page.fileRel}: pageConfig.completesOn is ignored — completion.mode is \"${config.completion?.mode ?? 'percentage'}\"`\n );\n }\n }\n for (const page of pageResults.pages) {\n if (page.completesOnView && page.hasQuiz) {\n warnings.push(\n `${page.fileRel}: completion fires on view, before the quiz can be answered — likely a mistake`\n );\n }\n }\n\n if (isManual) {\n const firstPage = pageResults.pages.find((p) => p.navIndex === 0);\n if (firstPage?.completesOnView) {\n warnings.push(\n `${firstPage.fileRel}: pageConfig.completesOn: \"view\" is on the first page — the course will complete immediately on launch, before the learner sees any other content.`\n );\n }\n }\n\n // SCORM 1.2 + high page count warning\n if (config.export?.standard === 'scorm12') {\n // Estimate worst-case suspend_data size when all pages are visited, all\n // quizzes completed, all chunks revealed, and a modest amount of\n // usePersistence / standalone-question state has accumulated.\n //\n // SavedState shape (see runtime/persistence.ts) — single-letter keys:\n // b (bookmark), v (visited[]), q (quiz scores), d (duration),\n // c (chunk progress), s (standalone scores), gs (graded standalone pages),\n // u (user state from usePersistence)\n //\n // We can't statically detect calls to `useQuestion({ graded: true })` or\n // `usePersistence`, so reserve a fixed buffer per page for those.\n let visitedChars = 0;\n for (let i = 0; i < pageResults.totalPages; i++) {\n visitedChars += String(i).length + 1; // digit chars + comma\n }\n const overhead = 60; // top-level JSON overhead with all keys\n const quizBytes = pageResults.totalQuizzes * 15; // q: \"NNN\":100,\n const chunkBytes = pageResults.totalPages * 12; // c: \"NNN\":NN,\n const standaloneBytes = pageResults.totalPages * 30;// s/gs: conservative buffer per page\n const userStateBuffer = 256; // usePersistence headroom\n const estimatedSize =\n overhead +\n visitedChars +\n quizBytes +\n chunkBytes +\n standaloneBytes +\n userStateBuffer;\n\n if (estimatedSize > 3200) {\n warnings.push(\n `Course has ${pageResults.totalPages} pages with ${pageResults.totalQuizzes} quizzes — estimated SCORM 1.2 suspend_data ~${estimatedSize} bytes may exceed the 4096-byte limit when fully populated (visited + chunks + standalone scores + usePersistence). Consider using \"scorm2004\" or \"cmi5\".`\n );\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tessera-learn",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "LMS tracking runtime for interactive learning content. One adapter layer (SCORM 1.2, SCORM 2004 4th Edition, cmi5, static Web), your choice of components.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -33,7 +33,6 @@
33
33
  "src",
34
34
  "styles",
35
35
  "README.md",
36
- "AGENTS.md",
37
36
  "LICENSE"
38
37
  ],
39
38
  "exports": {
@@ -52,6 +51,9 @@
52
51
  "./runtime/*": "./src/runtime/*"
53
52
  },
54
53
  "svelte": "./src/index.ts",
54
+ "bin": {
55
+ "tessera-validate": "./dist/plugin/cli.js"
56
+ },
55
57
  "engines": {
56
58
  "node": ">=24"
57
59
  },
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { validateProject } from './validation.js';
3
+
4
+ const projectRoot = process.cwd();
5
+ const { errors, warnings } = validateProject(projectRoot);
6
+
7
+ for (const warning of warnings) {
8
+ console.warn(`\x1b[33m[tessera warning]\x1b[0m ${warning}`);
9
+ }
10
+ for (const error of errors) {
11
+ console.error(`\x1b[31m[tessera error]\x1b[0m ${error}`);
12
+ }
13
+
14
+ if (errors.length > 0) {
15
+ const summary =
16
+ `Validation failed with ${errors.length} error(s)` +
17
+ (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : '') +
18
+ '.';
19
+ console.error(`\n\x1b[31m${summary}\x1b[0m`);
20
+ process.exit(1);
21
+ }
22
+
23
+ if (warnings.length > 0) {
24
+ console.log(
25
+ `\n\x1b[33mValidation passed with ${warnings.length} warning(s).\x1b[0m`
26
+ );
27
+ } else {
28
+ console.log('\x1b[32m[tessera]\x1b[0m Validation passed — no issues found.');
29
+ }
30
+ process.exit(0);