spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1# Scoring — metrics, indices, baselines, signals, trends23Owner: `services/metrics.py` (`metrics-v1`, CCI `cci-v1`), `services/signals.py` (`signals-v1`), `services/trends.py` (`trends-v1`).4Parameters live in `taxonomy.METRIC_PARAMS` / `taxonomy.CCI_WEIGHTS` / `config.Settings`; **bump the formula version whenever they change**5(spec §177). Every `metrics_current` row stores `value`, `confidence`, `inputs` (the numbers the formula saw), `formula_version`, `computed_at`;6`metric_series` keeps one point per company/metric/day. **No inputs → no row** (never 0-as-measured).78Schedules: `metrics-hourly` (`CA_METRICS_CRON`, default `7 * * * *`) for companies with activity in the last `CA_METRICS_ACTIVE_WINDOW_DAYS` (90);9`daily-aggregates` (`CA_DAILY_CRON`, `20 0 * * *`) = catch-up of missed days + all-company recompute; `signals` hourly; `trends` at :25.10CLI: `catlas metrics [--all] [--company slug]`, `catlas daily [--day|--catch-up]`, `catlas signals`, `catlas trends`.1112Notation: `sat(x, k) = 1 − e^(−x/k)` (0–1, x=k → 0.63); `decay(age) = e^(−age/τ)`; ages in days.1314## Company metrics (`metrics_current`)1516| Metric | Formula | Inputs stored | Emitted when |17|---|---|---|---|18| `activity_score` (0–100) | `raw = Σ_changes w(kind)·decay(age, τ=10) + Σ_events importance·decay(age, τ)` over 30 d, `w` = meaningful 1 / major 2 / critical 3; `density = raw / active_sensors^0.5` (coverage normalisation); `score = 100·log1p(density)/log1p(6)` capped at 100 | changes_30d, events_30d, raw_changes, raw_events, active_sensors, density, τ, exponent, D_max | ≥ 1 active sensor or any activity |19| `open_jobs` | count of `jobs.status='open'` | jobs_new_30d, jobs_removed_30d, remote_ratio, by_country, by_department, ai_open | company has a careers/jobs_board sensor or any job rows |20| `hiring_momentum_{7,30,90}d` (%) | `(open_now − open_then)/open_then × 100`, `open_then` reconstructed from `first_seen_at ≤ t−N and (removed_at is null or > t−N)` | open_now, open_then, window (+ 30 d extras) | `open_then ≥ 3` (hiring_min_listings) — no % from 0 or 2 listings |21| `ai_adoption` (0–100) | components: jobs `min(1, ai_open/open × 2)` (w 0.5), events `sat(ai_events_90d, 3)` (w 0.25: AI_HIRING/AI_LAUNCH/tag `ai`), keywords `sat(hits, 5)` (w 0.25: `AI_KEYWORDS` in product names, 90 d news titles, latest snapshot titles); weights renormalised over available components | ai_open, open_jobs, ai_events_90d, keyword_hits, components, weights_used | any component has inputs |22| `product_velocity` | `100·sat(Σ importance·decay(age, 30), 6)` over 90 d PRODUCT + changelog/docs/API/SDK subtypes | events_90d, surfaces | products/changelog/docs/api/developer/services sensor or a PRODUCT event |23| `developer_momentum` | `100·sat(Σ DEVELOPER importance·decay + 0.5·meaningful dev-surface changes 30 d, 6)` | events_90d, meaningful_changes_30d, surfaces | docs/api/developer/changelog sensor or DEVELOPER event |24| `communication_activity` | `100·sat(max(news_30d, comm_events_30d) + 0.5·min(·), 8)` | news_items_30d, events_30d | newsroom/blog/feed/IR/research sensor or news rows |25| `pricing_activity` | `100·sat(Σ PRICING importance·decay + 0.25·pricing changes 30 d, 3)` | events_90d, meaningful_changes_30d | pricing sensor or PRICING event |26| `leadership_activity` | `100·sat(Σ LEADERSHIP importance·decay, 3)` | events_90d | leadership sensor or LEADERSHIP event |27| `geo_expansion` | `100·sat(new_locations_90d + 3·|new countries| + Σ COUNTRY_EXPANSION importance, 4)`; new countries = countries whose first location **or** first job appeared within 90 d | new_locations_90d, new_countries_90d, countries_listed, location_events_90d | locations/contact/about/careers sensor, location rows or jobs |28| `corporate_change_index` (`cci-v1`) | `Σ w_i·c_i / Σ w_i` over **available** components with `CCI_WEIGHTS` (hiring 0.25 · product 0.20 · geo 0.15 · leadership 0.15 · developer 0.10 · communication 0.10 · pricing 0.05); hiring momentum (%) mapped to `50 + clamp(m, −100, 100)/2`; confidence `0.4 + 0.6·weight_coverage` | components, weights, weight_coverage | ≥ 1 component |29| `anomaly_score` (z) | `(this_week_meaningful − mean) / max(stddev, 0.5)` against `baselines.meaningful_changes_weekly` | this_week, mean, stddev, samples | baseline with ≥ 4 samples |30| `historical_coverage` (0–100) | `100·(0.5·min(1, observed_ok/expected) + 0.3·continuity + 0.2·min(1, surfaces/8))`; `expected = Σ_sensors age_s / current_interval_s`; `continuity = days_with_observations / days_since_first_observed` | observed, expected, days_with_obs, days_since_first, surfaces | `first_observed_at` set and ≥ 1 sensor |3132Confidence conventions: activity `min(0.95, 0.5 + 0.05·sensors)`; hiring 0.9 with a structured jobs_board sensor else 0.75; AI adoption 0.633(inferred from public signals only); saturating scores 0.7; coverage 0.8.3435## Daily aggregates3637`compute_daily(day)` (UTC day, idempotent upserts):3839- `company_daily` — observations, changes, meaningful_changes, events (+ `events_by_type`), jobs_open / jobs_new / jobs_removed / jobs_ai_open at end of40 day (reconstructed from `first_seen_at` / `removed_at`), news_items, sensors_active. Only companies with activity that day get a row.41- `global_daily` — sums + `companies_active` / `sensors_active` (distinct in observations that day; falls back to active sensors when no observation42 rows exist), `events_by_type`, `jobs_open`, `by_country` / `by_industry` (`{companies, meaningful_changes, events}` per key) and the43 **Global Corporate Activity Index**: `ratio = meaningful_changes / sensors_active`; `baseline = mean(ratio)` over the trailing 28 days with44 sensors; `activity_index = ratio / baseline × 100` (baseline = 100). Null when no baseline exists yet (never fabricated).45- `baselines` — per company, weekly buckets of `company_daily` over `CA_BASELINE_WINDOW_DAYS` (56): `meaningful_changes_weekly`, `jobs_new_weekly`,46 `news_weekly` → population mean / stddev / samples. Requires ≥ 2 observed weeks. Consumed by `anomaly_score`, `HIRING_SURGE` and signals.47- `compute_daily_catch_up()` fills every day between the last computed day (or the first observation/change/event) and yesterday.4849Coverage normalisation (spec §146): activity uses per-sensor density, the global index divides by active sensors and compares to a trailing50baseline, trends divide mentions by active companies, and the CCI renormalises over available components — growth of the sensor network alone51must not move any index.5253## Signals (`signals`, `signals-v1`)5455Labelled as signals, with `strength` 0–1, `confidence`, `explanation`, `evidence` (event ids + metric values), `window_days`, `expires_at`56(`CA_SIGNALS_TTL_DAYS` = 14). One active row per (company, kind): re-detection updates it, disappearance sets `status='expired'`.5758| kind | trigger |59|---|---|60| `hiring_surge` | `hiring_momentum_30d ≥ +30 %` with ≥ 10 open, or a `HIRING_SURGE` event in the window |61| `hiring_freeze` | `hiring_momentum_30d ≤ −30 %` with ≤ 1 new listing, or a `HIRING_FREEZE_SIGNAL` event — worded "listings no longer visible" |62| `launch_buildup` | within 14 d ≥ 3 of {product, docs/API, changelog, careers, messaging} categories including product, or ≥ 4 categories — "Possible launch preparation signal" |63| `expansion` | `COUNTRY_EXPANSION` event, or new countries (locations or job countries, 90 d) with location events / new jobs |64| `pricing_migration` | ≥ 2 PRICING events in 30 d, or a tier added **and** a tier removed |65| `developer_push` | ≥ 3 DEVELOPER events in 30 d or `developer_momentum ≥ 60` |66| `enterprise_repositioning` | new contact-sales tier(s) in 30 d, or ≥ 2 events mentioning enterprise / SSO / audit logs / contact sales |67| `ai_acceleration` | ≥ 2 AI-tagged events in 30 d, or ≥ 25 % of ≥ 4 new listings are AI-related |68| `abnormal_activity` | `anomaly_score ≥ CA_ANOMALY_Z` (2.5) |6970Scope aggregates (`scope = industry | country`): ≥ 3 companies with the same active kind → one aggregate signal per (scope, key, kind) with the71company slugs as evidence; aggregates expire when the support drops below the threshold.7273## Trends (`trends`, `trends-v1`)7475Terms = lowercase 1–3-grams from event titles (prefix like "News release:" removed) and news titles of the day, company names removed,76stopwords excluded at n-gram edges, tokens ≥ 3 chars (unigrams ≥ 4). A term is kept for a day only when ≥ `CA_TRENDS_MIN_COMPANIES` (3) distinct77companies used it (`mentions`, `companies`). Momentum for a window W ∈ {7, 30, 90}: `rate = mentions / mean(companies_active)` over the window,78`momentum = (rate_now − rate_prev) / max(rate_prev, ε)`; snapshots are stored in `settings_kv['trends:momentum:<W>d']` for the API.79