@goflag/cli 0.2.5 → 0.2.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.
package/dist/cli.js CHANGED
@@ -4,12 +4,15 @@ import {
4
4
  Logger,
5
5
  PROFILES,
6
6
  PROFILE_NAMES,
7
+ PROSE_RULES,
8
+ RULES,
9
+ SITE_RULES,
7
10
  exitCode,
8
11
  renderSummaryTerminal,
9
12
  renderTerminal,
10
13
  runAudit,
11
14
  summarize
12
- } from "./chunk-5FO2Q3II.js";
15
+ } from "./chunk-UOKYUKHQ.js";
13
16
 
14
17
  // src/report/diff.ts
15
18
  function collectFindings(report) {
@@ -292,14 +295,23 @@ var HELP = `goflag \u2014 audit a site for broken links, missing translations, a
292
295
 
293
296
  Usage:
294
297
  goflag <url> [options]
298
+ goflag rules Print the rule catalogue as JSON and exit. No crawl
299
+ and no network: every rule with its severity, its
300
+ rigor and the documents it cites.
295
301
 
296
302
  Options:
297
303
  --json Print the JSON report to stdout (nothing else).
298
304
  --summary, -s Roll findings up (dedup by link/rule/code). Pairs
299
305
  with --json for a compact, agent-friendly payload.
300
306
  --report <file> Write the (full) JSON report to <file>.
301
- --depth <n> Crawl depth (0 = entry page only). Default: 2.
302
- --max-pages <n> Hard cap on pages crawled. Default: 200.
307
+ --depth <n> How far to follow links out of each page (0 = follow
308
+ none). Sitemap URLs are seeded regardless, so --depth 0
309
+ alone is not "entry page only" \u2014 add --no-sitemap for
310
+ that. Default: 2.
311
+ --max-pages <n> Page budget for the crawl. A hard cap under
312
+ --coverage all; under structural coverage the selection
313
+ wins and the budget is max(<n>, selected + 5).
314
+ Default: 200.
303
315
  --coverage <mode> How to choose which pages to audit.
304
316
  "structural" (default when a sitemap is found) keeps
305
317
  every standalone page and samples families of pages
@@ -308,9 +320,12 @@ Options:
308
320
  --include <glob> Only crawl paths matching <glob> (repeatable).
309
321
  --exclude <glob> Skip paths matching <glob> (repeatable).
310
322
  --locales <list> Comma-separated locales the site serves, e.g.
311
- "fr,en,pt-br". Authoritative: overrides what the
312
- sitemap and crawl suggest, and makes a locale the
313
- site does not serve yet show up as missing.
323
+ "fr,en,pt-br". Unioned with what the sitemap shows,
324
+ never substituted for it, and the only way to make a
325
+ locale the site does not serve yet show up as missing.
326
+ Also folds /en/\u2026 and /fr/\u2026 into one route family for
327
+ structural coverage, so pass it on any locale-prefixed
328
+ site.
314
329
  --ignore-holes <glob> Route (locale-free) that is deliberately not
315
330
  translated everywhere, so its gaps are not reported
316
331
  as missing translations (repeatable).
@@ -352,7 +367,9 @@ ${PROFILE_HELP}
352
367
  --no-external Do not probe off-origin (external) links.
353
368
  --static Static HTML only; never launch headless Chromium.
354
369
  --allow-insecure-tls Accept self-signed / invalid TLS (localhost, tunnels).
355
- --timeout <ms> Per-request timeout in ms. Default: 8000.
370
+ --timeout <ms> Per-request timeout in ms, for page fetches and link
371
+ probes alike. Unset, the defaults differ: 8000 for link
372
+ probes, 15000 for page fetches.
356
373
  --verbose, -V Log every page as it is analyzed.
357
374
  --quiet, -q Suppress the live progress output.
358
375
  --no-color Disable coloured output.
@@ -518,7 +535,8 @@ function parseArgs(argv) {
518
535
  break;
519
536
  default:
520
537
  if (arg && arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
521
- if (arg && !parsed.url) parsed.url = arg;
538
+ if (arg === "rules" && i === 0) parsed.command = "rules";
539
+ else if (arg && !parsed.url) parsed.url = arg;
522
540
  else if (arg) throw new Error(`unexpected argument: ${arg}`);
523
541
  }
524
542
  }
@@ -541,6 +559,476 @@ function toInt(value, flag) {
541
559
  return n;
542
560
  }
543
561
 
562
+ // src/lib/rules/sources/index.ts
563
+ var NORMATIVE = [
564
+ {
565
+ id: "whatwg-html-title",
566
+ publisher: "WHATWG",
567
+ rigor: "normative",
568
+ title: "HTML Living Standard \u2014 the title element",
569
+ url: "https://html.spec.whatwg.org/multipage/semantics.html",
570
+ anchor: "the-title-element",
571
+ retrievedAt: "2026-08-06",
572
+ note: "Documents must have exactly one title element in the head, and its text should identify the document even out of context (e.g. in history or bookmarks)."
573
+ },
574
+ {
575
+ id: "whatwg-html-meta",
576
+ publisher: "WHATWG",
577
+ rigor: "normative",
578
+ title: "HTML Living Standard \u2014 the meta element",
579
+ url: "https://html.spec.whatwg.org/multipage/semantics.html",
580
+ anchor: "the-meta-element",
581
+ retrievedAt: "2026-08-06",
582
+ note: "Defines the syntax of meta: exactly one of name, http-equiv, charset or itemprop must be present, and name/http-equiv variants require a content attribute."
583
+ },
584
+ {
585
+ id: "whatwg-html-standard-metadata-names",
586
+ publisher: "WHATWG",
587
+ rigor: "normative",
588
+ title: "HTML Living Standard \u2014 standard metadata names",
589
+ url: "https://html.spec.whatwg.org/multipage/semantics.html",
590
+ anchor: "standard-metadata-names",
591
+ retrievedAt: "2026-08-06",
592
+ note: "The registry of standard meta names \u2014 description, viewport, theme-color, robots, generator, \u2026 \u2014 and the conformance requirements for each (e.g. at most one meta description per document)."
593
+ },
594
+ {
595
+ id: "whatwg-html-link",
596
+ publisher: "WHATWG",
597
+ rigor: "normative",
598
+ title: "HTML Living Standard \u2014 the link element",
599
+ url: "https://html.spec.whatwg.org/multipage/semantics.html",
600
+ anchor: "the-link-element",
601
+ retrievedAt: "2026-08-06",
602
+ note: "Defines the syntax of link: a rel attribute (or itemprop) is required, and href must be a valid non-empty URL potentially surrounded by spaces."
603
+ },
604
+ {
605
+ id: "whatwg-html-link-types",
606
+ publisher: "WHATWG",
607
+ rigor: "normative",
608
+ title: "HTML Living Standard \u2014 link types",
609
+ url: "https://html.spec.whatwg.org/multipage/links.html",
610
+ anchor: "linkTypes",
611
+ retrievedAt: "2026-08-06",
612
+ note: "The registry of rel keywords \u2014 canonical, alternate, icon, manifest, \u2026 \u2014 including which are allowed on link vs a/area, and the semantics of each (e.g. alternate + hreflang designates a translation)."
613
+ },
614
+ {
615
+ id: "whatwg-html-lang",
616
+ publisher: "WHATWG",
617
+ rigor: "normative",
618
+ title: "HTML Living Standard \u2014 the lang and xml:lang attributes",
619
+ url: "https://html.spec.whatwg.org/multipage/dom.html",
620
+ anchor: "the-lang-and-xml:lang-attributes",
621
+ retrievedAt: "2026-08-06",
622
+ note: "lang gives the language of the element's contents and must be a valid BCP 47 language tag or the empty string; authors are encouraged to set it on the root html element."
623
+ },
624
+ {
625
+ id: "whatwg-html-dir",
626
+ publisher: "WHATWG",
627
+ rigor: "normative",
628
+ title: "HTML Living Standard \u2014 the dir attribute",
629
+ url: "https://html.spec.whatwg.org/multipage/dom.html",
630
+ anchor: "the-dir-attribute",
631
+ retrievedAt: "2026-08-06",
632
+ note: "dir declares text directionality: ltr, rtl, or auto. Governs whether an rtl-language page declares its direction explicitly."
633
+ },
634
+ {
635
+ id: "whatwg-url",
636
+ publisher: "WHATWG",
637
+ rigor: "normative",
638
+ title: "URL Standard",
639
+ url: "https://url.spec.whatwg.org/",
640
+ retrievedAt: "2026-08-06",
641
+ note: "How browsers actually parse URLs \u2014 the parsing/validity contract goflag applies when it checks that a canonical or alternate href is a valid, absolute URL."
642
+ },
643
+ {
644
+ id: "whatwg-encoding",
645
+ publisher: "WHATWG",
646
+ rigor: "normative",
647
+ title: "Encoding Standard",
648
+ url: "https://encoding.spec.whatwg.org/",
649
+ retrievedAt: "2026-08-06",
650
+ note: "Defines character encodings for the web; UTF-8 is the mandatory encoding for new content, which is what a meta charset declaration should say."
651
+ },
652
+ {
653
+ id: "ietf-rfc3986",
654
+ publisher: "IETF",
655
+ rigor: "normative",
656
+ title: "RFC 3986 \u2014 URI Generic Syntax",
657
+ url: "https://www.rfc-editor.org/rfc/rfc3986",
658
+ retrievedAt: "2026-08-06",
659
+ note: "The generic URI grammar: scheme, authority, path, query, fragment. The formal definition of what an absolute URL is."
660
+ },
661
+ {
662
+ id: "ietf-rfc3987",
663
+ publisher: "IETF",
664
+ rigor: "normative",
665
+ title: "RFC 3987 \u2014 Internationalized Resource Identifiers (IRIs)",
666
+ url: "https://www.rfc-editor.org/rfc/rfc3987",
667
+ retrievedAt: "2026-08-06",
668
+ note: "Extends URIs to non-ASCII characters and defines the IRI-to-URI mapping \u2014 relevant when localized sites use non-ASCII paths."
669
+ },
670
+ {
671
+ id: "ietf-bcp47",
672
+ publisher: "IETF",
673
+ rigor: "normative",
674
+ title: "RFC 5646 / BCP 47 \u2014 Tags for Identifying Languages",
675
+ url: "https://www.rfc-editor.org/rfc/rfc5646",
676
+ retrievedAt: "2026-08-06",
677
+ note: "The grammar of language tags used by lang and hreflang (language[-script][-region]\u2026). Tags are case-insensitive; the familiar en-US casing is only a convention."
678
+ },
679
+ {
680
+ id: "ietf-rfc6596",
681
+ publisher: "IETF",
682
+ rigor: "normative",
683
+ title: "RFC 6596 \u2014 The Canonical Link Relation",
684
+ url: "https://www.rfc-editor.org/rfc/rfc6596",
685
+ retrievedAt: "2026-08-06",
686
+ note: 'Defines rel="canonical": the preferred IRI for duplicative content. The target must not be a 404 and should not vary per duplicate (no self-referencing loops through redirects).'
687
+ },
688
+ {
689
+ id: "ietf-rfc9309",
690
+ publisher: "IETF",
691
+ rigor: "normative",
692
+ title: "RFC 9309 \u2014 Robots Exclusion Protocol",
693
+ url: "https://www.rfc-editor.org/rfc/rfc9309",
694
+ retrievedAt: "2026-08-06",
695
+ note: "The robots.txt file format and matching rules: group selection by user-agent, longest-match precedence, allow wins ties, and how errors/unavailability must be treated."
696
+ },
697
+ {
698
+ id: "sitemaps-protocol",
699
+ publisher: "sitemaps.org",
700
+ rigor: "normative",
701
+ title: "Sitemaps XML protocol",
702
+ url: "https://www.sitemaps.org/protocol.html",
703
+ retrievedAt: "2026-08-06",
704
+ note: "The sitemap.xml schema: urlset/url/loc structure, optional lastmod/changefreq/priority, the 50,000-URL / 50 MB limits, and sitemap index files."
705
+ },
706
+ {
707
+ id: "w3c-appmanifest",
708
+ publisher: "W3C",
709
+ rigor: "normative",
710
+ title: "Web Application Manifest",
711
+ url: "https://www.w3.org/TR/appmanifest/",
712
+ retrievedAt: "2026-08-06",
713
+ note: 'The manifest.json format linked via link rel="manifest": members like name, icons, start_url, display, and how user agents process them.'
714
+ },
715
+ {
716
+ id: "w3c-wcag22",
717
+ publisher: "W3C",
718
+ rigor: "normative",
719
+ title: "WCAG 2.2",
720
+ url: "https://www.w3.org/TR/WCAG22/",
721
+ retrievedAt: "2026-08-06",
722
+ note: "Accessibility success criteria \u2014 language of page (3.1.1), non-text content / alt (1.1.1), contrast, \u2026 Reserved for the future a11y vertical."
723
+ },
724
+ {
725
+ id: "w3c-wai-aria12",
726
+ publisher: "W3C",
727
+ rigor: "normative",
728
+ title: "WAI-ARIA 1.2",
729
+ url: "https://www.w3.org/TR/wai-aria-1.2/",
730
+ retrievedAt: "2026-08-06",
731
+ note: "ARIA roles, states and properties. Reserved for the future a11y vertical."
732
+ },
733
+ {
734
+ id: "w3c-i18n-language-tags",
735
+ publisher: "W3C",
736
+ rigor: "guideline",
737
+ title: "W3C i18n \u2014 Language tags in HTML and XML",
738
+ url: "https://www.w3.org/International/articles/language-tags/",
739
+ retrievedAt: "2026-08-06",
740
+ note: "W3C i18n guidance on applying BCP 47 in practice: keep tags as short as possible, subtag order and casing conventions, when to include script or region."
741
+ }
742
+ ];
743
+ var VENDOR = [
744
+ {
745
+ id: "ogp",
746
+ publisher: "ogp.me",
747
+ rigor: "vendor-spec",
748
+ title: "The Open Graph protocol",
749
+ url: "https://ogp.me/",
750
+ retrievedAt: "2026-08-06",
751
+ note: "The og:* vocabulary. og:title, og:type, og:image and og:url are required for every page that wants to be a rich object in the social graph; og:description and others are optional."
752
+ },
753
+ {
754
+ id: "x-cards",
755
+ publisher: "X (Twitter)",
756
+ rigor: "vendor-spec",
757
+ title: "X Cards \u2014 about cards markup",
758
+ url: "https://developer.x.com/en/docs/twitter-for-websites/cards/overview/abouts-cards",
759
+ retrievedAt: "2026-08-06",
760
+ note: "The twitter:* card vocabulary (twitter:card, twitter:title, \u2026) and the fallback behavior: X's crawler falls back to og:* tags when twitter:* equivalents are absent. X has since removed most of this documentation (the URL now redirects to docs.x.com/overview) but the crawler behavior and tag names are unchanged \u2014 the tags stayed twitter:*, not x:*."
761
+ },
762
+ {
763
+ id: "google-canonicalization",
764
+ publisher: "Google",
765
+ rigor: "vendor-spec",
766
+ title: "Consolidate duplicate URLs (canonicalization)",
767
+ url: "https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls",
768
+ retrievedAt: "2026-08-06",
769
+ note: `How Google treats rel="canonical": a strong hint (not a directive), absolute URLs recommended, don't point to redirecting or 404 targets, one canonical per page.`
770
+ },
771
+ {
772
+ id: "google-robots-meta",
773
+ publisher: "Google",
774
+ rigor: "vendor-spec",
775
+ title: "Robots meta tag and X-Robots-Tag",
776
+ url: "https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag",
777
+ retrievedAt: "2026-08-06",
778
+ note: "The robots meta / X-Robots-Tag directives (noindex, nofollow, \u2026), how meta and header directives combine, and that the most restrictive directive wins on conflict."
779
+ },
780
+ {
781
+ id: "google-robots-intro",
782
+ publisher: "Google",
783
+ rigor: "vendor-spec",
784
+ title: "Introduction to robots.txt",
785
+ url: "https://developers.google.com/search/docs/crawling-indexing/robots/intro",
786
+ retrievedAt: "2026-08-06",
787
+ note: "Google's robots.txt behavior on top of RFC 9309 \u2014 notably that robots.txt controls crawling, not indexing: a blocked URL can still be indexed from links, so noindex needs the page to be crawlable."
788
+ },
789
+ {
790
+ id: "google-hreflang",
791
+ publisher: "Google",
792
+ rigor: "vendor-spec",
793
+ title: "Localized versions of your pages (hreflang)",
794
+ url: "https://developers.google.com/search/docs/specialty/international/localized-versions",
795
+ retrievedAt: "2026-08-06",
796
+ note: "hreflang via link annotations, headers or sitemaps: every language version must list itself and all others (reciprocity), use absolute URLs, and x-default marks the unmatched-language fallback."
797
+ },
798
+ {
799
+ id: "google-title-link",
800
+ publisher: "Google",
801
+ rigor: "guideline",
802
+ title: "Influence your title links in search results",
803
+ url: "https://developers.google.com/search/docs/appearance/title-link",
804
+ retrievedAt: "2026-08-06",
805
+ note: "Write descriptive, concise, non-boilerplate titles unique to each page. Google may rewrite titles it considers poor; length itself is not a ranking factor."
806
+ },
807
+ {
808
+ id: "google-snippet",
809
+ publisher: "Google",
810
+ rigor: "guideline",
811
+ title: "Control your snippets in search results",
812
+ url: "https://developers.google.com/search/docs/appearance/snippet",
813
+ retrievedAt: "2026-08-06",
814
+ note: "Meta descriptions feed the search snippet but Google may substitute on-page text; write a unique, accurate summary per page rather than keyword stuffing."
815
+ },
816
+ {
817
+ id: "google-structured-data",
818
+ publisher: "Google",
819
+ rigor: "vendor-spec",
820
+ title: "Introduction to structured data markup",
821
+ url: "https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data",
822
+ retrievedAt: "2026-08-06",
823
+ note: "How Google consumes structured data (JSON-LD recommended) and what rich-result eligibility requires; the vocabulary itself is schema.org's."
824
+ },
825
+ {
826
+ id: "google-sitemaps",
827
+ publisher: "Google",
828
+ rigor: "guideline",
829
+ title: "Build and submit a sitemap",
830
+ url: "https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview",
831
+ retrievedAt: "2026-08-06",
832
+ note: "Google's sitemap practice on top of the sitemaps.org protocol: when a sitemap helps, supported formats, and submission via robots.txt or Search Console."
833
+ },
834
+ {
835
+ id: "schema-org",
836
+ publisher: "schema.org",
837
+ rigor: "vendor-spec",
838
+ title: "schema.org vocabulary",
839
+ url: "https://schema.org/",
840
+ retrievedAt: "2026-08-06",
841
+ note: "The shared vocabulary (types and properties) that structured data \u2014 JSON-LD in particular \u2014 is written in."
842
+ },
843
+ {
844
+ id: "apple-web-apps",
845
+ publisher: "Apple",
846
+ rigor: "vendor-spec",
847
+ title: "Configuring web applications (Safari Web Content Guide)",
848
+ url: "https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html",
849
+ retrievedAt: "2026-08-06",
850
+ note: "apple-touch-icon and the apple-mobile-web-app-* meta tags: how iOS picks home-screen icons and startup images. Archived but still the reference Apple points to."
851
+ },
852
+ {
853
+ id: "meta-og-sharing",
854
+ publisher: "Meta",
855
+ rigor: "vendor-spec",
856
+ title: "Sharing \u2014 webmasters (Open Graph usage)",
857
+ url: "https://developers.facebook.com/docs/sharing/webmasters/",
858
+ retrievedAt: "2026-08-06",
859
+ note: "How Meta's crawler builds link previews from og:* tags \u2014 including image size expectations (1200\xD7630 recommended, 200\xD7200 minimum) beyond what ogp.me specifies."
860
+ },
861
+ {
862
+ id: "bing-webmaster-guidelines",
863
+ publisher: "Microsoft",
864
+ rigor: "guideline",
865
+ title: "Bing Webmaster Guidelines",
866
+ url: "https://www.bing.com/webmasters/help/webmasters-guidelines-30fba23a",
867
+ retrievedAt: "2026-08-06",
868
+ note: "Bing's crawlability and content guidance \u2014 a cross-check that the catalog is not Google-only."
869
+ }
870
+ ];
871
+ var PRACTICAL = [
872
+ {
873
+ id: "mdn-meta-name",
874
+ publisher: "MDN / Mozilla",
875
+ rigor: "guideline",
876
+ title: "MDN \u2014 <meta> name attribute values",
877
+ url: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name",
878
+ retrievedAt: "2026-08-06",
879
+ note: "Practical reference for standard and de-facto meta names, browser support included."
880
+ },
881
+ {
882
+ id: "mdn-viewport",
883
+ publisher: "MDN / Mozilla",
884
+ rigor: "guideline",
885
+ title: "MDN \u2014 Viewport meta tag",
886
+ url: "https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag",
887
+ retrievedAt: "2026-08-06",
888
+ note: "width=device-width, initial-scale=1 and friends: what the viewport meta controls and the accessibility pitfalls (never disable user scaling)."
889
+ },
890
+ {
891
+ id: "mdn-link-rel",
892
+ publisher: "MDN / Mozilla",
893
+ rigor: "guideline",
894
+ title: "MDN \u2014 rel attribute values",
895
+ url: "https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel",
896
+ retrievedAt: "2026-08-06",
897
+ note: "Practical reference for rel values across link, a and form, with support notes."
898
+ },
899
+ {
900
+ id: "lighthouse-seo",
901
+ publisher: "Google",
902
+ rigor: "guideline",
903
+ title: "Lighthouse \u2014 SEO audits",
904
+ url: "https://developer.chrome.com/docs/lighthouse/seo/",
905
+ retrievedAt: "2026-08-06",
906
+ note: "The audit definitions Lighthouse ships (document has a title, has a meta description, valid hreflang, valid canonical, \u2026) \u2014 the closest thing to an industry-standard checklist to mirror."
907
+ },
908
+ {
909
+ id: "axe-core-rules",
910
+ publisher: "Deque",
911
+ rigor: "guideline",
912
+ title: "axe-core \u2014 rule descriptions",
913
+ url: "https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md",
914
+ retrievedAt: "2026-08-06",
915
+ note: "The a11y rule catalog to cross-check against when the a11y vertical lands (html-has-lang, valid-lang, image-alt, \u2026)."
916
+ },
917
+ {
918
+ id: "w3c-nu-checker",
919
+ publisher: "W3C",
920
+ rigor: "guideline",
921
+ title: "Nu HTML Checker",
922
+ url: "https://validator.w3.org/nu/",
923
+ retrievedAt: "2026-08-06",
924
+ note: "The reference HTML validator \u2014 the cross-check for goflag's document-level syntax rules."
925
+ },
926
+ {
927
+ id: "moz-title-tag",
928
+ publisher: "Moz",
929
+ rigor: "heuristic",
930
+ title: "Title tag best practices",
931
+ url: "https://moz.com/learn/seo/title-tag",
932
+ retrievedAt: "2026-08-06",
933
+ note: "The ~50\u201360 character title window is display folklore, not spec: it approximates SERP truncation width. Ships as heuristic so agents weight it accordingly."
934
+ },
935
+ {
936
+ id: "moz-meta-description",
937
+ publisher: "Moz",
938
+ rigor: "heuristic",
939
+ title: "Meta description best practices",
940
+ url: "https://moz.com/learn/seo/meta-description",
941
+ retrievedAt: "2026-08-06",
942
+ note: "The ~50\u2013160 character description window is display folklore, not spec: it approximates snippet truncation. Ships as heuristic so agents weight it accordingly."
943
+ }
944
+ ];
945
+ var SOURCES = [...NORMATIVE, ...VENDOR, ...PRACTICAL];
946
+ function getSource(id) {
947
+ return SOURCES.find((source) => source.id === id);
948
+ }
949
+
950
+ // src/lib/rules/catalog.ts
951
+ function fixOf(fix) {
952
+ if (!fix) return void 0;
953
+ return { title: fix.title, snippet: fix.snippet, language: fix.language ?? "ts" };
954
+ }
955
+ function buildRuleCatalog(version) {
956
+ const rules = [];
957
+ for (const rule of RULES) {
958
+ rules.push({
959
+ id: rule.id,
960
+ scope: "page",
961
+ category: rule.category,
962
+ severity: rule.kind === "boolean" ? rule.severity : rule.severityByBand.acceptable,
963
+ kind: rule.kind,
964
+ summary: rule.title,
965
+ why: rule.why,
966
+ rigor: rule.rigor,
967
+ sources: [...rule.sources],
968
+ reads: [...rule.reads],
969
+ ...rule.relates ? { relates: [...rule.relates] } : {},
970
+ ...rule.expected ? { expected: rule.expected } : {},
971
+ ...fixOf(rule.fix) ? { fix: fixOf(rule.fix) } : {},
972
+ ...rule.kind === "scored" ? { bands: { ...rule.bands }, severityByBand: { ...rule.severityByBand } } : {}
973
+ });
974
+ }
975
+ for (const rule of SITE_RULES) {
976
+ rules.push({
977
+ id: rule.id,
978
+ scope: "site",
979
+ severity: rule.severity,
980
+ kind: "boolean",
981
+ summary: rule.summary,
982
+ rigor: null,
983
+ sources: []
984
+ });
985
+ }
986
+ for (const rule of PROSE_RULES) {
987
+ rules.push({
988
+ id: rule.id,
989
+ scope: "prose",
990
+ category: rule.category,
991
+ severity: null,
992
+ kind: "prose",
993
+ summary: rule.title,
994
+ why: rule.why,
995
+ rigor: rule.rigor,
996
+ sources: [...rule.sources],
997
+ reads: [...rule.reads],
998
+ prose: rule.prose
999
+ });
1000
+ }
1001
+ rules.sort((a, b) => a.id.localeCompare(b.id));
1002
+ const cited = new Set(rules.flatMap((r) => r.sources));
1003
+ const sources = {};
1004
+ for (const id of [...cited].sort()) {
1005
+ const doc = getSource(id);
1006
+ if (doc) {
1007
+ sources[id] = {
1008
+ id: doc.id,
1009
+ publisher: doc.publisher,
1010
+ title: doc.title,
1011
+ rigor: doc.rigor,
1012
+ url: doc.url
1013
+ };
1014
+ }
1015
+ }
1016
+ return {
1017
+ ...version ? { version } : {},
1018
+ counts: {
1019
+ page: RULES.length,
1020
+ site: SITE_RULES.length,
1021
+ prose: PROSE_RULES.length
1022
+ },
1023
+ sources,
1024
+ rules
1025
+ };
1026
+ }
1027
+ function serialiseCatalog(catalog) {
1028
+ return `${JSON.stringify(catalog, null, 2)}
1029
+ `;
1030
+ }
1031
+
544
1032
  // src/cli.ts
545
1033
  async function readVersion() {
546
1034
  try {
@@ -582,6 +1070,10 @@ ${HELP}
582
1070
  `);
583
1071
  return 0;
584
1072
  }
1073
+ if (args.command === "rules") {
1074
+ process.stdout.write(serialiseCatalog(buildRuleCatalog(await readVersion())));
1075
+ return 0;
1076
+ }
585
1077
  if (!args.url) {
586
1078
  process.stderr.write(`goflag: missing <url>
587
1079