The roster writes itself. The scores do not. - db/ingest/sources.mjs: GitHub and Flathub adapters. GitHub is ranked by stars (usage); Flathub is a curated store of end-user Linux apps, so relevance is free there and has to be earned on GitHub. - db/ingest/run.mjs: discovers into the roster as unassessed products, never as scores. Every discovery leaves a receipt (source, url, usage metric, timestamp) so a reader can see where a product came from and why it ranks. - LooksLikeUserSoftware gate: topic:messaging sorts RabbitMQ to the top because brokers are highly starred and are not things people install. - build.mjs no longer wipes the database. The DB authors itself, so a rebuild would have thrown the roster away. --fresh is now explicit. - v_backlog view: what to assess next, ordered by measured usage. - packages/web/public/oath.html: the Ranch Oath, with an ASCII Bible crest, bound to a SHA-256 of catalog.json. The page recomputes the digest in the browser and reports VERIFIED or BROKEN. An oath you cannot break is decoration. Roster now 122 products, 1 assessed. All instrument checks pass.
196 lines
7.5 KiB
SQL
196 lines
7.5 KiB
SQL
-- bigtech.sucks — living database schema
|
|
-- The website is a view over this. Nothing in the site may hardcode a number
|
|
-- that lives here.
|
|
--
|
|
-- Design rule: an assessment is an ARTIFACT with a date, a version, a method and
|
|
-- evidence. A score without an assessment is a placeholder and is stored as NULL,
|
|
-- never as a guess.
|
|
|
|
PRAGMA foreign_keys = ON;
|
|
PRAGMA journal_mode = WAL;
|
|
|
|
-- ---------------------------------------------------------------- who owns it
|
|
CREATE TABLE IF NOT EXISTS vendor (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
legal_entity TEXT,
|
|
entity_type TEXT CHECK (entity_type IN ('nonprofit','private','public','state','unknown')),
|
|
jurisdiction TEXT, -- ISO 3166-1 alpha-2
|
|
business_model TEXT CHECK (business_model IN ('advertising','subscription','hardware','license','mixed','none','unknown')),
|
|
parent_id TEXT REFERENCES vendor(id),
|
|
founded TEXT,
|
|
notes TEXT
|
|
);
|
|
|
|
-- ---------------------------------------------------------------- what it is
|
|
CREATE TABLE IF NOT EXISTS category (
|
|
id TEXT PRIMARY KEY,
|
|
label TEXT NOT NULL,
|
|
sensitivity REAL NOT NULL CHECK (sensitivity BETWEEN 0 AND 1),
|
|
nextcloud TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS product (
|
|
id TEXT PRIMARY KEY,
|
|
vendor_id TEXT NOT NULL REFERENCES vendor(id),
|
|
name TEXT NOT NULL,
|
|
category_id TEXT REFERENCES category(id),
|
|
-- platform is part of the identity: Signal iOS != Signal Desktop
|
|
platform TEXT NOT NULL,
|
|
source_url TEXT,
|
|
license TEXT,
|
|
sovereign INTEGER NOT NULL DEFAULT 0 CHECK (sovereign IN (0,1)),
|
|
boss INTEGER NOT NULL DEFAULT 0 CHECK (boss IN (0,1)),
|
|
art_slot TEXT,
|
|
monthly_cost REAL,
|
|
notes TEXT,
|
|
UNIQUE(id, platform)
|
|
);
|
|
|
|
-- ---------------------------------------------------------------- the instrument
|
|
CREATE TABLE IF NOT EXISTS dimension (
|
|
id TEXT PRIMARY KEY,
|
|
label TEXT NOT NULL,
|
|
weight REAL NOT NULL CHECK (weight > 0),
|
|
anchor_10 TEXT NOT NULL,
|
|
anchor_0 TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS assessment (
|
|
id TEXT PRIMARY KEY,
|
|
product_id TEXT NOT NULL REFERENCES product(id),
|
|
version TEXT NOT NULL,
|
|
source_commit TEXT,
|
|
assessed_on TEXT NOT NULL,
|
|
method_version TEXT NOT NULL,
|
|
-- 0 = none, 1 = available but experimental/unverified, 2 = verified
|
|
reproducible_build INTEGER NOT NULL DEFAULT 0 CHECK (reproducible_build IN (0,1,2)),
|
|
weighted_total REAL NOT NULL CHECK (weighted_total BETWEEN 0 AND 100),
|
|
grade TEXT NOT NULL,
|
|
weakest_dimension TEXT NOT NULL REFERENCES dimension(id),
|
|
headline TEXT NOT NULL, -- the caveat, in one sentence
|
|
not_tested TEXT,
|
|
UNIQUE(product_id, version, assessed_on)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS score (
|
|
assessment_id TEXT NOT NULL REFERENCES assessment(id) ON DELETE CASCADE,
|
|
dimension_id TEXT NOT NULL REFERENCES dimension(id),
|
|
value REAL NOT NULL CHECK (value BETWEEN 0 AND 10),
|
|
rationale TEXT,
|
|
PRIMARY KEY (assessment_id, dimension_id)
|
|
);
|
|
|
|
-- evidence is the only thing that makes a score legitimate
|
|
CREATE TABLE IF NOT EXISTS evidence (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
assessment_id TEXT NOT NULL REFERENCES assessment(id) ON DELETE CASCADE,
|
|
dimension_id TEXT REFERENCES dimension(id),
|
|
claim TEXT NOT NULL,
|
|
tier INTEGER NOT NULL CHECK (tier IN (1,2,3,4)),
|
|
kind TEXT CHECK (kind IN ('source','capture','record','filing','advisory','paper','claim')),
|
|
artifact TEXT, -- path inside the vault, or URL
|
|
sha256 TEXT,
|
|
observed_at TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS finding (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
assessment_id TEXT NOT NULL REFERENCES assessment(id) ON DELETE CASCADE,
|
|
severity TEXT NOT NULL CHECK (severity IN ('critical','high','medium','low','info')),
|
|
title TEXT NOT NULL,
|
|
detail TEXT,
|
|
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open','fixed','by_design','disputed','accepted')),
|
|
disclosed_on TEXT,
|
|
cve TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS offramp (
|
|
product_id TEXT NOT NULL REFERENCES product(id),
|
|
replacement TEXT NOT NULL,
|
|
nextcloud TEXT,
|
|
notes TEXT,
|
|
PRIMARY KEY (product_id, replacement)
|
|
);
|
|
|
|
-- ---------------------------------------------------------------- discovery
|
|
-- The database authors itself. Every product the ingest finds leaves a receipt
|
|
-- here, so a reader can see WHERE it came from and HOW MUCH it is used.
|
|
-- Discovery may create a product. Discovery may NEVER create a score.
|
|
CREATE TABLE IF NOT EXISTS discovery (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
product_id TEXT NOT NULL REFERENCES product(id),
|
|
source TEXT NOT NULL CHECK (source IN ('github','flathub','alternativeto','manual','seed')),
|
|
source_url TEXT,
|
|
usage_signal INTEGER,
|
|
usage_metric TEXT,
|
|
observed_at TEXT NOT NULL,
|
|
raw TEXT,
|
|
UNIQUE(product_id, source, source_url)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_discovery_product ON discovery(product_id);
|
|
|
|
-- ---------------------------------------------------------------- derived views
|
|
-- The single number the game and the report both use.
|
|
CREATE VIEW IF NOT EXISTS v_product_score AS
|
|
SELECT
|
|
p.id AS product_id,
|
|
p.name AS product,
|
|
p.platform,
|
|
p.category_id,
|
|
v.id AS vendor_id,
|
|
v.name AS vendor,
|
|
v.business_model,
|
|
v.jurisdiction,
|
|
a.id AS assessment_id,
|
|
a.assessed_on,
|
|
a.version,
|
|
a.weighted_total,
|
|
a.grade,
|
|
a.weakest_dimension,
|
|
a.headline,
|
|
-- policy axis: who holds your data, and what can they be made to hand over
|
|
CAST(ROUND(10 * (
|
|
0.40 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'server_retention')
|
|
+ 0.30 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'ownership_jurisdiction')
|
|
+ 0.30 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'user_control_exit')
|
|
)) AS INTEGER) AS policy,
|
|
-- infra axis: can you verify it, and what does it leave on your disk
|
|
CAST(ROUND(10 * (
|
|
0.50 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'verifiability')
|
|
+ 0.30 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'dependency_surface')
|
|
+ 0.20 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'local_data_exposure')
|
|
)) AS INTEGER) AS infra
|
|
FROM product p
|
|
JOIN vendor v ON v.id = p.vendor_id
|
|
LEFT JOIN assessment a
|
|
ON a.product_id = p.id
|
|
AND a.assessed_on = (SELECT MAX(assessed_on) FROM assessment WHERE product_id = p.id);
|
|
|
|
-- A product with no assessment must surface as unassessed, not as a number.
|
|
CREATE VIEW IF NOT EXISTS v_coverage AS
|
|
SELECT
|
|
p.id AS product_id,
|
|
p.name,
|
|
p.platform,
|
|
CASE WHEN a.id IS NULL THEN 'unassessed' ELSE a.grade END AS status,
|
|
a.weighted_total
|
|
FROM product p
|
|
LEFT JOIN assessment a ON a.product_id = p.id
|
|
AND a.assessed_on = (SELECT MAX(assessed_on) FROM assessment WHERE product_id = p.id);
|
|
|
|
-- What to assess next, ordered by how much the thing is actually used.
|
|
CREATE VIEW IF NOT EXISTS v_backlog AS
|
|
SELECT
|
|
p.id AS product_id,
|
|
p.name,
|
|
p.platform,
|
|
p.category_id,
|
|
MAX(d.usage_signal) AS usage,
|
|
GROUP_CONCAT(DISTINCT d.source) AS found_via
|
|
FROM product p
|
|
JOIN discovery d ON d.product_id = p.id
|
|
WHERE NOT EXISTS (SELECT 1 FROM assessment a WHERE a.product_id = p.id)
|
|
GROUP BY p.id
|
|
ORDER BY usage DESC;
|