ultra11y 4.3.0 → 4.4.1

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 (45) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.opencode/plugins/ultra11y.js +1 -1
  5. package/README.md +40 -4
  6. package/package.json +3 -1
  7. package/scripts/build-guidance-wcag.mjs +335 -0
  8. package/scripts/build-standards.mjs +290 -1
  9. package/scripts/ultra11y.mjs +3805 -274
  10. package/scripts/vendor/wcag-2.2-fr.json +944 -0
  11. package/scripts/vendor/wcag-2.2-techniques.json +840 -0
  12. package/scripts/vendor/wcag-2.2-text.json +749 -0
  13. package/skills/review-a11y/SKILL.md +1 -1
  14. package/skills/review-a11y/scripts/ultra11y.mjs +3805 -274
  15. package/skills/ultra11y/SKILL.md +13 -4
  16. package/skills/ultra11y/references/guidance.md +61 -7
  17. package/skills/ultra11y/references/harnesses.md +10 -0
  18. package/skills/ultra11y/references/mcp.md +215 -0
  19. package/skills/ultra11y/references/packs.md +8 -0
  20. package/skills/ultra11y/references/standards.md +18 -0
  21. package/skills/ultra11y/scripts/ultra11y.mjs +3805 -274
  22. package/src/adjudicate.ts +9 -15
  23. package/src/adjudication-data.ts +38 -0
  24. package/src/check.ts +51 -1
  25. package/src/cli.ts +6 -2
  26. package/src/config.ts +27 -4
  27. package/src/criteria-view.ts +463 -0
  28. package/src/criteria.ts +14 -11
  29. package/src/data/guidance/wcag.json +1126 -0
  30. package/src/data/wcag.json +1112 -55
  31. package/src/guidance/index.ts +14 -0
  32. package/src/guidance/resolve.ts +71 -0
  33. package/src/mcp/handlers.ts +160 -15
  34. package/src/mcp/prompts.ts +13 -2
  35. package/src/mcp/server.ts +18 -4
  36. package/src/mcp/standards-resources.ts +174 -0
  37. package/src/mcp/tools.ts +140 -6
  38. package/src/method-view.ts +227 -0
  39. package/src/prd.ts +5 -16
  40. package/src/standards/coverage.ts +237 -0
  41. package/src/standards/index.ts +1 -0
  42. package/src/standards/pack.ts +19 -0
  43. package/src/standards/registry.ts +115 -9
  44. package/src/types.ts +24 -1
  45. package/src/wcag.ts +21 -1
@@ -14,6 +14,7 @@
14
14
  // node scripts/build-standards.mjs --refresh <dir> # re-derive the vendored AA snapshot from a w3c/wcag checkout
15
15
  // node scripts/build-standards.mjs --refresh-universe # re-fetch (network) the vendored FULL SC universe (all levels + removed 4.1.1)
16
16
  // node scripts/build-standards.mjs --refresh-fr # re-fetch (network) the vendored French SC/guideline/principle titles
17
+ // node scripts/build-standards.mjs --refresh-text # re-fetch (network) the vendored NORMATIVE SC text + the WCAG glossary
17
18
  import { writeFileSync, readFileSync, readdirSync, existsSync, mkdirSync } from "node:fs";
18
19
  import { execFileSync } from "node:child_process";
19
20
  import { dirname, join } from "node:path";
@@ -24,6 +25,7 @@ const DATA = join(root, "src", "data");
24
25
  const VENDOR = join(root, "scripts", "vendor", "wcag-2.2-sc.json");
25
26
  const VENDOR_UNIVERSE = join(root, "scripts", "vendor", "wcag-2.2-sc-universe.json");
26
27
  const VENDOR_FR = join(root, "scripts", "vendor", "wcag-2.2-fr.json");
28
+ const VENDOR_TEXT = join(root, "scripts", "vendor", "wcag-2.2-text.json");
27
29
  const PACKS_DIR = join(DATA, "standards");
28
30
  const BIOME = join(root, "node_modules", ".bin", "biome");
29
31
 
@@ -235,6 +237,88 @@ function deHtmlFr(s) {
235
237
  .trim();
236
238
  }
237
239
 
240
+ // The French page's own chrome, normalized to the shapes `deHtmlText` already knows.
241
+ //
242
+ // The single-page translation wraps a note or an example in
243
+ // `<div class="note"><div role="heading">…Note 1…</div><p>…</p></div>`, where the English
244
+ // fragments use a flat `<p class="note">`. Collapsing the heading AND the paragraph opener
245
+ // that follows keeps the label on the same line as the text it labels — "Note : …", not a
246
+ // bare "Note :" followed by an orphaned sentence.
247
+ function deHtmlFrBody(html, opts) {
248
+ let s = html;
249
+ s = s.replace(/<a class="self-link"[\s\S]*?<\/a>/gi, "");
250
+ s = s.replace(/<div class="doclinks">[\s\S]*?<\/div>/gi, "");
251
+ s = s.replace(/<div class="(note|example)"[^>]*>\s*<div role="heading"[^>]*>([\s\S]*?)<\/div>\s*(?:<p[^>]*>)?/gi, (_, _kind, heading) => {
252
+ // "Note 1" / "Note 2" are numbered per document; the number is noise in a lookup.
253
+ const label = heading
254
+ .replace(/<[^>]+>/g, "")
255
+ .replace(/\s+/g, " ")
256
+ .trim()
257
+ .replace(/\s*\d+$/, "");
258
+ return `${label} : `;
259
+ });
260
+ return deHtmlText(s, opts);
261
+ }
262
+
263
+ /** The criterion bodies and the glossary, off the French single-page translation. */
264
+ function deriveFrBodies(html, snap) {
265
+ // Every numbered heading, in document order: an SC's body is what sits between its own
266
+ // heading and the next one. The page numbers headings exactly like the English source.
267
+ const HEAD = /<h([234])[^>]*>\s*<bdi class="secno">([^<]*)<\/bdi>([\s\S]*?)<\/h\1>/g;
268
+ const heads = [];
269
+ let m;
270
+ while ((m = HEAD.exec(html))) {
271
+ const id = (m[2].match(/(\d+(?:\.\d+)*)/) || [])[1];
272
+ heads.push({ level: Number(m[1]), id, end: m.index + m[0].length, start: m.index });
273
+ }
274
+
275
+ const criteriaText = {};
276
+ const criteriaTerms = {};
277
+ for (let i = 0; i < heads.length; i++) {
278
+ const h = heads[i];
279
+ if (h.level !== 4 || !h.id || h.id.split(".").length !== 3) continue;
280
+ const slice = html.slice(h.end, heads[i + 1]?.start ?? html.length);
281
+ const body = deHtmlFrBody(slice);
282
+ if (body) criteriaText[h.id] = body;
283
+ // The FR page names its definitions with its OWN slugs — often the plural
284
+ // ("user-agents" where the English source file is "user-agent"), sometimes a different
285
+ // phrase entirely ("purpose-of-each-link" for "link-purpose"). Mapping one onto the
286
+ // other would be a guess, so each language keeps its own term list and its own
287
+ // glossary keys, and neither has to know about the other.
288
+ const cited = [...new Set([...slice.matchAll(/href="#dfn-([a-z0-9-]+)"/g)].map((x) => x[1]))];
289
+ if (cited.length) criteriaTerms[h.id] = cited;
290
+ }
291
+
292
+ // The glossary: `<dfn id="dfn-<slug>">title</dfn></dt>` then the definition up to the
293
+ // next term. The slugs are the SAME as the English source's file names, so the two
294
+ // languages key identically and a criterion's term links resolve in either.
295
+ // Attribute order is not guaranteed: the FR page writes `data-lt` before `id`.
296
+ const DFN = /<dfn\b[^>]*\bid="dfn-([a-z0-9-]+)"[^>]*>([\s\S]*?)<\/dfn>/g;
297
+ const glossary = {};
298
+ while ((m = DFN.exec(html))) {
299
+ const title = m[2]
300
+ .replace(/<[^>]+>/g, "")
301
+ .replace(/\s+/g, " ")
302
+ .trim();
303
+ const after = html.slice(m.index + m[0].length);
304
+ const stop = after.search(/<dt[\s>]|<\/dl>/i);
305
+ const body = deHtmlFrBody(after.slice(0, stop === -1 ? 8000 : stop));
306
+ if (title && body) glossary[m[1]] = { title, body };
307
+ }
308
+
309
+ // Same completeness rule as the titles: every SHIPPED criterion carries its French body
310
+ // or the build refuses. A half-translated reference is worse than a known-absent one,
311
+ // because the reader cannot tell which criteria they are missing.
312
+ const missing = snap.criteria.filter((c) => !criteriaText[c.sc]).map((c) => c.sc);
313
+ if (missing.length) {
314
+ console.error(
315
+ `build-standards --refresh-fr: no French body found on ${FR_SOURCE} for SC: ${missing.join(", ")}. Nothing invented — refusing to vendor a partial set.`,
316
+ );
317
+ process.exit(1);
318
+ }
319
+ return { criteriaText, criteriaTerms, glossary };
320
+ }
321
+
238
322
  async function deriveFr() {
239
323
  if (!existsSync(VENDOR)) {
240
324
  console.error(`build-standards --refresh-fr: missing vendored English snapshot ${VENDOR}. Run: node scripts/build-standards.mjs --refresh <w3c/wcag checkout> first.`);
@@ -290,21 +374,178 @@ async function deriveFr() {
290
374
  process.exit(1);
291
375
  }
292
376
 
377
+ // The same page carries each criterion's NORMATIVE BODY and the glossary, not just the
378
+ // titles. Vendoring only the titles left `--lang fr` rendering a French heading over
379
+ // English requirement prose — the jarring half-translation an RGAA auditor reads first.
380
+ const { criteriaText, criteriaTerms, glossary } = deriveFrBodies(html, snap);
381
+
293
382
  const out = {
294
383
  source: FR_SOURCE,
295
384
  fetchedAt: new Date().toISOString().slice(0, 10),
296
385
  principles,
297
386
  guidelines,
298
387
  criteria,
388
+ criteriaText,
389
+ criteriaTerms,
390
+ glossary,
299
391
  };
300
392
  mkdirSync(dirname(VENDOR_FR), { recursive: true });
301
393
  writeFileSync(VENDOR_FR, JSON.stringify(out, null, 2) + "\n");
302
394
  console.log(
303
- `build-standards --refresh-fr: ${Object.keys(criteria).length} SC + ${Object.keys(guidelines).length} guideline + ${Object.keys(principles).length} principle French titles → ${VENDOR_FR}`,
395
+ `build-standards --refresh-fr: ${Object.keys(criteria).length} SC + ${Object.keys(guidelines).length} guideline + ${Object.keys(principles).length} principle French titles, ` +
396
+ `${Object.keys(criteriaText).length} SC bodies, ${Object.keys(glossary).length} glossary terms → ${VENDOR_FR}`,
304
397
  );
305
398
  return out;
306
399
  }
307
400
 
401
+ // ---------------------------------------------------------------------------
402
+ // Text mode: the NORMATIVE WORDING of every shipped success criterion, plus the terms
403
+ // WCAG itself defines.
404
+ //
405
+ // The dataset used to carry only ids, titles and an Understanding URL — so an offline
406
+ // agent could name a criterion but not read it, and had to recall the requirement from
407
+ // memory. That is exactly how invented non-conformities get written. The RGAA pack ships
408
+ // its full criterion text and a 119-entry glossary; the worldwide core shipped neither.
409
+ //
410
+ // Nothing new is fetched: `deriveUniverse` already downloads these very fragments and
411
+ // keeps only the <h4> and the conformance level. WCAG 2.2 © W3C, reused under the W3C
412
+ // Document License; see NOTICE.
413
+ //
414
+ // node scripts/build-standards.mjs --refresh-text
415
+ // ---------------------------------------------------------------------------
416
+ const ENTITIES = {
417
+ "&nbsp;": " ",
418
+ "&amp;": "&",
419
+ "&lt;": "<",
420
+ "&gt;": ">",
421
+ "&quot;": '"',
422
+ "&apos;": "'",
423
+ "&rsquo;": "’",
424
+ "&lsquo;": "‘",
425
+ "&ldquo;": "“",
426
+ "&rdquo;": "”",
427
+ "&mdash;": "—",
428
+ "&ndash;": "–",
429
+ "&hellip;": "…",
430
+ "&times;": "×",
431
+ };
432
+
433
+ function decodeEntities(s) {
434
+ return s
435
+ .replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)))
436
+ .replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(Number.parseInt(h, 16)))
437
+ .replace(/&[a-z]+;/gi, (m) => ENTITIES[m] ?? m);
438
+ }
439
+
440
+ // W3C fragment → normative plaintext. Structure that CHANGES THE READING is kept as its
441
+ // own labelled line — an exception's name ("Large Text:") decides whether the exception
442
+ // applies, and a note is not normative the way the sentence above it is. Everything else
443
+ // is flattened, because the source wraps mid-sentence.
444
+ function deHtmlText(html, { dropDfn = false } = {}) {
445
+ const SEP = "";
446
+ let s = html;
447
+ s = s.replace(/<!--[\s\S]*?-->/g, "");
448
+ // The fragment's own chrome: title, conformance level, the 2.2 "New"/"Updated" marker.
449
+ s = s.replace(/<h4>[\s\S]*?<\/h4>/gi, "");
450
+ s = s.replace(/<p class="conformance-level">[\s\S]*?<\/p>/gi, "");
451
+ s = s.replace(/<p class="change">[\s\S]*?<\/p>/gi, "");
452
+ if (dropDfn) s = s.replace(/<dt>[\s\S]*?<\/dt>/i, "");
453
+ s = s.replace(/<p class="note">/gi, `${SEP}Note: `);
454
+ s = s.replace(/<aside class="example">/gi, `${SEP}Example: `);
455
+ s = s.replace(/<dt>([\s\S]*?)<\/dt>\s*<dd>/gi, (_, t) => `${SEP}${t.replace(/<[^>]+>/g, "").trim()}: `);
456
+ s = s.replace(/<li>/gi, `${SEP}• `);
457
+ s = s.replace(/<\/(p|dd|li|dl|ul|ol|aside|section|div|blockquote)>/gi, SEP);
458
+ s = s.replace(/<(p|dd|aside|div|blockquote)\b[^>]*>/gi, SEP);
459
+ s = s.replace(/<[^>]+>/g, "");
460
+ s = decodeEntities(s);
461
+ return s
462
+ .split(SEP)
463
+ .map((block) => block.replace(/\s+/g, " ").trim())
464
+ .filter(Boolean)
465
+ .join("\n");
466
+ }
467
+
468
+ const VER_OF = { "2.0": "20", "2.1": "21", "2.2": "22" };
469
+
470
+ async function fetchJson(url) {
471
+ const r = await fetch(url, { headers: { accept: "application/vnd.github+json" } });
472
+ if (!r.ok) throw new Error(`build-standards --refresh-text: GET ${url} → HTTP ${r.status}`);
473
+ return r.json();
474
+ }
475
+
476
+ async function deriveText() {
477
+ if (!existsSync(VENDOR)) {
478
+ console.error(`build-standards --refresh-text: missing vendored snapshot ${VENDOR}. Run --refresh first.`);
479
+ process.exit(1);
480
+ }
481
+ const snap = JSON.parse(readFileSync(VENDOR, "utf8"));
482
+
483
+ // The terms WCAG defines, across all three version directories. Built FIRST, because a
484
+ // criterion's wording links to them and those links are what makes the definitions
485
+ // findable from the criterion.
486
+ const glossary = {};
487
+ const bySurface = new Map();
488
+ const fold = (s) =>
489
+ s
490
+ .toLowerCase()
491
+ .replace(/[^a-z0-9]+/g, "-")
492
+ .replace(/^-+|-+$/g, "");
493
+ for (const ver of ["20", "21", "22"]) {
494
+ const listing = await fetchJson(`https://api.github.com/repos/w3c/wcag/contents/guidelines/terms/${ver}`);
495
+ for (const file of listing) {
496
+ if (file.type !== "file" || !file.name.endsWith(".html")) continue;
497
+ const html = await fetchText(`guidelines/terms/${ver}/${file.name}`);
498
+ const dfn = html.match(/<dfn([^>]*)>([\s\S]*?)<\/dfn>/);
499
+ const title = dfn?.[2]?.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
500
+ const body = deHtmlText(html, { dropDfn: true });
501
+ if (!title || !body) {
502
+ console.error(`build-standards --refresh-text: terms/${ver}/${file.name} has no <dfn> title or no body — refusing to vendor a partial glossary.`);
503
+ process.exit(1);
504
+ }
505
+ // Keyed by the W3C's own dfn slug, so a criterion's prose can point at it.
506
+ const slug = file.name.replace(/\.html$/, "");
507
+ glossary[slug] = { title, body };
508
+ // `data-lt` lists the other surface forms the spec links this term by ("accessibility
509
+ // support" → accessibility-supported), so a link in a criterion resolves either way.
510
+ const lt = (dfn?.[1]?.match(/data-lt="([^"]*)"/) || [])[1];
511
+ for (const surface of [title, ...(lt ? lt.split("|") : [])]) bySurface.set(fold(surface), slug);
512
+ }
513
+ }
514
+
515
+ const criteria = {};
516
+ const terms = {};
517
+ for (const c of snap.criteria) {
518
+ const ver = VER_OF[c.addedIn];
519
+ if (!ver) throw new Error(`build-standards --refresh-text: unknown addedIn "${c.addedIn}" for SC ${c.sc}`);
520
+ const html = await fetchText(`guidelines/sc/${ver}/${c.slug}.html`);
521
+ const text = deHtmlText(html);
522
+ if (!text) throw new Error(`build-standards --refresh-text: empty normative text for SC ${c.sc} (${c.slug})`);
523
+ criteria[c.sc] = text;
524
+ // A bare `<a>` with no href IS a glossary reference in the W3C source — that is how the
525
+ // spec marks a defined term. Resolved here rather than by matching prose, so a term is
526
+ // attached because the criterion cites it, never because a word happened to appear.
527
+ const cited = new Set();
528
+ for (const m of html.matchAll(/<a>([\s\S]*?)<\/a>/g)) {
529
+ const slug = bySurface.get(fold(m[1].replace(/<[^>]+>/g, "")));
530
+ if (slug) cited.add(slug);
531
+ }
532
+ if (cited.size) terms[c.sc] = [...cited];
533
+ }
534
+
535
+ const out = {
536
+ source: "https://github.com/w3c/wcag",
537
+ license: "W3C Document License",
538
+ fetchedAt: new Date().toISOString().slice(0, 10),
539
+ criteria,
540
+ terms,
541
+ glossary,
542
+ };
543
+ mkdirSync(dirname(VENDOR_TEXT), { recursive: true });
544
+ writeFileSync(VENDOR_TEXT, JSON.stringify(out, null, 2) + "\n");
545
+ console.log(`build-standards --refresh-text: ${Object.keys(criteria).length} SC texts + ${Object.keys(glossary).length} glossary terms → ${VENDOR_TEXT}`);
546
+ return out;
547
+ }
548
+
308
549
  // ---------------------------------------------------------------------------
309
550
  // Engine-specific decoration (the only hand-maintained part: which SCs the static
310
551
  // engine can fully decide, which need a rendered DOM, and the per-SC rule coverage).
@@ -453,8 +694,13 @@ function build() {
453
694
  console.error(`build-standards: missing vendored French titles ${VENDOR_FR}. Run: node scripts/build-standards.mjs --refresh-fr`);
454
695
  process.exit(1);
455
696
  }
697
+ if (!existsSync(VENDOR_TEXT)) {
698
+ console.error(`build-standards: missing vendored normative text ${VENDOR_TEXT}. Run: node scripts/build-standards.mjs --refresh-text`);
699
+ process.exit(1);
700
+ }
456
701
  const snap = JSON.parse(readFileSync(VENDOR, "utf8"));
457
702
  const fr = JSON.parse(readFileSync(VENDOR_FR, "utf8"));
703
+ const text = JSON.parse(readFileSync(VENDOR_TEXT, "utf8"));
458
704
  const { techniques, sources } = seedFromPacks();
459
705
 
460
706
  // Completeness guard: every shipped core-AA SC/guideline/principle MUST carry a French
@@ -467,6 +713,9 @@ function build() {
467
713
  const frPrinciples = fr.principles ?? {};
468
714
  const frGuidelines = fr.guidelines ?? {};
469
715
  const frCriteria = fr.criteria ?? {};
716
+ const frText = fr.criteriaText ?? {};
717
+ const frTerms = fr.criteriaTerms ?? {};
718
+ const frGlossary = fr.glossary ?? {};
470
719
  const missingFr = [
471
720
  ...snap.principles.filter((p) => !frPrinciples[String(p.number)]).map((p) => `principle ${p.number}`),
472
721
  ...snap.guidelines.filter((g) => !frGuidelines[g.number]).map((g) => `guideline ${g.number}`),
@@ -494,9 +743,44 @@ function build() {
494
743
  understanding: `https://www.w3.org/WAI/WCAG22/Understanding/${c.slug}.html`,
495
744
  };
496
745
  if (techniques[c.sc]?.length) sc.techniques = techniques[c.sc];
746
+ if (text.criteria?.[c.sc]) sc.text = text.criteria[c.sc];
747
+ if (frText[c.sc]) sc.textFr = frText[c.sc];
748
+ if (frTerms[c.sc]?.length) sc.termsFr = frTerms[c.sc];
749
+ if (text.terms?.[c.sc]?.length) sc.terms = text.terms[c.sc];
497
750
  return sc;
498
751
  });
499
752
 
753
+ // Same completeness rule as the French titles: a shipped criterion either carries its
754
+ // normative wording or the build fails. Half a reference is worse than a known-absent
755
+ // one, because a reader cannot tell which criteria they are missing.
756
+ const missingText = snap.criteria.filter((c) => !text.criteria?.[c.sc]).map((c) => `SC ${c.sc}`);
757
+ if (missingText.length) {
758
+ console.error(`build-standards: ${VENDOR_TEXT} is missing the normative text for: ${missingText.join(", ")}. Re-run: node scripts/build-standards.mjs --refresh-text`);
759
+ process.exit(1);
760
+ }
761
+ // The French body is held to the same rule as the French title: complete, or the build
762
+ // fails. `--lang fr` must never fall back to English prose under a French heading.
763
+ const missingFrText = snap.criteria.filter((c) => !frText[c.sc]).map((c) => `SC ${c.sc}`);
764
+ if (missingFrText.length) {
765
+ console.error(`build-standards: ${VENDOR_FR} is missing the French body for: ${missingFrText.join(", ")}. Re-run: node scripts/build-standards.mjs --refresh-fr`);
766
+ process.exit(1);
767
+ }
768
+
769
+ // Every term a criterion CITES must resolve in the glossary of its own language — the
770
+ // two pages use different slugs, so a cross-language mix-up would silently drop
771
+ // definitions rather than fail. Caught here instead.
772
+ for (const c of criteria) {
773
+ for (const [slug, set, which] of [
774
+ ...(c.terms ?? []).map((t) => [t, text.glossary ?? {}, "en"]),
775
+ ...(c.termsFr ?? []).map((t) => [t, frGlossary, "fr"]),
776
+ ]) {
777
+ if (!set[slug]) {
778
+ console.error(`build-standards: SC ${c.sc} cites the ${which} term "${slug}", which that language's glossary does not define.`);
779
+ process.exit(1);
780
+ }
781
+ }
782
+ }
783
+
500
784
  const out = {
501
785
  wcagVersion: snap.wcagVersion,
502
786
  level: "AA",
@@ -506,6 +790,10 @@ function build() {
506
790
  principles,
507
791
  guidelines,
508
792
  criteria,
793
+ glossary: text.glossary ?? {},
794
+ // Keyed by the SAME dfn slugs as the English glossary, so a criterion's term links
795
+ // resolve in either language off one list.
796
+ glossaryFr: frGlossary,
509
797
  };
510
798
  mkdirSync(DATA, { recursive: true });
511
799
  writeFileSync(join(DATA, "wcag.json"), biomeFormat(JSON.stringify(out, null, 2) + "\n", "src/data/wcag.json"));
@@ -543,6 +831,7 @@ async function main() {
543
831
  if (refreshIdx !== -1) deriveSnapshot(process.argv[refreshIdx + 1]);
544
832
  if (process.argv.includes("--refresh-universe")) await deriveUniverse();
545
833
  if (process.argv.includes("--refresh-fr")) await deriveFr();
834
+ if (process.argv.includes("--refresh-text")) await deriveText();
546
835
  build();
547
836
  }
548
837