@duffcloudservices/cms 0.13.3 → 0.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-OGAJX4KM.js → chunk-GBHJVHXR.js} +116 -3
- package/dist/chunk-GBHJVHXR.js.map +1 -0
- package/dist/{chunk-64BUBTW7.js → chunk-W5Q6SOAP.js} +35 -3
- package/dist/chunk-W5Q6SOAP.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/{installSeoHead-EE0Z7UmK.d.ts → installSeoHead-CuVdVCis.d.ts} +1 -1
- package/dist/plugins/index.d.ts +2 -2
- package/dist/plugins/index.js +12 -3
- package/dist/plugins/index.js.map +1 -1
- package/dist/seo/index.d.ts +132 -7
- package/dist/seo/index.js +2 -2
- package/dist/{vitepressTransform-Ds5Hff8t.d.ts → vitepressTransform-CZ_IB3dq.d.ts} +47 -1
- package/package.json +2 -2
- package/dist/chunk-64BUBTW7.js.map +0 -1
- package/dist/chunk-OGAJX4KM.js.map +0 -1
|
@@ -1,3 +1,116 @@
|
|
|
1
|
+
import { decodeHtmlEntities } from './chunk-W5Q6SOAP.js';
|
|
2
|
+
|
|
3
|
+
// src/seo/faqBodyHonesty.ts
|
|
4
|
+
function normalizeForPresence(value) {
|
|
5
|
+
return decodeHtmlEntities(value).replace(/[‘’‛]/g, "'").replace(/[“”]/g, '"').replace(/[–—]/g, "-").replace(/ /g, " ").replace(/\s+/g, " ").trim().normalize("NFC").toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
function stripComments(html) {
|
|
8
|
+
return html.replace(/<!--[\s\S]*?-->/g, " ");
|
|
9
|
+
}
|
|
10
|
+
function jsonLdBlocks(html) {
|
|
11
|
+
const out = [];
|
|
12
|
+
const re = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
13
|
+
let m;
|
|
14
|
+
const source = stripComments(html);
|
|
15
|
+
while (m = re.exec(source)) {
|
|
16
|
+
if (!/type\s*=\s*["']?application\/ld\+json/i.test(m[1])) continue;
|
|
17
|
+
out.push(m[2]);
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function walkNodes(value, visit) {
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
for (const item of value) walkNodes(item, visit);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!value || typeof value !== "object") return;
|
|
27
|
+
const node = value;
|
|
28
|
+
visit(node);
|
|
29
|
+
if ("@graph" in node) walkNodes(node["@graph"], visit);
|
|
30
|
+
}
|
|
31
|
+
function typesOf(node) {
|
|
32
|
+
const t = node["@type"];
|
|
33
|
+
if (typeof t === "string") return [t];
|
|
34
|
+
if (Array.isArray(t)) return t.filter((x) => typeof x === "string");
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
function extractFaqQuestions(html) {
|
|
38
|
+
const questions = [];
|
|
39
|
+
for (const raw of jsonLdBlocks(html)) {
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(decodeHtmlEntities(raw.trim()));
|
|
43
|
+
} catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
walkNodes(parsed, (node) => {
|
|
47
|
+
if (!typesOf(node).includes("FAQPage")) return;
|
|
48
|
+
const main = node.mainEntity;
|
|
49
|
+
const entries = Array.isArray(main) ? main : main ? [main] : [];
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry || typeof entry !== "object") continue;
|
|
52
|
+
const name = entry.name;
|
|
53
|
+
if (typeof name === "string" && name.trim()) questions.push(name.trim());
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return questions;
|
|
58
|
+
}
|
|
59
|
+
function extractPrerenderedBodyText(html) {
|
|
60
|
+
const bodyMatch = /<body\b[^>]*>([\s\S]*?)<\/body>/i.exec(html);
|
|
61
|
+
if (!bodyMatch) return "";
|
|
62
|
+
const stripped = stripComments(bodyMatch[1]).replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, " ").replace(/<[^>]+>/g, " ");
|
|
63
|
+
return normalizeForPresence(stripped);
|
|
64
|
+
}
|
|
65
|
+
function findFaqBodyHonestyViolations(observations, options = {}) {
|
|
66
|
+
const allowed = new Set(options.allow ?? []);
|
|
67
|
+
const violations = [];
|
|
68
|
+
for (const obs of observations) {
|
|
69
|
+
if (allowed.has(obs.route)) continue;
|
|
70
|
+
const questions = extractFaqQuestions(obs.html);
|
|
71
|
+
if (questions.length === 0) continue;
|
|
72
|
+
const body = extractPrerenderedBodyText(obs.html);
|
|
73
|
+
for (const question of questions) {
|
|
74
|
+
if (body.includes(normalizeForPresence(question))) continue;
|
|
75
|
+
violations.push({ route: obs.route, question, questionCount: questions.length });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return violations;
|
|
79
|
+
}
|
|
80
|
+
function formatFaqBodyHonestyReport(violations) {
|
|
81
|
+
const routes = new Set(violations.map((v) => v.route));
|
|
82
|
+
const lines = [
|
|
83
|
+
`[dcs-seo] FAQ body honesty: ${violations.length} FAQPage question(s) across ${routes.size} route(s) are declared in JSON-LD but appear NOWHERE in the prerendered body \u2014 structured data for content no visitor and no crawler sees.`
|
|
84
|
+
];
|
|
85
|
+
for (const v of violations) {
|
|
86
|
+
lines.push(` ${v.route} (${v.questionCount} question(s) declared)`);
|
|
87
|
+
lines.push(` not rendered: ${JSON.stringify(v.question)}`);
|
|
88
|
+
}
|
|
89
|
+
lines.push(
|
|
90
|
+
` Fix (pick one): render the same Q&A pairs as a visible FAQ section on the route, reading the SAME .dcs/content.yaml keys the schema is built from; or delete the faq entries so no FAQPage is emitted. Do NOT exempt the route \u2014 an exemption here asserts "this markup describes invisible copy on purpose", which is the spam signal itself.`
|
|
91
|
+
);
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
var FaqBodyHonestyError = class extends Error {
|
|
95
|
+
violations;
|
|
96
|
+
constructor(violations) {
|
|
97
|
+
super(formatFaqBodyHonestyReport(violations));
|
|
98
|
+
this.name = "FaqBodyHonestyError";
|
|
99
|
+
this.violations = violations;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
function assertFaqBodyHonesty(observations, options = {}) {
|
|
103
|
+
const { mode = "error", ...rest } = options;
|
|
104
|
+
if (mode === "off") return [];
|
|
105
|
+
const violations = findFaqBodyHonestyViolations(observations, rest);
|
|
106
|
+
if (violations.length === 0) return [];
|
|
107
|
+
if (mode === "warn") {
|
|
108
|
+
console.warn(formatFaqBodyHonestyReport(violations));
|
|
109
|
+
return violations;
|
|
110
|
+
}
|
|
111
|
+
throw new FaqBodyHonestyError(violations);
|
|
112
|
+
}
|
|
113
|
+
|
|
1
114
|
// src/seo/headContract.ts
|
|
2
115
|
var HeadContractError = class extends Error {
|
|
3
116
|
report;
|
|
@@ -495,6 +608,6 @@ The head-contract audit did not run against anything it could judge. Fix the inp
|
|
|
495
608
|
throw new HeadContractError(formatHeadContractReport(report), report);
|
|
496
609
|
}
|
|
497
610
|
|
|
498
|
-
export { HeadContractError, MIN_EXEMPTION_REASON_LENGTH, assertHeadContract, auditExemptionHosts, auditHeadContract, blankComments, formatExemptionAudit, formatHeadContractReport, hostsNamedIn, normalizeExemption, parseHostProbeHtml };
|
|
499
|
-
//# sourceMappingURL=chunk-
|
|
500
|
-
//# sourceMappingURL=chunk-
|
|
611
|
+
export { FaqBodyHonestyError, HeadContractError, MIN_EXEMPTION_REASON_LENGTH, assertFaqBodyHonesty, assertHeadContract, auditExemptionHosts, auditHeadContract, blankComments, extractFaqQuestions, extractPrerenderedBodyText, findFaqBodyHonestyViolations, formatExemptionAudit, formatFaqBodyHonestyReport, formatHeadContractReport, hostsNamedIn, normalizeExemption, normalizeForPresence, parseHostProbeHtml };
|
|
612
|
+
//# sourceMappingURL=chunk-GBHJVHXR.js.map
|
|
613
|
+
//# sourceMappingURL=chunk-GBHJVHXR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/seo/faqBodyHonesty.ts","../src/seo/headContract.ts"],"names":["end"],"mappings":"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","file":"chunk-GBHJVHXR.js","sourcesContent":["/**\n * FAQ body-honesty rail — a `FAQPage` node must describe copy the page RENDERS.\n *\n * ## The defect class\n *\n * `buildFaqPage` (`./schemaGraph.ts`) emits a `FAQPage` from any well-formed\n * `{q,a}` pair it finds in `.dcs/content.yaml`. That gate enforces the SHAPE of\n * the honesty rule (no invented Q&A) but it cannot enforce its SUBSTANCE: the\n * builder is pure and has no idea whether the page it decorates renders those\n * questions anywhere a human — or a crawler — can see them. Seeding\n * `pages.<slug>.faq` without also rendering a visible FAQ section therefore\n * ships structured data for content that does not exist on the page. Google's\n * FAQPage guidance requires the Q&A be visible to the user; markup for invisible\n * content is a spam signal, and it is the exact fabrication class the rest of\n * this module tree exists to prevent.\n *\n * Measured origin: the 2026-08-30 Iron Oak drafting pass\n * (`.docs/analysis/iron-oak-seo-drafts-2026-08-30.md` §3, \"The honesty condition\n * on all of the above\") — Iron Oak has Q&A-shaped copy and no FAQ section, so\n * applying the FAQPage finding as a one-step content seed would have produced\n * exactly this defect on a live customer site.\n *\n * ## The rail\n *\n * Given an emitted document, take every `Question.name` out of every `FAQPage`\n * JSON-LD node and require each one to appear in the PRERENDERED BODY TEXT.\n *\n * ## The vacuity trap this rail is built around\n *\n * The question text is, by construction, already present in the document — it is\n * sitting in the `<script type=\"application/ld+json\">` block being audited.\n * `dcsSeoPlugin` bakes that block into `<head>`, which `extractPrerenderedBodyText`\n * never reads, but not every emitter does (vite-ssg and hand-rolled prerenders\n * put JSON-LD in the body), and a body extractor that keeps `<script>` content\n * would then find every question in every document — green against a site with\n * no FAQ section at all. `extractPrerenderedBodyText` therefore strips\n * `<script>`, `<style>`, `<noscript>` and comments CONTENT-AND-ALL, and\n * `faqBodyHonesty.test.ts` exercises the body-placed case explicitly.\n */\n\nimport { decodeHtmlEntities } from './headHonesty'\nimport type { HonestyMode } from './headHonesty'\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/** One emitted document to audit. */\nexport interface FaqBodyObservation {\n /** Route path as it appears in `pages.yaml` (e.g. `/` or `/services`). */\n route: string\n /** The emitted HTML for that route, exactly as written to `dist/`. */\n html: string\n}\n\n/** A `FAQPage` question with no counterpart in the rendered body. */\nexport interface FaqBodyViolation {\n route: string\n /** The `Question.name` verbatim, as authored in the JSON-LD. */\n question: string\n /** How many questions that document's FAQPage node(s) declared in total. */\n questionCount: number\n}\n\n/** Configuration for the rail. */\nexport interface FaqBodyHonestyOptions {\n /**\n * Route paths exempted from the comparison. An exemption is still the caller's\n * to LOG — same contract as `HeadHonestyOptions.allow`.\n */\n allow?: string[]\n}\n\n// -----------------------------------------------------------------------------\n// Extraction\n// -----------------------------------------------------------------------------\n\n/**\n * Normalise a string for PRESENCE comparison (is this text on the page?), which\n * is a weaker question than head honesty's IDENTITY comparison and so normalises\n * more:\n * - HTML entities decoded (`&` ⇒ `&`) — the two sides are escaped\n * differently: JSON-LD escapes for JSON, body copy escapes for HTML.\n * - typographic look-alikes folded to ASCII (`’ ‘ “ ” — –` ⇒ `' ' \" \" - -`) —\n * the schema string and the rendered string come from the SAME content key\n * but the rendered one may pass through a typography transform. A rail that\n * reds because a renderer curled an apostrophe is an accusation, not a find.\n * - whitespace collapsed — rendered copy is wrapped and indented.\n * - lowercased — a question rendered as a title-cased heading is still visible.\n *\n * Deliberately NOT normalised: words. Nothing is stemmed, dropped or fuzzy-\n * matched, so a \"close enough\" body text does not satisfy the rail.\n */\nexport function normalizeForPresence(value: string): string {\n return decodeHtmlEntities(value)\n .replace(/[‘’‛]/g, \"'\")\n .replace(/[“”]/g, '\"')\n .replace(/[–—]/g, '-')\n .replace(/ /g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .normalize('NFC')\n .toLowerCase()\n}\n\n/** Strip HTML comments content-and-all. */\nfunction stripComments(html: string): string {\n return html.replace(/<!--[\\s\\S]*?-->/g, ' ')\n}\n\n/**\n * Every `<script type=\"application/ld+json\">` payload in the document.\n *\n * Comments are stripped FIRST, and that is load-bearing twice over. A\n * commented-out JSON-LD block is not emitted schema and must not be audited as\n * if it were. More sharply: a comment that merely MENTIONS the string\n * `<script type=\"application/ld+json\">` (documentation about the markup, which\n * both fixtures carry) gives the scanner a bogus opening tag, and the scan then\n * runs to the first REAL `</script>` — swallowing the genuine block into an\n * unparseable payload and silently reporting a page as having no FAQPage at\n * all. Measured while building this rail: without this line the planted-defect\n * fixture went GREEN.\n */\nfunction jsonLdBlocks(html: string): string[] {\n const out: string[] = []\n const re = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/gi\n let m: RegExpExecArray | null\n const source = stripComments(html)\n while ((m = re.exec(source))) {\n if (!/type\\s*=\\s*[\"']?application\\/ld\\+json/i.test(m[1])) continue\n out.push(m[2])\n }\n return out\n}\n\n/** Walk a parsed JSON-LD payload (object, array, or `@graph`) node by node. */\nfunction walkNodes(value: unknown, visit: (node: Record<string, unknown>) => void): void {\n if (Array.isArray(value)) {\n for (const item of value) walkNodes(item, visit)\n return\n }\n if (!value || typeof value !== 'object') return\n const node = value as Record<string, unknown>\n visit(node)\n if ('@graph' in node) walkNodes(node['@graph'], visit)\n}\n\nfunction typesOf(node: Record<string, unknown>): string[] {\n const t = node['@type']\n if (typeof t === 'string') return [t]\n if (Array.isArray(t)) return t.filter((x): x is string => typeof x === 'string')\n return []\n}\n\n/**\n * Collect every `Question.name` declared by every `FAQPage` node in a document.\n *\n * Unparseable JSON-LD is SKIPPED, not reported — `cli/seo-coverage-audit.mjs`\n * already fails a route for unparseable structured data, and this rail must not\n * double-report someone else's finding as an FAQ defect.\n */\nexport function extractFaqQuestions(html: string): string[] {\n const questions: string[] = []\n for (const raw of jsonLdBlocks(html)) {\n let parsed: unknown\n try {\n parsed = JSON.parse(decodeHtmlEntities(raw.trim()))\n } catch {\n continue\n }\n walkNodes(parsed, (node) => {\n if (!typesOf(node).includes('FAQPage')) return\n const main = node.mainEntity\n const entries = Array.isArray(main) ? main : main ? [main] : []\n for (const entry of entries) {\n if (!entry || typeof entry !== 'object') continue\n const name = (entry as Record<string, unknown>).name\n if (typeof name === 'string' && name.trim()) questions.push(name.trim())\n }\n })\n }\n return questions\n}\n\n/**\n * The visible text of the PRERENDERED BODY — what a non-JS crawler receives.\n *\n * `<script>`, `<style>`, `<noscript>` and HTML comments are removed CONTENT AND\n * ALL before tags are stripped. That is not tidiness: the JSON-LD under audit\n * lives in a `<script>`, so leaving script content in would make every question\n * trivially \"present\" and the rail vacuous (see the module header).\n *\n * Returns `''` when there is no `<body>` — an SPA shell with no prerendered body\n * has no visible copy, which is the honest answer, not a parse failure.\n */\nexport function extractPrerenderedBodyText(html: string): string {\n const bodyMatch = /<body\\b[^>]*>([\\s\\S]*?)<\\/body>/i.exec(html)\n if (!bodyMatch) return ''\n const stripped = stripComments(bodyMatch[1])\n .replace(/<script\\b[^>]*>[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style\\b[^>]*>[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<noscript\\b[^>]*>[\\s\\S]*?<\\/noscript>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n return normalizeForPresence(stripped)\n}\n\n// -----------------------------------------------------------------------------\n// The rail\n// -----------------------------------------------------------------------------\n\n/**\n * Report every `FAQPage` question that the route's prerendered body does not\n * render. A document with no `FAQPage` node yields nothing — the rail is about\n * schema that over-claims, never about pages that decline to have an FAQ.\n */\nexport function findFaqBodyHonestyViolations(\n observations: FaqBodyObservation[],\n options: FaqBodyHonestyOptions = {}\n): FaqBodyViolation[] {\n const allowed = new Set(options.allow ?? [])\n const violations: FaqBodyViolation[] = []\n\n for (const obs of observations) {\n if (allowed.has(obs.route)) continue\n const questions = extractFaqQuestions(obs.html)\n if (questions.length === 0) continue\n\n const body = extractPrerenderedBodyText(obs.html)\n for (const question of questions) {\n if (body.includes(normalizeForPresence(question))) continue\n violations.push({ route: obs.route, question, questionCount: questions.length })\n }\n }\n\n return violations\n}\n\n/**\n * Render the full report. Names the route, the question, and the fix — both\n * halves of the fix, because deleting the schema and rendering the section are\n * both valid resolutions and the operator picks.\n */\nexport function formatFaqBodyHonestyReport(violations: FaqBodyViolation[]): string {\n const routes = new Set(violations.map((v) => v.route))\n const lines: string[] = [\n `[dcs-seo] FAQ body honesty: ${violations.length} FAQPage question(s) across ` +\n `${routes.size} route(s) are declared in JSON-LD but appear NOWHERE in the ` +\n `prerendered body — structured data for content no visitor and no crawler sees.`,\n ]\n for (const v of violations) {\n lines.push(` ${v.route} (${v.questionCount} question(s) declared)`)\n lines.push(` not rendered: ${JSON.stringify(v.question)}`)\n }\n lines.push(\n ` Fix (pick one): render the same Q&A pairs as a visible FAQ section on the ` +\n `route, reading the SAME .dcs/content.yaml keys the schema is built from; ` +\n `or delete the faq entries so no FAQPage is emitted. Do NOT exempt the ` +\n `route — an exemption here asserts \"this markup describes invisible copy ` +\n `on purpose\", which is the spam signal itself.`\n )\n return lines.join('\\n')\n}\n\n/** Thrown in `error` mode so the build goes red on invisible FAQ markup. */\nexport class FaqBodyHonestyError extends Error {\n readonly violations: FaqBodyViolation[]\n constructor(violations: FaqBodyViolation[]) {\n super(formatFaqBodyHonestyReport(violations))\n this.name = 'FaqBodyHonestyError'\n this.violations = violations\n }\n}\n\n/**\n * Enforce the rail. `error` throws, `warn` logs, `off` does nothing.\n * Mirrors `assertHeadHonesty` so the three-mode contract is uniform across rails.\n */\nexport function assertFaqBodyHonesty(\n observations: FaqBodyObservation[],\n options: FaqBodyHonestyOptions & { mode?: HonestyMode } = {}\n): FaqBodyViolation[] {\n const { mode = 'error', ...rest } = options\n if (mode === 'off') return []\n const violations = findFaqBodyHonestyViolations(observations, rest)\n if (violations.length === 0) return []\n if (mode === 'warn') {\n console.warn(formatFaqBodyHonestyReport(violations))\n return violations\n }\n throw new FaqBodyHonestyError(violations)\n}\n","/**\n * THE HEAD-AUTHORITY CONTRACT (C-356) — a source-level audit.\n *\n * WHY THIS EXISTS. One undefined contract produced two OPPOSITE production\n * failures on the same shared composable, measured a day apart:\n *\n * KEPT 28 `applyHead({…})` callers OVERWROTE the baked head.\n * 93 title/description divergences across 49 routes — 100% of\n * routes served one <title> to a non-JS AI crawler and a\n * different one to Google and every human (C-338 matrix,\n * fixed C-341: 51/51, zero divergent).\n * boogie-babies 2 callers, 9 of 11 views that write NOTHING. Hard navigation\n * is correct; the first in-app click leaves the previous\n * route's title/description/canonical in the DOM (measured:\n * /pricing -> /parties still claims canonical /pricing).\n * just-posh 17 raw `useHead({title, meta, link})` blocks that never touch\n * the composable at all — 27 divergences, the fleet's second\n * largest. A contract about `applyHead` alone would not see it.\n *\n * THE CONTRACT, in one line: `.dcs/seo.yaml` is the ONLY writer of the managed\n * head fields, and it must be re-asserted on EVERY route, on EVERY navigation.\n * Full text + reasoning: `.docs/plans/dynamic-site-resolution/README.md`\n * § \"The head-authority contract (C-356)\".\n *\n * WHAT THIS MODULE PROVES AND WHAT IT DOES NOT.\n * - It proves there is exactly ONE WRITER in the source. That is authority.\n * - It does NOT prove the writer told the truth. That is P1\n * (`headHonesty.ts`), which compares the baked head to the rendered head on\n * a real build. Neither replaces the other, and an exemption here buys no\n * amnesty there.\n *\n * Deliberately framework-free and fs-free: it takes source strings, so the same\n * function runs in the package's own vitest fixtures, in a site's test script,\n * and in `cli/head-contract-audit.mjs` against a real repo. There is no second\n * implementation to drift.\n */\n\nimport type { UseSeoReturn } from '../types/seo'\n\n// =============================================================================\n// The compile-time half of the enforcement\n// =============================================================================\n\n/**\n * COMPILE-TIME PIN: `applyHead` must take ZERO parameters.\n *\n * `packages/cms/tsconfig.json` excludes `**\\/*.test.ts`, so a `@ts-expect-error`\n * written in a test file is never evaluated by `pnpm type-check` — it would be a\n * comment that looks like a gate. This pin lives in a checked source file\n * instead: re-add a parameter to `UseSeoReturn['applyHead']` and the assignment\n * below stops compiling, which reds `type-check` in this package and, through\n * the published `.d.ts`, in every site that runs one.\n */\nexport type ApplyHeadTakesNoArguments = Parameters<UseSeoReturn['applyHead']> extends []\n ? true\n : false\n\nexport const APPLY_HEAD_TAKES_NO_ARGUMENTS: ApplyHeadTakesNoArguments = true\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** Codes that FAIL the contract. */\nexport type HeadContractViolationCode =\n /** `applyHead({…})` — a second writer for a managed field. Never exemptible. */\n | 'override-argument'\n /** `useSEO('x')` where `.dcs/seo.yaml` has no `pages.x.title`. Never exemptible. */\n | 'undeclared-slug'\n /** `useHead` / `useSeoMeta` / `document.title =` — a writer outside the factory. */\n | 'foreign-head-writer'\n /** An exemption whose file no longer violates anything (anti-rot). */\n | 'stale-exemption'\n /** An exemption with no real reason recorded. */\n | 'empty-exemption-reason'\n /** An exemption that names a host but cannot be checked against it (C-420). */\n | 'unprobed-exemption'\n /** A live probe contradicts what the exemption asserts about its host (C-420). */\n | 'exemption-claim-refuted'\n /** The probe of an exemption's host did not complete — never a pass (C-420). */\n | 'exemption-probe-failed'\n /** The audit scanned no files — a pass that checked nothing is not a pass. */\n | 'scan-empty'\n /** No `.dcs/seo.yaml` was supplied, so authority cannot be established. */\n | 'no-seo-config'\n\n/** Codes that are REPORTED but do not fail. */\nexport type HeadContractWarningCode =\n /** A view that never re-asserts the head; SPA navigation to it is stale. */\n | 'uncovered-route'\n /** `useSEO(`blog-${slug}`)` — legal, but only P1 can judge it. */\n | 'dynamic-slug'\n\nexport interface HeadContractSourceFile {\n /** Path as the caller wants it reported (and as exemptions key on). */\n path: string\n source: string\n}\n\n/** The slice of `.dcs/seo.yaml` this audit needs. */\nexport interface HeadContractSeoConfig {\n global?: { titleTemplate?: string; defaultTitle?: string } & Record<string, unknown>\n pages?: Record<string, { title?: string } & Record<string, unknown>>\n}\n\n// =============================================================================\n// Exemptions and their LIVE claims (C-420)\n// =============================================================================\n\n/**\n * WHY EXEMPTIONS CARRY LIVE CLAIMS.\n *\n * An exemption used to be a sentence. A sentence is an assertion, and an\n * assertion nobody re-measures rots into a false premise that the audit then\n * defends: the measured case declared a host as a \"noindex cross-domain\n * handoff\", and every part of that was false in production — the host served\n * `index, follow`, a SELF-referential canonical, a 200 `robots.txt` that allows\n * every crawler, a sitemap and a full independent site. The audit was green\n * BECAUSE of the exemption, so the green was the bug.\n *\n * The discriminator is the exempted host's real response, not the bundle hash.\n * So an exemption that NAMES a host must now say what it expects that host to\n * do, and the caller must supply what the host actually did. A named host with\n * nothing checkable, or with nothing measured, is a VIOLATION — not a skip.\n *\n * This module still never touches the network: it takes observations. The\n * fetching lives in the runner (`cli/head-contract-audit.mjs`), the judging\n * lives here, and `parseHostProbeHtml` below is the shared, testable parser\n * between them so there is no second implementation to drift.\n */\nexport interface HeadContractExemptionExpectation {\n /** HTTP status, measured WITHOUT following redirects (a 301 is not a 200). */\n status?: number\n /** Substring that must appear in `<meta name=\"robots\">` (case-insensitive). */\n robots?: string\n /** Hostname of `<link rel=\"canonical\">` — the self-vs-cross-domain question. */\n canonicalHost?: string\n /** Substring that must appear in `<title>` (case-insensitive). */\n titleContains?: string\n}\n\nexport interface HeadContractExemptionRecord {\n /** Why this file cannot route its head through the factory. */\n reason: string\n /**\n * `host -> claims`. EVERY host the reason names must appear here, and every\n * entry must carry at least one claim, or the exemption is unprobed.\n */\n hosts?: Record<string, HeadContractExemptionExpectation>\n}\n\n/** A bare string is still a legal exemption — it just may not name a host. */\nexport type HeadContractExemptionValue = string | HeadContractExemptionRecord\n\n/** What a caller actually observed at a host. Never produced by this module. */\nexport interface HeadContractHostProbe {\n host: string\n /** `false` = the probe did not complete. A failed probe is never a pass. */\n ok: boolean\n error?: string\n status?: number\n robots?: string | null\n canonical?: string | null\n title?: string | null\n observedAt?: string\n}\n\nexport type HeadContractExemptionVerdict =\n /** Every claim agreed with the live response. */\n | 'live-true'\n /** The live response contradicts at least one claim. */\n | 'refuted'\n /** Names a host but nothing checkable reached the judge. */\n | 'unprobed'\n /** A probe was attempted and did not complete. */\n | 'probe-failed'\n /** Asserts nothing about any host — nothing to probe. */\n | 'no-host-claim'\n\nexport interface HeadContractExemptionCheck {\n host: string\n claim: keyof HeadContractExemptionExpectation\n expected: string\n actual: string\n pass: boolean\n}\n\nexport interface HeadContractExemptionAudit {\n file: string\n reason: string\n /** Hosts the PROSE names — the claim the author made whether or not they meant to. */\n hostsNamedInReason: string[]\n /** Hosts the author declared machine-checkable claims for. */\n hostsDeclared: string[]\n checks: HeadContractExemptionCheck[]\n verdict: HeadContractExemptionVerdict\n}\n\nexport interface HeadContractInput {\n files: HeadContractSourceFile[]\n seo: HeadContractSeoConfig | undefined\n /**\n * `path -> reason | record`. Declares a KNOWN foreign head writer so it stops\n * being a surprise. It is a declaration of debt, not a licence: P1 still\n * measures whether that writer agrees with the baked head, a stale entry is\n * itself a violation, and (C-420) any host the declaration names is measured\n * against its live response.\n */\n exemptions?: Record<string, HeadContractExemptionValue>\n /**\n * `host -> what that host actually did`, supplied by the caller. An exemption\n * that names a host and finds no observation here FAILS: \"not measured\" and\n * \"measured fine\" must never render the same.\n */\n hostProbes?: Record<string, HeadContractHostProbe>\n}\n\nexport interface HeadContractViolation {\n code: HeadContractViolationCode\n file: string\n line: number\n detail: string\n}\n\nexport interface HeadContractWarning {\n code: HeadContractWarningCode\n file: string\n detail: string\n}\n\nexport interface ForeignWriterSite {\n file: string\n line: number\n mechanism: 'useHead' | 'useSeoMeta' | 'document.title'\n exempted: boolean\n reason: string | null\n}\n\nexport interface HeadContractReport {\n /** Execution counts FIRST — a rail that did not run must not read as green. */\n scanned: number\n applyHeadCalls: number\n argumentFreeCalls: number\n /** Slugs written as string literals — checked against `.dcs/seo.yaml`. */\n useSeoSlugs: string[]\n /** Slugs composed at runtime — legal, UNCHECKED here, judged by P1. */\n dynamicSlugs: string[]\n foreignWriters: ForeignWriterSite[]\n /** `useHead({script})` blocks that ADD JSON-LD without restating a managed field. */\n additiveHeadCalls: number\n /**\n * EVERY declared exemption with its live verdict — dumped whether or not it\n * found anything, so \"0 refuted\" is a measurement of a listed set and not an\n * empty listing wearing a zero.\n */\n exemptionAudit: HeadContractExemptionAudit[]\n coverage: {\n /** Files scanned (the caller decides what counts as a routed view). */\n views: number\n /** Files that re-assert the head in some way. */\n asserting: number\n /** Files that write nothing — SPA navigation to them is stale. */\n silent: string[]\n }\n violations: HeadContractViolation[]\n warnings: HeadContractWarning[]\n ok: boolean\n}\n\nexport type HeadContractMode = 'error' | 'warn' | 'off'\n\nexport class HeadContractError extends Error {\n readonly report: HeadContractReport\n constructor(message: string, report: HeadContractReport) {\n super(message)\n this.name = 'HeadContractError'\n this.report = report\n }\n}\n\n/** Minimum characters an exemption reason must carry to count as a reason. */\nexport const MIN_EXEMPTION_REASON_LENGTH = 12\n\n// =============================================================================\n// Source scanning\n// =============================================================================\n\n/**\n * Blank out COMMENTS ONLY — line, block and HTML — tracking string/template\n * literals just well enough not to mistake a `https://…` inside a string for a\n * line comment.\n *\n * Replacement preserves LENGTH and NEWLINES (every removed character becomes a\n * space, newlines survive), so every offset and line number computed on the\n * result is still valid against the original file.\n *\n * WHY COMMENTS ONLY. Two hazards pull in opposite directions and the asymmetry\n * decides it:\n * - boogie-babies' PrivacyView.vue carries a docblock reading \"applyHead takes\n * NO arguments\". Scanning raw source reports it. So comments MUST go.\n * - Blanking string BODIES as well would let a regex literal containing a\n * quote (`/['\"]/`) swallow the following lines and HIDE a real violation.\n * A gate that silently misses is worse than one that occasionally shouts:\n * a false positive is loud and fixed in a minute; a false negative is\n * exactly the class this whole contract exists to kill.\n * So string bodies are left intact, and an `applyHead(` written inside a string\n * literal is deliberately reported.\n */\nexport function blankComments(source: string): string {\n const out = source.split('')\n const n = source.length\n let i = 0\n\n const blank = (from: number, to: number) => {\n for (let k = from; k < to && k < n; k += 1) {\n if (out[k] !== '\\n') out[k] = ' '\n }\n }\n\n while (i < n) {\n const c = source[i]\n const next = source[i + 1]\n\n // HTML comment (Vue SFC <template> / index.html)\n if (c === '<' && source.startsWith('<!--', i)) {\n const end = source.indexOf('-->', i + 4)\n const stop = end === -1 ? n : end + 3\n blank(i, stop)\n i = stop\n continue\n }\n\n // Block comment\n if (c === '/' && next === '*') {\n const end = source.indexOf('*/', i + 2)\n const stop = end === -1 ? n : end + 2\n blank(i, stop)\n i = stop\n continue\n }\n\n // Line comment\n if (c === '/' && next === '/') {\n let end = source.indexOf('\\n', i + 2)\n if (end === -1) end = n\n blank(i, end)\n i = end\n continue\n }\n\n // String / template literal — SKIPPED OVER, never blanked. Tracking it here\n // is only so a `//` inside a URL is not read as a line comment.\n if (c === '\"' || c === \"'\" || c === '`') {\n const quote = c\n let k = i + 1\n while (k < n) {\n if (source[k] === '\\\\') {\n k += 2\n continue\n }\n if (source[k] === quote) break\n // An unterminated single/double quote must not swallow the file.\n if (quote !== '`' && source[k] === '\\n') break\n k += 1\n }\n i = Math.min(k + 1, n)\n continue\n }\n\n i += 1\n }\n\n return out.join('')\n}\n\nfunction lineOf(source: string, index: number): number {\n let line = 1\n for (let i = 0; i < index && i < source.length; i += 1) {\n if (source[i] === '\\n') line += 1\n }\n return line\n}\n\n/** Call-shaped `applyHead(` — never the `const { applyHead } = …` destructure. */\nconst APPLY_HEAD_CALL = /(?<![.\\w$])applyHead\\s*\\(/g\n/** `useSEO(` — the first argument is then inspected by hand (it may not be a literal). */\nconst USE_SEO_CALL = /(?<![.\\w$])useSEO\\s*\\(\\s*/g\n\n/**\n * Read `useSEO`'s first argument.\n *\n * A dynamic route (`/blog/:slug`, `/topics/:topic`) cannot name a static slug —\n * it composes the per-route key the EMITTER uses. That is legal under the\n * contract and it is NOT statically checkable, so it is reported as dynamic and\n * handed to P1, which renders each real route and compares the two heads. What\n * is NOT legal is a dynamic route hand-building its own title string; that shows\n * up here as an override or a foreign writer like any other.\n */\nfunction readUseSeoArgument(\n source: string,\n from: number\n): { kind: 'static'; slug: string } | { kind: 'dynamic'; expression: string } {\n const opener = source[from]\n if (opener === \"'\" || opener === '\"') {\n const end = source.indexOf(opener, from + 1)\n if (end === -1) return { kind: 'dynamic', expression: source.slice(from, from + 40) }\n return { kind: 'static', slug: source.slice(from + 1, end).trim() }\n }\n if (opener === '`') {\n const end = source.indexOf('`', from + 1)\n if (end === -1) return { kind: 'dynamic', expression: source.slice(from, from + 40) }\n const body = source.slice(from + 1, end)\n return body.includes('${')\n ? { kind: 'dynamic', expression: body }\n : { kind: 'static', slug: body.trim() }\n }\n const end = source.indexOf(')', from)\n return { kind: 'dynamic', expression: source.slice(from, end === -1 ? from + 40 : end).trim() }\n}\n\nconst FOREIGN_WRITERS: Array<{ re: RegExp; mechanism: ForeignWriterSite['mechanism'] }> = [\n { re: /(?<![.\\w$])useHead\\s*\\(/g, mechanism: 'useHead' },\n { re: /(?<![.\\w$])useSeoMeta\\s*\\(/g, mechanism: 'useSeoMeta' },\n { re: /document\\s*\\.\\s*title\\s*=(?!=)/g, mechanism: 'document.title' },\n]\n\n/** Head keys whose value the contract reserves for `.dcs/seo.yaml`. */\nconst MANAGED_HEAD_KEYS = /(^|[{,\\s([])(title|titleTemplate|meta|link|templateParams)\\s*:/\n/** The one head key a page may legitimately add on its own. */\nconst ADDITIVE_HEAD_KEYS = /(^|[{,\\s([])script\\s*:/\n/** Cap so a runaway scan cannot walk the whole file. */\nconst ARG_SCAN_LIMIT = 6000\n\n/**\n * Is this `useHead(...)` call ADDITIVE-ONLY?\n *\n * A page may legitimately add JSON-LD that `seo.yaml` cannot express per route —\n * a per-post `BlogPosting`, for instance. `useHead({ script: [...] })` that sets\n * no `title`/`meta`/`link` adds a tag; it does not restate a managed one, and\n * treating it as a violation would push every blog template into the exemption\n * list. An allow-list padded with legal entries is an allow-list nobody reads,\n * so the discrimination belongs here, not in each site's exemption file.\n *\n * Conservative by construction: anything it cannot read as additive-only — a\n * spread, a variable, an unbalanced or over-long argument — is reported.\n */\nfunction isAdditiveOnlyHeadCall(code: string, argStart: number): boolean {\n let depth = 0\n let i = argStart\n const end = Math.min(code.length, argStart + ARG_SCAN_LIMIT)\n for (; i < end; i += 1) {\n const c = code[i]\n if (c === '(' || c === '{' || c === '[') depth += 1\n else if (c === ')' || c === '}' || c === ']') {\n depth -= 1\n if (depth === 0) break\n }\n }\n if (depth !== 0) return false // unbalanced or truncated — do not guess\n const arg = code.slice(argStart, i + 1)\n if (arg.includes('...')) return false // a spread can carry anything\n return ADDITIVE_HEAD_KEYS.test(arg) && !MANAGED_HEAD_KEYS.test(arg)\n}\n\n// =============================================================================\n// Exemption host claims — the live half (C-420)\n// =============================================================================\n\n/**\n * A hostname written in prose. The TLD suffix list is deliberate: a bare\n * `\\w+(\\.\\w+)+` also matches `.dcs/seo.yaml`, `HomeView.vue` and `useSEO().\n * applyHead`, which would turn every exemption into a false host claim.\n */\nconst HOSTNAME_IN_TEXT =\n /\\b(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+(?:com|net|org|io|dev|co|us|ai|app|site|xyz)\\b/gi\n\nfunction normalizeHost(value: string): string {\n return value\n .trim()\n .toLowerCase()\n .replace(/^https?:\\/\\//, '')\n .replace(/\\/.*$/, '')\n .replace(/\\.$/, '')\n}\n\n/** A bare string exemption is a record with no host claims. */\nexport function normalizeExemption(value: HeadContractExemptionValue): HeadContractExemptionRecord {\n if (typeof value === 'string') return { reason: value }\n return { reason: value?.reason ?? '', hosts: value?.hosts }\n}\n\n/** Every host the prose names, deduped and normalised. */\nexport function hostsNamedIn(reason: string): string[] {\n const found = reason.match(HOSTNAME_IN_TEXT) ?? []\n const out: string[] = []\n for (const raw of found) {\n const host = normalizeHost(raw)\n if (!out.includes(host)) out.push(host)\n }\n return out\n}\n\n/**\n * Pull the three head fields an exemption can make a claim about out of a live\n * response body. Shared by the runner and by this module's own tests so the\n * thing that fetches and the thing that judges cannot disagree about what a\n * `robots` meta is.\n */\nexport function parseHostProbeHtml(html: string): {\n robots: string | null\n canonical: string | null\n title: string | null\n} {\n const robots =\n /<meta[^>]+name\\s*=\\s*[\"']robots[\"'][^>]*>/i\n .exec(html)?.[0]\n ?.match(/content\\s*=\\s*[\"']([^\"']*)[\"']/i)?.[1]\n ?.trim() ?? null\n const canonical =\n /<link[^>]+rel\\s*=\\s*[\"']canonical[\"'][^>]*>/i\n .exec(html)?.[0]\n ?.match(/href\\s*=\\s*[\"']([^\"']*)[\"']/i)?.[1]\n ?.trim() ?? null\n const title = /<title[^>]*>([\\s\\S]*?)<\\/title>/i.exec(html)?.[1]?.trim() ?? null\n return { robots, canonical, title }\n}\n\nfunction hostOfUrl(url: string): string {\n const match = /^(?:https?:)?\\/\\/([^/?#]+)/i.exec(url.trim())\n if (match) return normalizeHost(match[1])\n return normalizeHost(url)\n}\n\nfunction claimChecks(\n host: string,\n expect: HeadContractExemptionExpectation,\n probe: HeadContractHostProbe\n): HeadContractExemptionCheck[] {\n const checks: HeadContractExemptionCheck[] = []\n if (typeof expect.status === 'number') {\n checks.push({\n host,\n claim: 'status',\n expected: String(expect.status),\n actual: probe.status === undefined ? '(none)' : String(probe.status),\n pass: probe.status === expect.status,\n })\n }\n if (typeof expect.robots === 'string') {\n const actual = probe.robots ?? '(absent)'\n checks.push({\n host,\n claim: 'robots',\n expected: `contains \"${expect.robots}\"`,\n actual,\n pass: actual.toLowerCase().includes(expect.robots.toLowerCase()),\n })\n }\n if (typeof expect.canonicalHost === 'string') {\n const actual = probe.canonical ? hostOfUrl(probe.canonical) : '(absent)'\n checks.push({\n host,\n claim: 'canonicalHost',\n expected: normalizeHost(expect.canonicalHost),\n actual,\n pass: actual === normalizeHost(expect.canonicalHost),\n })\n }\n if (typeof expect.titleContains === 'string') {\n const actual = probe.title ?? '(absent)'\n checks.push({\n host,\n claim: 'titleContains',\n expected: `contains \"${expect.titleContains}\"`,\n actual,\n pass: actual.toLowerCase().includes(expect.titleContains.toLowerCase()),\n })\n }\n return checks\n}\n\n/**\n * Judge every exemption against the live behaviour of the host it names.\n *\n * Four ways to fail and only one way to pass, because the failure this exists\n * to catch is a green:\n * - the prose names a host that carries no machine-checkable claim\n * - a declared host asserts nothing (a claim block with no claims)\n * - a declared host has no observation (nothing was measured)\n * - an observation disagrees with a claim, or the probe itself did not run\n * Pure: it fetches nothing and reads no files.\n */\nexport function auditExemptionHosts(\n exemptions: Record<string, HeadContractExemptionValue>,\n hostProbes: Record<string, HeadContractHostProbe>\n): { audits: HeadContractExemptionAudit[]; violations: HeadContractViolation[] } {\n const audits: HeadContractExemptionAudit[] = []\n const violations: HeadContractViolation[] = []\n\n // WORST WINS, and \"a claim was contradicted\" is worse than \"a claim was\n // missing\": a multi-host exemption with one dead premise must summarise as\n // REFUTED, or the headline count disagrees with the violations underneath it.\n const RANK: Record<HeadContractExemptionVerdict, number> = {\n refuted: 4,\n 'probe-failed': 3,\n unprobed: 2,\n 'live-true': 1,\n 'no-host-claim': 0,\n }\n\n for (const file of Object.keys(exemptions)) {\n const record = normalizeExemption(exemptions[file])\n const named = hostsNamedIn(record.reason)\n const declaredMap = record.hosts ?? {}\n const declared = Object.keys(declaredMap).map(normalizeHost)\n const checks: HeadContractExemptionCheck[] = []\n let verdict: HeadContractExemptionVerdict = 'no-host-claim'\n const escalate = (next: HeadContractExemptionVerdict) => {\n if (RANK[next] > RANK[verdict]) verdict = next\n }\n\n const undeclared = named.filter((h) => !declared.includes(h))\n if (undeclared.length > 0) {\n escalate('unprobed')\n violations.push({\n code: 'unprobed-exemption',\n file,\n line: 0,\n detail:\n `This exemption's reason names ${undeclared.join(', ')} but declares no claim about ` +\n `it, so nothing checks whether the host still behaves the way the reason says. ` +\n `Add \"hosts\": { \"${undeclared[0]}\": { … } } with at least one of status / robots / ` +\n `canonicalHost / titleContains. An exemption that names a host and is never ` +\n `measured against it is an assertion, not a finding.`,\n })\n }\n\n for (const rawHost of Object.keys(declaredMap)) {\n const host = normalizeHost(rawHost)\n const expect = declaredMap[rawHost] ?? {}\n const claimCount = (\n ['status', 'robots', 'canonicalHost', 'titleContains'] as const\n ).filter((k) => expect[k] !== undefined).length\n\n if (claimCount === 0) {\n escalate('unprobed')\n violations.push({\n code: 'unprobed-exemption',\n file,\n line: 0,\n detail:\n `The exemption declares ${host} but asserts nothing about it. A host entry with ` +\n `no claim is probed into a guaranteed pass — the vacuous green this rail exists ` +\n `to refuse. State what the host must do: status / robots / canonicalHost / ` +\n `titleContains.`,\n })\n continue\n }\n\n const probe = hostProbes[host]\n if (!probe) {\n escalate('unprobed')\n violations.push({\n code: 'unprobed-exemption',\n file,\n line: 0,\n detail:\n `The exemption claims ${claimCount} thing(s) about ${host} and NO observation of ` +\n `that host reached the audit. \"Not measured\" must never render as \"measured ` +\n `fine\": run the audit with host probing enabled, or delete the claim.`,\n })\n continue\n }\n\n if (!probe.ok) {\n escalate('probe-failed')\n violations.push({\n code: 'exemption-probe-failed',\n file,\n line: 0,\n detail:\n `The probe of ${host} did not complete (${probe.error ?? 'no error recorded'}), so ` +\n `this exemption's premise is UNVERIFIED. A probe that could not run is a failure, ` +\n `not a skip — the alternative is an allow-list that goes green whenever the ` +\n `network does.`,\n })\n continue\n }\n\n const hostChecks = claimChecks(host, expect, probe)\n checks.push(...hostChecks)\n for (const check of hostChecks.filter((c) => !c.pass)) {\n escalate('refuted')\n violations.push({\n code: 'exemption-claim-refuted',\n file,\n line: 0,\n detail:\n `DEAD PREMISE. The exemption asserts ${check.host} ${check.claim} ` +\n `${check.expected}; the live host answered \"${check.actual}\". The exemption is ` +\n `why this file passes, so the pass rests on something production stopped doing. ` +\n `Re-word it to what is true, or delete it and the writer it protects.`,\n })\n }\n if (hostChecks.length > 0 && hostChecks.every((c) => c.pass)) escalate('live-true')\n }\n\n audits.push({\n file,\n reason: record.reason,\n hostsNamedInReason: named,\n hostsDeclared: declared,\n checks,\n verdict,\n })\n }\n\n return { audits, violations }\n}\n\n// =============================================================================\n// The audit\n// =============================================================================\n\n/**\n * Audit a set of source files against the head-authority contract.\n *\n * Site-agnostic by construction: there is no site name, no path convention and\n * no per-shape branch anywhere in this function. The three measured shapes\n * (KEPT / boogie-babies / bryans) go through the identical call.\n */\nexport function auditHeadContract(input: HeadContractInput): HeadContractReport {\n const files = input.files ?? []\n const exemptions = input.exemptions ?? {}\n const violations: HeadContractViolation[] = []\n const warnings: HeadContractWarning[] = []\n const foreignWriters: ForeignWriterSite[] = []\n const useSeoSlugs: string[] = []\n const dynamicSlugs: string[] = []\n const silent: string[] = []\n\n let applyHeadCalls = 0\n let additiveHeadCalls = 0\n let argumentFreeCalls = 0\n let asserting = 0\n\n for (const { path, source } of files) {\n const code = blankComments(source)\n let fileAsserts = false\n\n // ── V1 — applyHead must take no arguments ───────────────────────────────\n APPLY_HEAD_CALL.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = APPLY_HEAD_CALL.exec(code)) !== null) {\n applyHeadCalls += 1\n fileAsserts = true\n const rest = code.slice(m.index + m[0].length)\n if (/^\\s*\\)/.test(rest)) {\n argumentFreeCalls += 1\n } else {\n const preview = source\n .slice(m.index, m.index + 140)\n .replace(/\\s+/g, ' ')\n .trim()\n violations.push({\n code: 'override-argument',\n file: path,\n line: lineOf(source, m.index),\n detail:\n `applyHead() must take no arguments — .dcs/seo.yaml is the only writer of ` +\n `title/description/keywords/canonical/OG/Twitter/JSON-LD. Move the value there ` +\n `and delete the argument. Found: ${preview}…`,\n })\n }\n }\n\n // ── V3 — every STATIC useSEO slug must resolve to a declared page ───────\n USE_SEO_CALL.lastIndex = 0\n while ((m = USE_SEO_CALL.exec(code)) !== null) {\n fileAsserts = true\n const arg = readUseSeoArgument(source, m.index + m[0].length)\n if (arg.kind === 'dynamic') {\n if (!dynamicSlugs.includes(arg.expression)) dynamicSlugs.push(arg.expression)\n warnings.push({\n code: 'dynamic-slug',\n file: path,\n detail:\n `useSEO(${arg.expression.slice(0, 60)}) resolves its page key at runtime, so this ` +\n `audit cannot check it against .dcs/seo.yaml. That is legal — a dynamic route must ` +\n `resolve the SAME per-route key the emitter uses — but it is UNCHECKED HERE. P1 ` +\n `(baked vs rendered, on a real build of every route) is the rail that judges it.`,\n })\n continue\n }\n const slug = arg.slug\n // A slug seen in an earlier file has already been judged; reporting it\n // again would multiply one seo.yaml gap into many identical findings.\n if (!slug || useSeoSlugs.includes(slug)) continue\n useSeoSlugs.push(slug)\n const page = input.seo?.pages?.[slug]\n if (!page || typeof page.title !== 'string' || page.title.trim() === '') {\n violations.push({\n code: 'undeclared-slug',\n file: path,\n line: lineOf(source, m.index),\n detail:\n `useSEO('${slug}') has no pages.${slug}.title in .dcs/seo.yaml. At runtime it ` +\n `collapses to global.defaultTitle while the build-time emitter falls back to the ` +\n `pages.yaml route title — the same baked-vs-rendered divergence wearing a ` +\n `different hat. Declare the entry.`,\n })\n }\n }\n\n // ── V2 — no writer outside the factory ──────────────────────────────────\n for (const { re, mechanism } of FOREIGN_WRITERS) {\n re.lastIndex = 0\n while ((m = re.exec(code)) !== null) {\n fileAsserts = true\n\n // Additive JSON-LD is not a second writer — see isAdditiveOnlyHeadCall.\n if (mechanism === 'useHead' && isAdditiveOnlyHeadCall(code, m.index + m[0].length - 1)) {\n additiveHeadCalls += 1\n continue\n }\n\n const reason = Object.prototype.hasOwnProperty.call(exemptions, path)\n ? normalizeExemption(exemptions[path]).reason\n : null\n const exempted = typeof reason === 'string' && reason.trim().length >= MIN_EXEMPTION_REASON_LENGTH\n foreignWriters.push({\n file: path,\n line: lineOf(source, m.index),\n mechanism,\n exempted,\n reason: reason ?? null,\n })\n if (exempted) continue\n if (reason !== null) {\n violations.push({\n code: 'empty-exemption-reason',\n file: path,\n line: lineOf(source, m.index),\n detail:\n `An exemption for ${path} exists but records no reason (needs >= ` +\n `${MIN_EXEMPTION_REASON_LENGTH} characters). An undocumented exemption is ` +\n `indistinguishable from an accident.`,\n })\n continue\n }\n violations.push({\n code: 'foreign-head-writer',\n file: path,\n line: lineOf(source, m.index),\n detail:\n `${mechanism} writes the head outside the .dcs/seo.yaml factory. Route the value ` +\n `through seo.yaml + useSEO().applyHead(), or declare this file as an exemption ` +\n `with the reason it genuinely cannot. Declaring it does NOT excuse it from P1: ` +\n `the baked head and the rendered head must still agree.`,\n })\n }\n }\n\n if (fileAsserts) asserting += 1\n else silent.push(path)\n }\n\n // ── V4 (warn) — a view that never re-asserts is stale after the first click ─\n for (const file of silent) {\n warnings.push({\n code: 'uncovered-route',\n file,\n detail:\n `This view never re-asserts the head, so an in-app navigation to it leaves the ` +\n `PREVIOUS route's title/description/canonical in the DOM. The baked head is correct ` +\n `exactly once — on the document that was fetched. Reported, not enforced: the fix is ` +\n `a router-level re-assert in the platform, not eleven views remembering.`,\n })\n }\n\n // ── Exemption hygiene — anti-rot (source) AND anti-dead-premise (live) ──\n // The stale check asks \"does this file still violate?\"; the host audit asks\n // \"does the host this exemption NAMES still behave the way it claims?\". The\n // measured failure passed the first and would have failed the second.\n const hostAudit = auditExemptionHosts(exemptions, input.hostProbes ?? {})\n violations.push(...hostAudit.violations)\n\n const violatingFiles = new Set(foreignWriters.map((f) => f.file))\n for (const path of Object.keys(exemptions)) {\n if (!violatingFiles.has(path)) {\n violations.push({\n code: 'stale-exemption',\n file: path,\n line: 0,\n detail:\n `An exemption is declared for ${path} but it writes no head outside the factory. ` +\n `Delete the exemption — a rotting allow-list is how the next real one gets waved ` +\n `through.`,\n })\n }\n }\n\n // ── Execution assertions — a check that checked nothing is a FAILURE ─────\n if (files.length === 0) {\n violations.push({\n code: 'scan-empty',\n file: '(none)',\n line: 0,\n detail:\n 'The head-contract audit SCANNED NOTHING. Zero files reached it, so it proved ' +\n 'nothing about this site. The file walk is broken, not the codebase.',\n })\n }\n if (!input.seo || !input.seo.pages || Object.keys(input.seo.pages).length === 0) {\n violations.push({\n code: 'no-seo-config',\n file: '.dcs/seo.yaml',\n line: 0,\n detail:\n 'No .dcs/seo.yaml pages were supplied. Authority cannot be established without the ' +\n 'authority file, so every slug check silently passes — the vacuous-green shape this ' +\n 'audit exists to refuse.',\n })\n }\n\n return {\n scanned: files.length,\n applyHeadCalls,\n argumentFreeCalls,\n useSeoSlugs,\n dynamicSlugs,\n foreignWriters,\n additiveHeadCalls,\n exemptionAudit: hostAudit.audits,\n coverage: { views: files.length, asserting, silent },\n violations,\n warnings,\n ok: violations.length === 0,\n }\n}\n\n/**\n * Dump the exemption set with a per-entry live verdict.\n *\n * Printed on EVERY run, including the empty one. \"0 refuted\" out of an unlisted\n * set is indistinguishable from a rail that inspected nothing, so the set is\n * always enumerated and the empty case says so in words.\n */\nexport function formatExemptionAudit(report: HeadContractReport): string[] {\n const audits = report.exemptionAudit ?? []\n const lines: string[] = []\n const count = (v: HeadContractExemptionVerdict) => audits.filter((a) => a.verdict === v).length\n\n if (audits.length === 0) {\n lines.push(\n `[head-contract] exemptions: 0 declared — the allow-list is EMPTY, not unchecked ` +\n `(${report.scanned} file(s) scanned, ${report.foreignWriters.length} foreign head ` +\n `writer(s) found, so there was nothing to declare).`\n )\n return lines\n }\n\n lines.push(\n `[head-contract] exemptions: ${audits.length} declared — ${count('live-true')} live-true, ` +\n `${count('refuted')} REFUTED, ${count('unprobed')} unprobed, ` +\n `${count('probe-failed')} probe-failed, ${count('no-host-claim')} host-free`\n )\n for (const audit of audits) {\n const hosts = audit.hostsDeclared.length > 0 ? audit.hostsDeclared.join(', ') : '(no host declared)'\n lines.push(` ${audit.verdict.toUpperCase()} ${audit.file} [${hosts}]`)\n if (audit.hostsNamedInReason.length > 0) {\n lines.push(` reason names: ${audit.hostsNamedInReason.join(', ')}`)\n }\n for (const check of audit.checks) {\n lines.push(\n ` ${check.pass ? 'ok ' : 'FAIL'} ${check.host} ${check.claim}: ` +\n `expected ${check.expected}, live \"${check.actual}\"`\n )\n }\n if (audit.checks.length === 0 && audit.verdict !== 'no-host-claim') {\n lines.push(' (no live check ran — see the violations above)')\n }\n }\n return lines\n}\n\n/** Human-readable report. Execution counts are printed FIRST, always. */\nexport function formatHeadContractReport(report: HeadContractReport): string {\n const lines: string[] = []\n lines.push(\n `[head-contract] scanned ${report.scanned} file(s); ` +\n `${report.applyHeadCalls} applyHead call(s), ${report.argumentFreeCalls} argument-free; ` +\n `${report.useSeoSlugs.length} static useSEO slug(s), ` +\n `${report.dynamicSlugs.length} dynamic (unchecked here — P1 judges those); ` +\n `${report.foreignWriters.length} foreign head writer(s) ` +\n `(${report.foreignWriters.filter((f) => f.exempted).length} declared); ` +\n `${report.additiveHeadCalls} additive JSON-LD useHead call(s)`\n )\n lines.push(\n `[head-contract] SPA coverage: ${report.coverage.asserting}/${report.coverage.views} view(s) ` +\n `re-assert the head; ${report.coverage.silent.length} silent`\n )\n if (report.scanned === 0) {\n lines.push('[head-contract] SCANNED NOTHING this run — this is a failure, not a pass.')\n }\n lines.push(...formatExemptionAudit(report))\n for (const v of report.violations) {\n lines.push(` VIOLATION ${v.code} ${v.file}${v.line ? `:${v.line}` : ''} — ${v.detail}`)\n }\n for (const w of report.warnings) {\n lines.push(` warn ${w.code} ${w.file} — ${w.detail}`)\n }\n if (report.violations.length === 0) {\n lines.push('[head-contract] PASS — .dcs/seo.yaml is the only writer of the managed head.')\n }\n return lines.join('\\n')\n}\n\n/**\n * Assert the contract. `error` throws; `warn` prints; `off` is a no-op — except\n * that `scan-empty` and `no-seo-config` are NEVER downgraded, because \"the rail\n * did not run\" must not be silenceable by the mode that silences findings.\n */\nexport function assertHeadContract(\n input: HeadContractInput,\n mode: HeadContractMode = 'error',\n log: (msg: string) => void = console.warn\n): HeadContractReport {\n const report = auditHeadContract(input)\n const notRun = report.violations.filter(\n (v) => v.code === 'scan-empty' || v.code === 'no-seo-config'\n )\n\n if (notRun.length > 0) {\n throw new HeadContractError(\n `${formatHeadContractReport(report)}\\n\\nThe head-contract audit did not run against ` +\n `anything it could judge. Fix the inputs before reading this as a pass.`,\n report\n )\n }\n\n if (mode === 'off') return report\n if (report.violations.length === 0) return report\n\n if (mode === 'warn') {\n log(formatHeadContractReport(report))\n log('[head-contract] in error mode the above would be RED.')\n return report\n }\n\n throw new HeadContractError(formatHeadContractReport(report), report)\n}\n"]}
|
|
@@ -80,6 +80,38 @@ function findLocalBusinessSchema(global) {
|
|
|
80
80
|
}
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
|
+
var REQUIRED_POSTAL_ADDRESS_FIELDS = [
|
|
84
|
+
"streetAddress",
|
|
85
|
+
"addressLocality",
|
|
86
|
+
"addressRegion",
|
|
87
|
+
"postalCode",
|
|
88
|
+
"addressCountry"
|
|
89
|
+
];
|
|
90
|
+
function findPostalAddressGaps(global) {
|
|
91
|
+
const found = findLocalBusinessSchema(global);
|
|
92
|
+
if (!found) return [];
|
|
93
|
+
const address = (found.schema.properties ?? {})["address"];
|
|
94
|
+
if (!address || typeof address !== "object" || Array.isArray(address)) return [];
|
|
95
|
+
const fields = address;
|
|
96
|
+
const missing = [];
|
|
97
|
+
const present = [];
|
|
98
|
+
for (const key of REQUIRED_POSTAL_ADDRESS_FIELDS) {
|
|
99
|
+
const value = fields[key];
|
|
100
|
+
const filled = typeof value === "string" ? value.trim().length > 0 : value != null;
|
|
101
|
+
(filled ? present : missing).push(key);
|
|
102
|
+
}
|
|
103
|
+
if (missing.length === 0) return [];
|
|
104
|
+
return [{ businessType: found.schema.type ?? "LocalBusiness", missing, present }];
|
|
105
|
+
}
|
|
106
|
+
function formatPostalAddressGapReport(gaps) {
|
|
107
|
+
const lines = [];
|
|
108
|
+
for (const gap of gaps) {
|
|
109
|
+
lines.push(
|
|
110
|
+
`[dcs-seo] LocalBusiness (${gap.businessType}) emits a PARTIAL PostalAddress: missing ${gap.missing.join(", ")} (present: ${gap.present.join(", ") || "nothing"}). A half-authored address ships to crawlers as if it were complete and will not match the business to a place. Fix: complete the address in .dcs/seo.yaml, or \u2014 if this is a service-area business with no public storefront \u2014 remove the address block entirely and rely on areaServed, which is the correct shape for it.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
83
115
|
function graphAbsorbs(schema, global) {
|
|
84
116
|
if (schema.type === "Organization" || schema.type === "WebSite") return true;
|
|
85
117
|
const found = findLocalBusinessSchema(global);
|
|
@@ -2186,6 +2218,6 @@ function createSeoTransformPageData(options) {
|
|
|
2186
2218
|
};
|
|
2187
2219
|
}
|
|
2188
2220
|
|
|
2189
|
-
export { AI_BOTS, BodyPrerenderError, CANONICAL_CHARSET_META, CHARSET_BUDGET_BYTES, CHARSET_HEADROOM_WARN_BYTES, CharsetBudgetError, EmittedUrlError, EmittedUrlNotRunError, HeadHonestyError, HeadHonestyNotRunError, SEO_HEAD_ROUTE_HAS_NO_META, absolutizeUrl, assertHeadHonesty, assertHeadHonestyExecuted, auditCheckedTotal, auditSameOriginAssets, breadcrumbTrailFromRoute, buildBlogPosting, buildBreadcrumbList, buildFaqPage, buildGlobalGraph, buildHasCredential, buildHeadTags, buildLlmsTxt, buildReviewSchemaParts, buildRobotsTxt, buildSeoHeadRouteMap, buildSitemapXml, buildVitePressSeoHead, checkCharsetBudget, checkRemoteAssets, checkSameOriginAssets, classifyUrl, collectEmittedUrlsFromFiles, collectUrlsFromHtml, collectUrlsFromLlmsTxt, collectUrlsFromSitemap, createSeoTransformPageData, decodeHtmlEntities, dedupeUrls, defaultRelativePathToRoute, deriveSameAs, escapeJsonLd, extractBakedHead, filterRealFaq, filterRealReviews, findBusinessLicense, findCharsetByteOffset, findDuplicateNormalizedPaths, findGlobalSchemasOfType, findHeadHonestyExecutionFaults, findHeadHonestyViolations, findLocalBusinessSchema, findReviewItemsForPage, formatCharsetHeadroomWarning, formatDuplicateNormalizedPaths, formatEmittedUrlAudit, formatEmittedUrlReport, formatHeadHonestyExecutionReport, formatHeadHonestyReport, generateJsonLd, generateOpenGraphMeta, generateTwitterMeta, graphAbsorbs, graphIds, headInputIdentities, hoistCharsetMeta, installSeoHead, isBodyPrerenderEnabled, isHandAuthoredRobotsAcceptable, isLocalBusinessType, isManagedLinkRel, isManagedMetaTag, isRouteEmitted, isRouteIndexable, loadPagesManifest, normalizeHeadText, normalizeSeoHeadPath, parsePagesManifest, prerenderBodies, renderHeadTags, resolveHonestyMode, resolvePageSeo, routeExclusionReason, routeToOutputFile, slugToTitle, snapshotBakedManagedHeadTags, spliceBodyHtml, spliceHeadHtml, stripManagedHeadTags, sweepBakedManagedHeadTags };
|
|
2190
|
-
//# sourceMappingURL=chunk-
|
|
2191
|
-
//# sourceMappingURL=chunk-
|
|
2221
|
+
export { AI_BOTS, BodyPrerenderError, CANONICAL_CHARSET_META, CHARSET_BUDGET_BYTES, CHARSET_HEADROOM_WARN_BYTES, CharsetBudgetError, EmittedUrlError, EmittedUrlNotRunError, HeadHonestyError, HeadHonestyNotRunError, REQUIRED_POSTAL_ADDRESS_FIELDS, SEO_HEAD_ROUTE_HAS_NO_META, absolutizeUrl, assertHeadHonesty, assertHeadHonestyExecuted, auditCheckedTotal, auditSameOriginAssets, breadcrumbTrailFromRoute, buildBlogPosting, buildBreadcrumbList, buildFaqPage, buildGlobalGraph, buildHasCredential, buildHeadTags, buildLlmsTxt, buildReviewSchemaParts, buildRobotsTxt, buildSeoHeadRouteMap, buildSitemapXml, buildVitePressSeoHead, checkCharsetBudget, checkRemoteAssets, checkSameOriginAssets, classifyUrl, collectEmittedUrlsFromFiles, collectUrlsFromHtml, collectUrlsFromLlmsTxt, collectUrlsFromSitemap, createSeoTransformPageData, decodeHtmlEntities, dedupeUrls, defaultRelativePathToRoute, deriveSameAs, escapeJsonLd, extractBakedHead, filterRealFaq, filterRealReviews, findBusinessLicense, findCharsetByteOffset, findDuplicateNormalizedPaths, findGlobalSchemasOfType, findHeadHonestyExecutionFaults, findHeadHonestyViolations, findLocalBusinessSchema, findPostalAddressGaps, findReviewItemsForPage, formatCharsetHeadroomWarning, formatDuplicateNormalizedPaths, formatEmittedUrlAudit, formatEmittedUrlReport, formatHeadHonestyExecutionReport, formatHeadHonestyReport, formatPostalAddressGapReport, generateJsonLd, generateOpenGraphMeta, generateTwitterMeta, graphAbsorbs, graphIds, headInputIdentities, hoistCharsetMeta, installSeoHead, isBodyPrerenderEnabled, isHandAuthoredRobotsAcceptable, isLocalBusinessType, isManagedLinkRel, isManagedMetaTag, isRouteEmitted, isRouteIndexable, loadPagesManifest, normalizeHeadText, normalizeSeoHeadPath, parsePagesManifest, prerenderBodies, renderHeadTags, resolveHonestyMode, resolvePageSeo, routeExclusionReason, routeToOutputFile, slugToTitle, snapshotBakedManagedHeadTags, spliceBodyHtml, spliceHeadHtml, stripManagedHeadTags, sweepBakedManagedHeadTags };
|
|
2222
|
+
//# sourceMappingURL=chunk-W5Q6SOAP.js.map
|
|
2223
|
+
//# sourceMappingURL=chunk-W5Q6SOAP.js.map
|