File size: 6,760 Bytes
b7c46f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
-- Intel store: Phase 0 + Phase 1 of the predictive business-intelligence system
-- (docs/blueprint/intelligence-system.md, ADR-0017..0021).
--
-- Portable SQL: runs unchanged on SQLite (Phase 1, local β€” ADR-0020) and on
-- Postgres/Supabase when the store moves. The price of that portability:
-- timestamps are ISO-8601 UTC text, booleans are 0/1 integers, JSON is text, ids
-- are generated in Python (no serial/uuid defaults). Revisit all three when the
-- store moves to Postgres for good.
--
-- Applied by `py -m intel.cli init` (idempotent β€” every statement is IF NOT EXISTS).

-- ─── Gazetteer ──────────────────────────────────────────────────────────────
-- Everything in the store is keyed by location, so this comes first. Ids are
-- dotted paths: 'ng', 'ng.lagos', 'ng.lagos.ikeja'. State/LGA names are exactly
-- the ones users pick in onboarding (data/nigeria-locations.ts β†’ nigeria_locations.json).
create table if not exists locations (
  id         text primary key,
  level      text not null check (level in ('country', 'state', 'lga', 'town')),
  name       text not null,
  parent_id  text references locations(id),
  lat        real,
  lon        real,
  source     text not null
);

-- Normalised alternative names β†’ location. Seeded from the gazetteer itself,
-- boundary-file spellings, a short curated city list, and place-data localities
-- (intel/locations.py). The seed of the Layer 5 search/geocoding service.
create table if not exists location_aliases (
  alias        text not null,
  location_id  text not null references locations(id),
  origin       text not null,   -- 'gazetteer' | 'boundary' | 'curated' | 'places'
  primary key (alias, location_id)
);

-- ─── Layer 1: stale awareness ───────────────────────────────────────────────
-- One row per external source. "Stale" means late against the source's OWN
-- cadence, and two dates are tracked because a fetch can succeed today and still
-- return last quarter's numbers.
create table if not exists sources (
  id                      text primary key,
  family                  text not null,
  provider                text not null,
  licence                 text not null,
  expected_cadence_hours  integer not null,
  latency_hours           integer not null default 0,  -- how far behind "now" the newest data legitimately is
  last_attempt_at         text,
  last_success_at         text,
  latest_data_period      text,                         -- newest period the source has given us
  last_error              text
);

-- Every layer reports "done" and the receiving layer acknowledges it. Layer 1
-- flags reports nobody received.
create table if not exists handoffs (
  id           text primary key,
  run_id       text not null,
  from_layer   text not null,
  to_layer     text not null,
  report       text not null,   -- JSON
  produced_at  text not null,
  received_at  text,
  received_by  text
);
create index if not exists handoffs_pending on handoffs (to_layer, received_at);

-- ─── Layer 2: data stream ───────────────────────────────────────────────────
-- A stream is one metric from one source, stored at its NATIVE resolution
-- (never copied down to finer locations β€” that would fake precision the models
-- would then learn from).
create table if not exists streams (
  id            text primary key,   -- 'weather.precipitation_sum'
  family        text not null,
  metric        text not null,
  unit          text not null,
  kind          text not null check (kind in ('driver', 'indicator')),
  native_level  text not null check (native_level in ('country', 'state', 'lga', 'town', 'point')),
  cadence       text not null check (cadence in ('daily', 'weekly', 'monthly', 'quarterly', 'yearly', 'event')),
  source_id     text not null references sources(id)
);

-- Numeric time series. Observed history and forecasts share one table:
-- history has is_forecast=0 and issued_at=''; each forecast run keeps its own
-- rows (issued_at = run date) so forecast skill can be measured later.
create table if not exists observations (
  stream_id     text not null references streams(id),
  location_id   text not null references locations(id),
  period_start  text not null,
  is_forecast   integer not null default 0 check (is_forecast in (0, 1)),
  issued_at     text not null default '',
  value         real not null,
  ingested_at   text not null,
  primary key (stream_id, location_id, period_start, is_forecast, issued_at)
);
create index if not exists observations_loc on observations (location_id, stream_id, period_start);

-- Things that happen rather than values that move: a policy enacted, a holiday,
-- an incident, a project gazetted. Kept permanently β€” this is the history the
-- correlation step and models learn from (unlike news_items, which expire from
-- the user-facing feed).
create table if not exists events (
  id           text primary key,
  family       text not null,   -- policy | weather | infrastructure | funding | security | calendar | other
  type         text not null,   -- e.g. 'public_holiday', 'religious_holiday', 'season', 'news'
  location_id  text references locations(id),
  starts_on    text not null,
  ends_on      text,
  title        text not null,
  detail       text,
  is_estimate  integer not null default 0 check (is_estimate in (0, 1)),
  source_id    text not null references sources(id),
  source_ref   text not null,   -- natural key at the source, so re-ingest is idempotent
  ingested_at  text not null,
  unique (source_id, source_ref)
);
create index if not exists events_loc on events (location_id, starts_on);

-- Points of interest. subsector is one of backend/news/subsectors.json, or null
-- for a context place (worship, civic, landmark) β€” those still matter as
-- footfall generators even though they aren't competitors.
create table if not exists places (
  id                text primary key,   -- Overture GERS id
  name              text,
  category          text,
  taxonomy          text,
  subsector         text,
  confidence        real,
  operating_status  text,
  lat               real not null,
  lon               real not null,
  location_id       text references locations(id),   -- the LGA it falls in
  locality          text,
  source_id         text not null references sources(id),
  release           text not null
);
create index if not exists places_loc on places (location_id, subsector);