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, links entities, scores
importance, preserves evidence and publishes events through a WebSocket feed. The product brief that drove the
design is docs/PRODUCT-BRIEF.md; this file is the working guide for the code.
Layout (pnpm workspace, TypeScript ESM, Node ≥ 22.15)
packages/core— taxonomy (event types + intrinsic severity, tiers, categories), ids, SSRF policy (assertUrlAllowed,safeLookup), hashing (sha256, simhash, shingles/Jaccard), canonical extraction (canonicalizeHtml: strips scripts/nav/footer/cookie chrome, timestamps, tokens, counters), diff engines (text / json / keyed list), stage-1 heuristics (evaluateChange,describeChange), scoring (importance components, confidence, novelty helpers, trending, activity anomaly), adaptive schedule.@websensor/core/clientis the browser-safe subset (nonode:imports) — client components must import it.packages/db— plain SQL migrations (migrations/*.sql, applied bymigrate()with an advisory lock) + Drizzle schema for typed access.textArray()must be used for= any(...)/&&with JS arrays (Drizzle spreads arrays into parameter lists).packages/store— content-addressed blob store (sha256/ab/cd/<hash>.zst, zstd vianode:zlib). Raw bodies, canonical representations and full diffs live there, never in Postgres. Interface ready for S3/MinIO.packages/connectors— connector SDK (WebSensorConnector.fetch/normalize), safe fetcher (conditional GET, manual redirects validated per hop, HTTP/2→1.1 pin on NGHTTP2 errors, browser-UA second attempt on 403/resets, size + time limits), connectors:http(HTML/JSON/text/HEAD),rss(RSS/Atom/RDF/JSON Feed),sitemap(index + news),statuspage(Atlassian v2 summary),github(releases/tags/commits Atom, advisories REST),jsonlist(keyed records from any JSON API;{now-2h}placeholders),discovery(robots sitemaps,<link rel=alternate>, well-known feed paths, linked status pages — every candidate is fetched and parsed),scrapfly(fallback, budgeted, only whenfallback.scrapflyis set on the source).apps/engine— scheduler (FOR UPDATE SKIP LOCKEDclaims, global + per-host concurrency), pipeline (fetch → normalize → snapshot → diff → heuristics → change → event: entities, novelty, LLM interpretation, clustering, importance/confidence, silent-change detection, publish to Redis), registry sync fromconfig/sources.yaml, discovery, connector health rollups, Prometheus metrics on :8262. CLI:src/cli.ts(sync,discover,probe <domain>,run-once <sensor>,run-due [n]).apps/api— Fastify gateway (:8260): REST/api/v1/*, WebSocket/api/v1/live(Redis pub/sub fan-out, channelsevents:*,entity:*,source:*,watchlist:*),/api/v1/feed.rss,/api/health/api/ready/api/metrics, apex→www redirect, and a reverse proxy to the Next.js app for everything else.apps/web— Next.js 16 frontend (:8261, loopback). Pages: live, breaking, explore, sources, entities, timelines, silent changes, watchlists, alerts, event detail with diff viewer, health, API docs.config/sources.yaml— the 200-organization registry (sources, curated sensors, product entities, discovery flags, fallbacks).docs/connectors/*.mddocument each connector family.
Rules
- Every URL the engine touches — seeds, discovered candidates, redirects, Scrapfly targets — goes through
assertUrlAllowed(). Private ranges, metadata endpoints,.maclustr.io/.ts.netand single-label hosts are blocked; the dispatcher's DNS lookup only returns approved addresses. - Raw evidence is immutable: snapshots and diffs are never rewritten. Re-interpretation creates a new row in
interpretations(versioned);events.interpretationholds the latest. - LLM cost control: heuristics first; Claude is called only for candidates above
WS_LLM_MIN_IMPORTANCE, with a daily call budget; the deep model only aboveWS_LLM_DEEP_MIN_IMPORTANCE. Output is strict JSON (output_config.format), never free text. No key → heuristics only, the system still works. - Feeds: items are "new" only if their key was never seen (sensor
state.seenKeys) and they are not older than 14 days; items scrolling out of the window are never "removed". Sitemaps/statuspages: a >50 % shrink is treated as a partial response, not mass deletion. A 404 becomespage_removedonly afterWS_DELETE_CONFIRMATIONSchecks separated byWS_DELETE_SEPARATION_MIN. - Never label inference as fact: events carry
evidence_label(OBSERVED / INFERRED / CONFIRMED / UNCONFIRMED) and the interpretation keepsobservedandinferredapart. Silent changes are flagged, never asserted as "unannounced" without checking recent announcement-type events of the same source. - Do not add sensors that are blocked with 403 by design (Akamai/Cloudflare bot management) unless the source
has
fallback.scrapfly: trueand a tier ≥ C; Scrapfly is a budgeted fallback, not the foundation. - All timestamps UTC. Ids are prefixed (
src_,sen_,snap_,chg_,evt_,clu_,ent_…).
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
pnpm test · pnpm typecheckRun CLI/engine from the repo root (config paths are relative to cwd).
Deploy (MacLustr)
mld stage . websensor && mld deploy websensor --node M4M64b. Manifest deploy/websensor.mld.json (secrets only
on M1M32). Processes: websensor-api (:8260, ngrok www.websensor.io), websensor-web (:8261 loopback),
websensor-engine (metrics :8262). Postgres 17 websensor + Redis local. Blob store ~/websensor-data/blobs.
See deploy/README.md.