vagejs 0.0.0-stage → 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 +21 -0
- package/README.md +244 -2
- package/dist/audit/a11y.d.ts +13 -0
- package/dist/audit/a11y.js +33 -0
- package/dist/audit/audit.d.ts +31 -0
- package/dist/audit/audit.js +143 -0
- package/dist/audit/budgets.d.ts +22 -0
- package/dist/audit/budgets.js +106 -0
- package/dist/audit/lighthouse.d.ts +3 -0
- package/dist/audit/lighthouse.js +48 -0
- package/dist/audit/remote.d.ts +65 -0
- package/dist/audit/remote.js +123 -0
- package/dist/audit/vitals.d.ts +28 -0
- package/dist/audit/vitals.js +18 -0
- package/dist/checks/check.d.ts +31 -0
- package/dist/checks/check.js +434 -0
- package/dist/checks/integrity.d.ts +36 -0
- package/dist/checks/integrity.js +145 -0
- package/dist/checks/links.d.ts +18 -0
- package/dist/checks/links.js +47 -0
- package/dist/checks/verify.d.ts +35 -0
- package/dist/checks/verify.js +167 -0
- package/dist/cli/create.d.ts +10 -0
- package/dist/cli/create.js +39 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +710 -0
- package/dist/cli/info.d.ts +82 -0
- package/dist/cli/info.js +100 -0
- package/dist/content/images.d.ts +23 -0
- package/dist/content/images.js +96 -0
- package/dist/content/markdown.d.ts +28 -0
- package/dist/content/markdown.js +56 -0
- package/dist/content/sanitize.d.ts +16 -0
- package/dist/content/sanitize.js +57 -0
- package/dist/core/build.d.ts +25 -0
- package/dist/core/build.js +54 -0
- package/dist/core/clone.d.ts +6 -0
- package/dist/core/clone.js +16 -0
- package/dist/core/config.d.ts +180 -0
- package/dist/core/config.js +260 -0
- package/dist/core/errors.d.ts +7 -0
- package/dist/core/errors.js +12 -0
- package/dist/core/html.d.ts +24 -0
- package/dist/core/html.js +42 -0
- package/dist/core/loader.d.ts +10 -0
- package/dist/core/loader.js +16 -0
- package/dist/core/pages.d.ts +40 -0
- package/dist/core/pages.js +93 -0
- package/dist/core/project-config.d.ts +3 -0
- package/dist/core/project-config.js +14 -0
- package/dist/core/render.d.ts +53 -0
- package/dist/core/render.js +104 -0
- package/dist/core/routes.d.ts +24 -0
- package/dist/core/routes.js +70 -0
- package/dist/core/site.d.ts +42 -0
- package/dist/core/site.js +182 -0
- package/dist/core/validate.d.ts +12 -0
- package/dist/core/validate.js +36 -0
- package/dist/deploy/cloudflare-pages.d.ts +20 -0
- package/dist/deploy/cloudflare-pages.js +42 -0
- package/dist/deploy/cloudflare-workers.d.ts +59 -0
- package/dist/deploy/cloudflare-workers.js +223 -0
- package/dist/deploy/git.d.ts +20 -0
- package/dist/deploy/git.js +55 -0
- package/dist/deploy/github-pages.d.ts +33 -0
- package/dist/deploy/github-pages.js +99 -0
- package/dist/deploy/headers.d.ts +11 -0
- package/dist/deploy/headers.js +103 -0
- package/dist/deploy/publish.d.ts +45 -0
- package/dist/deploy/publish.js +148 -0
- package/dist/deploy/redirects.d.ts +7 -0
- package/dist/deploy/redirects.js +18 -0
- package/dist/deploy/secrets.d.ts +6 -0
- package/dist/deploy/secrets.js +66 -0
- package/dist/dev/guard.d.ts +9 -0
- package/dist/dev/guard.js +57 -0
- package/dist/dev/listen.d.ts +6 -0
- package/dist/dev/listen.js +13 -0
- package/dist/dev/preview.d.ts +6 -0
- package/dist/dev/preview.js +72 -0
- package/dist/dev/server.d.ts +8 -0
- package/dist/dev/server.js +104 -0
- package/dist/discovery/crawlers.d.ts +13 -0
- package/dist/discovery/crawlers.js +2 -0
- package/dist/discovery/llms.d.ts +12 -0
- package/dist/discovery/llms.js +19 -0
- package/dist/discovery/robots.d.ts +8 -0
- package/dist/discovery/robots.js +40 -0
- package/dist/discovery/sitemap.d.ts +8 -0
- package/dist/discovery/sitemap.js +11 -0
- package/dist/guidelines/monitor.d.ts +55 -0
- package/dist/guidelines/monitor.js +126 -0
- package/dist/guidelines/pack.d.ts +41 -0
- package/dist/guidelines/pack.js +13 -0
- package/dist/guidelines/rule-pack.json +611 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/integrations/api-keys.d.ts +26 -0
- package/dist/integrations/api-keys.js +65 -0
- package/dist/integrations/ga4-data.d.ts +13 -0
- package/dist/integrations/ga4-data.js +39 -0
- package/dist/integrations/google-auth.d.ts +47 -0
- package/dist/integrations/google-auth.js +157 -0
- package/dist/integrations/http.d.ts +15 -0
- package/dist/integrations/http.js +40 -0
- package/dist/integrations/indexnow.d.ts +18 -0
- package/dist/integrations/indexnow.js +38 -0
- package/dist/integrations/search-console.d.ts +25 -0
- package/dist/integrations/search-console.js +39 -0
- package/dist/integrations/tags.d.ts +22 -0
- package/dist/integrations/tags.js +67 -0
- package/dist/seo/head.d.ts +20 -0
- package/dist/seo/head.js +85 -0
- package/dist/seo/jsonld.d.ts +35 -0
- package/dist/seo/jsonld.js +31 -0
- package/dist/seo/urls.d.ts +3 -0
- package/dist/seo/urls.js +10 -0
- package/package.json +81 -4
- package/templates/starter/.env.example +12 -0
- package/templates/starter/.github/dependabot.yml +11 -0
- package/templates/starter/.github/workflows/ci.yml +30 -0
- package/templates/starter/.github/workflows/verify.yml +32 -0
- package/templates/starter/AGENTS.md +166 -0
- package/templates/starter/CLAUDE.md +3 -0
- package/templates/starter/_gitignore +8 -0
- package/templates/starter/links.lock.json +4 -0
- package/templates/starter/package.json +16 -0
- package/templates/starter/public/apple-touch-icon.png +0 -0
- package/templates/starter/public/favicon.ico +0 -0
- package/templates/starter/public/favicon.svg +1 -0
- package/templates/starter/src/layouts/blog.ts +13 -0
- package/templates/starter/src/layouts/default.ts +38 -0
- package/templates/starter/src/pages/404.md +7 -0
- package/templates/starter/src/pages/about.md +8 -0
- package/templates/starter/src/pages/blog/first-post.md +10 -0
- package/templates/starter/src/pages/blog/index.md +7 -0
- package/templates/starter/src/pages/index.md +8 -0
- package/templates/starter/vage.config.ts +38 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { VageError } from "../core/errors.js";
|
|
2
|
+
import { AI_CRAWLERS } from "./crawlers.js";
|
|
3
|
+
/**
|
|
4
|
+
* robots.txt from explicit config only. Unset AI options add nothing, so the
|
|
5
|
+
* owner's `*` policy applies. robots.txt is advisory and is not access control.
|
|
6
|
+
*/
|
|
7
|
+
export function robotsTxt(site, options) {
|
|
8
|
+
const groups = options.rules.map((r) => ({
|
|
9
|
+
agents: Array.isArray(r.userAgent) ? r.userAgent : [r.userAgent],
|
|
10
|
+
allow: r.allow,
|
|
11
|
+
disallow: r.disallow,
|
|
12
|
+
}));
|
|
13
|
+
const named = new Set(groups.flatMap((g) => g.agents.map((a) => a.toLowerCase())));
|
|
14
|
+
for (const [purpose, policy] of [
|
|
15
|
+
["training", options.aiTraining],
|
|
16
|
+
["search", options.aiSearch],
|
|
17
|
+
]) {
|
|
18
|
+
if (!policy)
|
|
19
|
+
continue;
|
|
20
|
+
for (const crawler of AI_CRAWLERS.filter((c) => c.purpose === purpose)) {
|
|
21
|
+
if (named.has(crawler.token.toLowerCase())) {
|
|
22
|
+
throw new VageError(`robots: "${crawler.token}" is set both in seo.robots.rules and by seo.robots.ai${purpose === "training" ? "Training" : "Search"}. Keep only one.`);
|
|
23
|
+
}
|
|
24
|
+
groups.push({ agents: [crawler.token], allow: policy === "allow" ? ["/"] : [], disallow: policy === "disallow" ? ["/"] : [] });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!named.has("*"))
|
|
28
|
+
groups.push({ agents: ["*"], allow: [], disallow: [] });
|
|
29
|
+
const body = groups
|
|
30
|
+
.map((g) => {
|
|
31
|
+
const lines = g.agents.map((a) => `User-agent: ${a}`);
|
|
32
|
+
lines.push(...g.allow.map((p) => `Allow: ${p}`), ...g.disallow.map((p) => `Disallow: ${p}`));
|
|
33
|
+
if (!g.allow.length && !g.disallow.length)
|
|
34
|
+
lines.push("Disallow:");
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
})
|
|
37
|
+
.join("\n\n");
|
|
38
|
+
const sitemap = site.seo.sitemap && site.site ? `\n\nSitemap: ${site.site}/sitemap.xml` : "";
|
|
39
|
+
return `${body}${sitemap}\n`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SitemapEntry {
|
|
2
|
+
url: string;
|
|
3
|
+
/** Only set from frontmatter dates; file timestamps are not trustworthy after a git checkout. */
|
|
4
|
+
lastmod?: string;
|
|
5
|
+
}
|
|
6
|
+
/** A single urlset. The 50,000-URL limit per file is enforced by the caller. */
|
|
7
|
+
export declare function sitemapXml(entries: SitemapEntry[]): string;
|
|
8
|
+
export declare const SITEMAP_URL_LIMIT: number;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ruleData } from "../guidelines/pack.js";
|
|
2
|
+
const XML_ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
3
|
+
const xml = (s) => s.replace(/[&<>"']/g, (c) => XML_ESCAPES[c]);
|
|
4
|
+
/** A single urlset. The 50,000-URL limit per file is enforced by the caller. */
|
|
5
|
+
export function sitemapXml(entries) {
|
|
6
|
+
const urls = entries
|
|
7
|
+
.map((e) => ` <url>\n <loc>${xml(e.url)}</loc>${e.lastmod ? `\n <lastmod>${xml(e.lastmod)}</lastmod>` : ""}\n </url>`)
|
|
8
|
+
.join("\n");
|
|
9
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`;
|
|
10
|
+
}
|
|
11
|
+
export const SITEMAP_URL_LIMIT = ruleData("sitemap.limits").maxUrls;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { RulePack, Source } from "./pack.js";
|
|
2
|
+
/**
|
|
3
|
+
* Change detection for official sources. Snapshots keep only section headings
|
|
4
|
+
* and hashes, not the documents' text, so nothing copyrighted is stored; the
|
|
5
|
+
* live page is the evidence. Reports are inputs to human review, never
|
|
6
|
+
* automatic rule changes.
|
|
7
|
+
*/
|
|
8
|
+
export interface SourceSnapshot {
|
|
9
|
+
id: string;
|
|
10
|
+
url: string;
|
|
11
|
+
fetchedAt: string;
|
|
12
|
+
status: number;
|
|
13
|
+
/** Final URL after redirects. */
|
|
14
|
+
finalUrl: string;
|
|
15
|
+
lastModified?: string;
|
|
16
|
+
contentHash: string;
|
|
17
|
+
sections: {
|
|
18
|
+
heading: string;
|
|
19
|
+
hash: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
export interface SourceChange {
|
|
23
|
+
source: Source;
|
|
24
|
+
kind: "changed" | "unreachable" | "unreadable" | "moved" | "new";
|
|
25
|
+
added: string[];
|
|
26
|
+
removed: string[];
|
|
27
|
+
modified: string[];
|
|
28
|
+
detail?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Splits the main content into sections by h1–h3 and hashes each section's normalized text. */
|
|
31
|
+
export declare function sectionize(html: string): {
|
|
32
|
+
heading: string;
|
|
33
|
+
text: string;
|
|
34
|
+
}[];
|
|
35
|
+
/** Fetches a source. `texts` (heading -> current text) is for classification only and is never persisted. */
|
|
36
|
+
export declare function fetchSource(source: Source, fetchImpl?: typeof fetch, now?: Date): Promise<{
|
|
37
|
+
snapshot: SourceSnapshot;
|
|
38
|
+
texts: Map<string, string>;
|
|
39
|
+
}>;
|
|
40
|
+
export declare function compareSnapshots(source: Source, before: SourceSnapshot | undefined, after: SourceSnapshot): SourceChange | undefined;
|
|
41
|
+
export interface Classification {
|
|
42
|
+
sourceId: string;
|
|
43
|
+
section: string;
|
|
44
|
+
classification: "informative" | "recommended" | "required" | "deprecated" | "unclear";
|
|
45
|
+
rationale: string;
|
|
46
|
+
suggestedAction: string;
|
|
47
|
+
}
|
|
48
|
+
/** Markdown change report: what changed, which rules depend on it, and what needs human review. */
|
|
49
|
+
export declare function changeReport(pack: RulePack, changes: SourceChange[], date: string, classifications?: Classification[]): string;
|
|
50
|
+
/**
|
|
51
|
+
* Fetched headings and LLM output are untrusted: shown as inline code so they
|
|
52
|
+
* can't become links, images, HTML or @-mentions in the issue.
|
|
53
|
+
*/
|
|
54
|
+
export declare function untrusted(text: string, max?: number): string;
|
|
55
|
+
export declare function affectedRules(pack: RulePack, sourceId: string): import("./pack.js").Rule[];
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { parse } from "node-html-parser";
|
|
3
|
+
const NOISE = "script, style, noscript, nav, header, footer, aside, form, button, svg, [aria-hidden=true], .devsite-article-meta, .devsite-feedback";
|
|
4
|
+
/** Splits the main content into sections by h1–h3 and hashes each section's normalized text. */
|
|
5
|
+
export function sectionize(html) {
|
|
6
|
+
const doc = parse(html);
|
|
7
|
+
const main = doc.querySelector("main article") ?? doc.querySelector("article") ?? doc.querySelector("main") ?? doc.querySelector("[role=main]") ?? doc.querySelector("body") ?? doc;
|
|
8
|
+
for (const el of main.querySelectorAll(NOISE))
|
|
9
|
+
el.remove();
|
|
10
|
+
const sections = [{ heading: "(intro)", text: [] }];
|
|
11
|
+
const walk = (node) => {
|
|
12
|
+
for (const child of node.childNodes) {
|
|
13
|
+
if (child.nodeType === 3) {
|
|
14
|
+
sections.at(-1).text.push(child.rawText);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const el = child;
|
|
18
|
+
if (/^h[1-3]$/i.test(el.tagName ?? ""))
|
|
19
|
+
sections.push({ heading: normalize(el.text), text: [] });
|
|
20
|
+
else if (el.tagName)
|
|
21
|
+
walk(el);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
walk(main);
|
|
25
|
+
return sections
|
|
26
|
+
.map((s) => ({ heading: s.heading, text: normalize(decode(s.text.join(" "))) }))
|
|
27
|
+
.filter((s) => s.text || s.heading !== "(intro)");
|
|
28
|
+
}
|
|
29
|
+
/** Fetches a source. `texts` (heading -> current text) is for classification only and is never persisted. */
|
|
30
|
+
export async function fetchSource(source, fetchImpl = fetch, now = new Date()) {
|
|
31
|
+
const res = await fetchImpl(source.url, {
|
|
32
|
+
redirect: "follow",
|
|
33
|
+
// English, so localized variants don't show up as changes.
|
|
34
|
+
headers: { "user-agent": "vagejs-guidelines-monitor/0.1", accept: "text/html", "accept-language": "en-US,en;q=0.9" },
|
|
35
|
+
signal: AbortSignal.timeout(60_000),
|
|
36
|
+
});
|
|
37
|
+
const html = res.ok ? await res.text() : "";
|
|
38
|
+
const parts = sectionize(html);
|
|
39
|
+
const sections = parts.map((s) => ({ heading: s.heading, hash: hash(`${s.heading}\n${s.text}`) }));
|
|
40
|
+
const snapshot = {
|
|
41
|
+
id: source.id,
|
|
42
|
+
url: source.url,
|
|
43
|
+
fetchedAt: now.toISOString(),
|
|
44
|
+
status: res.status,
|
|
45
|
+
finalUrl: res.url || source.url,
|
|
46
|
+
lastModified: res.headers.get("last-modified") ?? undefined,
|
|
47
|
+
contentHash: hash(sections.map((s) => s.hash).join("")),
|
|
48
|
+
sections,
|
|
49
|
+
};
|
|
50
|
+
return { snapshot, texts: new Map(parts.map((p) => [p.heading, p.text])) };
|
|
51
|
+
}
|
|
52
|
+
export function compareSnapshots(source, before, after) {
|
|
53
|
+
const base = { source, added: [], removed: [], modified: [] };
|
|
54
|
+
if (after.status < 200 || after.status >= 300)
|
|
55
|
+
return { ...base, kind: "unreachable", detail: `HTTP ${after.status}` };
|
|
56
|
+
if (!after.sections.length) {
|
|
57
|
+
return { ...base, kind: "unreadable", detail: "no readable text (probably rendered client-side); check manually or mark the source monitor: manual" };
|
|
58
|
+
}
|
|
59
|
+
if (!before)
|
|
60
|
+
return { ...base, kind: "new", added: after.sections.map((s) => s.heading) };
|
|
61
|
+
if (new URL(after.finalUrl).pathname !== new URL(before.finalUrl).pathname) {
|
|
62
|
+
return { ...base, kind: "moved", detail: `${before.finalUrl} → ${after.finalUrl}` };
|
|
63
|
+
}
|
|
64
|
+
if (before.contentHash === after.contentHash)
|
|
65
|
+
return undefined;
|
|
66
|
+
const old = new Map(before.sections.map((s) => [s.heading, s.hash]));
|
|
67
|
+
const now = new Map(after.sections.map((s) => [s.heading, s.hash]));
|
|
68
|
+
return {
|
|
69
|
+
...base,
|
|
70
|
+
kind: "changed",
|
|
71
|
+
added: [...now.keys()].filter((h) => !old.has(h)),
|
|
72
|
+
removed: [...old.keys()].filter((h) => !now.has(h)),
|
|
73
|
+
modified: [...now.keys()].filter((h) => old.has(h) && old.get(h) !== now.get(h)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Markdown change report: what changed, which rules depend on it, and what needs human review. */
|
|
77
|
+
export function changeReport(pack, changes, date, classifications = []) {
|
|
78
|
+
const lines = [`# Guideline source changes — ${date}`, "", `Rule pack in use: ${pack.version}. Snapshots store headings and hashes only; open each link to read the current text.`, ""];
|
|
79
|
+
const manual = pack.sources.filter((s) => s.monitor === "manual");
|
|
80
|
+
if (manual.length)
|
|
81
|
+
lines.push(`Not monitored automatically (check by hand): ${manual.map((s) => `[${s.title}](${s.url})`).join(", ")}.`, "");
|
|
82
|
+
if (!changes.length)
|
|
83
|
+
return `${lines.join("\n")}No changes detected.\n`;
|
|
84
|
+
const review = changes.some((c) => affectedRules(pack, c.source.id).some((r) => r.behaviorChanging));
|
|
85
|
+
lines.push(review
|
|
86
|
+
? "**Human review required:** at least one change affects a rule that changes crawler access, indexing, privacy, security or build output."
|
|
87
|
+
: "No behavior-changing rules are affected; review is still recommended.", "");
|
|
88
|
+
for (const c of changes) {
|
|
89
|
+
lines.push(`## ${c.source.title} (${c.source.provider})`, "", `- Source: ${c.source.url}`, `- Change: ${c.kind}${c.detail ? ` (${c.detail})` : ""}`);
|
|
90
|
+
for (const [label, list] of [["Added sections", c.added], ["Removed sections", c.removed], ["Modified sections", c.modified]]) {
|
|
91
|
+
if (list.length)
|
|
92
|
+
lines.push(`- ${label}: ${list.map((h) => untrusted(h, 120)).join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
const rules = affectedRules(pack, c.source.id);
|
|
95
|
+
lines.push(`- Affected rules: ${rules.length ? rules.map((r) => `\`${r.id}\`${r.behaviorChanging ? " (changes output)" : ""}`).join(", ") : "none (tracked only)"}`);
|
|
96
|
+
const drafts = classifications.filter((x) => x.sourceId === c.source.id);
|
|
97
|
+
if (drafts.length) {
|
|
98
|
+
lines.push("- Draft classification (LLM, not authoritative):");
|
|
99
|
+
for (const d of drafts)
|
|
100
|
+
lines.push(` - ${untrusted(d.section, 120)}: **${d.classification}**: ${untrusted(d.rationale)} Suggested: ${untrusted(d.suggestedAction)}`);
|
|
101
|
+
}
|
|
102
|
+
lines.push("");
|
|
103
|
+
}
|
|
104
|
+
lines.push("## Next steps", "", "1. Read each changed source.", "2. If a rule must change, edit `src/guidelines/rule-pack.json`, bump `version`, add a changelog entry (`reviewedBy`, new `contentHash`).", "3. Run `npm test` (golden outputs show the effect) and release.", "4. Then run the monitor with `--update` to accept the new snapshots.", "");
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Fetched headings and LLM output are untrusted: shown as inline code so they
|
|
109
|
+
* can't become links, images, HTML or @-mentions in the issue.
|
|
110
|
+
*/
|
|
111
|
+
export function untrusted(text, max = 300) {
|
|
112
|
+
const one = text.replace(/\s+/g, " ").replace(/`/g, "'").trim();
|
|
113
|
+
return `\`${one.length > max ? `${one.slice(0, max)}…` : one}\``;
|
|
114
|
+
}
|
|
115
|
+
export function affectedRules(pack, sourceId) {
|
|
116
|
+
return pack.rules.filter((r) => r.sources.includes(sourceId));
|
|
117
|
+
}
|
|
118
|
+
function hash(text) {
|
|
119
|
+
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
120
|
+
}
|
|
121
|
+
function normalize(text) {
|
|
122
|
+
return text.replace(/\s+/g, " ").trim();
|
|
123
|
+
}
|
|
124
|
+
function decode(text) {
|
|
125
|
+
return text.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
126
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule pack: provider facts the framework relies on, each tied to official
|
|
3
|
+
* sources. Changing it is a reviewed release (see docs/GUIDELINES.md); the
|
|
4
|
+
* monitor only reports source changes and never edits it.
|
|
5
|
+
*/
|
|
6
|
+
export interface Source {
|
|
7
|
+
id: string;
|
|
8
|
+
provider: string;
|
|
9
|
+
title: string;
|
|
10
|
+
url: string;
|
|
11
|
+
category: string;
|
|
12
|
+
cadence: "daily" | "weekly";
|
|
13
|
+
/** "manual": the page can't be read automatically (e.g. client-rendered). */
|
|
14
|
+
monitor?: "manual";
|
|
15
|
+
}
|
|
16
|
+
export interface Rule {
|
|
17
|
+
id: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
behaviorChanging: boolean;
|
|
20
|
+
sources: string[];
|
|
21
|
+
implementedIn: string[];
|
|
22
|
+
data?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export interface ChangelogEntry {
|
|
25
|
+
version: string;
|
|
26
|
+
date: string;
|
|
27
|
+
summary: string;
|
|
28
|
+
reviewedBy: string;
|
|
29
|
+
contentHash: string;
|
|
30
|
+
}
|
|
31
|
+
export interface RulePack {
|
|
32
|
+
version: string;
|
|
33
|
+
released: string;
|
|
34
|
+
sources: Source[];
|
|
35
|
+
rules: Rule[];
|
|
36
|
+
changelog: ChangelogEntry[];
|
|
37
|
+
}
|
|
38
|
+
export declare const rulePack: RulePack;
|
|
39
|
+
export declare function ruleData<T>(id: string): T;
|
|
40
|
+
/** Hash of sources + rules (not the changelog), recorded in each changelog entry. */
|
|
41
|
+
export declare function packContentHash(pack?: RulePack): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import raw from "./rule-pack.json" with { type: "json" };
|
|
3
|
+
export const rulePack = raw;
|
|
4
|
+
export function ruleData(id) {
|
|
5
|
+
const rule = rulePack.rules.find((r) => r.id === id);
|
|
6
|
+
if (!rule?.data)
|
|
7
|
+
throw new Error(`Rule pack ${rulePack.version} has no data for rule "${id}".`);
|
|
8
|
+
return rule.data;
|
|
9
|
+
}
|
|
10
|
+
/** Hash of sources + rules (not the changelog), recorded in each changelog entry. */
|
|
11
|
+
export function packContentHash(pack = rulePack) {
|
|
12
|
+
return createHash("sha256").update(JSON.stringify({ sources: pack.sources, rules: pack.rules })).digest("hex");
|
|
13
|
+
}
|