@contentrain/verify 0.1.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.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # @contentrain/verify
2
+
3
+ Portable gates for a migrated site: identity, indexing, status, hreflang,
4
+ structured data, navigation and assets — checked over documents you already
5
+ have, with no network and no dependencies.
6
+
7
+ MIT. Part of the [Contentrain](https://contentrain.io) migration pipeline, and
8
+ usable on its own against any static build.
9
+
10
+ ```bash
11
+ npx @contentrain/verify dist --site https://example.com
12
+ ```
13
+
14
+ ## Why it does not fetch
15
+
16
+ A hardened crawler is a security surface of its own — SSRF, DNS rebinding,
17
+ redirect limits, byte and time caps, robots handling — and it belongs where that
18
+ hardening is maintained, not inside the MIT library every consumer embeds.
19
+
20
+ So this package takes documents. That constraint turns out to be the feature:
21
+ the same gate runs over a `dist/` directory, over pages a migration engine
22
+ already captured, and inside CI, with no network calls to mock and no flake.
23
+ Whoever holds a safe fetcher supplies the pages.
24
+
25
+ ## Usage
26
+
27
+ ```ts
28
+ import { verify, formatReport } from '@contentrain/verify'
29
+
30
+ const report = verify({
31
+ site: 'https://example.com',
32
+ documents: [{ url: '/about', html, status: 200, headers }],
33
+ assets: ['/img/hero.png'],
34
+ sitemap: sitemapXml,
35
+ redirects: [{ from: '/old', to: '/about', status: 301 }],
36
+ baseline: { documents: oldPages }, // optional: enables the parity checks
37
+ })
38
+
39
+ report.passed // no error findings — this is the gate
40
+ report.findings // every finding, with a stable `check` id
41
+ report.skipped // what could not run, and why
42
+ console.log(formatReport(report))
43
+ ```
44
+
45
+ Or over a built directory:
46
+
47
+ ```ts
48
+ import { loadSiteDirectory, verify } from '@contentrain/verify'
49
+
50
+ const report = verify(await loadSiteDirectory('dist', 'https://example.com'))
51
+ ```
52
+
53
+ ## The checks
54
+
55
+ Severity is decided by one rule: **error** means the built site is wrong — a
56
+ visitor or a crawler gets something it should not. **warning** means it is worse
57
+ than the site it replaced, or worse than it should be. A migration that improves
58
+ a page must not fail its own gate.
59
+
60
+ | Group | Check | Severity | What it catches |
61
+ |---|---|---|---|
62
+ | identity | `identity.title-missing` | error | No non-empty `<title>` |
63
+ | | `identity.title-duplicate` | error | Two titles in the head — a crawler picks one, undefined which. A `<title>` inside an inline `<svg>` is an accessibility label and is not counted |
64
+ | | `identity.canonical-missing` | warning | No canonical link |
65
+ | | `identity.canonical-duplicate` | error | Two canonicals: the page says two things about itself |
66
+ | | `identity.canonical-mismatch` | error | **The clone failure.** A page carrying the template's canonical points the whole site at one URL and removes it from the index. Invisible in a browser, worse than having no canonical |
67
+ | | `identity.description-missing` | warning | No meta description |
68
+ | | `identity.open-graph-incomplete` | warning | Missing `og:title` / `og:type` / `og:url` — shares render without a card |
69
+ | | `identity.twitter-card-missing` | info | No `twitter:card` |
70
+ | indexing | `indexing.noindex` | error | **The staging leak.** A `noindex` in the meta or `X-Robots-Tag` survives deploy and the site disappears from search over the following weeks |
71
+ | | `indexing.sitemap-missing-entry` | warning | An indexable page is not in the sitemap (a `noindex` page is not asked to be) |
72
+ | | `indexing.sitemap-stale-entry` | warning | The sitemap lists a page the build does not serve |
73
+ | status | `status.mismatch` | error | The status changed against the baseline |
74
+ | | `status.soft-404` | error | A "not found" page answering 200 — invisible to every status monitor, indexed as content |
75
+ | | `status.redirect-chain` | warning | More hops than `maxRedirectHops` (default 1) |
76
+ | | `status.redirect-loop` | error | A chain that returns to itself |
77
+ | | `status.redirect-target-missing` | error | An internal redirect pointing at a page the build does not serve |
78
+ | international | `international.hreflang-not-reciprocal` | error | A names B but B does not name A. Search engines ignore one-sided alternates entirely, so half the work is worth nothing |
79
+ | | `international.hreflang-no-self` | warning | A page with alternates that does not list itself |
80
+ | | `international.hreflang-unknown-target` | warning | An alternate pointing at a page that does not exist |
81
+ | structured | `structured.jsonld-invalid` | error | A JSON-LD block that does not parse — consumers discard the whole block |
82
+ | | `structured.type-lost` | warning | A `@type` the old page had is gone. Types inside `@graph` are counted, which is how themes actually ship them |
83
+ | navigation | `navigation.broken-internal-link` | error | An internal link that goes nowhere. A path a redirect rule covers is fine; external, `mailto:`, `tel:` and fragment links are not checked |
84
+ | | `navigation.pagination-broken` | warning | `rel="next"` / `rel="prev"` pointing at a page the build does not serve |
85
+ | | `navigation.feed-lost` | warning | The old page advertised a feed and this one does not |
86
+ | assets | `assets.broken-image` | error | An internal image that resolves to nothing the build serves. `data:` URIs and third-party hosts are skipped |
87
+ | | `assets.missing-alt` | warning | An `<img>` with no `alt` attribute. `alt=""` is a decision (decorative) and is not reported |
88
+ | | `assets.alt-lost` | warning | Alt text the old page had for the same image is gone |
89
+
90
+ ## Address identity
91
+
92
+ Every "does this go somewhere that exists" question is a normalisation question,
93
+ and getting it wrong in either direction is worse than not asking: too strict and
94
+ a correct site fails, too loose and a broken one passes.
95
+
96
+ - `/about`, `/about/` and `/about/index.html` are one address, because static
97
+ hosts serve them that way.
98
+ - Query strings and fragments are not part of a document's identity.
99
+ - Case is preserved, because static hosts are case-sensitive even when the
100
+ author assumed otherwise.
101
+
102
+ Without `site`, relative links still resolve (against a reserved origin) and
103
+ absolute links to real hosts are treated as external. Supplying `site` is better:
104
+ it is what makes `canonical-mismatch` and cross-origin checks exact.
105
+
106
+ ## What a missing input does
107
+
108
+ Nothing is silently skipped. A run without a baseline, a sitemap or redirect
109
+ rules reports those absences in `report.skipped` — a green report over a run that
110
+ compared nothing is the most dangerous output this package could produce.
111
+
112
+ ## CLI
113
+
114
+ ```
115
+ contentrain-verify <dist-dir> [options]
116
+
117
+ --site <url> Canonical origin
118
+ --baseline <dir> The old site, as a directory of captured pages
119
+ --redirects <file> JSON: [{ "from": "/a", "to": "/b", "status": 301 }]
120
+ --groups <list> identity,indexing,status,international,structured,navigation,assets
121
+ --max-redirect-hops <n>
122
+ --json
123
+ ```
124
+
125
+ Exits 1 when any finding is an error, 2 on a usage error. Unknown options and
126
+ unknown group names are rejected rather than ignored.
127
+
128
+ ## Reading HTML
129
+
130
+ Targeted regexes, not a parser — the same choice `@contentrain/emitter-astro`
131
+ makes, for the same reason: these are machine-generated documents whose head the
132
+ pipeline controls, the extracted set is small and fixed, and a parser would put a
133
+ dependency into the one package a migration should be able to run anywhere.
134
+
135
+ The limits are honoured rather than papered over. Attribute reads require
136
+ whitespace before the name, so `data-src` never answers a read of `src` — which
137
+ matters, because lazy-loading WordPress themes put the real URL in `data-src` on
138
+ almost every image. Where a check would be wrong under these limits, it is not
139
+ written.
140
+
141
+ ## Related
142
+
143
+ - [`@contentrain/emitter-astro`](../emitter-astro) — produces the site this verifies
144
+ - [`@contentrain/wp-import`](../wp-import) — produces the content
145
+ - [`@contentrain/types`](../types) — the shared contracts
146
+
147
+ ## License
148
+
149
+ MIT
package/dist/bin.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/bin.mjs ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./cli.mjs";
3
+ //#region src/bin.ts
4
+ main(process.argv.slice(2)).then((code) => {
5
+ process.exitCode = code;
6
+ }, (error) => {
7
+ console.error(error instanceof Error ? error.message : String(error));
8
+ process.exitCode = 2;
9
+ });
10
+ //#endregion
11
+ export {};
12
+
13
+ //# sourceMappingURL=bin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.mjs","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n// The executable. Kept separate from `cli.ts` so the shell can be imported and\n// tested without a module side effect that runs it.\n\nimport { main } from './cli.js'\n\nmain(process.argv.slice(2)).then(\n (code) => { process.exitCode = code },\n (error: unknown) => {\n console.error(error instanceof Error ? error.message : String(error))\n process.exitCode = 2\n },\n)\n"],"mappings":";;;AAMA,KAAK,QAAQ,KAAK,MAAM,EAAE,CAAC,CAAC,MACzB,SAAS;AAAE,SAAQ,WAAW;IAC9B,UAAmB;AAClB,SAAQ,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,CAAC;AACrE,SAAQ,WAAW;EAEtB"}
package/dist/cli.d.mts ADDED
@@ -0,0 +1,24 @@
1
+ import { n as CheckGroup } from "./types-C_hmH1n3.mjs";
2
+
3
+ //#region src/cli.d.ts
4
+ declare const USAGE: string;
5
+ interface Args {
6
+ dir?: string;
7
+ site?: string;
8
+ baseline?: string;
9
+ redirects?: string;
10
+ groups?: CheckGroup[];
11
+ maxRedirectHops?: number;
12
+ json: boolean;
13
+ help: boolean;
14
+ }
15
+ declare function parseArgs(argv: readonly string[]): Args;
16
+ interface Console2 {
17
+ log: (line: string) => void;
18
+ error: (line: string) => void;
19
+ }
20
+ /** Returns the process exit code. `io` is injectable so tests read the output. */
21
+ declare function main(argv: readonly string[], io?: Console2): Promise<number>;
22
+ //#endregion
23
+ export { Args, Console2, USAGE, main, parseArgs };
24
+ //# sourceMappingURL=cli.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.mts","names":[],"sources":["../src/cli.ts"],"mappings":";;;cAca,KAAA;AAAA,UAWI,IAAA;EACf,GAAA;EACA,IAAA;EACA,QAAA;EACA,SAAA;EACA,MAAA,GAAS,UAAA;EACT,eAAA;EACA,IAAA;EACA,IAAA;AAAA;AAAA,iBAGc,SAAA,CAAU,IAAA,sBAA0B,IAAA;AAAA,UAyBnC,QAAA;EACf,GAAA,GAAM,IAAA;EACN,KAAA,GAAQ,IAAA;AAAA;;iBAIY,IAAA,CAAK,IAAA,qBAAyB,EAAA,GAAI,QAAA,GAAqB,OAAA"}
package/dist/cli.mjs ADDED
@@ -0,0 +1,68 @@
1
+ import { i as CHECK_GROUPS, n as formatReport, r as verify, t as loadSiteDirectory } from "./load-KUG-8Ug4.mjs";
2
+ import { readFile } from "node:fs/promises";
3
+ //#region src/cli.ts
4
+ const USAGE = `contentrain-verify <dist-dir> [options]
5
+
6
+ --site <url> Canonical origin, e.g. https://example.com
7
+ --baseline <dir> The old site, as a directory of captured pages
8
+ --redirects <file> JSON: [{ "from": "/a", "to": "/b", "status": 301 }]
9
+ --groups <list> Comma-separated: ${CHECK_GROUPS.join(",")}
10
+ --max-redirect-hops <n>
11
+ --json Print the report as JSON instead of text
12
+
13
+ Exits 1 when any finding is an error, 2 on a usage error.`;
14
+ function parseArgs(argv) {
15
+ const args = {
16
+ json: false,
17
+ help: false
18
+ };
19
+ for (let i = 0; i < argv.length; i += 1) {
20
+ const arg = argv[i];
21
+ const next = () => argv[++i];
22
+ if (arg === "--help" || arg === "-h") args.help = true;
23
+ else if (arg === "--json") args.json = true;
24
+ else if (arg === "--site") args.site = next();
25
+ else if (arg === "--baseline") args.baseline = next();
26
+ else if (arg === "--redirects") args.redirects = next();
27
+ else if (arg === "--max-redirect-hops") {
28
+ const value = Number(next());
29
+ if (!Number.isInteger(value) || value < 0) throw new Error("--max-redirect-hops expects a non-negative integer");
30
+ args.maxRedirectHops = value;
31
+ } else if (arg === "--groups") {
32
+ const list = (next() ?? "").split(",").map((g) => g.trim()).filter(Boolean);
33
+ const unknown = list.filter((g) => !CHECK_GROUPS.includes(g));
34
+ if (unknown.length) throw new Error(`unknown group(s): ${unknown.join(", ")}`);
35
+ args.groups = list;
36
+ } else if (arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
37
+ else args.dir ??= arg;
38
+ }
39
+ return args;
40
+ }
41
+ /** Returns the process exit code. `io` is injectable so tests read the output. */
42
+ async function main(argv, io = console) {
43
+ let args;
44
+ try {
45
+ args = parseArgs(argv);
46
+ } catch (error) {
47
+ io.error(`${error.message}\n\n${USAGE}`);
48
+ return 2;
49
+ }
50
+ if (args.help || !args.dir) {
51
+ io.log(USAGE);
52
+ return args.help ? 0 : 2;
53
+ }
54
+ const input = {
55
+ ...await loadSiteDirectory(args.dir, args.site),
56
+ groups: args.groups,
57
+ options: args.maxRedirectHops === void 0 ? void 0 : { maxRedirectHops: args.maxRedirectHops }
58
+ };
59
+ if (args.baseline) input.baseline = { documents: (await loadSiteDirectory(args.baseline, args.site)).documents };
60
+ if (args.redirects) input.redirects = JSON.parse(await readFile(args.redirects, "utf8"));
61
+ const report = verify(input);
62
+ io.log(args.json ? JSON.stringify(report, null, 2) : formatReport(report));
63
+ return report.passed ? 0 : 1;
64
+ }
65
+ //#endregion
66
+ export { USAGE, main, parseArgs };
67
+
68
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["// ─── contentrain-verify ───\n//\n// A thin shell over `verify()`: read a built directory, run the gates, print\n// the report, return an exit code. Everything decisional lives in the library —\n// this adds argument parsing and a number, nothing else. `bin.ts` is the\n// executable; keeping the shell importable is what makes it testable without\n// spawning a process.\n\nimport { readFile } from 'node:fs/promises'\nimport { verify, formatReport } from './verify.js'\nimport { loadSiteDirectory } from './load.js'\nimport { CHECK_GROUPS } from './types.js'\nimport type { CheckGroup, VerifyInput } from './types.js'\n\nexport const USAGE = `contentrain-verify <dist-dir> [options]\n\n --site <url> Canonical origin, e.g. https://example.com\n --baseline <dir> The old site, as a directory of captured pages\n --redirects <file> JSON: [{ \"from\": \"/a\", \"to\": \"/b\", \"status\": 301 }]\n --groups <list> Comma-separated: ${CHECK_GROUPS.join(',')}\n --max-redirect-hops <n>\n --json Print the report as JSON instead of text\n\nExits 1 when any finding is an error, 2 on a usage error.`\n\nexport interface Args {\n dir?: string\n site?: string\n baseline?: string\n redirects?: string\n groups?: CheckGroup[]\n maxRedirectHops?: number\n json: boolean\n help: boolean\n}\n\nexport function parseArgs(argv: readonly string[]): Args {\n const args: Args = { json: false, help: false }\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i]!\n const next = (): string | undefined => argv[++i]\n if (arg === '--help' || arg === '-h') args.help = true\n else if (arg === '--json') args.json = true\n else if (arg === '--site') args.site = next()\n else if (arg === '--baseline') args.baseline = next()\n else if (arg === '--redirects') args.redirects = next()\n else if (arg === '--max-redirect-hops') {\n const value = Number(next())\n if (!Number.isInteger(value) || value < 0) throw new Error('--max-redirect-hops expects a non-negative integer')\n args.maxRedirectHops = value\n } else if (arg === '--groups') {\n const list = (next() ?? '').split(',').map(g => g.trim()).filter(Boolean)\n const unknown = list.filter(g => !(CHECK_GROUPS as readonly string[]).includes(g))\n if (unknown.length) throw new Error(`unknown group(s): ${unknown.join(', ')}`)\n args.groups = list as CheckGroup[]\n } else if (arg.startsWith('-')) throw new Error(`unknown option: ${arg}`)\n else args.dir ??= arg\n }\n return args\n}\n\nexport interface Console2 {\n log: (line: string) => void\n error: (line: string) => void\n}\n\n/** Returns the process exit code. `io` is injectable so tests read the output. */\nexport async function main(argv: readonly string[], io: Console2 = console): Promise<number> {\n let args: Args\n try {\n args = parseArgs(argv)\n } catch (error) {\n io.error(`${(error as Error).message}\\n\\n${USAGE}`)\n return 2\n }\n\n if (args.help || !args.dir) {\n io.log(USAGE)\n return args.help ? 0 : 2\n }\n\n const loaded = await loadSiteDirectory(args.dir, args.site)\n const input: VerifyInput = {\n ...loaded,\n groups: args.groups,\n options: args.maxRedirectHops === undefined ? undefined : { maxRedirectHops: args.maxRedirectHops },\n }\n\n if (args.baseline) {\n const before = await loadSiteDirectory(args.baseline, args.site)\n input.baseline = { documents: before.documents }\n }\n if (args.redirects) {\n input.redirects = JSON.parse(await readFile(args.redirects, 'utf8')) as VerifyInput['redirects']\n }\n\n const report = verify(input)\n io.log(args.json ? JSON.stringify(report, null, 2) : formatReport(report))\n return report.passed ? 0 : 1\n}\n"],"mappings":";;;AAcA,MAAa,QAAQ;;;;;2CAKsB,aAAa,KAAK,IAAI,CAAC;;;;;AAiBlE,SAAgB,UAAU,MAA+B;CACvD,MAAM,OAAa;EAAE,MAAM;EAAO,MAAM;EAAO;AAC/C,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;EACvC,MAAM,MAAM,KAAK;EACjB,MAAM,aAAiC,KAAK,EAAE;AAC9C,MAAI,QAAQ,YAAY,QAAQ,KAAM,MAAK,OAAO;WACzC,QAAQ,SAAU,MAAK,OAAO;WAC9B,QAAQ,SAAU,MAAK,OAAO,MAAM;WACpC,QAAQ,aAAc,MAAK,WAAW,MAAM;WAC5C,QAAQ,cAAe,MAAK,YAAY,MAAM;WAC9C,QAAQ,uBAAuB;GACtC,MAAM,QAAQ,OAAO,MAAM,CAAC;AAC5B,OAAI,CAAC,OAAO,UAAU,MAAM,IAAI,QAAQ,EAAG,OAAM,IAAI,MAAM,qDAAqD;AAChH,QAAK,kBAAkB;aACd,QAAQ,YAAY;GAC7B,MAAM,QAAQ,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC,CAAC,OAAO,QAAQ;GACzE,MAAM,UAAU,KAAK,QAAO,MAAK,CAAE,aAAmC,SAAS,EAAE,CAAC;AAClF,OAAI,QAAQ,OAAQ,OAAM,IAAI,MAAM,qBAAqB,QAAQ,KAAK,KAAK,GAAG;AAC9E,QAAK,SAAS;aACL,IAAI,WAAW,IAAI,CAAE,OAAM,IAAI,MAAM,mBAAmB,MAAM;MACpE,MAAK,QAAQ;;AAEpB,QAAO;;;AAST,eAAsB,KAAK,MAAyB,KAAe,SAA0B;CAC3F,IAAI;AACJ,KAAI;AACF,SAAO,UAAU,KAAK;UACf,OAAO;AACd,KAAG,MAAM,GAAI,MAAgB,QAAQ,MAAM,QAAQ;AACnD,SAAO;;AAGT,KAAI,KAAK,QAAQ,CAAC,KAAK,KAAK;AAC1B,KAAG,IAAI,MAAM;AACb,SAAO,KAAK,OAAO,IAAI;;CAIzB,MAAM,QAAqB;EACzB,GAFa,MAAM,kBAAkB,KAAK,KAAK,KAAK,KAAK;EAGzD,QAAQ,KAAK;EACb,SAAS,KAAK,oBAAoB,KAAA,IAAY,KAAA,IAAY,EAAE,iBAAiB,KAAK,iBAAiB;EACpG;AAED,KAAI,KAAK,SAEP,OAAM,WAAW,EAAE,YADJ,MAAM,kBAAkB,KAAK,UAAU,KAAK,KAAK,EAC3B,WAAW;AAElD,KAAI,KAAK,UACP,OAAM,YAAY,KAAK,MAAM,MAAM,SAAS,KAAK,WAAW,OAAO,CAAC;CAGtE,MAAM,SAAS,OAAO,MAAM;AAC5B,IAAG,IAAI,KAAK,OAAO,KAAK,UAAU,QAAQ,MAAM,EAAE,GAAG,aAAa,OAAO,CAAC;AAC1E,QAAO,OAAO,SAAS,IAAI"}
@@ -0,0 +1,29 @@
1
+ import { a as VerifyDocument, c as VerifyRedirect, i as Severity, l as VerifyReport, n as CheckGroup, o as VerifyInput, r as Finding, s as VerifyOptions, t as CHECK_GROUPS } from "./types-C_hmH1n3.mjs";
2
+
3
+ //#region src/verify.d.ts
4
+ /**
5
+ * Run the gates over a built site.
6
+ *
7
+ * Nothing is fetched. `input.documents` is the built site as served, and
8
+ * `input.baseline` — when the caller has one — is the site it replaced. Without
9
+ * a baseline every self-check still runs; only the parity checks (status
10
+ * change, lost structured data, lost alt text, lost feed) go quiet, because
11
+ * there is nothing to compare against.
12
+ */
13
+ declare function verify(input: VerifyInput): VerifyReport;
14
+ /** A report as lines, most severe first. Used by the CLI and by consumers. */
15
+ declare function formatReport(report: VerifyReport): string;
16
+ //#endregion
17
+ //#region src/load.d.ts
18
+ /**
19
+ * Load a static build as a verification input: every `.html` becomes a
20
+ * document served at its path, everything else becomes an asset.
21
+ *
22
+ * `dist/about/index.html` is served at `/about/`, which `identity()` then
23
+ * reduces to `/about` — the same address a link to `/about/` or
24
+ * `/about/index.html` resolves to.
25
+ */
26
+ declare function loadSiteDirectory(dir: string, site?: string): Promise<Pick<VerifyInput, 'documents' | 'assets' | 'sitemap' | 'site'>>;
27
+ //#endregion
28
+ export { CHECK_GROUPS, type CheckGroup, type Finding, type Severity, type VerifyDocument, type VerifyInput, type VerifyOptions, type VerifyRedirect, type VerifyReport, formatReport, loadSiteDirectory, verify };
29
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/verify.ts","../src/load.ts"],"mappings":";;;;;AAsDA;;;;;;;iBAAgB,MAAA,CAAO,KAAA,EAAO,WAAA,GAAc,YAAA;;iBAuC5B,YAAA,CAAa,MAAA,EAAQ,YAAA;;;;;AAvCrC;;;;;;iBCpBsB,iBAAA,CAAkB,GAAA,UAAa,IAAA,YAAgB,OAAA,CAAQ,IAAA,CAAK,WAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { i as CHECK_GROUPS, n as formatReport, r as verify, t as loadSiteDirectory } from "./load-KUG-8Ug4.mjs";
2
+ export { CHECK_GROUPS, formatReport, loadSiteDirectory, verify };