@contentrain/verify 0.1.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Contentrain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -67,6 +67,7 @@ a page must not fail its own gate.
67
67
  | | `identity.description-missing` | warning | No meta description |
68
68
  | | `identity.open-graph-incomplete` | warning | Missing `og:title` / `og:type` / `og:url` — shares render without a card |
69
69
  | | `identity.twitter-card-missing` | info | No `twitter:card` |
70
+ | identity | `identity.lang-missing` | warning | No `lang` on `<html>` — what a screen reader picks a voice from |
70
71
  | 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
72
  | | `indexing.sitemap-missing-entry` | warning | An indexable page is not in the sitemap (a `noindex` page is not asked to be) |
72
73
  | | `indexing.sitemap-stale-entry` | warning | The sitemap lists a page the build does not serve |
@@ -75,6 +76,7 @@ a page must not fail its own gate.
75
76
  | | `status.redirect-chain` | warning | More hops than `maxRedirectHops` (default 1) |
76
77
  | | `status.redirect-loop` | error | A chain that returns to itself |
77
78
  | | `status.redirect-target-missing` | error | An internal redirect pointing at a page the build does not serve |
79
+ | | `status.not-found-page-missing` | error | The build has no `404.html`. Only checked for a build (`input.build`); migrated navigation is root-relative, so a wrong path is not rare, and without one the visitor lands on the host's page with none of the site's chrome |
78
80
  | 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
81
  | | `international.hreflang-no-self` | warning | A page with alternates that does not list itself |
80
82
  | | `international.hreflang-unknown-target` | warning | An alternate pointing at a page that does not exist |
@@ -86,6 +88,19 @@ a page must not fail its own gate.
86
88
  | assets | `assets.broken-image` | error | An internal image that resolves to nothing the build serves. `data:` URIs and third-party hosts are skipped |
87
89
  | | `assets.missing-alt` | warning | An `<img>` with no `alt` attribute. `alt=""` is a decision (decorative) and is not reported |
88
90
  | | `assets.alt-lost` | warning | Alt text the old page had for the same image is gone |
91
+ | | `assets.source-origin-reference` | error | A reference to `options.sourceOrigin` — the host the content was migrated away from. The new site then works only while the old one stays up. Scans documents, and stylesheets and scripts when `input.files` carries their contents; `options.allowHosts` exempts a CDN the customer kept |
92
+
93
+ ## The 404 page
94
+
95
+ A static host hands `404.html` to a visitor who asked for something else, so it
96
+ is not served at its own address and the checks that assume otherwise do not
97
+ apply to it: canonical, sitemap membership, and — the one that matters —
98
+ soft-404. A correct 404 page says "not found" and is short, which is exactly the
99
+ shape the soft-404 check looks for, so without this exemption every site that
100
+ did the right thing would fail on it.
101
+
102
+ Recognised at either spelling a build produces: `404.html` at the root, or
103
+ `404/index.html`.
89
104
 
90
105
  ## Address identity
91
106
 
@@ -117,6 +132,8 @@ contentrain-verify <dist-dir> [options]
117
132
  --site <url> Canonical origin
118
133
  --baseline <dir> The old site, as a directory of captured pages
119
134
  --redirects <file> JSON: [{ "from": "/a", "to": "/b", "status": 301 }]
135
+ --source-origin <host> The old host content was migrated away from
136
+ --allow-host <host> A host that may still be referenced (repeatable)
120
137
  --groups <list> identity,indexing,status,international,structured,navigation,assets
121
138
  --max-redirect-hops <n>
122
139
  --json
package/dist/cli.d.mts CHANGED
@@ -1,10 +1,12 @@
1
- import { n as CheckGroup } from "./types-C_hmH1n3.mjs";
1
+ import { n as CheckGroup } from "./types-bYfzrRnM.mjs";
2
2
 
3
3
  //#region src/cli.d.ts
4
4
  declare const USAGE: string;
5
5
  interface Args {
6
6
  dir?: string;
7
7
  site?: string;
8
+ sourceOrigin?: string;
9
+ allowHosts?: string[];
8
10
  baseline?: string;
9
11
  redirects?: string;
10
12
  groups?: CheckGroup[];
@@ -1 +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"}
1
+ {"version":3,"file":"cli.d.mts","names":[],"sources":["../src/cli.ts"],"mappings":";;;cAca,KAAA;AAAA,UAaI,IAAA;EACf,GAAA;EACA,IAAA;EACA,YAAA;EACA,UAAA;EACA,QAAA;EACA,SAAA;EACA,MAAA,GAAS,UAAA;EACT,eAAA;EACA,IAAA;EACA,IAAA;AAAA;AAAA,iBAGc,SAAA,CAAU,IAAA,sBAA0B,IAAA;AAAA,UA2BnC,QAAA;EACf,GAAA,GAAM,IAAA;EACN,KAAA,GAAQ,IAAA;AAAA;;iBAIY,IAAA,CAAK,IAAA,qBAAyB,EAAA,GAAI,QAAA,GAAqB,OAAA"}
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as CHECK_GROUPS, n as formatReport, r as verify, t as loadSiteDirectory } from "./load-KUG-8Ug4.mjs";
1
+ import { i as CHECK_GROUPS, n as formatReport, r as verify, t as loadSiteDirectory } from "./load-DUyyLerx.mjs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  //#region src/cli.ts
4
4
  const USAGE = `contentrain-verify <dist-dir> [options]
@@ -6,6 +6,8 @@ const USAGE = `contentrain-verify <dist-dir> [options]
6
6
  --site <url> Canonical origin, e.g. https://example.com
7
7
  --baseline <dir> The old site, as a directory of captured pages
8
8
  --redirects <file> JSON: [{ "from": "/a", "to": "/b", "status": 301 }]
9
+ --source-origin <host> The old host content was migrated away from
10
+ --allow-host <host> A host that may still be referenced (repeatable)
9
11
  --groups <list> Comma-separated: ${CHECK_GROUPS.join(",")}
10
12
  --max-redirect-hops <n>
11
13
  --json Print the report as JSON instead of text
@@ -22,6 +24,8 @@ function parseArgs(argv) {
22
24
  if (arg === "--help" || arg === "-h") args.help = true;
23
25
  else if (arg === "--json") args.json = true;
24
26
  else if (arg === "--site") args.site = next();
27
+ else if (arg === "--source-origin") args.sourceOrigin = next();
28
+ else if (arg === "--allow-host") (args.allowHosts ??= []).push(next() ?? "");
25
29
  else if (arg === "--baseline") args.baseline = next();
26
30
  else if (arg === "--redirects") args.redirects = next();
27
31
  else if (arg === "--max-redirect-hops") {
@@ -51,10 +55,15 @@ async function main(argv, io = console) {
51
55
  io.log(USAGE);
52
56
  return args.help ? 0 : 2;
53
57
  }
58
+ const loaded = await loadSiteDirectory(args.dir, args.site);
59
+ const options = {};
60
+ if (args.maxRedirectHops !== void 0) options.maxRedirectHops = args.maxRedirectHops;
61
+ if (args.sourceOrigin !== void 0) options.sourceOrigin = args.sourceOrigin;
62
+ if (args.allowHosts?.length) options.allowHosts = args.allowHosts;
54
63
  const input = {
55
- ...await loadSiteDirectory(args.dir, args.site),
64
+ ...loaded,
56
65
  groups: args.groups,
57
- options: args.maxRedirectHops === void 0 ? void 0 : { maxRedirectHops: args.maxRedirectHops }
66
+ options: Object.keys(options).length ? options : void 0
58
67
  };
59
68
  if (args.baseline) input.baseline = { documents: (await loadSiteDirectory(args.baseline, args.site)).documents };
60
69
  if (args.redirects) input.redirects = JSON.parse(await readFile(args.redirects, "utf8"));
package/dist/cli.mjs.map CHANGED
@@ -1 +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"}
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 --source-origin <host> The old host content was migrated away from\n --allow-host <host> A host that may still be referenced (repeatable)\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 sourceOrigin?: string\n allowHosts?: 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 === '--source-origin') args.sourceOrigin = next()\n else if (arg === '--allow-host') (args.allowHosts ??= []).push(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 options: VerifyInput['options'] = {}\n if (args.maxRedirectHops !== undefined) options.maxRedirectHops = args.maxRedirectHops\n if (args.sourceOrigin !== undefined) options.sourceOrigin = args.sourceOrigin\n if (args.allowHosts?.length) options.allowHosts = args.allowHosts\n\n const input: VerifyInput = {\n ...loaded,\n groups: args.groups,\n options: Object.keys(options).length ? options : undefined,\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;;;;;;;2CAOsB,aAAa,KAAK,IAAI,CAAC;;;;;AAmBlE,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,kBAAmB,MAAK,eAAe,MAAM;WACrD,QAAQ,eAAgB,EAAC,KAAK,eAAe,EAAE,EAAE,KAAK,MAAM,IAAI,GAAG;WACnE,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;;CAGzB,MAAM,SAAS,MAAM,kBAAkB,KAAK,KAAK,KAAK,KAAK;CAC3D,MAAM,UAAkC,EAAE;AAC1C,KAAI,KAAK,oBAAoB,KAAA,EAAW,SAAQ,kBAAkB,KAAK;AACvE,KAAI,KAAK,iBAAiB,KAAA,EAAW,SAAQ,eAAe,KAAK;AACjE,KAAI,KAAK,YAAY,OAAQ,SAAQ,aAAa,KAAK;CAEvD,MAAM,QAAqB;EACzB,GAAG;EACH,QAAQ,KAAK;EACb,SAAS,OAAO,KAAK,QAAQ,CAAC,SAAS,UAAU,KAAA;EAClD;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"}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
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";
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-bYfzrRnM.mjs";
2
2
 
3
3
  //#region src/verify.d.ts
4
4
  /**
@@ -23,7 +23,7 @@ declare function formatReport(report: VerifyReport): string;
23
23
  * reduces to `/about` — the same address a link to `/about/` or
24
24
  * `/about/index.html` resolves to.
25
25
  */
26
- declare function loadSiteDirectory(dir: string, site?: string): Promise<Pick<VerifyInput, 'documents' | 'assets' | 'sitemap' | 'site'>>;
26
+ declare function loadSiteDirectory(dir: string, site?: string): Promise<Pick<VerifyInput, 'documents' | 'assets' | 'files' | 'sitemap' | 'site' | 'build'>>;
27
27
  //#endregion
28
28
  export { CHECK_GROUPS, type CheckGroup, type Finding, type Severity, type VerifyDocument, type VerifyInput, type VerifyOptions, type VerifyRedirect, type VerifyReport, formatReport, loadSiteDirectory, verify };
29
29
  //# sourceMappingURL=index.d.mts.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/verify.ts","../src/load.ts"],"mappings":";;;;;AA+DA;;;;;;;iBAAgB,MAAA,CAAO,KAAA,EAAO,WAAA,GAAc,YAAA;;iBAyC5B,YAAA,CAAa,MAAA,EAAQ,YAAA;;;;;AAzCrC;;;;;;iBC5BsB,iBAAA,CAAkB,GAAA,UAAa,IAAA,YAAgB,OAAA,CAAQ,IAAA,CAAK,WAAA"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { i as CHECK_GROUPS, n as formatReport, r as verify, t as loadSiteDirectory } from "./load-KUG-8Ug4.mjs";
1
+ import { i as CHECK_GROUPS, n as formatReport, r as verify, t as loadSiteDirectory } from "./load-DUyyLerx.mjs";
2
2
  export { CHECK_GROUPS, formatReport, loadSiteDirectory, verify };
@@ -201,6 +201,20 @@ function absolute(url, site) {
201
201
  //#endregion
202
202
  //#region src/checks.ts
203
203
  const NOT_FOUND_TEXT = /\b(404|not found|page not found|sayfa bulunamad[ıi]|nicht gefunden|introuvable|no se encontr)/i;
204
+ /**
205
+ * The build's 404 page, which is not served at its own address.
206
+ *
207
+ * A static host hands this document to a visitor who asked for something else,
208
+ * so every check that assumes "this document is served at this URL" is wrong
209
+ * about it: its canonical does not describe a page, it does not belong in the
210
+ * sitemap, and — the one that matters — it says "not found" and is short by
211
+ * design, which is precisely the shape of a soft-404. Without this exemption
212
+ * every correctly built site would fail on having done the right thing.
213
+ */
214
+ function isNotFoundDocument(doc, ctx) {
215
+ const id = identity(doc.url, ctx.input.site);
216
+ return id === "/404" || id === "/404.html";
217
+ }
204
218
  function finding(f) {
205
219
  return f;
206
220
  }
@@ -233,7 +247,7 @@ function identityChecks(doc, ctx) {
233
247
  message: "No meta description."
234
248
  }));
235
249
  const canonicals = linksRel(doc.html, "canonical");
236
- if (canonicals.length === 0) out.push(finding({
250
+ if (isNotFoundDocument(doc, ctx)) {} else if (canonicals.length === 0) out.push(finding({
237
251
  group,
238
252
  check: "identity.canonical-missing",
239
253
  severity: "warning",
@@ -260,6 +274,13 @@ function identityChecks(doc, ctx) {
260
274
  detail: `${canonicals[0].href} ≠ ${url}`
261
275
  }));
262
276
  }
277
+ if (!/<html\b[^>]*\slang\s*=\s*["']?[a-zA-Z]/.test(doc.html)) out.push(finding({
278
+ group,
279
+ check: "identity.lang-missing",
280
+ severity: "warning",
281
+ url,
282
+ message: "No lang attribute on <html>."
283
+ }));
263
284
  const missingOg = [
264
285
  "og:title",
265
286
  "og:type",
@@ -302,7 +323,7 @@ function indexingChecks(doc, ctx) {
302
323
  detail: reason
303
324
  }));
304
325
  if (ctx.input.sitemap !== void 0) {
305
- if (!new Set(sitemapLocations(ctx.input.sitemap).map((loc) => identity(loc, ctx.input.site))).has(identity(url, ctx.input.site)) && !reason) out.push(finding({
326
+ if (!new Set(sitemapLocations(ctx.input.sitemap).map((loc) => identity(loc, ctx.input.site))).has(identity(url, ctx.input.site)) && !reason && !isNotFoundDocument(doc, ctx)) out.push(finding({
306
327
  group,
307
328
  check: "indexing.sitemap-missing-entry",
308
329
  severity: "warning",
@@ -348,7 +369,7 @@ function statusChecks(doc, ctx) {
348
369
  if (status === 200) {
349
370
  const bodyText = text(doc.html);
350
371
  const title = titles(doc.html)[0] ?? "";
351
- if ((NOT_FOUND_TEXT.test(title) || NOT_FOUND_TEXT.test(bodyText.slice(0, 400))) && bodyText.length <= ctx.options.soft404MaxTextLength) out.push(finding({
372
+ if ((NOT_FOUND_TEXT.test(title) || NOT_FOUND_TEXT.test(bodyText.slice(0, 400))) && bodyText.length <= ctx.options.soft404MaxTextLength && !isNotFoundDocument(doc, ctx)) out.push(finding({
352
373
  group,
353
374
  check: "status.soft-404",
354
375
  severity: "error",
@@ -359,6 +380,29 @@ function statusChecks(doc, ctx) {
359
380
  }
360
381
  return out;
361
382
  }
383
+ /**
384
+ * A build with no `404.html`.
385
+ *
386
+ * Only meaningful for a build: a set of pages captured from a running site
387
+ * cannot show one, so asserting it there would report a defect the input cannot
388
+ * express. Measured across a blind cohort every site lacked one — and because
389
+ * migrated navigation is root-relative, a wrong path is not a rare event: it
390
+ * lands the visitor on the host's generic page, with none of the site's chrome
391
+ * and no way back.
392
+ */
393
+ function notFoundPageChecks(ctx) {
394
+ if (!ctx.input.build) return [];
395
+ if (ctx.input.documents.some((doc) => {
396
+ const id = identity(doc.url, ctx.input.site);
397
+ return id === "/404" || id === "/404.html";
398
+ })) return [];
399
+ return [finding({
400
+ group: "status",
401
+ check: "status.not-found-page-missing",
402
+ severity: "error",
403
+ message: "The build has no 404.html, so a wrong path lands on the host’s page instead of the site’s."
404
+ })];
405
+ }
362
406
  /** Redirect rules: chains, loops, and targets that do not exist. */
363
407
  function redirectChecks(ctx) {
364
408
  const rules = ctx.input.redirects;
@@ -533,6 +577,66 @@ function navigationChecks(doc, ctx) {
533
577
  }
534
578
  return out;
535
579
  }
580
+ /**
581
+ * References to the origin the content was migrated away from.
582
+ *
583
+ * A generated page that still points at the old WordPress host has not been
584
+ * migrated, it has been mirrored: the new site works only while the old one
585
+ * stays up, and the day it goes away the images and stylesheets go with it.
586
+ * Across a blind cohort this was the single largest thing keeping sites from
587
+ * being deliverable, so it is an error rather than a warning.
588
+ *
589
+ * The scan is over raw text, not parsed attributes, because the references hide
590
+ * in every shape a reference can take — `src`, `srcset`, `href`, `url()` in a
591
+ * style attribute, a string inside inline script. That over-matches slightly:
592
+ * the origin written in a comment or in visible prose counts too. Reported that
593
+ * way on purpose — a literal old-host URL on the page is worth looking at even
594
+ * when it is not fetched.
595
+ *
596
+ * Stylesheets and scripts are scanned only when their contents are supplied
597
+ * (`input.files`). Without them the count is HTML-only, which is a smaller
598
+ * number than the truth rather than a wrong one — and the report says so.
599
+ */
600
+ function sourceOriginChecks(ctx) {
601
+ const origin = ctx.options.sourceOrigin;
602
+ if (!origin) return [];
603
+ let host;
604
+ try {
605
+ host = new URL(origin.includes("://") ? origin : `https://${origin}`).host;
606
+ } catch {
607
+ return [finding({
608
+ group: "assets",
609
+ check: "assets.source-origin-invalid",
610
+ severity: "warning",
611
+ message: `sourceOrigin "${origin}" is not a host or a URL; the scan did not run.`
612
+ })];
613
+ }
614
+ if ((ctx.options.allowHosts ?? []).includes(host)) return [];
615
+ const out = [];
616
+ const pattern = new RegExp(host.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`), "gi");
617
+ const count = (source) => (source.match(pattern) ?? []).length;
618
+ for (const doc of ctx.input.documents) {
619
+ const hits = count(doc.html);
620
+ if (hits) out.push(finding({
621
+ group: "assets",
622
+ check: "assets.source-origin-reference",
623
+ severity: "error",
624
+ url: doc.url,
625
+ message: `${hits} reference${hits === 1 ? "" : "s"} to the source origin ${host} — this page still depends on the site it replaced.`
626
+ }));
627
+ }
628
+ for (const file of ctx.input.files ?? []) {
629
+ const hits = count(file.content);
630
+ if (hits) out.push(finding({
631
+ group: "assets",
632
+ check: "assets.source-origin-reference",
633
+ severity: "error",
634
+ url: file.path,
635
+ message: `${hits} reference${hits === 1 ? "" : "s"} to the source origin ${host}.`
636
+ }));
637
+ }
638
+ return out;
639
+ }
536
640
  function assetChecks(doc, ctx) {
537
641
  const out = [];
538
642
  const group = "assets";
@@ -611,6 +715,18 @@ function absentInputs(input, groups) {
611
715
  group: "status",
612
716
  reason: "no redirect rules — chain and target checks did not run"
613
717
  });
718
+ if (!input.build && groups.includes("status")) skipped.push({
719
+ group: "status",
720
+ reason: "not a build directory — the 404 page check did not run"
721
+ });
722
+ if (!input.options?.sourceOrigin && groups.includes("assets")) skipped.push({
723
+ group: "assets",
724
+ reason: "no sourceOrigin — the old-host reference scan did not run"
725
+ });
726
+ else if (!input.files?.length && groups.includes("assets")) skipped.push({
727
+ group: "assets",
728
+ reason: "no stylesheet or script contents — the old-host scan covered HTML only"
729
+ });
614
730
  return skipped;
615
731
  }
616
732
  function buildContext(input) {
@@ -655,7 +771,9 @@ function verify(input) {
655
771
  }
656
772
  run("indexing", () => sitemapStaleChecks(ctx));
657
773
  run("status", () => redirectChecks(ctx));
774
+ run("status", () => notFoundPageChecks(ctx));
658
775
  run("international", () => internationalChecks(ctx));
776
+ run("assets", () => sourceOriginChecks(ctx));
659
777
  const counts = {
660
778
  error: 0,
661
779
  warning: 0,
@@ -691,6 +809,7 @@ function formatReport(report) {
691
809
  //#endregion
692
810
  //#region src/load.ts
693
811
  const DOCUMENT_EXT = /\.html?$/i;
812
+ const TEXT_EXT = /\.(css|m?js|cjs|json|xml|txt|svg)$/i;
694
813
  async function walk(dir, root, files = []) {
695
814
  const entries = await readdir(dir, { withFileTypes: true });
696
815
  for (const entry of entries) {
@@ -715,6 +834,7 @@ async function loadSiteDirectory(dir, site) {
715
834
  const files = await walk(dir, dir);
716
835
  const documents = [];
717
836
  const assets = [];
837
+ const texts = [];
718
838
  let sitemap;
719
839
  for (const file of files.toSorted()) {
720
840
  const served = `/${file.split(sep).join("/")}`;
@@ -727,16 +847,22 @@ async function loadSiteDirectory(dir, site) {
727
847
  continue;
728
848
  }
729
849
  if (/^\/sitemap[^/]*\.xml$/i.test(served)) sitemap = await readFile(join(dir, file), "utf8");
850
+ if (TEXT_EXT.test(file)) texts.push({
851
+ path: served,
852
+ content: await readFile(join(dir, file), "utf8")
853
+ });
730
854
  assets.push(served);
731
855
  }
732
856
  return {
733
857
  documents,
734
858
  assets,
859
+ files: texts,
735
860
  sitemap,
736
- site
861
+ site,
862
+ build: true
737
863
  };
738
864
  }
739
865
  //#endregion
740
866
  export { CHECK_GROUPS as i, formatReport as n, verify as r, loadSiteDirectory as t };
741
867
 
742
- //# sourceMappingURL=load-KUG-8Ug4.mjs.map
868
+ //# sourceMappingURL=load-DUyyLerx.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-DUyyLerx.mjs","names":[],"sources":["../src/types.ts","../src/html.ts","../src/url.ts","../src/checks.ts","../src/verify.ts","../src/load.ts"],"sourcesContent":["// ─── What a verification run is given, and what it answers with ───\n//\n// Everything here is data the caller already has. The package never fetches:\n// a hardened crawler is a security surface of its own (SSRF, DNS rebinding,\n// redirect and byte limits) and belongs where that hardening lives, not in the\n// MIT library every consumer embeds. Whoever holds a safe fetcher — or a\n// directory of built files — supplies the documents.\n//\n// The consequence is that this runs identically over a `dist/` directory, over\n// a captured baseline, and inside another tool's pipeline, with no network and\n// no mocking.\n\n/** One page as served: the address it answers at and what it answered with. */\nexport interface VerifyDocument {\n /** Absolute URL, or a site-relative path (`/about/`). */\n url: string\n html: string\n /** Defaults to 200 when the caller did not record one. */\n status?: number\n /** Response headers, lowercased keys. `x-robots-tag` is read here. */\n headers?: Record<string, string>\n}\n\nexport interface VerifyRedirect {\n from: string\n to: string\n status: number\n}\n\nexport const CHECK_GROUPS = [\n /** canonical, `<title>`, description, Open Graph, Twitter card */\n 'identity',\n /** robots meta, X-Robots-Tag, noindex leaks, sitemap membership */\n 'indexing',\n /** status parity, soft-404, redirect chains */\n 'status',\n /** hreflang pairs and reciprocity */\n 'international',\n /** JSON-LD validity and type parity */\n 'structured',\n /** internal links, pagination, feeds */\n 'navigation',\n /** image resolution and alt preservation */\n 'assets',\n] as const\n\nexport type CheckGroup = (typeof CHECK_GROUPS)[number]\n\nexport type Severity = 'error' | 'warning' | 'info'\n\n/**\n * One thing that is wrong, or worth knowing.\n *\n * `check` is a stable id (`identity.canonical-mismatch`) so a consumer can\n * suppress, threshold or track one finding without matching on prose.\n */\nexport interface Finding {\n group: CheckGroup\n check: string\n severity: Severity\n /** The document the finding is about, when it is about one. */\n url?: string\n message: string\n detail?: string\n}\n\nexport interface VerifyReport {\n /** No `error` findings. This is the gate. */\n passed: boolean\n findings: Finding[]\n counts: Record<Severity, number>\n /** How many documents were examined. */\n documents: number\n /** Which groups actually ran. */\n groups: CheckGroup[]\n /** Groups that were asked for but could not run, and why. */\n skipped: { group: CheckGroup, reason: string }[]\n}\n\n/** A text file that is not a document — a stylesheet, a script. */\nexport interface VerifyFile {\n /** Path as served (`/assets/app.css`). */\n path: string\n content: string\n}\n\nexport interface VerifyOptions {\n /**\n * A redirect chain longer than this is reported. One hop is normal; two is a\n * link-equity leak and a latency cost, and chains grow silently as redirect\n * rules accumulate over migrations.\n */\n maxRedirectHops?: number\n /**\n * A 200 response shorter than this many characters of text, whose title or\n * body says \"not found\", is treated as a soft-404. Kept configurable because\n * a legitimately short page exists; the default is deliberately low.\n */\n soft404MaxTextLength?: number\n /** Hosts to treat as internal besides `site`'s own. */\n internalHosts?: string[]\n /**\n * The origin the content was migrated away from — the old WordPress host.\n *\n * A migrated page that still points at it is not finished: the new site\n * depends on the old one staying up, and the moment it does not, images and\n * stylesheets disappear. Measured across a blind cohort it was the single\n * largest blocker, so this is checked as an error when the origin is known.\n */\n sourceOrigin?: string\n /**\n * Hosts that may legitimately still be referenced even though they are not\n * the site's own — a CDN the customer keeps. Applies to\n * `assets.source-origin-reference` only.\n */\n allowHosts?: string[]\n}\n\nexport interface VerifyInput {\n /** The built site. */\n documents: VerifyDocument[]\n /** The site's canonical origin (`https://example.com`). Several checks need it. */\n site?: string\n /** `sitemap.xml` as produced by the build. */\n sitemap?: string\n /** Redirect rules the host will serve. */\n redirects?: VerifyRedirect[]\n /** Non-document files the build produced, as served (`/images/a.png`). */\n assets?: string[]\n /**\n * Contents of non-document text files — stylesheets, scripts. Only the checks\n * that scan content read these; everything else works from `documents` and\n * the `assets` path list.\n *\n * Supplying them matters for `assets.source-origin-reference`: a `url()` in a\n * stylesheet pointing at the old host is exactly as fatal as one in the HTML,\n * and a scan that silently skips CSS reports a smaller number than a scan\n * that does not — which is worse than not counting at all when two tools\n * compare figures.\n */\n files?: VerifyFile[]\n /** The old site, for parity checks. Absent means only self-checks run. */\n baseline?: { documents: VerifyDocument[] }\n /**\n * Whether `documents` are a static build directory rather than pages captured\n * from a running site. Build-level expectations — a `404.html` exists — are\n * only meaningful for a build, and asserting them against a capture would\n * report a defect the capture simply cannot show. `loadSiteDirectory` sets it.\n */\n build?: boolean\n /** Defaults to every group. */\n groups?: readonly CheckGroup[]\n options?: VerifyOptions\n}\n","// ─── Reading the few things a document has to say ───\n//\n// Targeted regexes, not a parser. The same choice `@contentrain/emitter-astro`\n// makes, for the same reason: these documents are machine-generated HTML whose\n// head we already control, the extracted set is small and fixed, and a parser\n// would put a dependency into the one package a migration is supposed to be\n// able to run anywhere.\n//\n// The limits are real and bounded. `head()` splits on the first `</head>`, so a\n// document without one yields nothing rather than guessing; attribute reads are\n// quote-aware but do not resolve entities beyond the five that matter. Where a\n// check would be wrong under those limits, it is not written.\n\n/** Everything before the first `</head>`, or '' when there is none. */\nexport function head(html: string): string {\n const end = html.search(/<\\/head\\s*>/i)\n return end === -1 ? '' : html.slice(0, end)\n}\n\n/** Everything after the first `</head>`, or the whole document when there is none. */\nexport function body(html: string): string {\n const end = html.search(/<\\/head\\s*>/i)\n return end === -1 ? html : html.slice(end)\n}\n\nconst ENTITIES: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\", '#39': \"'\" }\n\nexport function decodeEntities(value: string): string {\n return value.replace(/&(#39|amp|lt|gt|quot|apos);/g, (_m, name: string) => ENTITIES[name] ?? _m)\n}\n\n/**\n * Read one attribute off a single tag's source. Quote-aware; unquoted allowed.\n *\n * The name must be preceded by whitespace, not merely a word boundary: `-` is\n * not a word character, so `\\bhref` matches inside `data-href` — and lazy-\n * loading WordPress themes put the real URL in `data-src` and a placeholder in\n * `src` on almost every image.\n */\nexport function attr(tag: string, name: string): string | undefined {\n const re = new RegExp(`(?:^|\\\\s)${name}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s\"'>]+))`, 'i')\n const match = re.exec(tag)\n if (!match) return undefined\n return decodeEntities(match[1] ?? match[2] ?? match[3] ?? '')\n}\n\n/** Every `<tag …>` opening tag in the source, as raw strings. */\nexport function tags(source: string, name: string): string[] {\n return [...source.matchAll(new RegExp(`<${name}\\\\b[^>]*>`, 'gi'))].map(m => m[0])\n}\n\n/**\n * `<title>` texts from the head. `<svg>` blocks are removed first: a `<title>`\n * inside one is an accessibility label, valid markup, and not the page title —\n * counting it would report a duplicate on every page with an inline icon.\n */\nexport function titles(html: string): string[] {\n const source = head(html).replace(/<svg\\b[\\s\\S]*?<\\/svg\\s*>/gi, '')\n return [...source.matchAll(/<title\\b[^>]*>([\\s\\S]*?)<\\/title\\s*>/gi)]\n .map(m => decodeEntities(m[1] ?? '').trim())\n}\n\n/** `<meta name=\"…\">` / `<meta property=\"…\">` content, keyed by lowercased key. */\nexport function metaMap(html: string): Map<string, string[]> {\n const found = new Map<string, string[]>()\n for (const tag of tags(head(html), 'meta')) {\n const key = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n if (!key) continue\n const content = attr(tag, 'content') ?? ''\n found.set(key, [...(found.get(key) ?? []), content])\n }\n return found\n}\n\nexport interface LinkTag {\n rel: string\n href: string\n hreflang?: string\n type?: string\n}\n\n/** `<link>` tags from the head, with `rel` lowercased. */\nexport function links(html: string): LinkTag[] {\n const found: LinkTag[] = []\n for (const tag of tags(head(html), 'link')) {\n const rel = attr(tag, 'rel')?.toLowerCase()\n const href = attr(tag, 'href')\n if (!rel || href === undefined) continue\n found.push({ rel, href, hreflang: attr(tag, 'hreflang'), type: attr(tag, 'type') })\n }\n return found\n}\n\n/** Links whose `rel` list contains the given token. */\nexport function linksRel(html: string, token: string): LinkTag[] {\n return links(html).filter(link => link.rel.split(/\\s+/).includes(token))\n}\n\nexport interface JsonLdBlock {\n raw: string\n /** Parsed value, or undefined when the block is not valid JSON. */\n value?: unknown\n}\n\n/** Every `application/ld+json` block in the document, parsed where possible. */\nexport function jsonLd(html: string): JsonLdBlock[] {\n const blocks: JsonLdBlock[] = []\n const re = /<script\\b[^>]*\\btype\\s*=\\s*[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script\\s*>/gi\n for (const match of html.matchAll(re)) {\n const raw = (match[1] ?? '').trim()\n try {\n blocks.push({ raw, value: JSON.parse(raw) as unknown })\n } catch {\n blocks.push({ raw })\n }\n }\n return blocks\n}\n\n/**\n * `@type` values in a JSON-LD block, including those nested in `@graph` —\n * themes commonly ship one graph holding Organization, WebSite and WebPage,\n * and reading only the root would see none of them.\n */\nexport function jsonLdTypes(value: unknown): string[] {\n const found: string[] = []\n const walk = (node: unknown): void => {\n if (Array.isArray(node)) {\n for (const item of node) walk(item)\n return\n }\n if (!node || typeof node !== 'object') return\n const record = node as Record<string, unknown>\n const type = record['@type']\n if (typeof type === 'string') found.push(type)\n else if (Array.isArray(type)) for (const t of type) if (typeof t === 'string') found.push(t)\n if ('@graph' in record) walk(record['@graph'])\n }\n walk(value)\n return found\n}\n\n/** `href` of every `<a>` in the document body. */\nexport function anchors(html: string): string[] {\n return tags(body(html), 'a')\n .map(tag => attr(tag, 'href'))\n .filter((href): href is string => href !== undefined)\n}\n\nexport interface ImageRef {\n src: string\n /** Absent means no `alt` attribute at all — different from `alt=\"\"`. */\n alt?: string\n}\n\n/** Every `<img>` in the document body. */\nexport function images(html: string): ImageRef[] {\n return tags(body(html), 'img')\n .map(tag => ({ src: attr(tag, 'src') ?? '', alt: attr(tag, 'alt') }))\n .filter(image => image.src !== '')\n}\n\n/** Visible text, roughly: script/style stripped, tags removed, runs collapsed. */\nexport function text(html: string): string {\n return body(html)\n .replace(/<(script|style)\\b[\\s\\S]*?<\\/\\1\\s*>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/** `<loc>` entries of a sitemap or sitemap index. */\nexport function sitemapLocations(xml: string): string[] {\n return [...xml.matchAll(/<loc>\\s*([\\s\\S]*?)\\s*<\\/loc>/gi)].map(m => decodeEntities((m[1] ?? '').trim()))\n}\n","// ─── Addresses ───\n//\n// Every check that asks \"does this link go somewhere that exists\" is really\n// asking a normalisation question, and getting it wrong in either direction is\n// worse than not asking: too strict and a correct site fails its gate, too\n// loose and a broken one passes.\n//\n// The rules here are deliberately conservative. A trailing slash and an\n// `index.html` are the same address, because static hosts serve them that way.\n// A query string and a fragment are not part of a document's identity. Case is\n// preserved in the path, because static hosts are case-sensitive even when the\n// author assumed otherwise.\n\nconst SITE_FALLBACK = 'https://contentrain.invalid'\n\n/** Absolute URL for `href` as seen from `base`, or undefined when unresolvable. */\nexport function resolve(href: string, base: string, site?: string): string | undefined {\n try {\n const origin = site ?? SITE_FALLBACK\n const absoluteBase = base.startsWith('http') ? base : new URL(base, origin).href\n return new URL(href, absoluteBase).href\n } catch {\n return undefined\n }\n}\n\n/** Whether a resolved URL belongs to the site under test. */\nexport function isInternal(url: string, site?: string, extraHosts: readonly string[] = []): boolean {\n try {\n const parsed = new URL(url)\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false\n if (!site) return parsed.origin === SITE_FALLBACK || extraHosts.includes(parsed.host)\n return parsed.host === new URL(site).host || extraHosts.includes(parsed.host)\n } catch {\n return false\n }\n}\n\n/**\n * The identity of a document: path only, with `index.html` and a trailing\n * slash removed, query and fragment dropped. `/` stays `/`.\n */\nexport function identity(url: string, site?: string): string {\n const full = url.startsWith('http') ? url : resolve(url, '/', site) ?? url\n let path: string\n try {\n path = new URL(full).pathname\n } catch {\n path = full.split(/[?#]/)[0] ?? full\n }\n path = path.replace(/\\/index\\.html?$/i, '/')\n if (path.length > 1) path = path.replace(/\\/+$/, '')\n return path === '' ? '/' : path\n}\n\n/** Absolute form of a document's own address, when the site origin is known. */\nexport function absolute(url: string, site?: string): string | undefined {\n if (url.startsWith('http')) return url\n if (!site) return undefined\n try {\n return new URL(url, site).href\n } catch {\n return undefined\n }\n}\n","// ─── The gates ───\n//\n// Seven groups, from the migration contract's verification scope. Each check\n// has a stable id and answers one question; none of them fetches anything.\n//\n// Two rules shaped every check below. A finding is an `error` only when the\n// built site is *wrong* — a visitor or a crawler gets something it should not.\n// Everything that is merely worse than the old site is a `warning`, because a\n// migration that improves a page should not fail its own gate.\n\nimport type { Finding, VerifyDocument, VerifyInput, VerifyOptions } from './types.js'\nimport {\n anchors, images, jsonLd, jsonLdTypes, linksRel, metaMap, sitemapLocations, text, titles,\n} from './html.js'\nimport { absolute, identity, isInternal, resolve } from './url.js'\n\nexport interface Context {\n input: VerifyInput\n options: Required<Pick<VerifyOptions, 'maxRedirectHops' | 'soft404MaxTextLength'>> & VerifyOptions\n /** Document identity → document, for the built site. */\n byIdentity: Map<string, VerifyDocument>\n /** Baseline document identity → document. */\n baselineByIdentity: Map<string, VerifyDocument>\n /** Identities of everything the build serves: documents plus assets. */\n served: Set<string>\n}\n\nconst NOT_FOUND_TEXT = /\\b(404|not found|page not found|sayfa bulunamad[ıi]|nicht gefunden|introuvable|no se encontr)/i\n\n/**\n * The build's 404 page, which is not served at its own address.\n *\n * A static host hands this document to a visitor who asked for something else,\n * so every check that assumes \"this document is served at this URL\" is wrong\n * about it: its canonical does not describe a page, it does not belong in the\n * sitemap, and — the one that matters — it says \"not found\" and is short by\n * design, which is precisely the shape of a soft-404. Without this exemption\n * every correctly built site would fail on having done the right thing.\n */\nfunction isNotFoundDocument(doc: VerifyDocument, ctx: Context): boolean {\n const id = identity(doc.url, ctx.input.site)\n return id === '/404' || id === '/404.html'\n}\n\nfunction finding(f: Finding): Finding {\n return f\n}\n\n// ─── identity ───\n\nexport function identityChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const url = doc.url\n const group = 'identity' as const\n const found = titles(doc.html)\n\n if (found.every(t => t === '')) {\n out.push(finding({ group, check: 'identity.title-missing', severity: 'error', url, message: 'No non-empty <title> in the head.' }))\n } else if (found.length > 1) {\n out.push(finding({\n group,\n check: 'identity.title-duplicate',\n severity: 'error',\n url,\n message: `${found.length} <title> elements in the head — a crawler takes one and it is not defined which.`,\n detail: found.join(' | '),\n }))\n }\n\n const meta = metaMap(doc.html)\n const description = meta.get('description')?.[0]?.trim()\n if (!description) {\n out.push(finding({ group, check: 'identity.description-missing', severity: 'warning', url, message: 'No meta description.' }))\n }\n\n const canonicals = linksRel(doc.html, 'canonical')\n if (isNotFoundDocument(doc, ctx)) {\n // No address of its own, so nothing to be canonical about.\n } else if (canonicals.length === 0) {\n out.push(finding({ group, check: 'identity.canonical-missing', severity: 'warning', url, message: 'No canonical link.' }))\n } else if (canonicals.length > 1) {\n out.push(finding({\n group,\n check: 'identity.canonical-duplicate',\n severity: 'error',\n url,\n message: `${canonicals.length} canonical links — the page tells crawlers two different things about itself.`,\n detail: canonicals.map(c => c.href).join(' | '),\n }))\n } else {\n // The failure this catches is a template's canonical surviving a clone:\n // every page canonicalises to one URL and the site removes itself from the\n // index. It is worse than having no canonical at all.\n const declared = resolve(canonicals[0]!.href, url, ctx.input.site)\n const self = absolute(url, ctx.input.site) ?? resolve(url, '/', ctx.input.site)\n if (declared && self && identity(declared, ctx.input.site) !== identity(self, ctx.input.site)) {\n out.push(finding({\n group,\n check: 'identity.canonical-mismatch',\n severity: 'error',\n url,\n message: 'Canonical points at a different page.',\n detail: `${canonicals[0]!.href} ≠ ${url}`,\n }))\n }\n }\n\n // `<html lang>` is what a screen reader picks a voice from and what a\n // translation prompt keys on. A migrated page that lost it is not visibly\n // broken, which is why it stays lost.\n if (!/<html\\b[^>]*\\slang\\s*=\\s*[\"']?[a-zA-Z]/.test(doc.html)) {\n out.push(finding({ group, check: 'identity.lang-missing', severity: 'warning', url, message: 'No lang attribute on <html>.' }))\n }\n\n const missingOg = ['og:title', 'og:type', 'og:url'].filter(key => !meta.get(key)?.[0])\n if (missingOg.length) {\n out.push(finding({\n group,\n check: 'identity.open-graph-incomplete',\n severity: 'warning',\n url,\n message: `Open Graph incomplete — missing ${missingOg.join(', ')}. Shares render without a card.`,\n }))\n }\n if (!meta.get('twitter:card')?.[0]) {\n out.push(finding({ group, check: 'identity.twitter-card-missing', severity: 'info', url, message: 'No twitter:card.' }))\n }\n\n return out\n}\n\n// ─── indexing ───\n\n/** `noindex` as declared by a robots meta or an X-Robots-Tag header. */\nfunction noindexReason(doc: VerifyDocument): string | undefined {\n const robots = metaMap(doc.html).get('robots')?.join(',') ?? ''\n if (/\\bnoindex\\b/i.test(robots)) return `<meta name=\"robots\" content=\"${robots}\">`\n const header = doc.headers?.['x-robots-tag'] ?? ''\n if (/\\bnoindex\\b/i.test(header)) return `X-Robots-Tag: ${header}`\n return undefined\n}\n\nexport function indexingChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'indexing' as const\n const url = doc.url\n\n // A staging `noindex` carried into production is silent: the site builds,\n // deploys, serves — and disappears from search over the following weeks.\n const reason = noindexReason(doc)\n if (reason) {\n out.push(finding({ group, check: 'indexing.noindex', severity: 'error', url, message: 'Page is marked noindex.', detail: reason }))\n }\n\n if (ctx.input.sitemap !== undefined) {\n const listed = new Set(sitemapLocations(ctx.input.sitemap).map(loc => identity(loc, ctx.input.site)))\n if (!listed.has(identity(url, ctx.input.site)) && !reason && !isNotFoundDocument(doc, ctx)) {\n out.push(finding({ group, check: 'indexing.sitemap-missing-entry', severity: 'warning', url, message: 'Indexable page is not in the sitemap.' }))\n }\n }\n\n return out\n}\n\n/** Sitemap entries pointing at pages the build does not serve. */\nexport function sitemapStaleChecks(ctx: Context): Finding[] {\n if (ctx.input.sitemap === undefined) return []\n const out: Finding[] = []\n for (const loc of sitemapLocations(ctx.input.sitemap)) {\n const id = identity(loc, ctx.input.site)\n if (ctx.byIdentity.has(id)) continue\n out.push(finding({\n group: 'indexing',\n check: 'indexing.sitemap-stale-entry',\n severity: 'warning',\n url: loc,\n message: 'Sitemap lists a page the build does not serve.',\n }))\n }\n return out\n}\n\n// ─── status ───\n\nexport function statusChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'status' as const\n const url = doc.url\n const status = doc.status ?? 200\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const was = before.status ?? 200\n if (was !== status) {\n out.push(finding({\n group,\n check: 'status.mismatch',\n severity: 'error',\n url,\n message: `Status changed: ${was} → ${status}.`,\n }))\n }\n }\n\n // A \"not found\" page answering 200 is invisible to every monitor that looks\n // at status codes, and a crawler indexes it as content.\n if (status === 200) {\n const bodyText = text(doc.html)\n const title = titles(doc.html)[0] ?? ''\n const saysNotFound = NOT_FOUND_TEXT.test(title) || NOT_FOUND_TEXT.test(bodyText.slice(0, 400))\n if (saysNotFound && bodyText.length <= ctx.options.soft404MaxTextLength && !isNotFoundDocument(doc, ctx)) {\n out.push(finding({\n group,\n check: 'status.soft-404',\n severity: 'error',\n url,\n message: 'Page says it was not found but answers 200.',\n detail: title || bodyText.slice(0, 120),\n }))\n }\n }\n\n return out\n}\n\n/**\n * A build with no `404.html`.\n *\n * Only meaningful for a build: a set of pages captured from a running site\n * cannot show one, so asserting it there would report a defect the input cannot\n * express. Measured across a blind cohort every site lacked one — and because\n * migrated navigation is root-relative, a wrong path is not a rare event: it\n * lands the visitor on the host's generic page, with none of the site's chrome\n * and no way back.\n */\nexport function notFoundPageChecks(ctx: Context): Finding[] {\n if (!ctx.input.build) return []\n // Both spellings a static build produces: Astro writes `404.html` at the\n // root, and a directory-style build writes `404/index.html`, which the\n // address rule reduces to `/404`.\n const served = ctx.input.documents.some((doc) => {\n const id = identity(doc.url, ctx.input.site)\n return id === '/404' || id === '/404.html'\n })\n if (served) return []\n return [finding({\n group: 'status',\n check: 'status.not-found-page-missing',\n severity: 'error',\n message: 'The build has no 404.html, so a wrong path lands on the host\\u2019s page instead of the site\\u2019s.',\n })]\n}\n\n/** Redirect rules: chains, loops, and targets that do not exist. */\nexport function redirectChecks(ctx: Context): Finding[] {\n const rules = ctx.input.redirects\n if (!rules?.length) return []\n const out: Finding[] = []\n const group = 'status' as const\n const byFrom = new Map(rules.map(rule => [identity(rule.from, ctx.input.site), rule]))\n\n for (const rule of rules) {\n const seen = new Set<string>([identity(rule.from, ctx.input.site)])\n let hops = 1\n let target = identity(rule.to, ctx.input.site)\n\n while (byFrom.has(target)) {\n if (seen.has(target)) {\n out.push(finding({\n group,\n check: 'status.redirect-loop',\n severity: 'error',\n url: rule.from,\n message: 'Redirect chain loops.',\n detail: [...seen, target].join(' → '),\n }))\n break\n }\n seen.add(target)\n target = identity(byFrom.get(target)!.to, ctx.input.site)\n hops += 1\n }\n\n if (hops > ctx.options.maxRedirectHops) {\n out.push(finding({\n group,\n check: 'status.redirect-chain',\n severity: 'warning',\n url: rule.from,\n message: `Redirect chain is ${hops} hops.`,\n detail: [...seen, target].join(' → '),\n }))\n }\n\n const external = !isInternal(resolve(rule.to, rule.from, ctx.input.site) ?? '', ctx.input.site, ctx.options.internalHosts)\n if (!external && !ctx.served.has(target) && !byFrom.has(target)) {\n out.push(finding({\n group,\n check: 'status.redirect-target-missing',\n severity: 'error',\n url: rule.from,\n message: 'Redirect points at a page the build does not serve.',\n detail: rule.to,\n }))\n }\n }\n\n return out\n}\n\n// ─── international ───\n\n/**\n * hreflang has to be symmetric: if A offers B as its Turkish alternate, B must\n * offer A back. A one-sided declaration is ignored by search engines entirely,\n * so a half-migrated set of alternates is not a partial win — it is nothing.\n */\nexport function internationalChecks(ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'international' as const\n const declared = new Map<string, Map<string, string>>()\n\n for (const doc of ctx.input.documents) {\n const self = identity(doc.url, ctx.input.site)\n const alternates = new Map<string, string>()\n for (const link of linksRel(doc.html, 'alternate')) {\n if (!link.hreflang) continue\n const target = resolve(link.href, doc.url, ctx.input.site)\n if (!target) continue\n alternates.set(link.hreflang.toLowerCase(), identity(target, ctx.input.site))\n }\n if (alternates.size) declared.set(self, alternates)\n }\n\n for (const [self, alternates] of declared) {\n const listsSelf = [...alternates.values()].includes(self)\n if (!listsSelf) {\n out.push(finding({\n group,\n check: 'international.hreflang-no-self',\n severity: 'warning',\n url: self,\n message: 'Page declares hreflang alternates but does not list itself.',\n }))\n }\n for (const [lang, target] of alternates) {\n if (target === self) continue\n if (!ctx.byIdentity.has(target)) {\n out.push(finding({\n group,\n check: 'international.hreflang-unknown-target',\n severity: 'warning',\n url: self,\n message: `hreflang=\"${lang}\" points at a page the build does not serve.`,\n detail: target,\n }))\n continue\n }\n const back = declared.get(target)\n if (!back || ![...back.values()].includes(self)) {\n out.push(finding({\n group,\n check: 'international.hreflang-not-reciprocal',\n severity: 'error',\n url: self,\n message: `hreflang=\"${lang}\" names ${target}, which does not name it back — search engines ignore one-sided alternates.`,\n }))\n }\n }\n }\n\n return out\n}\n\n// ─── structured data ───\n\nexport function structuredChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'structured' as const\n const url = doc.url\n const blocks = jsonLd(doc.html)\n\n for (const block of blocks) {\n if (block.value === undefined) {\n out.push(finding({\n group,\n check: 'structured.jsonld-invalid',\n severity: 'error',\n url,\n message: 'A JSON-LD block does not parse — the whole block is discarded by consumers.',\n detail: block.raw.slice(0, 120),\n }))\n }\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (!before) return out\n\n const had = new Set(jsonLd(before.html).flatMap(block => jsonLdTypes(block.value)))\n const has = new Set(blocks.flatMap(block => jsonLdTypes(block.value)))\n const lost = [...had].filter(type => !has.has(type))\n if (lost.length) {\n out.push(finding({\n group,\n check: 'structured.type-lost',\n severity: 'warning',\n url,\n message: `Structured data types the old page had are gone: ${lost.join(', ')}.`,\n }))\n }\n\n return out\n}\n\n// ─── navigation ───\n\nexport function navigationChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'navigation' as const\n const url = doc.url\n const redirected = new Set((ctx.input.redirects ?? []).map(rule => identity(rule.from, ctx.input.site)))\n\n const broken = new Set<string>()\n for (const href of anchors(doc.html)) {\n if (/^(#|mailto:|tel:|javascript:|data:)/i.test(href)) continue\n const target = resolve(href, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n const id = identity(target, ctx.input.site)\n if (ctx.served.has(id) || redirected.has(id)) continue\n broken.add(href)\n }\n for (const href of broken) {\n out.push(finding({ group, check: 'navigation.broken-internal-link', severity: 'error', url, message: 'Internal link goes nowhere.', detail: href }))\n }\n\n for (const token of ['next', 'prev'] as const) {\n for (const link of linksRel(doc.html, token)) {\n const target = resolve(link.href, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n if (ctx.served.has(identity(target, ctx.input.site))) continue\n out.push(finding({\n group,\n check: 'navigation.pagination-broken',\n severity: 'warning',\n url,\n message: `rel=\"${token}\" points at a page the build does not serve.`,\n detail: link.href,\n }))\n }\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const feedRel = (html: string) => linksRel(html, 'alternate').some(link => /rss|atom|feed/i.test(link.type ?? ''))\n if (feedRel(before.html) && !feedRel(doc.html)) {\n out.push(finding({ group, check: 'navigation.feed-lost', severity: 'warning', url, message: 'The old page advertised a feed; this one does not.' }))\n }\n }\n\n return out\n}\n\n/**\n * References to the origin the content was migrated away from.\n *\n * A generated page that still points at the old WordPress host has not been\n * migrated, it has been mirrored: the new site works only while the old one\n * stays up, and the day it goes away the images and stylesheets go with it.\n * Across a blind cohort this was the single largest thing keeping sites from\n * being deliverable, so it is an error rather than a warning.\n *\n * The scan is over raw text, not parsed attributes, because the references hide\n * in every shape a reference can take — `src`, `srcset`, `href`, `url()` in a\n * style attribute, a string inside inline script. That over-matches slightly:\n * the origin written in a comment or in visible prose counts too. Reported that\n * way on purpose — a literal old-host URL on the page is worth looking at even\n * when it is not fetched.\n *\n * Stylesheets and scripts are scanned only when their contents are supplied\n * (`input.files`). Without them the count is HTML-only, which is a smaller\n * number than the truth rather than a wrong one — and the report says so.\n */\nexport function sourceOriginChecks(ctx: Context): Finding[] {\n const origin = ctx.options.sourceOrigin\n if (!origin) return []\n\n let host: string\n try {\n host = new URL(origin.includes('://') ? origin : `https://${origin}`).host\n } catch {\n return [finding({\n group: 'assets',\n check: 'assets.source-origin-invalid',\n severity: 'warning',\n message: `sourceOrigin \"${origin}\" is not a host or a URL; the scan did not run.`,\n })]\n }\n if ((ctx.options.allowHosts ?? []).includes(host)) return []\n\n const out: Finding[] = []\n const pattern = new RegExp(host.replace(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`), 'gi')\n const count = (source: string) => (source.match(pattern) ?? []).length\n\n for (const doc of ctx.input.documents) {\n const hits = count(doc.html)\n if (hits) {\n out.push(finding({\n group: 'assets',\n check: 'assets.source-origin-reference',\n severity: 'error',\n url: doc.url,\n message: `${hits} reference${hits === 1 ? '' : 's'} to the source origin ${host} — this page still depends on the site it replaced.`,\n }))\n }\n }\n\n for (const file of ctx.input.files ?? []) {\n const hits = count(file.content)\n if (hits) {\n out.push(finding({\n group: 'assets',\n check: 'assets.source-origin-reference',\n severity: 'error',\n url: file.path,\n message: `${hits} reference${hits === 1 ? '' : 's'} to the source origin ${host}.`,\n }))\n }\n }\n\n return out\n}\n\n// ─── assets ───\n\nexport function assetChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'assets' as const\n const url = doc.url\n const refs = images(doc.html)\n\n const broken = new Set<string>()\n for (const image of refs) {\n if (/^data:/i.test(image.src)) continue\n const target = resolve(image.src, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n if (ctx.served.has(identity(target, ctx.input.site))) continue\n broken.add(image.src)\n }\n for (const src of broken) {\n out.push(finding({ group, check: 'assets.broken-image', severity: 'error', url, message: 'Image does not resolve to anything the build serves.', detail: src }))\n }\n\n // `alt=\"\"` is a decision (decorative); no `alt` attribute at all is an\n // omission. Only the second is reported.\n const noAlt = refs.filter(image => image.alt === undefined)\n if (noAlt.length) {\n out.push(finding({\n group,\n check: 'assets.missing-alt',\n severity: 'warning',\n url,\n message: `${noAlt.length} image${noAlt.length === 1 ? '' : 's'} without an alt attribute.`,\n detail: noAlt.slice(0, 5).map(image => image.src).join(', '),\n }))\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const had = new Map(images(before.html).filter(i => i.alt).map(i => [identity(resolve(i.src, before.url, ctx.input.site) ?? i.src, ctx.input.site), i.alt!]))\n const now = new Map(refs.map(i => [identity(resolve(i.src, url, ctx.input.site) ?? i.src, ctx.input.site), i.alt]))\n const lost = [...had.keys()].filter(src => now.has(src) && !now.get(src))\n if (lost.length) {\n out.push(finding({\n group,\n check: 'assets.alt-lost',\n severity: 'warning',\n url,\n message: `${lost.length} image${lost.length === 1 ? '' : 's'} lost alt text the old page had.`,\n detail: lost.slice(0, 5).join(', '),\n }))\n }\n }\n\n return out\n}\n","// ─── Running the gates ───\n\nimport type { CheckGroup, Finding, Severity, VerifyInput, VerifyReport } from './types.js'\nimport { CHECK_GROUPS } from './types.js'\nimport type { Context } from './checks.js'\nimport {\n assetChecks, identityChecks, indexingChecks, internationalChecks, navigationChecks,\n notFoundPageChecks, redirectChecks, sitemapStaleChecks, sourceOriginChecks,\n statusChecks, structuredChecks,\n} from './checks.js'\nimport { identity } from './url.js'\n\nconst DEFAULTS = { maxRedirectHops: 1, soft404MaxTextLength: 600 }\n\n/**\n * Checks that need an input the caller did not supply. Reported rather than\n * silently omitted: a green report over a run that never compared anything is\n * the most dangerous output this package could produce.\n */\nfunction absentInputs(input: VerifyInput, groups: readonly CheckGroup[]): { group: CheckGroup, reason: string }[] {\n const skipped: { group: CheckGroup, reason: string }[] = []\n if (!input.baseline) {\n for (const group of ['status', 'structured', 'navigation', 'assets'] as const) {\n if (groups.includes(group)) skipped.push({ group, reason: 'no baseline — parity checks did not run' })\n }\n }\n if (input.sitemap === undefined && groups.includes('indexing')) {\n skipped.push({ group: 'indexing', reason: 'no sitemap — membership checks did not run' })\n }\n if (!input.redirects?.length && groups.includes('status')) {\n skipped.push({ group: 'status', reason: 'no redirect rules — chain and target checks did not run' })\n }\n if (!input.build && groups.includes('status')) {\n skipped.push({ group: 'status', reason: 'not a build directory — the 404 page check did not run' })\n }\n if (!input.options?.sourceOrigin && groups.includes('assets')) {\n skipped.push({ group: 'assets', reason: 'no sourceOrigin — the old-host reference scan did not run' })\n } else if (!input.files?.length && groups.includes('assets')) {\n skipped.push({ group: 'assets', reason: 'no stylesheet or script contents — the old-host scan covered HTML only' })\n }\n return skipped\n}\n\nfunction buildContext(input: VerifyInput): Context {\n const options = { ...DEFAULTS, ...input.options }\n const byIdentity = new Map(input.documents.map(doc => [identity(doc.url, input.site), doc]))\n const baselineByIdentity = new Map((input.baseline?.documents ?? []).map(doc => [identity(doc.url, input.site), doc]))\n const served = new Set<string>([\n ...byIdentity.keys(),\n ...(input.assets ?? []).map(asset => identity(asset, input.site)),\n ])\n return { input, options, byIdentity, baselineByIdentity, served }\n}\n\n/**\n * Run the gates over a built site.\n *\n * Nothing is fetched. `input.documents` is the built site as served, and\n * `input.baseline` — when the caller has one — is the site it replaced. Without\n * a baseline every self-check still runs; only the parity checks (status\n * change, lost structured data, lost alt text, lost feed) go quiet, because\n * there is nothing to compare against.\n */\nexport function verify(input: VerifyInput): VerifyReport {\n const groups = [...(input.groups ?? CHECK_GROUPS)]\n const ctx = buildContext(input)\n const findings: Finding[] = []\n const skipped = absentInputs(input, groups)\n\n const run = (group: CheckGroup, fn: () => Finding[]) => {\n if (!groups.includes(group)) return\n findings.push(...fn())\n }\n\n for (const doc of input.documents) {\n run('identity', () => identityChecks(doc, ctx))\n run('indexing', () => indexingChecks(doc, ctx))\n run('status', () => statusChecks(doc, ctx))\n run('structured', () => structuredChecks(doc, ctx))\n run('navigation', () => navigationChecks(doc, ctx))\n run('assets', () => assetChecks(doc, ctx))\n }\n\n // Site-wide checks: these read the whole set, not one document.\n run('indexing', () => sitemapStaleChecks(ctx))\n run('status', () => redirectChecks(ctx))\n run('status', () => notFoundPageChecks(ctx))\n run('international', () => internationalChecks(ctx))\n run('assets', () => sourceOriginChecks(ctx))\n\n const counts: Record<Severity, number> = { error: 0, warning: 0, info: 0 }\n for (const item of findings) counts[item.severity] += 1\n\n return {\n passed: counts.error === 0,\n findings,\n counts,\n documents: input.documents.length,\n groups,\n skipped,\n }\n}\n\n/** A report as lines, most severe first. Used by the CLI and by consumers. */\nexport function formatReport(report: VerifyReport): string {\n const order: Severity[] = ['error', 'warning', 'info']\n const lines: string[] = []\n for (const severity of order) {\n for (const item of report.findings.filter(f => f.severity === severity)) {\n const where = item.url ? ` ${item.url}` : ''\n const detail = item.detail ? `\\n ${item.detail}` : ''\n lines.push(`${severity.toUpperCase().padEnd(7)} ${item.check}${where}\\n ${item.message}${detail}`)\n }\n }\n for (const { group, reason } of report.skipped) {\n lines.push(`SKIPPED ${group} — ${reason}`)\n }\n lines.push(\n `\\n${report.documents} document${report.documents === 1 ? '' : 's'} · `\n + `${report.counts.error} error · ${report.counts.warning} warning · ${report.counts.info} info · `\n + (report.passed ? 'PASS' : 'FAIL'),\n )\n return lines.join('\\n')\n}\n","// ─── Reading a built directory ───\n//\n// The one place this package touches a filesystem. Kept apart from the checks\n// so the engine stays pure: a consumer that already holds the documents — a\n// migration runner, a CI step with an artefact — imports `verify` and never\n// loads this module.\n\nimport { readFile, readdir } from 'node:fs/promises'\nimport { join, relative, sep } from 'node:path'\nimport type { VerifyDocument, VerifyFile, VerifyInput } from './types.js'\n\nconst DOCUMENT_EXT = /\\.html?$/i\nconst TEXT_EXT = /\\.(css|m?js|cjs|json|xml|txt|svg)$/i\n\nasync function walk(dir: string, root: string, files: string[] = []): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) {\n await walk(full, root, files)\n continue\n }\n files.push(relative(root, full))\n }\n return files\n}\n\n/**\n * Load a static build as a verification input: every `.html` becomes a\n * document served at its path, everything else becomes an asset.\n *\n * `dist/about/index.html` is served at `/about/`, which `identity()` then\n * reduces to `/about` — the same address a link to `/about/` or\n * `/about/index.html` resolves to.\n */\nexport async function loadSiteDirectory(dir: string, site?: string): Promise<Pick<VerifyInput, 'documents' | 'assets' | 'files' | 'sitemap' | 'site' | 'build'>> {\n const files = await walk(dir, dir)\n const documents: VerifyDocument[] = []\n const assets: string[] = []\n const texts: VerifyFile[] = []\n let sitemap: string | undefined\n\n for (const file of files.toSorted()) {\n const served = `/${file.split(sep).join('/')}`\n if (DOCUMENT_EXT.test(file)) {\n documents.push({ url: served, html: await readFile(join(dir, file), 'utf8'), status: 200 })\n continue\n }\n if (/^\\/sitemap[^/]*\\.xml$/i.test(served)) sitemap = await readFile(join(dir, file), 'utf8')\n // Stylesheets and scripts are read as text so the content scans can see\n // them. A `url()` pointing at the old host is exactly as fatal as one in\n // the HTML, and a scan that skips CSS reports a number smaller than the\n // truth — which is worse than not counting when two tools compare figures.\n if (TEXT_EXT.test(file)) {\n texts.push({ path: served, content: await readFile(join(dir, file), 'utf8') })\n }\n assets.push(served)\n }\n\n return { documents, assets, files: texts, sitemap, site, build: true }\n}\n"],"mappings":"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"}
@@ -47,6 +47,12 @@ interface VerifyReport {
47
47
  reason: string;
48
48
  }[];
49
49
  }
50
+ /** A text file that is not a document — a stylesheet, a script. */
51
+ interface VerifyFile {
52
+ /** Path as served (`/assets/app.css`). */
53
+ path: string;
54
+ content: string;
55
+ }
50
56
  interface VerifyOptions {
51
57
  /**
52
58
  * A redirect chain longer than this is reported. One hop is normal; two is a
@@ -62,6 +68,21 @@ interface VerifyOptions {
62
68
  soft404MaxTextLength?: number;
63
69
  /** Hosts to treat as internal besides `site`'s own. */
64
70
  internalHosts?: string[];
71
+ /**
72
+ * The origin the content was migrated away from — the old WordPress host.
73
+ *
74
+ * A migrated page that still points at it is not finished: the new site
75
+ * depends on the old one staying up, and the moment it does not, images and
76
+ * stylesheets disappear. Measured across a blind cohort it was the single
77
+ * largest blocker, so this is checked as an error when the origin is known.
78
+ */
79
+ sourceOrigin?: string;
80
+ /**
81
+ * Hosts that may legitimately still be referenced even though they are not
82
+ * the site's own — a CDN the customer keeps. Applies to
83
+ * `assets.source-origin-reference` only.
84
+ */
85
+ allowHosts?: string[];
65
86
  }
66
87
  interface VerifyInput {
67
88
  /** The built site. */
@@ -74,14 +95,33 @@ interface VerifyInput {
74
95
  redirects?: VerifyRedirect[];
75
96
  /** Non-document files the build produced, as served (`/images/a.png`). */
76
97
  assets?: string[];
98
+ /**
99
+ * Contents of non-document text files — stylesheets, scripts. Only the checks
100
+ * that scan content read these; everything else works from `documents` and
101
+ * the `assets` path list.
102
+ *
103
+ * Supplying them matters for `assets.source-origin-reference`: a `url()` in a
104
+ * stylesheet pointing at the old host is exactly as fatal as one in the HTML,
105
+ * and a scan that silently skips CSS reports a smaller number than a scan
106
+ * that does not — which is worse than not counting at all when two tools
107
+ * compare figures.
108
+ */
109
+ files?: VerifyFile[];
77
110
  /** The old site, for parity checks. Absent means only self-checks run. */
78
111
  baseline?: {
79
112
  documents: VerifyDocument[];
80
113
  };
114
+ /**
115
+ * Whether `documents` are a static build directory rather than pages captured
116
+ * from a running site. Build-level expectations — a `404.html` exists — are
117
+ * only meaningful for a build, and asserting them against a capture would
118
+ * report a defect the capture simply cannot show. `loadSiteDirectory` sets it.
119
+ */
120
+ build?: boolean;
81
121
  /** Defaults to every group. */
82
122
  groups?: readonly CheckGroup[];
83
123
  options?: VerifyOptions;
84
124
  }
85
125
  //#endregion
86
126
  export { VerifyDocument as a, VerifyRedirect as c, Severity as i, VerifyReport as l, CheckGroup as n, VerifyInput as o, Finding as r, VerifyOptions as s, CHECK_GROUPS as t };
87
- //# sourceMappingURL=types-C_hmH1n3.d.mts.map
127
+ //# sourceMappingURL=types-bYfzrRnM.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-bYfzrRnM.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;UAaiB,cAAA;EAAc;EAE7B,GAAA;EACA,IAAA;EADA;EAGA,MAAA;EAAA;EAEA,OAAA,GAAU,MAAA;AAAA;AAAA,UAGK,cAAA;EACf,IAAA;EACA,EAAA;EACA,MAAA;AAAA;AAAA,cAGW,YAAA;AAAA,KAiBD,UAAA,WAAqB,YAAA;AAAA,KAErB,QAAA;;;;AAnBZ;;;UA2BiB,OAAA;EACf,KAAA,EAAO,UAAA;EACP,KAAA;EACA,QAAA,EAAU,QAAA;;EAEV,GAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,UAGe,YAAA;EAlBG;EAoBlB,MAAA;EACA,QAAA,EAAU,OAAA;EACV,MAAA,EAAQ,MAAA,CAAO,QAAA;;EAEf,SAAA;EAfA;EAiBA,MAAA,EAAQ,UAAA;EAhBR;EAkBA,OAAA;IAAW,KAAA,EAAO,UAAA;IAAY,MAAA;EAAA;AAAA;;UAIf,UAAA;EAdA;EAgBf,IAAA;EACA,OAAA;AAAA;AAAA,UAGe,aAAA;EAhBP;;;;;EAsBR,eAAA;EAvBA;;;;;EA6BA,oBAAA;EAxBA;EA0BA,aAAA;EAxBA;;;;;;AAIF;;EA6BE,YAAA;EA3BA;;AAIF;;;EA6BE,UAAA;AAAA;AAAA,UAGe,WAAA;EAlBf;EAoBA,SAAA,EAAW,cAAA;EALX;EAOA,IAAA;EAPU;EASV,OAAA;EAN0B;EAQ1B,SAAA,GAAY,cAAA;EAND;EAQX,MAAA;EAYQ;;;;;;;;;;;EAAR,KAAA,GAAQ,UAAA;EAZR;EAcA,QAAA;IAAa,SAAA,EAAW,cAAA;EAAA;EAAX;;;;;;EAOb,KAAA;EAGuB;EADvB,MAAA,YAAkB,UAAA;EAClB,OAAA,GAAU,aAAA;AAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/verify",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "description": "Portable SEO, indexing, link and asset gates for a migrated site — offline, zero-dependency",
6
6
  "type": "module",
@@ -40,16 +40,16 @@
40
40
  "files": [
41
41
  "dist"
42
42
  ],
43
+ "devDependencies": {
44
+ "@types/node": "^22.10.0",
45
+ "tsdown": "^0.21.0",
46
+ "typescript": "^5.7.0"
47
+ },
43
48
  "scripts": {
44
49
  "build": "tsdown src/index.ts src/cli.ts src/bin.ts --format esm --dts",
45
50
  "dev": "tsdown src/index.ts src/cli.ts src/bin.ts --format esm --dts --watch",
46
51
  "test": "vitest run",
47
52
  "typecheck": "tsc --noEmit",
48
53
  "clean": "rm -rf dist"
49
- },
50
- "devDependencies": {
51
- "@types/node": "^22.10.0",
52
- "tsdown": "^0.21.0",
53
- "typescript": "^5.7.0"
54
54
  }
55
- }
55
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"load-KUG-8Ug4.mjs","names":[],"sources":["../src/types.ts","../src/html.ts","../src/url.ts","../src/checks.ts","../src/verify.ts","../src/load.ts"],"sourcesContent":["// ─── What a verification run is given, and what it answers with ───\n//\n// Everything here is data the caller already has. The package never fetches:\n// a hardened crawler is a security surface of its own (SSRF, DNS rebinding,\n// redirect and byte limits) and belongs where that hardening lives, not in the\n// MIT library every consumer embeds. Whoever holds a safe fetcher — or a\n// directory of built files — supplies the documents.\n//\n// The consequence is that this runs identically over a `dist/` directory, over\n// a captured baseline, and inside another tool's pipeline, with no network and\n// no mocking.\n\n/** One page as served: the address it answers at and what it answered with. */\nexport interface VerifyDocument {\n /** Absolute URL, or a site-relative path (`/about/`). */\n url: string\n html: string\n /** Defaults to 200 when the caller did not record one. */\n status?: number\n /** Response headers, lowercased keys. `x-robots-tag` is read here. */\n headers?: Record<string, string>\n}\n\nexport interface VerifyRedirect {\n from: string\n to: string\n status: number\n}\n\nexport const CHECK_GROUPS = [\n /** canonical, `<title>`, description, Open Graph, Twitter card */\n 'identity',\n /** robots meta, X-Robots-Tag, noindex leaks, sitemap membership */\n 'indexing',\n /** status parity, soft-404, redirect chains */\n 'status',\n /** hreflang pairs and reciprocity */\n 'international',\n /** JSON-LD validity and type parity */\n 'structured',\n /** internal links, pagination, feeds */\n 'navigation',\n /** image resolution and alt preservation */\n 'assets',\n] as const\n\nexport type CheckGroup = (typeof CHECK_GROUPS)[number]\n\nexport type Severity = 'error' | 'warning' | 'info'\n\n/**\n * One thing that is wrong, or worth knowing.\n *\n * `check` is a stable id (`identity.canonical-mismatch`) so a consumer can\n * suppress, threshold or track one finding without matching on prose.\n */\nexport interface Finding {\n group: CheckGroup\n check: string\n severity: Severity\n /** The document the finding is about, when it is about one. */\n url?: string\n message: string\n detail?: string\n}\n\nexport interface VerifyReport {\n /** No `error` findings. This is the gate. */\n passed: boolean\n findings: Finding[]\n counts: Record<Severity, number>\n /** How many documents were examined. */\n documents: number\n /** Which groups actually ran. */\n groups: CheckGroup[]\n /** Groups that were asked for but could not run, and why. */\n skipped: { group: CheckGroup, reason: string }[]\n}\n\nexport interface VerifyOptions {\n /**\n * A redirect chain longer than this is reported. One hop is normal; two is a\n * link-equity leak and a latency cost, and chains grow silently as redirect\n * rules accumulate over migrations.\n */\n maxRedirectHops?: number\n /**\n * A 200 response shorter than this many characters of text, whose title or\n * body says \"not found\", is treated as a soft-404. Kept configurable because\n * a legitimately short page exists; the default is deliberately low.\n */\n soft404MaxTextLength?: number\n /** Hosts to treat as internal besides `site`'s own. */\n internalHosts?: string[]\n}\n\nexport interface VerifyInput {\n /** The built site. */\n documents: VerifyDocument[]\n /** The site's canonical origin (`https://example.com`). Several checks need it. */\n site?: string\n /** `sitemap.xml` as produced by the build. */\n sitemap?: string\n /** Redirect rules the host will serve. */\n redirects?: VerifyRedirect[]\n /** Non-document files the build produced, as served (`/images/a.png`). */\n assets?: string[]\n /** The old site, for parity checks. Absent means only self-checks run. */\n baseline?: { documents: VerifyDocument[] }\n /** Defaults to every group. */\n groups?: readonly CheckGroup[]\n options?: VerifyOptions\n}\n","// ─── Reading the few things a document has to say ───\n//\n// Targeted regexes, not a parser. The same choice `@contentrain/emitter-astro`\n// makes, for the same reason: these documents are machine-generated HTML whose\n// head we already control, the extracted set is small and fixed, and a parser\n// would put a dependency into the one package a migration is supposed to be\n// able to run anywhere.\n//\n// The limits are real and bounded. `head()` splits on the first `</head>`, so a\n// document without one yields nothing rather than guessing; attribute reads are\n// quote-aware but do not resolve entities beyond the five that matter. Where a\n// check would be wrong under those limits, it is not written.\n\n/** Everything before the first `</head>`, or '' when there is none. */\nexport function head(html: string): string {\n const end = html.search(/<\\/head\\s*>/i)\n return end === -1 ? '' : html.slice(0, end)\n}\n\n/** Everything after the first `</head>`, or the whole document when there is none. */\nexport function body(html: string): string {\n const end = html.search(/<\\/head\\s*>/i)\n return end === -1 ? html : html.slice(end)\n}\n\nconst ENTITIES: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\", '#39': \"'\" }\n\nexport function decodeEntities(value: string): string {\n return value.replace(/&(#39|amp|lt|gt|quot|apos);/g, (_m, name: string) => ENTITIES[name] ?? _m)\n}\n\n/**\n * Read one attribute off a single tag's source. Quote-aware; unquoted allowed.\n *\n * The name must be preceded by whitespace, not merely a word boundary: `-` is\n * not a word character, so `\\bhref` matches inside `data-href` — and lazy-\n * loading WordPress themes put the real URL in `data-src` and a placeholder in\n * `src` on almost every image.\n */\nexport function attr(tag: string, name: string): string | undefined {\n const re = new RegExp(`(?:^|\\\\s)${name}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s\"'>]+))`, 'i')\n const match = re.exec(tag)\n if (!match) return undefined\n return decodeEntities(match[1] ?? match[2] ?? match[3] ?? '')\n}\n\n/** Every `<tag …>` opening tag in the source, as raw strings. */\nexport function tags(source: string, name: string): string[] {\n return [...source.matchAll(new RegExp(`<${name}\\\\b[^>]*>`, 'gi'))].map(m => m[0])\n}\n\n/**\n * `<title>` texts from the head. `<svg>` blocks are removed first: a `<title>`\n * inside one is an accessibility label, valid markup, and not the page title —\n * counting it would report a duplicate on every page with an inline icon.\n */\nexport function titles(html: string): string[] {\n const source = head(html).replace(/<svg\\b[\\s\\S]*?<\\/svg\\s*>/gi, '')\n return [...source.matchAll(/<title\\b[^>]*>([\\s\\S]*?)<\\/title\\s*>/gi)]\n .map(m => decodeEntities(m[1] ?? '').trim())\n}\n\n/** `<meta name=\"…\">` / `<meta property=\"…\">` content, keyed by lowercased key. */\nexport function metaMap(html: string): Map<string, string[]> {\n const found = new Map<string, string[]>()\n for (const tag of tags(head(html), 'meta')) {\n const key = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n if (!key) continue\n const content = attr(tag, 'content') ?? ''\n found.set(key, [...(found.get(key) ?? []), content])\n }\n return found\n}\n\nexport interface LinkTag {\n rel: string\n href: string\n hreflang?: string\n type?: string\n}\n\n/** `<link>` tags from the head, with `rel` lowercased. */\nexport function links(html: string): LinkTag[] {\n const found: LinkTag[] = []\n for (const tag of tags(head(html), 'link')) {\n const rel = attr(tag, 'rel')?.toLowerCase()\n const href = attr(tag, 'href')\n if (!rel || href === undefined) continue\n found.push({ rel, href, hreflang: attr(tag, 'hreflang'), type: attr(tag, 'type') })\n }\n return found\n}\n\n/** Links whose `rel` list contains the given token. */\nexport function linksRel(html: string, token: string): LinkTag[] {\n return links(html).filter(link => link.rel.split(/\\s+/).includes(token))\n}\n\nexport interface JsonLdBlock {\n raw: string\n /** Parsed value, or undefined when the block is not valid JSON. */\n value?: unknown\n}\n\n/** Every `application/ld+json` block in the document, parsed where possible. */\nexport function jsonLd(html: string): JsonLdBlock[] {\n const blocks: JsonLdBlock[] = []\n const re = /<script\\b[^>]*\\btype\\s*=\\s*[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script\\s*>/gi\n for (const match of html.matchAll(re)) {\n const raw = (match[1] ?? '').trim()\n try {\n blocks.push({ raw, value: JSON.parse(raw) as unknown })\n } catch {\n blocks.push({ raw })\n }\n }\n return blocks\n}\n\n/**\n * `@type` values in a JSON-LD block, including those nested in `@graph` —\n * themes commonly ship one graph holding Organization, WebSite and WebPage,\n * and reading only the root would see none of them.\n */\nexport function jsonLdTypes(value: unknown): string[] {\n const found: string[] = []\n const walk = (node: unknown): void => {\n if (Array.isArray(node)) {\n for (const item of node) walk(item)\n return\n }\n if (!node || typeof node !== 'object') return\n const record = node as Record<string, unknown>\n const type = record['@type']\n if (typeof type === 'string') found.push(type)\n else if (Array.isArray(type)) for (const t of type) if (typeof t === 'string') found.push(t)\n if ('@graph' in record) walk(record['@graph'])\n }\n walk(value)\n return found\n}\n\n/** `href` of every `<a>` in the document body. */\nexport function anchors(html: string): string[] {\n return tags(body(html), 'a')\n .map(tag => attr(tag, 'href'))\n .filter((href): href is string => href !== undefined)\n}\n\nexport interface ImageRef {\n src: string\n /** Absent means no `alt` attribute at all — different from `alt=\"\"`. */\n alt?: string\n}\n\n/** Every `<img>` in the document body. */\nexport function images(html: string): ImageRef[] {\n return tags(body(html), 'img')\n .map(tag => ({ src: attr(tag, 'src') ?? '', alt: attr(tag, 'alt') }))\n .filter(image => image.src !== '')\n}\n\n/** Visible text, roughly: script/style stripped, tags removed, runs collapsed. */\nexport function text(html: string): string {\n return body(html)\n .replace(/<(script|style)\\b[\\s\\S]*?<\\/\\1\\s*>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/** `<loc>` entries of a sitemap or sitemap index. */\nexport function sitemapLocations(xml: string): string[] {\n return [...xml.matchAll(/<loc>\\s*([\\s\\S]*?)\\s*<\\/loc>/gi)].map(m => decodeEntities((m[1] ?? '').trim()))\n}\n","// ─── Addresses ───\n//\n// Every check that asks \"does this link go somewhere that exists\" is really\n// asking a normalisation question, and getting it wrong in either direction is\n// worse than not asking: too strict and a correct site fails its gate, too\n// loose and a broken one passes.\n//\n// The rules here are deliberately conservative. A trailing slash and an\n// `index.html` are the same address, because static hosts serve them that way.\n// A query string and a fragment are not part of a document's identity. Case is\n// preserved in the path, because static hosts are case-sensitive even when the\n// author assumed otherwise.\n\nconst SITE_FALLBACK = 'https://contentrain.invalid'\n\n/** Absolute URL for `href` as seen from `base`, or undefined when unresolvable. */\nexport function resolve(href: string, base: string, site?: string): string | undefined {\n try {\n const origin = site ?? SITE_FALLBACK\n const absoluteBase = base.startsWith('http') ? base : new URL(base, origin).href\n return new URL(href, absoluteBase).href\n } catch {\n return undefined\n }\n}\n\n/** Whether a resolved URL belongs to the site under test. */\nexport function isInternal(url: string, site?: string, extraHosts: readonly string[] = []): boolean {\n try {\n const parsed = new URL(url)\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false\n if (!site) return parsed.origin === SITE_FALLBACK || extraHosts.includes(parsed.host)\n return parsed.host === new URL(site).host || extraHosts.includes(parsed.host)\n } catch {\n return false\n }\n}\n\n/**\n * The identity of a document: path only, with `index.html` and a trailing\n * slash removed, query and fragment dropped. `/` stays `/`.\n */\nexport function identity(url: string, site?: string): string {\n const full = url.startsWith('http') ? url : resolve(url, '/', site) ?? url\n let path: string\n try {\n path = new URL(full).pathname\n } catch {\n path = full.split(/[?#]/)[0] ?? full\n }\n path = path.replace(/\\/index\\.html?$/i, '/')\n if (path.length > 1) path = path.replace(/\\/+$/, '')\n return path === '' ? '/' : path\n}\n\n/** Absolute form of a document's own address, when the site origin is known. */\nexport function absolute(url: string, site?: string): string | undefined {\n if (url.startsWith('http')) return url\n if (!site) return undefined\n try {\n return new URL(url, site).href\n } catch {\n return undefined\n }\n}\n","// ─── The gates ───\n//\n// Seven groups, from the migration contract's verification scope. Each check\n// has a stable id and answers one question; none of them fetches anything.\n//\n// Two rules shaped every check below. A finding is an `error` only when the\n// built site is *wrong* — a visitor or a crawler gets something it should not.\n// Everything that is merely worse than the old site is a `warning`, because a\n// migration that improves a page should not fail its own gate.\n\nimport type { Finding, VerifyDocument, VerifyInput, VerifyOptions } from './types.js'\nimport {\n anchors, images, jsonLd, jsonLdTypes, linksRel, metaMap, sitemapLocations, text, titles,\n} from './html.js'\nimport { absolute, identity, isInternal, resolve } from './url.js'\n\nexport interface Context {\n input: VerifyInput\n options: Required<Pick<VerifyOptions, 'maxRedirectHops' | 'soft404MaxTextLength'>> & VerifyOptions\n /** Document identity → document, for the built site. */\n byIdentity: Map<string, VerifyDocument>\n /** Baseline document identity → document. */\n baselineByIdentity: Map<string, VerifyDocument>\n /** Identities of everything the build serves: documents plus assets. */\n served: Set<string>\n}\n\nconst NOT_FOUND_TEXT = /\\b(404|not found|page not found|sayfa bulunamad[ıi]|nicht gefunden|introuvable|no se encontr)/i\n\nfunction finding(f: Finding): Finding {\n return f\n}\n\n// ─── identity ───\n\nexport function identityChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const url = doc.url\n const group = 'identity' as const\n const found = titles(doc.html)\n\n if (found.every(t => t === '')) {\n out.push(finding({ group, check: 'identity.title-missing', severity: 'error', url, message: 'No non-empty <title> in the head.' }))\n } else if (found.length > 1) {\n out.push(finding({\n group,\n check: 'identity.title-duplicate',\n severity: 'error',\n url,\n message: `${found.length} <title> elements in the head — a crawler takes one and it is not defined which.`,\n detail: found.join(' | '),\n }))\n }\n\n const meta = metaMap(doc.html)\n const description = meta.get('description')?.[0]?.trim()\n if (!description) {\n out.push(finding({ group, check: 'identity.description-missing', severity: 'warning', url, message: 'No meta description.' }))\n }\n\n const canonicals = linksRel(doc.html, 'canonical')\n if (canonicals.length === 0) {\n out.push(finding({ group, check: 'identity.canonical-missing', severity: 'warning', url, message: 'No canonical link.' }))\n } else if (canonicals.length > 1) {\n out.push(finding({\n group,\n check: 'identity.canonical-duplicate',\n severity: 'error',\n url,\n message: `${canonicals.length} canonical links — the page tells crawlers two different things about itself.`,\n detail: canonicals.map(c => c.href).join(' | '),\n }))\n } else {\n // The failure this catches is a template's canonical surviving a clone:\n // every page canonicalises to one URL and the site removes itself from the\n // index. It is worse than having no canonical at all.\n const declared = resolve(canonicals[0]!.href, url, ctx.input.site)\n const self = absolute(url, ctx.input.site) ?? resolve(url, '/', ctx.input.site)\n if (declared && self && identity(declared, ctx.input.site) !== identity(self, ctx.input.site)) {\n out.push(finding({\n group,\n check: 'identity.canonical-mismatch',\n severity: 'error',\n url,\n message: 'Canonical points at a different page.',\n detail: `${canonicals[0]!.href} ≠ ${url}`,\n }))\n }\n }\n\n const missingOg = ['og:title', 'og:type', 'og:url'].filter(key => !meta.get(key)?.[0])\n if (missingOg.length) {\n out.push(finding({\n group,\n check: 'identity.open-graph-incomplete',\n severity: 'warning',\n url,\n message: `Open Graph incomplete — missing ${missingOg.join(', ')}. Shares render without a card.`,\n }))\n }\n if (!meta.get('twitter:card')?.[0]) {\n out.push(finding({ group, check: 'identity.twitter-card-missing', severity: 'info', url, message: 'No twitter:card.' }))\n }\n\n return out\n}\n\n// ─── indexing ───\n\n/** `noindex` as declared by a robots meta or an X-Robots-Tag header. */\nfunction noindexReason(doc: VerifyDocument): string | undefined {\n const robots = metaMap(doc.html).get('robots')?.join(',') ?? ''\n if (/\\bnoindex\\b/i.test(robots)) return `<meta name=\"robots\" content=\"${robots}\">`\n const header = doc.headers?.['x-robots-tag'] ?? ''\n if (/\\bnoindex\\b/i.test(header)) return `X-Robots-Tag: ${header}`\n return undefined\n}\n\nexport function indexingChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'indexing' as const\n const url = doc.url\n\n // A staging `noindex` carried into production is silent: the site builds,\n // deploys, serves — and disappears from search over the following weeks.\n const reason = noindexReason(doc)\n if (reason) {\n out.push(finding({ group, check: 'indexing.noindex', severity: 'error', url, message: 'Page is marked noindex.', detail: reason }))\n }\n\n if (ctx.input.sitemap !== undefined) {\n const listed = new Set(sitemapLocations(ctx.input.sitemap).map(loc => identity(loc, ctx.input.site)))\n if (!listed.has(identity(url, ctx.input.site)) && !reason) {\n out.push(finding({ group, check: 'indexing.sitemap-missing-entry', severity: 'warning', url, message: 'Indexable page is not in the sitemap.' }))\n }\n }\n\n return out\n}\n\n/** Sitemap entries pointing at pages the build does not serve. */\nexport function sitemapStaleChecks(ctx: Context): Finding[] {\n if (ctx.input.sitemap === undefined) return []\n const out: Finding[] = []\n for (const loc of sitemapLocations(ctx.input.sitemap)) {\n const id = identity(loc, ctx.input.site)\n if (ctx.byIdentity.has(id)) continue\n out.push(finding({\n group: 'indexing',\n check: 'indexing.sitemap-stale-entry',\n severity: 'warning',\n url: loc,\n message: 'Sitemap lists a page the build does not serve.',\n }))\n }\n return out\n}\n\n// ─── status ───\n\nexport function statusChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'status' as const\n const url = doc.url\n const status = doc.status ?? 200\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const was = before.status ?? 200\n if (was !== status) {\n out.push(finding({\n group,\n check: 'status.mismatch',\n severity: 'error',\n url,\n message: `Status changed: ${was} → ${status}.`,\n }))\n }\n }\n\n // A \"not found\" page answering 200 is invisible to every monitor that looks\n // at status codes, and a crawler indexes it as content.\n if (status === 200) {\n const bodyText = text(doc.html)\n const title = titles(doc.html)[0] ?? ''\n const saysNotFound = NOT_FOUND_TEXT.test(title) || NOT_FOUND_TEXT.test(bodyText.slice(0, 400))\n if (saysNotFound && bodyText.length <= ctx.options.soft404MaxTextLength) {\n out.push(finding({\n group,\n check: 'status.soft-404',\n severity: 'error',\n url,\n message: 'Page says it was not found but answers 200.',\n detail: title || bodyText.slice(0, 120),\n }))\n }\n }\n\n return out\n}\n\n/** Redirect rules: chains, loops, and targets that do not exist. */\nexport function redirectChecks(ctx: Context): Finding[] {\n const rules = ctx.input.redirects\n if (!rules?.length) return []\n const out: Finding[] = []\n const group = 'status' as const\n const byFrom = new Map(rules.map(rule => [identity(rule.from, ctx.input.site), rule]))\n\n for (const rule of rules) {\n const seen = new Set<string>([identity(rule.from, ctx.input.site)])\n let hops = 1\n let target = identity(rule.to, ctx.input.site)\n\n while (byFrom.has(target)) {\n if (seen.has(target)) {\n out.push(finding({\n group,\n check: 'status.redirect-loop',\n severity: 'error',\n url: rule.from,\n message: 'Redirect chain loops.',\n detail: [...seen, target].join(' → '),\n }))\n break\n }\n seen.add(target)\n target = identity(byFrom.get(target)!.to, ctx.input.site)\n hops += 1\n }\n\n if (hops > ctx.options.maxRedirectHops) {\n out.push(finding({\n group,\n check: 'status.redirect-chain',\n severity: 'warning',\n url: rule.from,\n message: `Redirect chain is ${hops} hops.`,\n detail: [...seen, target].join(' → '),\n }))\n }\n\n const external = !isInternal(resolve(rule.to, rule.from, ctx.input.site) ?? '', ctx.input.site, ctx.options.internalHosts)\n if (!external && !ctx.served.has(target) && !byFrom.has(target)) {\n out.push(finding({\n group,\n check: 'status.redirect-target-missing',\n severity: 'error',\n url: rule.from,\n message: 'Redirect points at a page the build does not serve.',\n detail: rule.to,\n }))\n }\n }\n\n return out\n}\n\n// ─── international ───\n\n/**\n * hreflang has to be symmetric: if A offers B as its Turkish alternate, B must\n * offer A back. A one-sided declaration is ignored by search engines entirely,\n * so a half-migrated set of alternates is not a partial win — it is nothing.\n */\nexport function internationalChecks(ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'international' as const\n const declared = new Map<string, Map<string, string>>()\n\n for (const doc of ctx.input.documents) {\n const self = identity(doc.url, ctx.input.site)\n const alternates = new Map<string, string>()\n for (const link of linksRel(doc.html, 'alternate')) {\n if (!link.hreflang) continue\n const target = resolve(link.href, doc.url, ctx.input.site)\n if (!target) continue\n alternates.set(link.hreflang.toLowerCase(), identity(target, ctx.input.site))\n }\n if (alternates.size) declared.set(self, alternates)\n }\n\n for (const [self, alternates] of declared) {\n const listsSelf = [...alternates.values()].includes(self)\n if (!listsSelf) {\n out.push(finding({\n group,\n check: 'international.hreflang-no-self',\n severity: 'warning',\n url: self,\n message: 'Page declares hreflang alternates but does not list itself.',\n }))\n }\n for (const [lang, target] of alternates) {\n if (target === self) continue\n if (!ctx.byIdentity.has(target)) {\n out.push(finding({\n group,\n check: 'international.hreflang-unknown-target',\n severity: 'warning',\n url: self,\n message: `hreflang=\"${lang}\" points at a page the build does not serve.`,\n detail: target,\n }))\n continue\n }\n const back = declared.get(target)\n if (!back || ![...back.values()].includes(self)) {\n out.push(finding({\n group,\n check: 'international.hreflang-not-reciprocal',\n severity: 'error',\n url: self,\n message: `hreflang=\"${lang}\" names ${target}, which does not name it back — search engines ignore one-sided alternates.`,\n }))\n }\n }\n }\n\n return out\n}\n\n// ─── structured data ───\n\nexport function structuredChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'structured' as const\n const url = doc.url\n const blocks = jsonLd(doc.html)\n\n for (const block of blocks) {\n if (block.value === undefined) {\n out.push(finding({\n group,\n check: 'structured.jsonld-invalid',\n severity: 'error',\n url,\n message: 'A JSON-LD block does not parse — the whole block is discarded by consumers.',\n detail: block.raw.slice(0, 120),\n }))\n }\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (!before) return out\n\n const had = new Set(jsonLd(before.html).flatMap(block => jsonLdTypes(block.value)))\n const has = new Set(blocks.flatMap(block => jsonLdTypes(block.value)))\n const lost = [...had].filter(type => !has.has(type))\n if (lost.length) {\n out.push(finding({\n group,\n check: 'structured.type-lost',\n severity: 'warning',\n url,\n message: `Structured data types the old page had are gone: ${lost.join(', ')}.`,\n }))\n }\n\n return out\n}\n\n// ─── navigation ───\n\nexport function navigationChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'navigation' as const\n const url = doc.url\n const redirected = new Set((ctx.input.redirects ?? []).map(rule => identity(rule.from, ctx.input.site)))\n\n const broken = new Set<string>()\n for (const href of anchors(doc.html)) {\n if (/^(#|mailto:|tel:|javascript:|data:)/i.test(href)) continue\n const target = resolve(href, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n const id = identity(target, ctx.input.site)\n if (ctx.served.has(id) || redirected.has(id)) continue\n broken.add(href)\n }\n for (const href of broken) {\n out.push(finding({ group, check: 'navigation.broken-internal-link', severity: 'error', url, message: 'Internal link goes nowhere.', detail: href }))\n }\n\n for (const token of ['next', 'prev'] as const) {\n for (const link of linksRel(doc.html, token)) {\n const target = resolve(link.href, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n if (ctx.served.has(identity(target, ctx.input.site))) continue\n out.push(finding({\n group,\n check: 'navigation.pagination-broken',\n severity: 'warning',\n url,\n message: `rel=\"${token}\" points at a page the build does not serve.`,\n detail: link.href,\n }))\n }\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const feedRel = (html: string) => linksRel(html, 'alternate').some(link => /rss|atom|feed/i.test(link.type ?? ''))\n if (feedRel(before.html) && !feedRel(doc.html)) {\n out.push(finding({ group, check: 'navigation.feed-lost', severity: 'warning', url, message: 'The old page advertised a feed; this one does not.' }))\n }\n }\n\n return out\n}\n\n// ─── assets ───\n\nexport function assetChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'assets' as const\n const url = doc.url\n const refs = images(doc.html)\n\n const broken = new Set<string>()\n for (const image of refs) {\n if (/^data:/i.test(image.src)) continue\n const target = resolve(image.src, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n if (ctx.served.has(identity(target, ctx.input.site))) continue\n broken.add(image.src)\n }\n for (const src of broken) {\n out.push(finding({ group, check: 'assets.broken-image', severity: 'error', url, message: 'Image does not resolve to anything the build serves.', detail: src }))\n }\n\n // `alt=\"\"` is a decision (decorative); no `alt` attribute at all is an\n // omission. Only the second is reported.\n const noAlt = refs.filter(image => image.alt === undefined)\n if (noAlt.length) {\n out.push(finding({\n group,\n check: 'assets.missing-alt',\n severity: 'warning',\n url,\n message: `${noAlt.length} image${noAlt.length === 1 ? '' : 's'} without an alt attribute.`,\n detail: noAlt.slice(0, 5).map(image => image.src).join(', '),\n }))\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const had = new Map(images(before.html).filter(i => i.alt).map(i => [identity(resolve(i.src, before.url, ctx.input.site) ?? i.src, ctx.input.site), i.alt!]))\n const now = new Map(refs.map(i => [identity(resolve(i.src, url, ctx.input.site) ?? i.src, ctx.input.site), i.alt]))\n const lost = [...had.keys()].filter(src => now.has(src) && !now.get(src))\n if (lost.length) {\n out.push(finding({\n group,\n check: 'assets.alt-lost',\n severity: 'warning',\n url,\n message: `${lost.length} image${lost.length === 1 ? '' : 's'} lost alt text the old page had.`,\n detail: lost.slice(0, 5).join(', '),\n }))\n }\n }\n\n return out\n}\n","// ─── Running the gates ───\n\nimport type { CheckGroup, Finding, Severity, VerifyInput, VerifyReport } from './types.js'\nimport { CHECK_GROUPS } from './types.js'\nimport type { Context } from './checks.js'\nimport {\n assetChecks, identityChecks, indexingChecks, internationalChecks, navigationChecks,\n redirectChecks, sitemapStaleChecks, statusChecks, structuredChecks,\n} from './checks.js'\nimport { identity } from './url.js'\n\nconst DEFAULTS = { maxRedirectHops: 1, soft404MaxTextLength: 600 }\n\n/**\n * Checks that need an input the caller did not supply. Reported rather than\n * silently omitted: a green report over a run that never compared anything is\n * the most dangerous output this package could produce.\n */\nfunction absentInputs(input: VerifyInput, groups: readonly CheckGroup[]): { group: CheckGroup, reason: string }[] {\n const skipped: { group: CheckGroup, reason: string }[] = []\n if (!input.baseline) {\n for (const group of ['status', 'structured', 'navigation', 'assets'] as const) {\n if (groups.includes(group)) skipped.push({ group, reason: 'no baseline — parity checks did not run' })\n }\n }\n if (input.sitemap === undefined && groups.includes('indexing')) {\n skipped.push({ group: 'indexing', reason: 'no sitemap — membership checks did not run' })\n }\n if (!input.redirects?.length && groups.includes('status')) {\n skipped.push({ group: 'status', reason: 'no redirect rules — chain and target checks did not run' })\n }\n return skipped\n}\n\nfunction buildContext(input: VerifyInput): Context {\n const options = { ...DEFAULTS, ...input.options }\n const byIdentity = new Map(input.documents.map(doc => [identity(doc.url, input.site), doc]))\n const baselineByIdentity = new Map((input.baseline?.documents ?? []).map(doc => [identity(doc.url, input.site), doc]))\n const served = new Set<string>([\n ...byIdentity.keys(),\n ...(input.assets ?? []).map(asset => identity(asset, input.site)),\n ])\n return { input, options, byIdentity, baselineByIdentity, served }\n}\n\n/**\n * Run the gates over a built site.\n *\n * Nothing is fetched. `input.documents` is the built site as served, and\n * `input.baseline` — when the caller has one — is the site it replaced. Without\n * a baseline every self-check still runs; only the parity checks (status\n * change, lost structured data, lost alt text, lost feed) go quiet, because\n * there is nothing to compare against.\n */\nexport function verify(input: VerifyInput): VerifyReport {\n const groups = [...(input.groups ?? CHECK_GROUPS)]\n const ctx = buildContext(input)\n const findings: Finding[] = []\n const skipped = absentInputs(input, groups)\n\n const run = (group: CheckGroup, fn: () => Finding[]) => {\n if (!groups.includes(group)) return\n findings.push(...fn())\n }\n\n for (const doc of input.documents) {\n run('identity', () => identityChecks(doc, ctx))\n run('indexing', () => indexingChecks(doc, ctx))\n run('status', () => statusChecks(doc, ctx))\n run('structured', () => structuredChecks(doc, ctx))\n run('navigation', () => navigationChecks(doc, ctx))\n run('assets', () => assetChecks(doc, ctx))\n }\n\n // Site-wide checks: these read the whole set, not one document.\n run('indexing', () => sitemapStaleChecks(ctx))\n run('status', () => redirectChecks(ctx))\n run('international', () => internationalChecks(ctx))\n\n const counts: Record<Severity, number> = { error: 0, warning: 0, info: 0 }\n for (const item of findings) counts[item.severity] += 1\n\n return {\n passed: counts.error === 0,\n findings,\n counts,\n documents: input.documents.length,\n groups,\n skipped,\n }\n}\n\n/** A report as lines, most severe first. Used by the CLI and by consumers. */\nexport function formatReport(report: VerifyReport): string {\n const order: Severity[] = ['error', 'warning', 'info']\n const lines: string[] = []\n for (const severity of order) {\n for (const item of report.findings.filter(f => f.severity === severity)) {\n const where = item.url ? ` ${item.url}` : ''\n const detail = item.detail ? `\\n ${item.detail}` : ''\n lines.push(`${severity.toUpperCase().padEnd(7)} ${item.check}${where}\\n ${item.message}${detail}`)\n }\n }\n for (const { group, reason } of report.skipped) {\n lines.push(`SKIPPED ${group} — ${reason}`)\n }\n lines.push(\n `\\n${report.documents} document${report.documents === 1 ? '' : 's'} · `\n + `${report.counts.error} error · ${report.counts.warning} warning · ${report.counts.info} info · `\n + (report.passed ? 'PASS' : 'FAIL'),\n )\n return lines.join('\\n')\n}\n","// ─── Reading a built directory ───\n//\n// The one place this package touches a filesystem. Kept apart from the checks\n// so the engine stays pure: a consumer that already holds the documents — a\n// migration runner, a CI step with an artefact — imports `verify` and never\n// loads this module.\n\nimport { readFile, readdir } from 'node:fs/promises'\nimport { join, relative, sep } from 'node:path'\nimport type { VerifyDocument, VerifyInput } from './types.js'\n\nconst DOCUMENT_EXT = /\\.html?$/i\n\nasync function walk(dir: string, root: string, files: string[] = []): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) {\n await walk(full, root, files)\n continue\n }\n files.push(relative(root, full))\n }\n return files\n}\n\n/**\n * Load a static build as a verification input: every `.html` becomes a\n * document served at its path, everything else becomes an asset.\n *\n * `dist/about/index.html` is served at `/about/`, which `identity()` then\n * reduces to `/about` — the same address a link to `/about/` or\n * `/about/index.html` resolves to.\n */\nexport async function loadSiteDirectory(dir: string, site?: string): Promise<Pick<VerifyInput, 'documents' | 'assets' | 'sitemap' | 'site'>> {\n const files = await walk(dir, dir)\n const documents: VerifyDocument[] = []\n const assets: string[] = []\n let sitemap: string | undefined\n\n for (const file of files.toSorted()) {\n const served = `/${file.split(sep).join('/')}`\n if (DOCUMENT_EXT.test(file)) {\n documents.push({ url: served, html: await readFile(join(dir, file), 'utf8'), status: 200 })\n continue\n }\n if (/^\\/sitemap[^/]*\\.xml$/i.test(served)) sitemap = await readFile(join(dir, file), 'utf8')\n assets.push(served)\n }\n\n return { documents, assets, sitemap, site }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-C_hmH1n3.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;UAaiB,cAAA;EAAc;EAE7B,GAAA;EACA,IAAA;EADA;EAGA,MAAA;EAAA;EAEA,OAAA,GAAU,MAAA;AAAA;AAAA,UAGK,cAAA;EACf,IAAA;EACA,EAAA;EACA,MAAA;AAAA;AAAA,cAGW,YAAA;AAAA,KAiBD,UAAA,WAAqB,YAAA;AAAA,KAErB,QAAA;;;;AAnBZ;;;UA2BiB,OAAA;EACf,KAAA,EAAO,UAAA;EACP,KAAA;EACA,QAAA,EAAU,QAAA;;EAEV,GAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,UAGe,YAAA;EAlBG;EAoBlB,MAAA;EACA,QAAA,EAAU,OAAA;EACV,MAAA,EAAQ,MAAA,CAAO,QAAA;;EAEf,SAAA;EAfA;EAiBA,MAAA,EAAQ,UAAA;EAhBR;EAkBA,OAAA;IAAW,KAAA,EAAO,UAAA;IAAY,MAAA;EAAA;AAAA;AAAA,UAGf,aAAA;EAhBT;AAGR;;;;EAmBE,eAAA;EAfQ;;;;;EAqBR,oBAAA;EAtBA;EAwBA,aAAA;AAAA;AAAA,UAGe,WAAA;EA1BA;EA4Bf,SAAA,EAAW,cAAA;EAxBX;EA0BA,IAAA;EAxBA;EA0BA,OAAA;EA1BkB;EA4BlB,SAAA,GAAY,cAAA;EA5BwB;EA8BpC,MAAA;EA3Be;EA6Bf,QAAA;IAAa,SAAA,EAAW,cAAA;EAAA;EAvBxB;EAyBA,MAAA,YAAkB,UAAA;EAClB,OAAA,GAAU,aAAA;AAAA"}