import { z } from "zod"; import { TIERS } from "./taxonomy"; /** * Coverage universes (`config/coverage/*.yaml`). A universe is the list of organizations WebSensor *should* * observe for a sector — the denominator of the Global Observation Coverage Score. The same files feed the * Source Factory: every member without a monitored domain becomes an expansion seed. * * One file per sector: * * sector: ai * label: Artificial intelligence * weight: 1.2 # weight of the sector in the global score (default 1) * categories: [ai, technology] # default categories applied to members (registry vocabulary) * tier: B # default tier for factory-created sources * universes: * - key: frontier-labs * label: Frontier labs & model providers * members: * - { name: OpenAI, domain: openai.com, country: US, importance: 3, hints: { github_org: openai, hf_author: openai } } */ export const memberHintUrlSchema = z.object({ url: z.string().url(), /** what the page is: news | press | blog | ir | filings | changelog | releases | security | pricing | legal | careers | docs | api | status | models | data | other */ kind: z.string().default("other"), connector: z.string().optional(), type: z.string().optional(), tier: z.enum(TIERS).optional(), config: z.record(z.string(), z.unknown()).optional(), name: z.string().optional(), }); export const memberHintsSchema = z.object({ /** SEC CIK (digits, any length) → EDGAR submissions sensor */ cik: z.union([z.string(), z.number()]).optional(), ticker: z.string().optional(), exchange: z.string().optional(), /** GitHub organization → most active repositories' releases */ github_org: z.string().optional(), /** explicit repositories `owner/name` */ github_repos: z.array(z.string()).optional(), /** Hugging Face author/organization → model list sensor */ hf_author: z.string().optional(), /** status page root (Atlassian Statuspage, Instatus, incident.io, Status.io) */ status_url: z.string().url().optional(), /** additional hosts that belong to the organization (blog.example.org, investors.example.org…) */ hosts: z.array(z.string()).optional(), /** explicit candidate URLs (validated by the factory before becoming sensors) */ urls: z.array(memberHintUrlSchema).optional(), /** probe robots/TLS/DNS/headers posture sensors (default: only for importance 3) */ posture: z.boolean().optional(), /** Wikidata QID, for provenance */ wikidata: z.string().optional(), }); export const coverageMemberSchema = z.object({ name: z.string().min(1), /** bare registrable domain, lower-case, no scheme, no `www.` */ domain: z .string() .min(3) .transform((d) => d.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "")), country: z.string().regex(/^[A-Z]{2,3}$/).optional(), language: z.string().regex(/^[a-z]{2}$/).optional(), /** 1 = long tail · 2 = notable (default) · 3 = systemically important */ importance: z.union([z.literal(1), z.literal(2), z.literal(3)]).default(2), aliases: z.array(z.string()).default([]), categories: z.array(z.string()).optional(), tier: z.enum(TIERS).optional(), first_party: z.boolean().optional(), hints: memberHintsSchema.default({}), note: z.string().optional(), }); export type CoverageMember = z.infer; export const coverageUniverseSchema = z.object({ key: z.string().regex(/^[a-z0-9][a-z0-9-]*$/), label: z.string(), description: z.string().optional(), /** provenance of the list (URL of the index constituents page, Wikidata query…) */ provenance: z.string().optional(), members: z.array(coverageMemberSchema), }); export type CoverageUniverse = z.infer; export const coverageSectorSchema = z.object({ sector: z.string().regex(/^[a-z0-9][a-z0-9-]*$/), label: z.string(), description: z.string().optional(), weight: z.number().positive().default(1), categories: z.array(z.string()).default([]), tier: z.enum(TIERS).default("B"), universes: z.array(coverageUniverseSchema), }); export type CoverageSector = z.infer; /** * Registrable domain (eTLD+1) with a compact public-suffix list covering the multi-label suffixes that * matter for official sources (governments, universities, companies). Good enough to match a sensor host * (`ir.example.com`, `www.gov.uk`) to a universe member; not a full PSL. */ const MULTI_SUFFIX = new Set([ "co.uk", "org.uk", "gov.uk", "ac.uk", "nhs.uk", "police.uk", "ltd.uk", "plc.uk", "me.uk", "net.uk", "sch.uk", "com.au", "net.au", "org.au", "gov.au", "edu.au", "id.au", "asn.au", "gc.ca", "qc.ca", "on.ca", "bc.ca", "ab.ca", "mb.ca", "sk.ca", "ns.ca", "nb.ca", "nl.ca", "pe.ca", "nt.ca", "nu.ca", "yk.ca", "gouv.qc.ca", "co.jp", "ne.jp", "or.jp", "go.jp", "ac.jp", "ad.jp", "lg.jp", "ed.jp", "co.kr", "go.kr", "or.kr", "ac.kr", "re.kr", "ne.kr", "com.cn", "gov.cn", "org.cn", "edu.cn", "net.cn", "ac.cn", "com.hk", "gov.hk", "org.hk", "edu.hk", "com.tw", "gov.tw", "org.tw", "edu.tw", "com.sg", "gov.sg", "org.sg", "edu.sg", "co.in", "gov.in", "nic.in", "org.in", "ac.in", "net.in", "res.in", "edu.in", "com.br", "gov.br", "org.br", "edu.br", "net.br", "leg.br", "jus.br", "mil.br", "com.mx", "gob.mx", "org.mx", "edu.mx", "com.ar", "gob.ar", "gov.ar", "org.ar", "edu.ar", "gob.cl", "gov.cl", "cl", "gob.pe", "gob.es", "gov.co", "gob.ec", "gob.bo", "gub.uy", "gob.pa", "gob.do", "gob.gt", "gob.hn", "gob.sv", "gob.ni", "gob.cu", "gob.ve", "gob.py", "co.za", "gov.za", "org.za", "ac.za", "net.za", "co.nz", "govt.nz", "org.nz", "ac.nz", "net.nz", "com.tr", "gov.tr", "org.tr", "edu.tr", "co.il", "gov.il", "org.il", "ac.il", "muni.il", "gov.ae", "ac.ae", "co.ae", "gov.sa", "com.sa", "edu.sa", "org.sa", "gov.qa", "com.qa", "gov.kw", "gov.bh", "gov.om", "gov.jo", "gov.lb", "gov.eg", "com.eg", "gov.ma", "gov.dz", "gov.tn", "gov.ng", "com.ng", "org.ng", "go.ke", "co.ke", "or.ke", "ac.ke", "gov.gh", "com.gh", "gov.et", "gov.rw", "go.tz", "co.tz", "go.ug", "co.ug", "gov.zm", "gov.zw", "gov.bw", "gov.na", "gov.mz", "gov.sn", "gouv.sn", "gouv.ci", "gouv.bf", "gouv.ml", "gouv.ne", "gouv.tg", "gouv.bj", "gouv.cm", "gov.mu", "com.my", "gov.my", "org.my", "edu.my", "go.th", "co.th", "or.th", "ac.th", "gov.vn", "com.vn", "edu.vn", "gov.ph", "com.ph", "org.ph", "edu.ph", "go.id", "co.id", "or.id", "ac.id", "gov.bd", "com.bd", "gov.pk", "com.pk", "edu.pk", "gov.lk", "gov.np", "gov.kh", "gov.la", "gov.mm", "gov.bn", "gov.mn", "gov.kz", "gov.uz", "gov.kg", "gov.tj", "gov.tm", "gov.az", "gov.ge", "gov.am", "gov.by", "gov.ua", "com.ua", "org.ua", "edu.ua", "gov.md", "gov.ru", "com.ru", "org.ru", "edu.ru", "ac.ru", "gov.pl", "com.pl", "org.pl", "edu.pl", "gov.cz", "gov.sk", "gov.hu", "gov.ro", "gov.bg", "gov.hr", "gov.si", "gov.rs", "gov.ba", "gov.mk", "gov.al", "gov.me", "gov.lv", "gov.lt", "gov.ee", "gov.gr", "gov.cy", "gov.mt", "gov.ie", "gov.pt", "gov.it", "gouv.fr", "gov.lu", "gov.be", "gov.nl", "gov.is", "gov.li", "com.es", "org.es", "edu.es", "gob.es", "com.pt", "edu.pt", "co.it", "edu.it", "com.gr", "edu.gr", "com.pl", "com.ro", "com.tr", "europa.eu", "canada.ca", "admin.ch", "bund.de", "edu.mo", "gov.mo", "ac.ir", "gov.ir", "edu.qa", "edu.lb", "edu.bn", "edu.kw", "edu.jo", "edu.om", "edu.eg", "ac.za", "edu.gh", "edu.ng", "ac.ke", "ac.tz", "ac.ug", "edu.et", "com.co", "edu.co", "org.co", "net.co", "gov.co", "gov.ky", "gov.bm", "gov.je", "gov.gg", "gov.uk", "parliament.uk", "judiciary.uk", "mod.uk", "nhs.uk", "royal.uk", "state.gov", "mil", "fed.us", "nsn.us", "k12.us", "lib.us", "dni.us", "isa.us", "kids.us", "ac.at", "co.at", "gv.at", "or.at", "ac.be", "co.nl", "ac.nz", "ac.uk", "ac.jp", "edu.au", "edu.sg", "edu.hk", "org.au", "com.au", "co.ke", ]); /** Generic second-level labels under country-code TLDs (`gov.py`, `go.cr`, `gouv.ht`, `ac.ir`, `co.zw`…): institutions there are distinct organizations. */ const CC_SECOND_LEVEL = new Set(["gov", "gouv", "gob", "go", "gv", "govt", "gub", "edu", "ac", "co", "com", "org", "net", "or", "ne", "mil", "sch", "nic", "res", "ltd", "plc", "me", "id", "asn", "lg", "ed", "ad", "re", "muni", "jus", "leg", "int", "info", "biz", "web", "pro", "nom", "ind", "firm", "gen", "per", "mod", "nhs", "police", "parliament", "judiciary", "govmu"]); export function registrableDomain(host: string): string { const h = host.toLowerCase().replace(/\.$/, "").replace(/^www\./, ""); const parts = h.split("."); if (parts.length <= 2) return h; const last2 = parts.slice(-2).join("."); const last3 = parts.slice(-3).join("."); if (MULTI_SUFFIX.has(last3) && parts.length >= 4) return parts.slice(-4).join("."); if (MULTI_SUFFIX.has(last2)) return last3; const tld = parts[parts.length - 1]!; if (tld.length === 2 && CC_SECOND_LEVEL.has(parts[parts.length - 2]!)) return last3; return last2; } /** Normalize a member/source domain to its comparison key. */ export function coverageKey(domain: string): string { return registrableDomain(domain.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "")); }