tessera-learn 0.3.0 → 0.4.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 (40) hide show
  1. package/AGENTS.md +17 -12
  2. package/README.md +1 -1
  3. package/dist/{audit-DkXqQTqn.js → audit-DsYqXbqm.js} +211 -183
  4. package/dist/audit-DsYqXbqm.js.map +1 -0
  5. package/dist/{build-commands-CyzuCDXg.js → build-commands-BFuiAxaR.js} +4 -4
  6. package/dist/build-commands-BFuiAxaR.js.map +1 -0
  7. package/dist/{inline-config-BEXyRqsJ.js → inline-config-DVvOCKht.js} +6 -6
  8. package/dist/inline-config-DVvOCKht.js.map +1 -0
  9. package/dist/plugin/cli.d.ts +5 -1
  10. package/dist/plugin/cli.d.ts.map +1 -1
  11. package/dist/plugin/cli.js +38 -7
  12. package/dist/plugin/cli.js.map +1 -1
  13. package/dist/plugin/index.d.ts +7 -1
  14. package/dist/plugin/index.d.ts.map +1 -1
  15. package/dist/plugin/index.js +2 -2
  16. package/dist/{plugin-CFUFgwHB.js → plugin-BuMiDTmU.js} +29 -38
  17. package/dist/plugin-BuMiDTmU.js.map +1 -0
  18. package/package.json +1 -1
  19. package/src/components/MultipleChoice.svelte +1 -2
  20. package/src/plugin/build-commands.ts +7 -4
  21. package/src/plugin/cli.ts +54 -3
  22. package/src/plugin/index.ts +31 -42
  23. package/src/plugin/inline-config.ts +4 -2
  24. package/src/plugin/manifest.ts +21 -0
  25. package/src/plugin/validate-cli.ts +5 -2
  26. package/src/plugin/validation.ts +214 -233
  27. package/src/runtime/App.svelte +4 -1
  28. package/src/runtime/adapters/scorm-base.ts +15 -14
  29. package/src/runtime/adapters/scorm12.ts +6 -25
  30. package/src/runtime/adapters/scorm2004.ts +12 -55
  31. package/src/runtime/adapters/web.ts +5 -13
  32. package/src/runtime/fingerprint.ts +28 -0
  33. package/src/runtime/interaction-format.ts +0 -1
  34. package/src/runtime/persistence.ts +4 -0
  35. package/src/runtime/types.ts +3 -0
  36. package/src/runtime/xapi/publisher.ts +11 -14
  37. package/dist/audit-DkXqQTqn.js.map +0 -1
  38. package/dist/build-commands-CyzuCDXg.js.map +0 -1
  39. package/dist/inline-config-BEXyRqsJ.js.map +0 -1
  40. package/dist/plugin-CFUFgwHB.js.map +0 -1
@@ -282,6 +282,33 @@ function readCourseConfig(projectRoot) {
282
282
  }
283
283
  }
284
284
  /**
285
+ * Resolve a project's effective export standard once: the CLI `--standard`
286
+ * override wins, else `export.standard`, else `'web'`. An unreadable config with
287
+ * no override fails closed with `'unknown'` so callers withhold standard-specific
288
+ * output rather than guess. The returned `config` already has the override
289
+ * applied, so consumers read it back directly. Exported for tests.
290
+ */
291
+ function readResolvedConfig(projectRoot, standardOverride) {
292
+ const read = readCourseConfig(projectRoot);
293
+ if (!read.ok) return {
294
+ ...read,
295
+ standard: standardOverride ?? "unknown"
296
+ };
297
+ const override = standardOverride;
298
+ const config = override ? {
299
+ ...read.config,
300
+ export: {
301
+ ...read.config.export,
302
+ standard: override
303
+ }
304
+ } : read.config;
305
+ return {
306
+ ok: true,
307
+ config,
308
+ standard: config.export?.standard || "web"
309
+ };
310
+ }
311
+ /**
285
312
  * Read a _meta.js file and extract its default export object.
286
313
  * Uses the same JSON5 approach as pageConfig extraction — find the object literal
287
314
  * after `export default` and parse it.
@@ -676,6 +703,17 @@ function isVideoEmbed(src) {
676
703
  }
677
704
  //#endregion
678
705
  //#region src/plugin/validation.ts
706
+ /** Collects errors and warnings so checkers thread one argument, not a pair. */
707
+ var Diagnostics = class {
708
+ errors = [];
709
+ warnings = [];
710
+ error(message) {
711
+ this.errors.push(message);
712
+ }
713
+ warn(message) {
714
+ this.warnings.push(message);
715
+ }
716
+ };
679
717
  /** Tier-1b rule IDs. `a11y.ignore` matches these literally. */
680
718
  const A11Y_IDS = {
681
719
  imageAlt: "tessera/image-alt",
@@ -729,21 +767,21 @@ function normalizeA11y(raw) {
729
767
  * promotable a11y warnings to errors) to a result in place. `ignore` suppresses
730
768
  * at any severity, including hard contract errors; `level` only re-rates.
731
769
  */
732
- function applyA11ySettings(result, settings) {
770
+ function applyA11ySettings(d, settings) {
733
771
  if (settings.ignore.length > 0) {
734
772
  const ignored = new Set(settings.ignore);
735
773
  const keep = (msg) => !isIgnored(msg, ignored);
736
- result.errors = result.errors.filter(keep);
737
- result.warnings = result.warnings.filter(keep);
774
+ d.errors = d.errors.filter(keep);
775
+ d.warnings = d.warnings.filter(keep);
738
776
  }
739
777
  if (settings.level === "error") {
740
778
  const remaining = [];
741
- for (const msg of result.warnings) {
779
+ for (const msg of d.warnings) {
742
780
  const id = diagnosticId(msg);
743
- if (id !== null && PROMOTABLE_A11Y_IDS.has(id)) result.errors.push(msg);
781
+ if (id !== null && PROMOTABLE_A11Y_IDS.has(id)) d.error(msg);
744
782
  else remaining.push(msg);
745
783
  }
746
- result.warnings = remaining;
784
+ d.warnings = remaining;
747
785
  }
748
786
  }
749
787
  /** Print validation warnings (yellow) then errors (red). Shared by the dev/build plugin and the CLI. */
@@ -757,6 +795,7 @@ const KNOWN_CONFIG_FIELDS = /* @__PURE__ */ new Set([
757
795
  "description",
758
796
  "author",
759
797
  "version",
798
+ "resume",
760
799
  "language",
761
800
  "branding",
762
801
  "navigation",
@@ -793,82 +832,77 @@ const VALID_RETRY_MODES = RETRY_MODES;
793
832
  * Validate a Tessera project at the given root.
794
833
  * Returns errors (block build) and warnings (informational).
795
834
  */
796
- function validateProject(projectRoot) {
835
+ function validateProject(projectRoot, standardOverride) {
797
836
  clearParseCache();
798
- const errors = [];
799
- const warnings = [];
837
+ const d = new Diagnostics();
800
838
  if (!existsSync(resolve(projectRoot, "course.config.js"))) {
801
- errors.push("course.config.js not found in project root");
802
- return {
803
- errors,
804
- warnings
805
- };
839
+ d.error("course.config.js not found in project root");
840
+ return d;
806
841
  }
807
- const config = parseConfig(projectRoot, errors, warnings);
808
- const pageResults = validatePages(resolve(projectRoot, "pages"), resolve(projectRoot, "assets"), projectRoot, config?.export?.standard);
809
- errors.push(...pageResults.errors);
810
- warnings.push(...pageResults.warnings);
842
+ const config = parseConfig(projectRoot, d, standardOverride);
843
+ const pageResults = validatePages(resolve(projectRoot, "pages"), resolve(projectRoot, "assets"), projectRoot, d, config?.export?.standard);
811
844
  for (const shellFile of ["layout.svelte", "quiz.svelte"]) {
812
845
  const shellPath = resolve(projectRoot, shellFile);
813
- if (existsSync(shellPath)) validateContractBypass(readSourceFileCached(shellPath), shellFile, errors);
846
+ if (existsSync(shellPath)) validateContractBypass(readSourceFileCached(shellPath), shellFile, d);
814
847
  }
815
- if (config) crossValidate(config, pageResults, errors, warnings);
816
- const result = {
817
- errors,
818
- warnings
819
- };
820
- applyA11ySettings(result, normalizeA11y(config?.a11y));
821
- return result;
848
+ if (config) crossValidate(config, pageResults, d);
849
+ applyA11ySettings(d, normalizeA11y(config?.a11y));
850
+ return d;
822
851
  }
823
- function parseConfig(projectRoot, errors, warnings) {
852
+ function parseConfig(projectRoot, d, standardOverride) {
824
853
  const read = readCourseConfig(projectRoot);
825
854
  if (!read.ok) {
826
- if (read.reason === "no-export") errors.push("course.config.js: must use `export default { ... }` syntax");
827
- else if (read.reason === "parse-error") errors.push("course.config.js: could not parse — JavaScript syntax error");
855
+ if (read.reason === "no-export") d.error("course.config.js: must use `export default { ... }` syntax");
856
+ else if (read.reason === "parse-error") d.error("course.config.js: could not parse — JavaScript syntax error");
828
857
  return null;
829
858
  }
830
859
  const config = read.config;
831
- for (const key of Object.keys(config)) if (!KNOWN_CONFIG_FIELDS.has(key)) warnings.push(`course.config.js: unknown field "${key}" — will be ignored`);
832
- if (config.title !== void 0 && typeof config.title !== "string") errors.push(`course.config.js: "title" must be a string, got ${typeof config.title}`);
833
- else if (config.title === void 0 || config.title === "") warnings.push("course.config.js: \"title\" is missing or empty — the course will ship as \"Untitled Course\"");
834
- else if (config.title.trim() === "") warnings.push("course.config.js: \"title\" is only whitespace — it ships verbatim and will not fall back to \"Untitled Course\"");
835
- if (config.branding !== void 0) validateBranding(config.branding, warnings);
836
- if (config.language === void 0) warnings.push(tag(A11Y_IDS.lang, `course.config.js: "language" is not set — defaulting <html lang> to "en". Set it to the course's language (BCP-47, e.g. "en", "fr-CA") for WCAG 3.1.1.`));
837
- else if (!isPlausibleLanguageTag(config.language)) warnings.push(tag(A11Y_IDS.lang, `course.config.js: "language" (${JSON.stringify(config.language)}) is not a plausible BCP-47 tag — use e.g. "en", "es", or "fr-CA"`));
860
+ for (const key of Object.keys(config)) if (!KNOWN_CONFIG_FIELDS.has(key)) d.warn(`course.config.js: unknown field "${key}" — will be ignored`);
861
+ if (config.title !== void 0 && typeof config.title !== "string") d.error(`course.config.js: "title" must be a string, got ${typeof config.title}`);
862
+ else if (config.title === void 0 || config.title === "") d.warn("course.config.js: \"title\" is missing or empty — the course will ship as \"Untitled Course\"");
863
+ else if (config.title.trim() === "") d.warn("course.config.js: \"title\" is only whitespace — it ships verbatim and will not fall back to \"Untitled Course\"");
864
+ if (config.branding !== void 0) validateBranding(config.branding, d);
865
+ if (config.language === void 0) d.warn(tag(A11Y_IDS.lang, `course.config.js: "language" is not set — defaulting <html lang> to "en". Set it to the course's language (BCP-47, e.g. "en", "fr-CA") for WCAG 3.1.1.`));
866
+ else if (!isPlausibleLanguageTag(config.language)) d.warn(tag(A11Y_IDS.lang, `course.config.js: "language" (${JSON.stringify(config.language)}) is not a plausible BCP-47 tag — use e.g. "en", "es", or "fr-CA"`));
867
+ if (config.export?.standard !== void 0) {
868
+ if (!VALID_EXPORT_STANDARDS.includes(config.export.standard)) d.error(`course.config.js: "export.standard" must be "web", "scorm12", "scorm2004", "cmi5", or "xapi", got "${config.export.standard}"`);
869
+ }
870
+ if (standardOverride) config.export = {
871
+ ...config.export,
872
+ standard: standardOverride
873
+ };
838
874
  const standard = config.export?.standard;
839
- if ((standard === void 0 || standard === "web" || standard === "cmi5" || standard === "xapi") && !courseIdentity(config)) warnings.push(`course.config.js: no "id" set — the web storage key and cmi5/xAPI activity id then share a fixed fallback that collides across courses. Add a unique id (e.g. "urn:uuid:…"); scaffolded courses include one.`);
840
- if (config.a11y !== void 0) validateA11yConfig(config.a11y, errors);
875
+ if ((standard === void 0 || standard === "web" || standard === "cmi5" || standard === "xapi") && !courseIdentity(config)) d.warn(`course.config.js: no "id" set — the web storage key and cmi5/xAPI activity id then share a fixed fallback that collides across courses. Add a unique id (e.g. "urn:uuid:…"); scaffolded courses include one.`);
876
+ if (config.a11y !== void 0) validateA11yConfig(config.a11y, d);
841
877
  if (config.navigation?.mode !== void 0) {
842
- if (!VALID_NAV_MODES.includes(config.navigation.mode)) errors.push(`course.config.js: "navigation.mode" must be "free" or "sequential", got "${config.navigation.mode}"`);
878
+ if (!VALID_NAV_MODES.includes(config.navigation.mode)) d.error(`course.config.js: "navigation.mode" must be "free" or "sequential", got "${config.navigation.mode}"`);
843
879
  }
844
880
  if (config.completion?.mode !== void 0) {
845
- if (!VALID_COMPLETION_MODES.includes(config.completion.mode)) errors.push(`course.config.js: "completion.mode" must be "quiz", "percentage", or "manual", got "${config.completion.mode}"`);
881
+ if (!VALID_COMPLETION_MODES.includes(config.completion.mode)) d.error(`course.config.js: "completion.mode" must be "quiz", "percentage", or "manual", got "${config.completion.mode}"`);
846
882
  }
847
883
  if (config.completion?.trigger !== void 0) {
848
- if (config.completion.mode !== "manual") warnings.push(`course.config.js: "completion.trigger" is ignored unless completion.mode is "manual"`);
849
- else if (!VALID_MANUAL_TRIGGERS.includes(config.completion.trigger)) errors.push(`course.config.js: "completion.trigger" must be "page" or omitted, got "${config.completion.trigger}"`);
884
+ if (config.completion.mode !== "manual") d.warn(`course.config.js: "completion.trigger" is ignored unless completion.mode is "manual"`);
885
+ else if (!VALID_MANUAL_TRIGGERS.includes(config.completion.trigger)) d.error(`course.config.js: "completion.trigger" must be "page" or omitted, got "${config.completion.trigger}"`);
850
886
  }
851
887
  if (config.completion?.requireSuccessStatus !== void 0) {
852
- if (config.completion.mode !== "manual") warnings.push(`course.config.js: "completion.requireSuccessStatus" is ignored unless completion.mode is "manual"`);
853
- else if (!VALID_REQUIRE_SUCCESS_STATUS.includes(config.completion.requireSuccessStatus)) errors.push(`course.config.js: "completion.requireSuccessStatus" must be "passed" or "failed" (omit for "unknown"), got "${config.completion.requireSuccessStatus}"`);
854
- }
855
- if (config.export?.standard !== void 0) {
856
- if (!VALID_EXPORT_STANDARDS.includes(config.export.standard)) errors.push(`course.config.js: "export.standard" must be "web", "scorm12", "scorm2004", "cmi5", or "xapi", got "${config.export.standard}"`);
888
+ if (config.completion.mode !== "manual") d.warn(`course.config.js: "completion.requireSuccessStatus" is ignored unless completion.mode is "manual"`);
889
+ else if (!VALID_REQUIRE_SUCCESS_STATUS.includes(config.completion.requireSuccessStatus)) d.error(`course.config.js: "completion.requireSuccessStatus" must be "passed" or "failed" (omit for "unknown"), got "${config.completion.requireSuccessStatus}"`);
857
890
  }
891
+ if (config.resume !== void 0 && config.resume !== "auto" && config.resume !== "never") d.error(`course.config.js: "resume" must be "auto" or "never", got "${config.resume}"`);
858
892
  if (config.export?.csp !== void 0) {
859
893
  const csp = config.export.csp;
860
- if (csp !== false && !isCspOverrides(csp)) warnings.push("course.config.js: \"export.csp\" must be false or an object of directive → string[]; ignoring it and using the baseline CSP");
861
- else if ((config.export.standard ?? "web") !== "web") warnings.push(`course.config.js: "export.csp" is ignored when "export.standard" is "${config.export.standard}" (the CSP meta is web-export only)`);
894
+ if (csp !== false && !isCspOverrides(csp)) d.warn("course.config.js: \"export.csp\" must be false or an object of directive → string[]; ignoring it and using the baseline CSP");
895
+ else if ((config.export.standard ?? "web") !== "web") d.warn(`course.config.js: "export.csp" is ignored when "export.standard" is "${config.export.standard}" (the CSP meta is web-export only)`);
862
896
  }
863
897
  if (config.scoring?.passingScore !== void 0) {
864
898
  const score = config.scoring.passingScore;
865
- if (typeof score !== "number" || score < 0 || score > 100) errors.push(`course.config.js: "scoring.passingScore" must be 0–100, got ${score}`);
899
+ if (typeof score !== "number" || score < 0 || score > 100) d.error(`course.config.js: "scoring.passingScore" must be 0–100, got ${score}`);
866
900
  }
867
901
  if (config.completion?.percentageThreshold !== void 0) {
868
902
  const threshold = config.completion.percentageThreshold;
869
- if (typeof threshold !== "number" || threshold < 0 || threshold > 100) errors.push(`course.config.js: "completion.percentageThreshold" must be 0–100, got ${threshold}`);
903
+ if (typeof threshold !== "number" || threshold < 0 || threshold > 100) d.error(`course.config.js: "completion.percentageThreshold" must be 0–100, got ${threshold}`);
870
904
  }
871
- if (config.xapi !== void 0) validateXAPIConfig(config.xapi, config.export?.standard ?? "web", errors, warnings);
905
+ if (config.xapi !== void 0) validateXAPIConfig(config.xapi, config.export?.standard ?? "web", d);
872
906
  return config;
873
907
  }
874
908
  const HEX_COLOR_RE = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
@@ -886,142 +920,142 @@ function isPlausibleColor(value) {
886
920
  function describeType(raw) {
887
921
  return raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw;
888
922
  }
889
- function validateBranding(raw, warnings) {
923
+ function validateBranding(raw, d) {
890
924
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
891
- warnings.push(`course.config.js: "branding" must be an object, got ${describeType(raw)} — will be ignored`);
925
+ d.warn(`course.config.js: "branding" must be an object, got ${describeType(raw)} — will be ignored`);
892
926
  return;
893
927
  }
894
928
  const branding = raw;
895
929
  const logo = branding.logo;
896
930
  if (logo !== void 0) {
897
- if (typeof logo !== "string") warnings.push(`course.config.js: "branding.logo" must be a string, got ${typeof logo}`);
898
- else if (logo.startsWith("$assets/")) warnings.push("course.config.js: \"branding.logo\" starts with \"$assets/\", but branding paths are not asset-resolved — it will ship as a literal, broken src. Use a URL or a path relative to the deployed root.");
931
+ if (typeof logo !== "string") d.warn(`course.config.js: "branding.logo" must be a string, got ${typeof logo}`);
932
+ else if (logo.startsWith("$assets/")) d.warn("course.config.js: \"branding.logo\" starts with \"$assets/\", but branding paths are not asset-resolved — it will ship as a literal, broken src. Use a URL or a path relative to the deployed root.");
899
933
  }
900
934
  const primaryColor = branding.primaryColor;
901
- if (primaryColor !== void 0) if (typeof primaryColor !== "string") warnings.push(`course.config.js: "branding.primaryColor" must be a string, got ${typeof primaryColor}`);
902
- else if (!isPlausibleColor(primaryColor)) warnings.push(`course.config.js: "branding.primaryColor" "${primaryColor}" does not look like a valid CSS color — the theme will fall back to its default shades if the browser can't parse it`);
935
+ if (primaryColor !== void 0) if (typeof primaryColor !== "string") d.warn(`course.config.js: "branding.primaryColor" must be a string, got ${typeof primaryColor}`);
936
+ else if (!isPlausibleColor(primaryColor)) d.warn(`course.config.js: "branding.primaryColor" "${primaryColor}" does not look like a valid CSS color — the theme will fall back to its default shades if the browser can't parse it`);
903
937
  else {
904
938
  const ratio = contrastRatio(primaryColor, "#ffffff");
905
- if (ratio !== null && ratio < 4.5) warnings.push(tag(A11Y_IDS.primaryContrast, `course.config.js: branding.primaryColor (${primaryColor}) is ${ratio.toFixed(2)}:1 against white — it's used both for links on the page background and as a button fill behind white text, and WCAG AA needs 4.5:1 for each`));
939
+ if (ratio !== null && ratio < 4.5) d.warn(tag(A11Y_IDS.primaryContrast, `course.config.js: branding.primaryColor (${primaryColor}) is ${ratio.toFixed(2)}:1 against white — it's used both for links on the page background and as a button fill behind white text, and WCAG AA needs 4.5:1 for each`));
906
940
  }
907
941
  const fontFamily = branding.fontFamily;
908
- if (fontFamily !== void 0 && typeof fontFamily !== "string") warnings.push(`course.config.js: "branding.fontFamily" must be a string, got ${typeof fontFamily}`);
942
+ if (fontFamily !== void 0 && typeof fontFamily !== "string") d.warn(`course.config.js: "branding.fontFamily" must be a string, got ${typeof fontFamily}`);
909
943
  }
910
944
  /** Shape-check the `a11y` block. Malformed values can't be silenced by `ignore`. */
911
- function validateA11yConfig(raw, errors) {
945
+ function validateA11yConfig(raw, d) {
912
946
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
913
- errors.push(`course.config.js: "a11y" must be an object, got ${describeType(raw)}`);
947
+ d.error(`course.config.js: "a11y" must be an object, got ${describeType(raw)}`);
914
948
  return;
915
949
  }
916
950
  const a11y = raw;
917
- if (a11y.level !== void 0 && !VALID_A11Y_LEVELS.includes(a11y.level)) errors.push(`course.config.js: "a11y.level" must be "warn" or "error", got ${JSON.stringify(a11y.level)}`);
918
- if (a11y.standard !== void 0 && !VALID_A11Y_STANDARDS.includes(a11y.standard)) errors.push(`course.config.js: "a11y.standard" must be "wcag2a", "wcag2aa", or "wcag21aa", got ${JSON.stringify(a11y.standard)}`);
951
+ if (a11y.level !== void 0 && !VALID_A11Y_LEVELS.includes(a11y.level)) d.error(`course.config.js: "a11y.level" must be "warn" or "error", got ${JSON.stringify(a11y.level)}`);
952
+ if (a11y.standard !== void 0 && !VALID_A11Y_STANDARDS.includes(a11y.standard)) d.error(`course.config.js: "a11y.standard" must be "wcag2a", "wcag2aa", or "wcag21aa", got ${JSON.stringify(a11y.standard)}`);
919
953
  if (a11y.ignore !== void 0) {
920
- if (!Array.isArray(a11y.ignore) || a11y.ignore.some((x) => typeof x !== "string")) errors.push(`course.config.js: "a11y.ignore" must be an array of rule-ID strings`);
954
+ if (!Array.isArray(a11y.ignore) || a11y.ignore.some((x) => typeof x !== "string")) d.error(`course.config.js: "a11y.ignore" must be an array of rule-ID strings`);
921
955
  }
922
956
  }
923
957
  const 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;
924
- function validateXAPIConfig(raw, standard, errors, warnings) {
958
+ function validateXAPIConfig(raw, standard, d) {
925
959
  if (raw === void 0 || raw === null) return;
926
960
  const entries = Array.isArray(raw) ? raw : [raw];
927
961
  if (Array.isArray(raw)) {
928
962
  if (entries.length === 0) {
929
- errors.push("course.config.js: xapi must contain at least one destination, or be omitted");
963
+ d.error("course.config.js: xapi must contain at least one destination, or be omitted");
930
964
  return;
931
965
  }
932
- if (entries.filter((e) => e && typeof e === "object" && e.endpoint === "lms").length > 1) errors.push("course.config.js: xapi has multiple entries with endpoint: 'lms' — only one launch-inherited destination is allowed");
966
+ if (entries.filter((e) => e && typeof e === "object" && e.endpoint === "lms").length > 1) d.error("course.config.js: xapi has multiple entries with endpoint: 'lms' — only one launch-inherited destination is allowed");
933
967
  const seen = /* @__PURE__ */ new Map();
934
968
  for (const e of entries) if (e && typeof e === "object") {
935
969
  const ep = e.endpoint;
936
970
  if (typeof ep === "string" && ep !== "lms") seen.set(ep, (seen.get(ep) ?? 0) + 1);
937
971
  }
938
- for (const [ep, count] of seen) if (count > 1) warnings.push(`course.config.js: xapi has ${count} entries with endpoint "${ep}" — usually a copy-paste mistake; fan-out to the same LRS with different actors/activityIds is supported but uncommon.`);
972
+ for (const [ep, count] of seen) if (count > 1) d.warn(`course.config.js: xapi has ${count} entries with endpoint "${ep}" — usually a copy-paste mistake; fan-out to the same LRS with different actors/activityIds is supported but uncommon.`);
939
973
  } else if (typeof raw !== "object") {
940
- errors.push("course.config.js: xapi must be an object or an array of objects");
974
+ d.error("course.config.js: xapi must be an object or an array of objects");
941
975
  return;
942
976
  }
943
977
  for (let i = 0; i < entries.length; i++) {
944
978
  const entry = entries[i];
945
979
  const label = Array.isArray(raw) ? `xapi[${i}]` : "xapi";
946
980
  if (!entry || typeof entry !== "object") {
947
- errors.push(`course.config.js: ${label} must be an object`);
981
+ d.error(`course.config.js: ${label} must be an object`);
948
982
  continue;
949
983
  }
950
- validateSingleXAPIEntry(entry, label, standard, errors, warnings);
984
+ validateSingleXAPIEntry(entry, label, standard, d);
951
985
  }
952
986
  }
953
- function validateSingleXAPIEntry(entry, label, standard, errors, warnings) {
987
+ function validateSingleXAPIEntry(entry, label, standard, d) {
954
988
  const endpoint = entry.endpoint;
955
989
  if (endpoint === void 0) {
956
- errors.push(`course.config.js: ${label}.endpoint is required`);
990
+ d.error(`course.config.js: ${label}.endpoint is required`);
957
991
  return;
958
992
  }
959
993
  if (typeof endpoint !== "string") {
960
- errors.push(`course.config.js: ${label}.endpoint must be a string`);
994
+ d.error(`course.config.js: ${label}.endpoint must be a string`);
961
995
  return;
962
996
  }
963
997
  if (endpoint === "lms") {
964
- if (standard !== "cmi5" && standard !== "xapi") errors.push(`course.config.js: ${label}.endpoint: 'lms' requires export.standard: 'cmi5' or 'xapi' (you have "${standard}"). Either change the export standard or specify an explicit LRS endpoint.`);
998
+ if (standard !== "cmi5" && standard !== "xapi") d.error(`course.config.js: ${label}.endpoint: 'lms' requires export.standard: 'cmi5' or 'xapi' (you have "${standard}"). Either change the export standard or specify an explicit LRS endpoint.`);
965
999
  for (const f of [
966
1000
  "auth",
967
1001
  "actor",
968
1002
  "activityId",
969
1003
  "registration",
970
1004
  "actorAccountHomePage"
971
- ]) if (entry[f] !== void 0) errors.push(`course.config.js: ${label}.${f} must be omitted when ${label}.endpoint is 'lms' — it is inherited from the launch.`);
1005
+ ]) if (entry[f] !== void 0) d.error(`course.config.js: ${label}.${f} must be omitted when ${label}.endpoint is 'lms' — it is inherited from the launch.`);
972
1006
  return;
973
1007
  }
974
1008
  let url;
975
1009
  try {
976
1010
  url = new URL(endpoint);
977
1011
  } catch {
978
- errors.push(`course.config.js: ${label}.endpoint must be an absolute http(s) URL, got "${endpoint}"`);
1012
+ d.error(`course.config.js: ${label}.endpoint must be an absolute http(s) URL, got "${endpoint}"`);
979
1013
  return;
980
1014
  }
981
1015
  if (url.protocol !== "http:" && url.protocol !== "https:") {
982
- errors.push(`course.config.js: ${label}.endpoint must use http: or https:, got "${url.protocol}"`);
1016
+ d.error(`course.config.js: ${label}.endpoint must use http: or https:, got "${url.protocol}"`);
983
1017
  return;
984
1018
  }
985
- if (url.protocol === "http:" && process.env.NODE_ENV === "production") warnings.push(`course.config.js: ${label}.endpoint uses http:; LRS credentials will travel in cleartext. Use https in production.`);
986
- if (!endpoint.endsWith("/")) warnings.push(`course.config.js: ${label}.endpoint should end with a slash to avoid concatenation surprises (e.g. 'https://lrs.example.com/xapi/' not 'https://lrs.example.com/xapi'). Runtime normalizes regardless.`);
1019
+ if (url.protocol === "http:" && process.env.NODE_ENV === "production") d.warn(`course.config.js: ${label}.endpoint uses http:; LRS credentials will travel in cleartext. Use https in production.`);
1020
+ if (!endpoint.endsWith("/")) d.warn(`course.config.js: ${label}.endpoint should end with a slash to avoid concatenation surprises (e.g. 'https://lrs.example.com/xapi/' not 'https://lrs.example.com/xapi'). Runtime normalizes regardless.`);
987
1021
  const auth = entry.auth;
988
- if (auth === void 0) errors.push(`course.config.js: ${label}.auth is required`);
1022
+ if (auth === void 0) d.error(`course.config.js: ${label}.auth is required`);
989
1023
  else if (typeof auth === "string") {
990
1024
  const authErr = validateAuthCredential(auth);
991
- if (authErr) errors.push(`course.config.js: ${joinFieldError(`${label}.auth`, authErr)}`);
992
- else warnings.push(`course.config.js: ${label}.auth is a static string and will be embedded in the bundle. For production, pass a function that fetches a short-lived token from a server endpoint.`);
993
- } else if (typeof auth !== "function") errors.push(`course.config.js: ${label}.auth must be a string or a function, got ${typeof auth}`);
1025
+ if (authErr) d.error(`course.config.js: ${joinFieldError(`${label}.auth`, authErr)}`);
1026
+ else d.warn(`course.config.js: ${label}.auth is a static string and will be embedded in the bundle. For production, pass a function that fetches a short-lived token from a server endpoint.`);
1027
+ } else if (typeof auth !== "function") d.error(`course.config.js: ${label}.auth must be a string or a function, got ${typeof auth}`);
994
1028
  const activityId = entry.activityId;
995
- if (activityId === void 0 || activityId === "") errors.push(`course.config.js: ${label}.activityId is required`);
996
- else if (typeof activityId !== "string") errors.push(`course.config.js: ${label}.activityId must be a string`);
1029
+ if (activityId === void 0 || activityId === "") d.error(`course.config.js: ${label}.activityId is required`);
1030
+ else if (typeof activityId !== "string") d.error(`course.config.js: ${label}.activityId must be a string`);
997
1031
  else try {
998
1032
  new URL(activityId);
999
1033
  } catch {
1000
- errors.push(`course.config.js: ${label}.activityId must be an absolute IRI, got "${activityId}"`);
1034
+ d.error(`course.config.js: ${label}.activityId must be an absolute IRI, got "${activityId}"`);
1001
1035
  }
1002
1036
  const actor = entry.actor;
1003
1037
  if (actor === void 0) {
1004
- if (standard === "web") errors.push(`course.config.js: ${label}.actor is required for web export — there is no LMS to derive a learner identity from. Provide either a static actor object or a function that resolves one (e.g. from your auth system).`);
1038
+ if (standard === "web") d.error(`course.config.js: ${label}.actor is required for web export — there is no LMS to derive a learner identity from. Provide either a static actor object or a function that resolves one (e.g. from your auth system).`);
1005
1039
  } else if (typeof actor === "object" && actor !== null) {
1006
1040
  const err = validateAgent(actor);
1007
- if (err) errors.push(`course.config.js: ${joinFieldError(`${label}.actor`, err)}`);
1008
- } else if (typeof actor !== "function") errors.push(`course.config.js: ${label}.actor must be an object or function, got ${typeof actor}`);
1041
+ if (err) d.error(`course.config.js: ${joinFieldError(`${label}.actor`, err)}`);
1042
+ } else if (typeof actor !== "function") d.error(`course.config.js: ${label}.actor must be an object or function, got ${typeof actor}`);
1009
1043
  const aahp = entry.actorAccountHomePage;
1010
1044
  if (aahp !== void 0) {
1011
- if (typeof aahp !== "string") errors.push(`course.config.js: ${label}.actorAccountHomePage must be a string`);
1045
+ if (typeof aahp !== "string") d.error(`course.config.js: ${label}.actorAccountHomePage must be a string`);
1012
1046
  else try {
1013
1047
  new URL(aahp);
1014
1048
  } catch {
1015
- errors.push(`course.config.js: ${label}.actorAccountHomePage must be an absolute URL`);
1049
+ d.error(`course.config.js: ${label}.actorAccountHomePage must be an absolute URL`);
1016
1050
  }
1017
- if (actor !== void 0) warnings.push(`course.config.js: ${label}.actorAccountHomePage is ignored when ${label}.actor is supplied explicitly.`);
1018
- if (standard === "cmi5" || standard === "xapi" || standard === "web") warnings.push(`course.config.js: ${label}.actorAccountHomePage is only used under scorm12/scorm2004 actor synthesis; ignored under "${standard}".`);
1051
+ if (actor !== void 0) d.warn(`course.config.js: ${label}.actorAccountHomePage is ignored when ${label}.actor is supplied explicitly.`);
1052
+ if (standard === "cmi5" || standard === "xapi" || standard === "web") d.warn(`course.config.js: ${label}.actorAccountHomePage is only used under scorm12/scorm2004 actor synthesis; ignored under "${standard}".`);
1019
1053
  }
1020
- if (actor === void 0 && (standard === "scorm12" || standard === "scorm2004") && typeof activityId === "string" && httpOrigin(activityId) === null && aahp === void 0) errors.push(`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. Provide ${label}.actorAccountHomePage explicitly.`);
1054
+ if (actor === void 0 && (standard === "scorm12" || standard === "scorm2004") && typeof activityId === "string" && httpOrigin(activityId) === null && aahp === void 0) d.error(`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. Provide ${label}.actorAccountHomePage explicitly.`);
1021
1055
  const registration = entry.registration;
1022
1056
  if (registration !== void 0) {
1023
- if (typeof registration !== "string" || !UUID_RE.test(registration)) errors.push(`course.config.js: ${label}.registration must be a UUID v4, got "${String(registration)}"`);
1024
- if (standard !== "cmi5" && standard !== "xapi") warnings.push(`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.`);
1057
+ if (typeof registration !== "string" || !UUID_RE.test(registration)) d.error(`course.config.js: ${label}.registration must be a UUID v4, got "${String(registration)}"`);
1058
+ if (standard !== "cmi5" && standard !== "xapi") d.warn(`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.`);
1025
1059
  }
1026
1060
  }
1027
1061
  /**
@@ -1029,12 +1063,12 @@ function validateSingleXAPIEntry(entry, label, standard, errors, warnings) {
1029
1063
  * lesson-level pages — the validation is identical, only the containing
1030
1064
  * directory differs.
1031
1065
  */
1032
- function validatePageFile(filePath, projectRoot, assetsDir, navIndex, errors, warnings, assetExistsCache, exportStandard) {
1066
+ function validatePageFile(filePath, projectRoot, assetsDir, navIndex, d, assetExistsCache, exportStandard) {
1033
1067
  const fileRel = relative(projectRoot, filePath);
1034
1068
  const content = readSourceFileCached(filePath);
1035
1069
  const parseError = getParseError(content);
1036
1070
  if (parseError) {
1037
- errors.push(`${fileRel}: could not parse — ${parseError}`);
1071
+ d.error(`${fileRel}: could not parse — ${parseError}`);
1038
1072
  return {
1039
1073
  page: {
1040
1074
  fileRel,
@@ -1048,20 +1082,20 @@ function validatePageFile(filePath, projectRoot, assetsDir, navIndex, errors, wa
1048
1082
  parseError: true
1049
1083
  };
1050
1084
  }
1051
- const pageConfig = validatePageConfig(content, fileRel, errors);
1085
+ const pageConfig = validatePageConfig(content, fileRel, d);
1052
1086
  const isQuiz = !!pageConfig?.quiz;
1053
1087
  let isGradedQuiz = false;
1054
1088
  if (pageConfig?.quiz) {
1055
- validateQuizConfig(pageConfig.quiz, fileRel, errors);
1089
+ validateQuizConfig(pageConfig.quiz, fileRel, d);
1056
1090
  if (pageConfig.quiz.graded === true) isGradedQuiz = true;
1057
1091
  }
1058
- const completesOnView = validateCompletesOn(pageConfig, fileRel, errors);
1059
- validateAssetRefs(content, fileRel, assetsDir, warnings, assetExistsCache);
1060
- validateQuestionComponents(content, fileRel, errors, warnings, exportStandard);
1061
- validateMediaComponents(content, fileRel, errors, warnings);
1062
- validateHeadingOrder(content, fileRel, warnings);
1063
- validateContractBypass(content, fileRel, errors);
1064
- if (pageConfig?.quiz && !HAS_USE_QUESTION_RE.test(content) && !HAS_QUESTION_TAG_RE.test(content) && !HAS_LOCAL_SVELTE_IMPORT_RE.test(content)) warnings.push(`${fileRel}: quiz page has no question components or useQuestion() calls — the quiz will have nothing to score`);
1092
+ const completesOnView = validateCompletesOn(pageConfig, fileRel, d);
1093
+ validateAssetRefs(content, fileRel, assetsDir, d, assetExistsCache);
1094
+ validateQuestionComponents(content, fileRel, d, exportStandard);
1095
+ validateMediaComponents(content, fileRel, d);
1096
+ validateHeadingOrder(content, fileRel, d);
1097
+ validateContractBypass(content, fileRel, d);
1098
+ if (pageConfig?.quiz && !HAS_USE_QUESTION_RE.test(content) && !HAS_QUESTION_TAG_RE.test(content) && !HAS_LOCAL_SVELTE_IMPORT_RE.test(content)) d.warn(`${fileRel}: quiz page has no question components or useQuestion() calls — the quiz will have nothing to score`);
1065
1099
  return {
1066
1100
  page: {
1067
1101
  fileRel,
@@ -1075,9 +1109,7 @@ function validatePageFile(filePath, projectRoot, assetsDir, navIndex, errors, wa
1075
1109
  parseError: false
1076
1110
  };
1077
1111
  }
1078
- function validatePages(pagesDir, assetsDir, projectRoot, exportStandard) {
1079
- const errors = [];
1080
- const warnings = [];
1112
+ function validatePages(pagesDir, assetsDir, projectRoot, d, exportStandard) {
1081
1113
  const pages = [];
1082
1114
  let totalPages = 0;
1083
1115
  let totalQuizzes = 0;
@@ -1085,10 +1117,8 @@ function validatePages(pagesDir, assetsDir, projectRoot, exportStandard) {
1085
1117
  let hasParseErrors = false;
1086
1118
  const assetExistsCache = /* @__PURE__ */ new Map();
1087
1119
  const noPages = () => {
1088
- errors.push("No pages found. Create at least one section with a lesson and page in pages/");
1120
+ d.error("No pages found. Create at least one section with a lesson and page in pages/");
1089
1121
  return {
1090
- errors,
1091
- warnings,
1092
1122
  totalPages,
1093
1123
  totalQuizzes,
1094
1124
  hasGradedQuiz,
@@ -1099,21 +1129,21 @@ function validatePages(pagesDir, assetsDir, projectRoot, exportStandard) {
1099
1129
  if (!existsSync(pagesDir)) return noPages();
1100
1130
  for (const entry of readdirSync(pagesDir)) {
1101
1131
  const fullPath = resolve(pagesDir, entry);
1102
- if (entry.endsWith(".svelte") && statSync(fullPath).isFile()) warnings.push(`${relative(projectRoot, fullPath)}: this file is outside the section/lesson structure and will be ignored`);
1132
+ if (entry.endsWith(".svelte") && statSync(fullPath).isFile()) d.warn(`${relative(projectRoot, fullPath)}: this file is outside the section/lesson structure and will be ignored`);
1103
1133
  }
1104
1134
  const sections = walkPages(pagesDir);
1105
1135
  if (sections.length === 0) return noPages();
1106
1136
  const validateLesson = (lesson, meta) => {
1107
1137
  if (meta?.pages) for (const pageName of meta.pages) {
1108
1138
  const fileName = ensureSvelteSuffix(pageName);
1109
- if (!lesson.files.includes(fileName)) errors.push(`${relative(projectRoot, lesson.metaPath)}: pages array lists "${pageName}" but ${fileName} not found in this directory`);
1139
+ if (!lesson.files.includes(fileName)) d.error(`${relative(projectRoot, lesson.metaPath)}: pages array lists "${pageName}" but ${fileName} not found in this directory`);
1110
1140
  }
1111
1141
  if (meta?.pages && meta.pages.length > 0) {
1112
1142
  const listedSet = new Set(meta.pages.map(ensureSvelteSuffix));
1113
- for (const file of lesson.files) if (!listedSet.has(file)) warnings.push(`${relative(projectRoot, resolve(lesson.dir, file))}: not listed in _meta.js pages array — will be appended at end`);
1143
+ for (const file of lesson.files) if (!listedSet.has(file)) d.warn(`${relative(projectRoot, resolve(lesson.dir, file))}: not listed in _meta.js pages array — will be appended at end`);
1114
1144
  }
1115
1145
  for (const fileName of orderPageFiles(lesson.files, meta?.pages)) {
1116
- const result = validatePageFile(resolve(lesson.dir, fileName), projectRoot, assetsDir, totalPages, errors, warnings, assetExistsCache, exportStandard);
1146
+ const result = validatePageFile(resolve(lesson.dir, fileName), projectRoot, assetsDir, totalPages, d, assetExistsCache, exportStandard);
1117
1147
  totalPages++;
1118
1148
  if (result.isQuiz) totalQuizzes++;
1119
1149
  if (result.isGradedQuiz) hasGradedQuiz = true;
@@ -1124,15 +1154,13 @@ function validatePages(pagesDir, assetsDir, projectRoot, exportStandard) {
1124
1154
  for (const section of sections) {
1125
1155
  const sectionRel = relative(projectRoot, section.dir);
1126
1156
  const pagesBeforeSection = totalPages;
1127
- const sectionMeta = validateMetaFile(section.metaPath, sectionRel, errors);
1157
+ const sectionMeta = validateMetaFile(section.metaPath, sectionRel, d);
1128
1158
  for (const lesson of section.lessons) if (lesson.name === null) validateLesson(lesson, sectionMeta);
1129
- else validateLesson(lesson, validateMetaFile(lesson.metaPath, relative(projectRoot, lesson.dir), errors));
1130
- if (totalPages === pagesBeforeSection) warnings.push(`${sectionRel}: section contributed no pages and will be empty`);
1159
+ else validateLesson(lesson, validateMetaFile(lesson.metaPath, relative(projectRoot, lesson.dir), d));
1160
+ if (totalPages === pagesBeforeSection) d.warn(`${sectionRel}: section contributed no pages and will be empty`);
1131
1161
  }
1132
1162
  if (totalPages === 0) return noPages();
1133
1163
  return {
1134
- errors,
1135
- warnings,
1136
1164
  totalPages,
1137
1165
  totalQuizzes,
1138
1166
  hasGradedQuiz,
@@ -1140,50 +1168,50 @@ function validatePages(pagesDir, assetsDir, projectRoot, exportStandard) {
1140
1168
  pages
1141
1169
  };
1142
1170
  }
1143
- function validateMetaFile(metaPath, parentRel, errors) {
1171
+ function validateMetaFile(metaPath, parentRel, d) {
1144
1172
  if (!existsSync(metaPath)) return null;
1145
1173
  const metaRel = `${parentRel}/_meta.js`;
1146
1174
  const result = defaultExportObjectLiteral(readSourceFileCached(metaPath));
1147
1175
  if (result.kind === "parse-error") {
1148
- errors.push(`${metaRel}: could not parse — JavaScript syntax error`);
1176
+ d.error(`${metaRel}: could not parse — JavaScript syntax error`);
1149
1177
  return null;
1150
1178
  }
1151
1179
  if (result.kind !== "literal") {
1152
- errors.push(`${metaRel}: syntax error — must export default { title: "..." }`);
1180
+ d.error(`${metaRel}: syntax error — must export default { title: "..." }`);
1153
1181
  return null;
1154
1182
  }
1155
1183
  let meta;
1156
1184
  try {
1157
1185
  meta = JSON5.parse(result.text);
1158
1186
  } catch {
1159
- errors.push(`${metaRel}: syntax error — must export default { title: "..." }`);
1187
+ d.error(`${metaRel}: syntax error — must export default { title: "..." }`);
1160
1188
  return null;
1161
1189
  }
1162
- if (!meta.title) errors.push(`${metaRel}: missing required "title" field`);
1190
+ if (!meta.title) d.error(`${metaRel}: missing required "title" field`);
1163
1191
  return meta;
1164
1192
  }
1165
- function validatePageConfig(content, fileRel, errors) {
1193
+ function validatePageConfig(content, fileRel, d) {
1166
1194
  const result = parsePageConfigFromSource(content);
1167
1195
  if (result.kind === "ok") return result.value;
1168
- if (result.kind === "invalid") errors.push(`${fileRel}: pageConfig must be a static object literal (no variables, function calls, or computed values)`);
1196
+ if (result.kind === "invalid") d.error(`${fileRel}: pageConfig must be a static object literal (no variables, function calls, or computed values)`);
1169
1197
  return null;
1170
1198
  }
1171
- function validateCompletesOn(pageConfig, fileRel, errors) {
1199
+ function validateCompletesOn(pageConfig, fileRel, d) {
1172
1200
  if (!pageConfig || pageConfig.completesOn === void 0) return false;
1173
1201
  if (pageConfig.completesOn === "view") return true;
1174
- errors.push(`${fileRel}: pageConfig.completesOn must be "view", got ${JSON.stringify(pageConfig.completesOn)}`);
1202
+ d.error(`${fileRel}: pageConfig.completesOn must be "view", got ${JSON.stringify(pageConfig.completesOn)}`);
1175
1203
  return false;
1176
1204
  }
1177
- function validateQuizConfig(quiz, fileRel, errors) {
1205
+ function validateQuizConfig(quiz, fileRel, d) {
1178
1206
  if (!quiz || typeof quiz !== "object") return;
1179
1207
  const cfg = quiz;
1180
1208
  if (cfg.maxAttempts !== void 0) {
1181
1209
  const val = cfg.maxAttempts;
1182
- if (val !== Infinity && (typeof val !== "number" || val <= 0 || !Number.isFinite(val))) errors.push(`${fileRel}: quiz.maxAttempts must be a positive number or Infinity, got ${String(val)}`);
1210
+ if (val !== Infinity && (typeof val !== "number" || val <= 0 || !Number.isFinite(val))) d.error(`${fileRel}: quiz.maxAttempts must be a positive number or Infinity, got ${String(val)}`);
1183
1211
  }
1184
- for (const field of ["graded", "gatesProgress"]) if (cfg[field] !== void 0 && typeof cfg[field] !== "boolean") errors.push(`${fileRel}: quiz.${field} must be a boolean, got ${typeof cfg[field]}`);
1185
- if (cfg.feedbackMode !== void 0 && !VALID_FEEDBACK_MODES.includes(cfg.feedbackMode)) errors.push(`${fileRel}: quiz.feedbackMode must be "review", "immediate", or "never", got "${String(cfg.feedbackMode)}"`);
1186
- if (cfg.retryMode !== void 0 && !VALID_RETRY_MODES.includes(cfg.retryMode)) errors.push(`${fileRel}: quiz.retryMode must be "full" or "incorrect-only", got "${String(cfg.retryMode)}"`);
1212
+ for (const field of ["graded", "gatesProgress"]) if (cfg[field] !== void 0 && typeof cfg[field] !== "boolean") d.error(`${fileRel}: quiz.${field} must be a boolean, got ${typeof cfg[field]}`);
1213
+ if (cfg.feedbackMode !== void 0 && !VALID_FEEDBACK_MODES.includes(cfg.feedbackMode)) d.error(`${fileRel}: quiz.feedbackMode must be "review", "immediate", or "never", got "${String(cfg.feedbackMode)}"`);
1214
+ if (cfg.retryMode !== void 0 && !VALID_RETRY_MODES.includes(cfg.retryMode)) d.error(`${fileRel}: quiz.retryMode must be "full" or "incorrect-only", got "${String(cfg.retryMode)}"`);
1187
1215
  }
1188
1216
  const QUESTION_COMPONENT_REQUIRED = {
1189
1217
  MultipleChoice: [
@@ -1218,63 +1246,63 @@ function staticNumber(prop) {
1218
1246
  return null;
1219
1247
  }
1220
1248
  }
1221
- function validateQuestionComponents(content, fileRel, errors, warnings, exportStandard) {
1249
+ function validateQuestionComponents(content, fileRel, d, exportStandard) {
1222
1250
  const components = findComponents(content, new Set(Object.keys(QUESTION_COMPONENT_REQUIRED)));
1223
1251
  if (!components) return;
1224
1252
  const seenIds = /* @__PURE__ */ new Set();
1225
1253
  const seenSanitized = /* @__PURE__ */ new Set();
1226
1254
  for (const { name, props, hasSpread } of components) {
1227
- for (const req of QUESTION_COMPONENT_REQUIRED[name]) if (!hasSpread && !props.has(req)) errors.push(`${fileRel}: <${name}> is missing required prop "${req}"`);
1228
- for (const labelProp of ["options", "answers"]) if (staticArray(props.get(labelProp))?.some((e) => typeof e === "string" && e.trim() === "")) warnings.push(tag(A11Y_IDS.questionLabel, `${fileRel}: <${name}> has an empty ${labelProp === "options" ? "option" : "answer"} label`));
1255
+ for (const req of QUESTION_COMPONENT_REQUIRED[name]) if (!hasSpread && !props.has(req)) d.error(`${fileRel}: <${name}> is missing required prop "${req}"`);
1256
+ for (const labelProp of ["options", "answers"]) if (staticArray(props.get(labelProp))?.some((e) => typeof e === "string" && e.trim() === "")) d.warn(tag(A11Y_IDS.questionLabel, `${fileRel}: <${name}> has an empty ${labelProp === "options" ? "option" : "answer"} label`));
1229
1257
  const idProp = props.get("id");
1230
1258
  if (idProp?.kind === "string") {
1231
- if (seenIds.has(idProp.value)) errors.push(`${fileRel}: duplicate question id "${idProp.value}" — each question on a page needs a unique id`);
1259
+ if (seenIds.has(idProp.value)) d.error(`${fileRel}: duplicate question id "${idProp.value}" — each question on a page needs a unique id`);
1232
1260
  else if (exportStandard === "scorm12") {
1233
1261
  const sane = shortIdentifier(idProp.value);
1234
- if (sane !== idProp.value) warnings.push(`${fileRel}: question id "${idProp.value}" will be rewritten to "${sane}" for SCORM 1.2 — use only letters and digits (underscores only between them)`);
1235
- if (seenSanitized.has(sane)) errors.push(`${fileRel}: question id "${idProp.value}" collides with a prior id after SCORM 1.2 sanitization ("${sane}")`);
1262
+ if (sane !== idProp.value) d.warn(`${fileRel}: question id "${idProp.value}" will be rewritten to "${sane}" for SCORM 1.2 — use only letters and digits (underscores only between them)`);
1263
+ if (seenSanitized.has(sane)) d.error(`${fileRel}: question id "${idProp.value}" collides with a prior id after SCORM 1.2 sanitization ("${sane}")`);
1236
1264
  seenSanitized.add(sane);
1237
1265
  }
1238
1266
  seenIds.add(idProp.value);
1239
1267
  }
1240
1268
  const weightProp = props.get("weight");
1241
- if (weightProp?.kind === "string") warnings.push(`${fileRel}: <${name}> weight="${weightProp.value}" is a string and is ignored (treated as 1) — pass a number: weight={${weightProp.value}}`);
1269
+ if (weightProp?.kind === "string") d.warn(`${fileRel}: <${name}> weight="${weightProp.value}" is a string and is ignored (treated as 1) — pass a number: weight={${weightProp.value}}`);
1242
1270
  else {
1243
1271
  const weight = staticNumber(weightProp);
1244
1272
  if (weight !== null) {
1245
- if (!Number.isFinite(weight)) errors.push(`${fileRel}: <${name}> weight must be finite — a non-finite weight makes the weighted score NaN, got ${weight}`);
1246
- else if (!(weight > 0)) warnings.push(`${fileRel}: <${name}> weight ${weight} is not positive and is ignored (treated as 1)`);
1273
+ if (!Number.isFinite(weight)) d.error(`${fileRel}: <${name}> weight must be finite — a non-finite weight makes the weighted score NaN, got ${weight}`);
1274
+ else if (!(weight > 0)) d.warn(`${fileRel}: <${name}> weight ${weight} is not positive and is ignored (treated as 1)`);
1247
1275
  }
1248
1276
  }
1249
1277
  if (name === "MultipleChoice") {
1250
1278
  const options = staticArray(props.get("options"));
1251
1279
  const correct = staticNumber(props.get("correct"));
1252
1280
  if (options && correct !== null) {
1253
- if (!Number.isInteger(correct) || correct < 0 || correct >= options.length) errors.push(`${fileRel}: <MultipleChoice> correct={${correct}} is out of range for ${options.length} options (valid: 0–${options.length - 1})`);
1281
+ if (!Number.isInteger(correct) || correct < 0 || correct >= options.length) d.error(`${fileRel}: <MultipleChoice> correct={${correct}} is out of range for ${options.length} options (valid: 0–${options.length - 1})`);
1254
1282
  }
1255
1283
  const optionFeedback = staticArray(props.get("optionFeedback"));
1256
- if (options && optionFeedback && optionFeedback.length > options.length) warnings.push(`${fileRel}: <MultipleChoice> optionFeedback has ${optionFeedback.length} entries but only ${options.length} options — the extra entries can never be shown`);
1284
+ if (options && optionFeedback && optionFeedback.length > options.length) d.warn(`${fileRel}: <MultipleChoice> optionFeedback has ${optionFeedback.length} entries but only ${options.length} options — the extra entries can never be shown`);
1257
1285
  } else if (name === "Sorting") {
1258
1286
  const items = staticArray(props.get("items"));
1259
1287
  const targets = staticArray(props.get("targets"));
1260
1288
  const correct = staticArray(props.get("correct"));
1261
- if (items && correct && correct.length !== items.length) errors.push(`${fileRel}: <Sorting> correct has ${correct.length} entries but items has ${items.length} — they must be parallel arrays`);
1289
+ if (items && correct && correct.length !== items.length) d.error(`${fileRel}: <Sorting> correct has ${correct.length} entries but items has ${items.length} — they must be parallel arrays`);
1262
1290
  if (targets && correct) {
1263
1291
  for (const idx of correct) if (typeof idx !== "number" || !Number.isInteger(idx) || idx < 0 || idx >= targets.length) {
1264
- errors.push(`${fileRel}: <Sorting> correct contains ${JSON.stringify(idx)}, out of range for ${targets.length} targets (valid: 0–${targets.length - 1})`);
1292
+ d.error(`${fileRel}: <Sorting> correct contains ${JSON.stringify(idx)}, out of range for ${targets.length} targets (valid: 0–${targets.length - 1})`);
1265
1293
  break;
1266
1294
  }
1267
1295
  }
1268
1296
  } else if (name === "Matching") {
1269
1297
  const pairs = staticArray(props.get("pairs"));
1270
1298
  if (pairs) {
1271
- if (pairs.some((p) => typeof p !== "object" || p === null || typeof p.left !== "string" || typeof p.right !== "string")) errors.push(`${fileRel}: <Matching> pairs must be an array of { left: string, right: string } objects`);
1299
+ if (pairs.some((p) => typeof p !== "object" || p === null || typeof p.left !== "string" || typeof p.right !== "string")) d.error(`${fileRel}: <Matching> pairs must be an array of { left: string, right: string } objects`);
1272
1300
  }
1273
1301
  } else if (name === "FillInTheBlank") {
1274
1302
  const answers = staticArray(props.get("answers"));
1275
1303
  if (answers) {
1276
- if (answers.length === 0) errors.push(`${fileRel}: <FillInTheBlank> answers must not be empty`);
1277
- else if (answers.some((a) => typeof a !== "string")) errors.push(`${fileRel}: <FillInTheBlank> answers must be an array of strings`);
1304
+ if (answers.length === 0) d.error(`${fileRel}: <FillInTheBlank> answers must not be empty`);
1305
+ else if (answers.some((a) => typeof a !== "string")) d.error(`${fileRel}: <FillInTheBlank> answers must be an array of strings`);
1278
1306
  }
1279
1307
  }
1280
1308
  }
@@ -1295,10 +1323,10 @@ function stripRepeated(input, patterns) {
1295
1323
  }
1296
1324
  /**
1297
1325
  * Sibling to validateQuestionComponents kept out of QUESTION_COMPONENT_REQUIRED
1298
- * so media isn't treated as gradable questions. Declares `warnings` directly.
1326
+ * so media isn't treated as gradable questions.
1299
1327
  * Non-static (kind 'expr') values are skipped, matching the rest of the linter.
1300
1328
  */
1301
- function validateMediaComponents(content, fileRel, errors, warnings) {
1329
+ function validateMediaComponents(content, fileRel, d) {
1302
1330
  const components = findComponents(content, /* @__PURE__ */ new Set([
1303
1331
  "Image",
1304
1332
  "Video",
@@ -1310,23 +1338,23 @@ function validateMediaComponents(content, fileRel, errors, warnings) {
1310
1338
  const alt = props.get("alt");
1311
1339
  const decorative = props.get("decorative");
1312
1340
  if (decorative?.kind === "string") {
1313
- errors.push(tag(A11Y_IDS.imageAlt, `${fileRel}: <Image> "decorative" must be a boolean — use decorative or decorative={true}, not the string ${JSON.stringify(decorative.value)}`));
1341
+ d.error(tag(A11Y_IDS.imageAlt, `${fileRel}: <Image> "decorative" must be a boolean — use decorative or decorative={true}, not the string ${JSON.stringify(decorative.value)}`));
1314
1342
  continue;
1315
1343
  }
1316
1344
  const hasDecorative = decorative?.kind === "bool" || decorative?.kind === "expr" && decorative.raw.trim() === "true";
1317
1345
  const altIsEmpty = alt?.kind === "string" && alt.value.trim() === "";
1318
- if (!hasDecorative && !hasSpread && (alt === void 0 || altIsEmpty)) errors.push(tag(A11Y_IDS.imageAlt, `${fileRel}: <Image> needs alt text, or mark it decorative={true} if purely ornamental`));
1319
- if (hasDecorative && alt?.kind === "string" && alt.value.trim() !== "") warnings.push(tag(A11Y_IDS.imageAlt, `${fileRel}: <Image> is decorative but also has alt text — the alt will be dropped`));
1346
+ if (!hasDecorative && !hasSpread && (alt === void 0 || altIsEmpty)) d.error(tag(A11Y_IDS.imageAlt, `${fileRel}: <Image> needs alt text, or mark it decorative={true} if purely ornamental`));
1347
+ if (hasDecorative && alt?.kind === "string" && alt.value.trim() !== "") d.warn(tag(A11Y_IDS.imageAlt, `${fileRel}: <Image> is decorative but also has alt text — the alt will be dropped`));
1320
1348
  continue;
1321
1349
  }
1322
1350
  const title = props.get("title");
1323
1351
  const titleIsEmpty = title?.kind === "string" && title.value.trim() === "";
1324
- if (!hasSpread && (title === void 0 || titleIsEmpty)) errors.push(tag(A11Y_IDS.mediaTitle, `${fileRel}: <${name}> needs a title — it's the accessible name for the player`));
1352
+ if (!hasSpread && (title === void 0 || titleIsEmpty)) d.error(tag(A11Y_IDS.mediaTitle, `${fileRel}: <${name}> needs a title — it's the accessible name for the player`));
1325
1353
  const src = props.get("src");
1326
1354
  const isEmbed = src?.kind === "string" && isVideoEmbed(src.value);
1327
- if (name === "Video" && !hasSpread && isEmbed && props.get("transcript") === void 0) warnings.push(tag(A11Y_IDS.mediaTranscript, `${fileRel}: <Video> embeds can't carry caption tracks — provide a transcript for WCAG 1.2`));
1328
- if (name === "Video" && !hasSpread && src?.kind === "string" && !isEmbed && props.get("tracks") === void 0 && props.get("transcript") === void 0) warnings.push(tag(A11Y_IDS.mediaCaptions, `${fileRel}: native <Video> has no caption tracks or transcript — add tracks={[…]} or a transcript for WCAG 1.2.2`));
1329
- if (name === "Audio" && !hasSpread && props.get("transcript") === void 0) warnings.push(tag(A11Y_IDS.mediaTranscript, `${fileRel}: <Audio> has no transcript — required for WCAG 1.2.1`));
1355
+ if (name === "Video" && !hasSpread && isEmbed && props.get("transcript") === void 0) d.warn(tag(A11Y_IDS.mediaTranscript, `${fileRel}: <Video> embeds can't carry caption tracks — provide a transcript for WCAG 1.2`));
1356
+ if (name === "Video" && !hasSpread && src?.kind === "string" && !isEmbed && props.get("tracks") === void 0 && props.get("transcript") === void 0) d.warn(tag(A11Y_IDS.mediaCaptions, `${fileRel}: native <Video> has no caption tracks or transcript — add tracks={[…]} or a transcript for WCAG 1.2.2`));
1357
+ if (name === "Audio" && !hasSpread && props.get("transcript") === void 0) d.warn(tag(A11Y_IDS.mediaTranscript, `${fileRel}: <Audio> has no transcript — required for WCAG 1.2.1`));
1330
1358
  }
1331
1359
  }
1332
1360
  /**
@@ -1336,11 +1364,11 @@ function validateMediaComponents(content, fileRel, errors, warnings) {
1336
1364
  * components emit headings a static scan can't see; that belongs to the Tier-2
1337
1365
  * audit.
1338
1366
  */
1339
- function validateHeadingOrder(content, fileRel, warnings) {
1367
+ function validateHeadingOrder(content, fileRel, d) {
1340
1368
  const levels = [...stripRepeated(content, [SCRIPT_STYLE_RE, HTML_COMMENT_RE]).matchAll(/<h([1-6])\b/gi)].map((h) => Number(h[1]));
1341
1369
  let prevSeen = null;
1342
1370
  for (const level of levels) {
1343
- if (prevSeen !== null && level - prevSeen > 1) warnings.push(tag(A11Y_IDS.headingOrder, `${fileRel}: heading level jumps from h${prevSeen} to h${level} — don't skip levels (WCAG 1.3.1)`));
1371
+ if (prevSeen !== null && level - prevSeen > 1) d.warn(tag(A11Y_IDS.headingOrder, `${fileRel}: heading level jumps from h${prevSeen} to h${level} — don't skip levels (WCAG 1.3.1)`));
1344
1372
  prevSeen = level;
1345
1373
  }
1346
1374
  }
@@ -1354,9 +1382,9 @@ const HAS_LOCAL_SVELTE_IMPORT_RE = /from\s+['"][^'"]+\.svelte['"]/;
1354
1382
  * source text for known escape hatches — they never inspect course content,
1355
1383
  * so they constrain how you wire things up, not what you build.
1356
1384
  */
1357
- function validateContractBypass(content, fileRel, errors) {
1358
- if (QUIZ_COMPLETE_DISPATCH_RE.test(content)) errors.push(`${fileRel}: dispatches "tessera-quiz-complete" directly — submit through useQuiz().submit() so the result reaches the LMS`);
1359
- if (RUNTIME_INTERNAL_IMPORT_RE.test(content)) errors.push(`${fileRel}: imports from tessera-learn/runtime/* — use the public hooks (useQuiz, useQuestion, useNavigation, …) instead`);
1385
+ function validateContractBypass(content, fileRel, d) {
1386
+ if (QUIZ_COMPLETE_DISPATCH_RE.test(content)) d.error(`${fileRel}: dispatches "tessera-quiz-complete" directly — submit through useQuiz().submit() so the result reaches the LMS`);
1387
+ if (RUNTIME_INTERNAL_IMPORT_RE.test(content)) d.error(`${fileRel}: imports from tessera-learn/runtime/* — use the public hooks (useQuiz, useQuestion, useNavigation, …) instead`);
1360
1388
  }
1361
1389
  const ASSET_REF_RE = /\$assets\/([^\s"'`)]+)/g;
1362
1390
  /** Match $assets/... refs in any context (src attrs, import statements, url() etc) and dedupe. */
@@ -1367,7 +1395,7 @@ function collectAssetRefs(content) {
1367
1395
  while ((match = ASSET_REF_RE.exec(content)) !== null) seen.add(match[1].replace(/[?#].*$/, ""));
1368
1396
  return [...seen];
1369
1397
  }
1370
- function validateAssetRefs(content, fileRel, assetsDir, warnings, existsCache) {
1398
+ function validateAssetRefs(content, fileRel, assetsDir, d, existsCache) {
1371
1399
  for (const assetPath of collectAssetRefs(content)) {
1372
1400
  const fullAssetPath = resolve(assetsDir, assetPath);
1373
1401
  let exists = existsCache.get(fullAssetPath);
@@ -1375,24 +1403,24 @@ function validateAssetRefs(content, fileRel, assetsDir, warnings, existsCache) {
1375
1403
  exists = existsSync(fullAssetPath);
1376
1404
  existsCache.set(fullAssetPath, exists);
1377
1405
  }
1378
- if (!exists) warnings.push(`${fileRel}: "$assets/${assetPath}" not found in assets/ directory`);
1406
+ if (!exists) d.warn(`${fileRel}: "$assets/${assetPath}" not found in assets/ directory`);
1379
1407
  }
1380
1408
  }
1381
- function crossValidate(config, pageResults, errors, warnings) {
1382
- if (config.completion?.mode === "quiz" && !pageResults.hasGradedQuiz && !pageResults.hasParseErrors) errors.push("completion.mode is \"quiz\" but no pages have quiz config with graded: true");
1383
- if (config.completion?.mode === "quiz" && config.scoring?.passingScore === void 0) warnings.push("completion.mode is \"quiz\" but scoring.passingScore is not set — defaulting to 70%. Set it explicitly to be sure.");
1409
+ function crossValidate(config, pageResults, d) {
1410
+ if (config.completion?.mode === "quiz" && !pageResults.hasGradedQuiz && !pageResults.hasParseErrors) d.error("completion.mode is \"quiz\" but no pages have quiz config with graded: true");
1411
+ if (config.completion?.mode === "quiz" && config.scoring?.passingScore === void 0) d.warn("completion.mode is \"quiz\" but scoring.passingScore is not set — defaulting to 70%. Set it explicitly to be sure.");
1384
1412
  const isManual = config.completion?.mode === "manual";
1385
1413
  const completesOnPages = pageResults.pages.filter((p) => p.completesOnView);
1386
- if (isManual && config.completion?.trigger === "page" && completesOnPages.length === 0 && !pageResults.hasParseErrors) errors.push("completion.mode is \"manual\" with trigger: \"page\", but no page declares pageConfig.completesOn: \"view\". Either add a completesOn page or remove the trigger field to drop the static check.");
1414
+ if (isManual && config.completion?.trigger === "page" && completesOnPages.length === 0 && !pageResults.hasParseErrors) d.error("completion.mode is \"manual\" with trigger: \"page\", but no page declares pageConfig.completesOn: \"view\". Either add a completesOn page or remove the trigger field to drop the static check.");
1387
1415
  if (isManual) {
1388
- for (const page of pageResults.pages) if (page.hasGradedQuiz) warnings.push(`${page.fileRel}: quiz.graded is true under completion.mode: "manual". The score will be reported to the LMS for transcripts, but it will not drive completion or success status — \`markComplete()\` / completesOn does. If that's not what you want, set graded: false or change completion.mode.`);
1416
+ for (const page of pageResults.pages) if (page.hasGradedQuiz) d.warn(`${page.fileRel}: quiz.graded is true under completion.mode: "manual". The score will be reported to the LMS for transcripts, but it will not drive completion or success status — \`markComplete()\` / completesOn does. If that's not what you want, set graded: false or change completion.mode.`);
1389
1417
  }
1390
- if (isManual && config.completion?.percentageThreshold !== void 0) warnings.push("course.config.js: \"completion.percentageThreshold\" is ignored under completion.mode: \"manual\"");
1391
- if (!isManual) for (const page of completesOnPages) warnings.push(`${page.fileRel}: pageConfig.completesOn is ignored — completion.mode is "${config.completion?.mode ?? "percentage"}"`);
1392
- for (const page of pageResults.pages) if (page.completesOnView && page.hasQuiz) warnings.push(`${page.fileRel}: completion fires on view, before the quiz can be answered — likely a mistake`);
1418
+ if (isManual && config.completion?.percentageThreshold !== void 0) d.warn("course.config.js: \"completion.percentageThreshold\" is ignored under completion.mode: \"manual\"");
1419
+ if (!isManual) for (const page of completesOnPages) d.warn(`${page.fileRel}: pageConfig.completesOn is ignored — completion.mode is "${config.completion?.mode ?? "percentage"}"`);
1420
+ for (const page of pageResults.pages) if (page.completesOnView && page.hasQuiz) d.warn(`${page.fileRel}: completion fires on view, before the quiz can be answered — likely a mistake`);
1393
1421
  if (isManual) {
1394
1422
  const firstPage = pageResults.pages.find((p) => p.navIndex === 0);
1395
- if (firstPage?.completesOnView) warnings.push(`${firstPage.fileRel}: pageConfig.completesOn: "view" is on the first page — the course will complete immediately on launch, before the learner sees any other content.`);
1423
+ if (firstPage?.completesOnView) d.warn(`${firstPage.fileRel}: pageConfig.completesOn: "view" is on the first page — the course will complete immediately on launch, before the learner sees any other content.`);
1396
1424
  }
1397
1425
  if (config.export?.standard === "scorm12") {
1398
1426
  let visitedChars = 0;
@@ -1402,7 +1430,7 @@ function crossValidate(config, pageResults, errors, warnings) {
1402
1430
  const chunkBytes = pageResults.totalPages * 12;
1403
1431
  const standaloneBytes = pageResults.totalPages * 30;
1404
1432
  const estimatedSize = overhead + visitedChars + quizBytes + chunkBytes + standaloneBytes + 256;
1405
- if (estimatedSize > 3200) warnings.push(`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".`);
1433
+ if (estimatedSize > 3200) d.warn(`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".`);
1406
1434
  }
1407
1435
  }
1408
1436
  //#endregion
@@ -1631,7 +1659,7 @@ async function runAudit(projectRoot, workspaceRoot, options = {}) {
1631
1659
  const disableRules = axeIgnoreRules(settings.ignore);
1632
1660
  const manifest = generateManifest(resolve(projectRoot, "pages"));
1633
1661
  const vite = await import("vite");
1634
- const { resolveTesseraConfig } = await import("./inline-config-BEXyRqsJ.js");
1662
+ const { resolveTesseraConfig } = await import("./inline-config-DVvOCKht.js");
1635
1663
  const auditBaseConfig = await resolveTesseraConfig(projectRoot, workspaceRoot, {
1636
1664
  command: "build",
1637
1665
  mode: "production"
@@ -1782,6 +1810,6 @@ function printSummary(report, reportPath) {
1782
1810
  }
1783
1811
  }
1784
1812
  //#endregion
1785
- export { isPlausibleLanguageTag as a, validateProject as c, generateManifest as d, readCourseConfig as f, isIgnored as i, buildCsp as l, runAudit as n, normalizeA11y as o, resolvePackageRoot as r, reportValidationIssues as s, AUDIT_ENV_FLAG as t, courseIdentity as u };
1813
+ export { isIgnored as a, reportValidationIssues as c, courseIdentity as d, generateManifest as f, VALID_EXPORT_STANDARDS as i, validateProject as l, readResolvedConfig as m, runAudit as n, isPlausibleLanguageTag as o, readCourseConfig as p, resolvePackageRoot as r, normalizeA11y as s, AUDIT_ENV_FLAG as t, buildCsp as u };
1786
1814
 
1787
- //# sourceMappingURL=audit-DkXqQTqn.js.map
1815
+ //# sourceMappingURL=audit-DsYqXbqm.js.map