WebSensor — repository guide
WebSensor (www.websensor.io) is a real-time web intelligence platform: a global sensor network for the changing
Web. It monitors official public sources, detects meaningful changes, classifies them semantically, links
entities, scores importance / confidence / novelty / impact / velocity / anomaly into a WebSensor Signal Score,
preserves immutable evidence, clusters signals into events with propagation timelines, and publishes them through
a replayable WebSocket feed. Product brief: docs/PRODUCT-BRIEF.md; architecture: docs/ARCHITECTURE.md. This file
is the working guide for the code (v0.3, 2026-09-13: Source Factory + Global Observation Coverage Score).
Layout (pnpm workspace, TypeScript ESM, Node ≥ 22.15)
packages/core— taxonomy (event types incl. cyber/finance/government/health/transport classes, event groups, silent-eligible types, change classes, cluster states, countries), ids, SSRF policy (assertUrlAllowed,safeLookup), hashing, canonical extraction, diff engines (text / json / keyed list), stage-1 heuristics (evaluateChange,describeChange,humanizeUrl), semantic diff (semantic.ts:classifyChange→ cosmetic / navigation / timestamp / advertisement / boilerplate vs meaningful / pricing / policy / product / personnel;extractFieldChanges→ field-level before → after pairs with % deltas), scoring (scoring.ts: importance components, confidence, impact, velocity, WebSensor Signal Score with explainable reasons,breakingState, entity rank, daily anomaly), adaptive schedule, search syntax (search.ts:entity:type:after:7dsilent:trueimportance:>70country:CA…), registry seed schema (registry-schema.ts, shared by engine, validator and the admin import endpoint).@websensor/core/clientis the browser-safe subset.packages/db— plain SQL migrations (migrations/*.sql, applied bymigrate()with an advisory lock) + Drizzle schema.0005_intelligence.sqladded provenance (first_party / country / language / kind / owner_token on sources), sensor lifecycle (status,priority,validated_at), semantic class + field changes on changes, signal/velocity/impact/anomaly scores, fingerprints, score reasons on events, cluster propagation columns,entity_daily,source_daily,bookmarks,saved_views, alert channel config, notification delivery.0006_backfill_states.sqlbackfills them.textArray()must be used for= any(...)/&&with JS arrays.packages/store— content-addressed blob store (sha256/ab/cd/<hash>.zst). Raw bodies, canonical forms and full diffs live there, never in Postgres.packages/connectors— connector SDK (fetch/normalize), safe fetcher (conditional GET, per-hop SSRF checks, HTTP/2→1.1 pin, browser-UA retry, UTF-16 BOM transcoding,postJsonfor signed webhooks), 16 connector families (http rss sitemap statuspage statusjson github jsonlist package edgar openapi csv pdf dns tls headers rdap) +discovery+scrapflyfallback.jsonlist.urlTemplatesupports{key}(path separators kept) and{field.path}.- Source Factory (
apps/engine/src/factory/, processapps/engine/src/factory-main.ts= PM2websensor-factory) — the system grows by itself: coverage universes (config/coverage/*.yaml, one sector per file, members = organizations with a verified domain +hints) andconfig/factory/seeds/*.yamlbecomefactory_seeds;runFactoryBatch()claims seeds (systemic first) and runs deep discovery (packages/connectors/src/discovery-deep.ts: robots → sitemaps → feeds incl. official sub-domains → navigation-classified pages (news/press/IR/changelog/security/pricing/legal/careers/ leadership/docs) → status-page providers → GitHub org repos → EDGAR (cik) → Hugging Face (hf_author) → OpenAPI → posture; every candidate is fetched and parsed, wildcard DNS detected, feeds/pages de-duplicated by content) →score.ts(evidence × page-class weight × organization importance × first-party confidence × change frequency − fetch cost, explainable reasons; caps per organization/class) → shadow sensors (sensors.status = 'SHADOW',config.factory/shadow/kind/seedId, priority 3: polled, snapshots + changes stored, never published; the pipeline andhandleMissingskip event creation) →shadow.tsevaluateShadows()every 15 min (reject: ≥ 50 % errors / all changes noise / duplicate content of an active sensor; accept after ≥ 5 checks and 24 h →ACTIVEwith its real priority; defer otherwise, max 120 h). Sources created by the factory carryorigin = 'factory'+sector; attached ones reuse the registry source (match by registrable domain,coverageKey()). Candidates and decisions live indiscovery_candidates(status candidate / shadow / accepted / rejected / duplicate,reason,score); funnel infactory_daily; heartbeat Redisws:factory:status.cli.ts expand <domain> [--hints=json]= dry run;cli.ts factory seed|run|evaluate|stats|export|requeue;factory exportwrites a reviewable YAML fragment (graduating accepted sensors intoconfig/sources.d/hands them to the registry sync). - Coverage (
apps/api/src/coverage.ts,GET /api/v1/coverage[/:sector], pages/coverage,/coverage/[sector], homepage rail) — Global Observation Coverage Score: per sectorbreadth(importance-weighted share of universe members with ≥ 1 active non-shadow sensor on their registrable domain),depth(min(1, sensors/5)),score = 100·(0.7·breadth + 0.3·depth), global = sector-weight-weighted mean. Universes are the denominator (withprovenance), never the registry. Validator:apps/engine/src/coverage-validate.ts [file] [--dns]. Guide:docs/registry/COVERAGE.md. Schema shared inpackages/core/src/coverage-schema.ts(+registrableDomain()). apps/engine— scheduler (priority-awareFOR UPDATE SKIP LOCKEDclaims, global + per-host concurrency, domain circuit breaker, heartbeat to Redisws:engine:status), pipeline (fetch → normalize → snapshot → diff → heuristics + semantic class → change → event: fingerprint idempotency, entity resolution with ambiguous-alias rules, novelty, optional Claude interpretation, impact / anomaly / signal scores, silent-change bar, clustering with propagation timeline + lead time + breaking state, entity/source daily counters, alert evaluation + webhook delivery, Redis stream + pub/sub), registry sync (config/sources.yaml+config/sources.d/), discovery, connector health rollups, retention (retention.ts), Prometheus metrics :8262. CLI:src/cli.ts(sync,discover,probe,expand,run-once,run-due,relink-entities [days],prune-blobs,refresh-clusters,llm-test,factory …);src/validate.ts(registry validator);src/coverage-validate.ts.apps/api— Fastify gateway (:8260): REST/api/v1/*(routes.tspublic,routes-user.tsowner-scoped: watchlists / alerts (+webhook) / notifications / bookmarks / saved views / custom monitors,routes-admin.tsbehindWS_ADMIN_TOKEN: ops, sensor & source actions, connector test, bulk import), intelligence read-models (intel.ts: breaking desk, pulse, radar, entity insights, rankings, cluster detail, country & category desks), TTL cache (cache.ts), WebSocket/api/v1/liveprotocol 2 (live.ts:sidon every frame,{"since"}replay from the durable stream, channelsevents:* group:* country:* state:* type:* entity:* source:* watchlist:*),/api/v1/feed.rss,/api/health|ready|metrics, apex→www redirect, reverse proxy to Next.apps/web— Next.js 16 (:8261 loopback). Design system incomponents/ui.tsx(Panel, Badge, Score, Tabs, Sparkline, Heatmap, Skeleton…),prefs.tsx(density compact/normal/comfortable, pause),event-drawer.tsx(intelligence panel),command-palette.tsx(⌘K),live-strip.tsx,live-feed.tsxv2 (URL-synced filters, pause, "↑ N new events", replay),field-changes.tsx. Pages://live/breaking/silent/pulse/radar/explore/entity/[id](/companyredirects)/cluster/[slug]/source/[id]/sensor/[id]/category/[c]/country/[slug]/event/[slug](+ OG image)/coverage/coverage/[sector]/bookmarks/watchlists/alerts/monitors/ops(+ Source Factory panel: funnel, sectors, candidates, accept/reject)/health/api.config/sources.yaml(founding registry) +config/sources.d/*.yamlfragments merged in file-name order (extend: trueadds to an earlier source). Fragments 10–35 = site classes; 40–46 (2026-09-11) = depth: AI frontier, cloud infrastructure, cybersecurity, finance & markets, governments, science & health, transport / telecom / sports / news; 47–54 (2026-09-11, wave 2) = breadth: open-source long tail, SaaS status/changelogs, EDGAR issuers, Federal Register agencies, cities/regions/public bodies, world governments & regulators, corporate pricing/legal/careers pages, sports clubs/entertainment/education, media long tail & think tanks (generatorsscripts/gen-*.py,scripts/prune-fragment.py). Seeds may carrycountry:(ISO-2,EU,INT),language:andfirst_party: false(media). Guide:docs/registry/AUTHORING.md. Validator:node node_modules/tsx/dist/cli.mjs apps/engine/src/validate.ts <fragment> [--all] [--json report.json]— nothing enters the registry without OK.
Rules
- Every URL the engine touches — seeds, discovered candidates, redirects, Scrapfly targets, webhooks, custom
monitors — goes through
assertUrlAllowed(). Private ranges, metadata endpoints,.maclustr.io/.ts.net, single-label hosts and NAT64-embedded private IPv4 are blocked. - Raw evidence is immutable: snapshots and diffs are never rewritten. Retention may drop the RAW body of
snapshots that are not referenced by any event (canonical form and hashes stay); event snapshots are kept forever.
Re-interpretation creates a new
interpretationsrow. - Noise never becomes an event: changes classified cosmetic / navigation / timestamp / advertisement /
boilerplate are stored as changes only. Routine batches from firehose feeds are damped. Ingestion is idempotent
(
events.fingerprint= sensor + before/after canonical hashes). - Silent change = first-party source · silent-eligible type (pricing, terms, policy, API, availability, docs,
shutdown, feature removed, leadership, page removed, crawler policy…) · no matching announcement within 12 h ·
importance ≥
WS_SILENT_MIN_IMPORTANCE· not a noise class. Never asserted as "unannounced" otherwise. - Never label inference as fact:
evidence_labelOBSERVED / INFERRED / CONFIRMED / UNCONFIRMED; AI text is labelled analysis;score_reasonsexplain every signal score. First-party evidence outweighs media reports (sources.first_party, media seeds carryfirst_party: false). - Entity aliases that are ordinary words (
first,has,who,make…) only match as exact upper-case acronyms in text (AMBIGUOUS_ALIASESinapps/engine/src/entities.ts); runcli.ts relink-entities 7after changing alias rules. - LLM cost control: heuristics first; Claude only above
WS_LLM_MIN_IMPORTANCE, daily budget, strict JSON output;llm: falseon firehoses (news, arXiv, NVD, package streams). No key → the system still works. - Feeds: items are "new" only if never seen and < 14 days old; >50 % list shrink = partial response; 404 becomes
page_removedafterWS_DELETE_CONFIRMATIONSseparated checks. - Shadow is not production:
status = 'SHADOW'sensors never create events, never count in public stats (stats.sensors, coverage) and never compete with production sensors (priority 3). Nothing the Factory finds is published beforeevaluateShadows()(or an operator) accepts it. Public source pages show shadow sensors with a SHADOW pill. - Coverage universes are lists of real organizations with verified domains (
--dns), provenance and retrieval dates — never guessed domains; several files may share a sector key (universes are merged). - Admin API is off unless
WS_ADMIN_TOKENis set; custom monitors are limited per owner and never appear in public feeds (sources.kind = 'custom'). - All timestamps UTC. Ids are prefixed (
src_,sen_,snap_,chg_,evt_,clu_,ent_,wl_,alr_…).
Dev
createdb websensor && cp .env.example .env
pnpm install
pnpm db:migrate # or let the engine migrate on start
npx tsx apps/engine/src/cli.ts sync # registry → DB
npx tsx apps/engine/src/cli.ts run-due 50
pnpm dev:api · pnpm dev:engine · pnpm dev:web # open the site through the gateway (:8260) so /api is same-origin
pnpm test · pnpm typecheckRun CLI/engine from the repo root. A copy of production data for UI work:
ssh M4M64b 'pg_dump websensor --data-only -t events -t …' | psql websensor then cli.ts sync.
Deploy (MacLustr)
mld stage . websensor && mld deploy websensor --node M4M64b. Manifest M1M32:~/dispatch/apps/websensor.json
(secrets incl. WS_ADMIN_TOKEN; gitignored copy deploy/websensor.mld.json). Processes: websensor-api (:8260,
public via MacLustr Tunnel www.websensor.io on BHS64), websensor-web (:8261 loopback), websensor-engine
(metrics :8262), websensor-factory (Source Factory, heartbeat ws:factory:status; post-sync hook factory seed --mode=hinted
re-seeds from the coverage universes at every deploy). Postgres 17 websensor + Redis local. Blob store ~/websensor-data/blobs. After a deploy that
changes alias or scoring rules: cli.ts relink-entities 7 and cli.ts refresh-clusters on the node. See
deploy/README.md.