feat: self-authoring ingest, relevance gate, and the Ranch Oath

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.
This commit is contained in:
CLU 2026-10-29 13:23:57 -05:00
parent b6a3b0722b
commit d433a0e75e
10 changed files with 6910 additions and 29 deletions

View file

@ -14,7 +14,8 @@
"preview": "npm run preview -w @bigtech-sucks/web", "preview": "npm run preview -w @bigtech-sucks/web",
"test": "npm run test -w @bigtech-sucks/core", "test": "npm run test -w @bigtech-sucks/core",
"db:build": "node packages/core/db/build.mjs", "db:build": "node packages/core/db/build.mjs",
"db:verify": "node packages/core/db/build.mjs verify" "db:verify": "node packages/core/db/build.mjs verify",
"db:ingest": "node packages/core/db/ingest/run.mjs"
}, },
"license": "UNLICENSED" "license": "UNLICENSED"
} }

View file

@ -14,6 +14,7 @@
*/ */
import { DatabaseSync } from 'node:sqlite'; import { DatabaseSync } from 'node:sqlite';
import { readFileSync, writeFileSync, readdirSync, mkdirSync, existsSync, unlinkSync } from 'node:fs'; import { readFileSync, writeFileSync, readdirSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { join, dirname } from 'node:path'; import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@ -56,6 +57,8 @@ const VENDORS = {
netflix: ['Netflix, Inc.', 'public', 'US', 'subscription'], netflix: ['Netflix, Inc.', 'public', 'US', 'subscription'],
'nextcloud-gmbh': ['Nextcloud GmbH', 'private', 'DE', 'subscription'], 'nextcloud-gmbh': ['Nextcloud GmbH', 'private', 'DE', 'subscription'],
'signal-foundation': ['Signal Technology Foundation', 'nonprofit', 'US', 'none'], 'signal-foundation': ['Signal Technology Foundation', 'nonprofit', 'US', 'none'],
// discovered products land here until a human or agent resolves the owner
unknown: ['Unknown vendor', 'unknown', 'unknown', 'unknown'],
}; };
const OUTLAW_VENDOR = { const OUTLAW_VENDOR = {
@ -80,8 +83,10 @@ const ID_OVERRIDES = { '1password': 'onepassword', '23andme': 'twentythreeandme'
const slug = (s) => ID_OVERRIDES[s] ?? s.replace(/[^a-z0-9]+/gi, '-').toLowerCase(); const slug = (s) => ID_OVERRIDES[s] ?? s.replace(/[^a-z0-9]+/gi, '-').toLowerCase();
// ---------------------------------------------------------------- build // ---------------------------------------------------------------- build
async function build() { async function build({ fresh = false } = {}) {
if (existsSync(DB_PATH)) unlinkSync(DB_PATH); // Never wipe by default: the database authors itself, and a rebuild that
// deletes discovered products would throw away the roster. --fresh is explicit.
if (fresh && existsSync(DB_PATH)) unlinkSync(DB_PATH);
const db = new DatabaseSync(DB_PATH); const db = new DatabaseSync(DB_PATH);
db.exec('PRAGMA foreign_keys = ON'); db.exec('PRAGMA foreign_keys = ON');
db.exec(readFileSync(join(HERE, 'schema.sql'), 'utf8')); db.exec(readFileSync(join(HERE, 'schema.sql'), 'utf8'));
@ -222,7 +227,27 @@ function exportSite() {
dimensions, products, assessments, scores, evidence, findings, dimensions, products, assessments, scores, evidence, findings,
}; };
writeFileSync(join(GEN_DIR, 'catalog.json'), JSON.stringify(payload, null, 2)); // The bytes we hash are the bytes we serve. The oath is bound to this digest,
// so an oath page that does not match the data is detectable by a stranger.
const catalogBytes = JSON.stringify(payload, null, 2);
writeFileSync(join(GEN_DIR, 'catalog.json'), catalogBytes);
const PUB = join(CORE, '..', 'web', 'public');
mkdirSync(PUB, { recursive: true });
writeFileSync(join(PUB, 'catalog.json'), catalogBytes);
const digest = createHash('sha256').update(catalogBytes).digest('hex');
const oath = {
digest,
algorithm: 'sha256',
file: 'catalog.json',
byte_length: Buffer.byteLength(catalogBytes),
rebuilt_at: new Date().toISOString(),
method_version: payload.method_version,
products: products.length,
assessed: products.filter((p) => p.weighted_total !== null).length,
};
writeFileSync(join(PUB, 'oath.json'), JSON.stringify(oath, null, 2));
const ts = `// GENERATED FILE - do not edit by hand. const ts = `// GENERATED FILE - do not edit by hand.
// Source of truth: packages/core/db/bigtech.db // Source of truth: packages/core/db/bigtech.db
@ -295,7 +320,8 @@ if (mode === 'verify') {
const r = verify(); const r = verify();
report(r); report(r);
} else { } else {
const { migrated, seeds } = await build(); const fresh = process.argv.includes('--fresh');
const { migrated, seeds } = await build({ fresh });
console.log(`built ${DB_PATH}`); console.log(`built ${DB_PATH}`);
console.log(` legacy roster absorbed: ${migrated} products (unassessed)`); console.log(` legacy roster absorbed: ${migrated} products (unassessed)`);
console.log(` seeds applied: ${seeds.join(', ')}`); console.log(` seeds applied: ${seeds.join(', ')}`);

View file

@ -0,0 +1,152 @@
#!/usr/bin/env node
/**
* Self-authoring ingest.
*
* The roster writes itself. The scores do not.
*
* DISCOVERY automatic. A source names a piece of software and a usage number.
* ASSESSMENT never automatic. A score requires a written rationale and tier-1
* or tier-2 evidence, and the build gate enforces that.
*
* That split is the whole design. If the ingest wrote scores, the oath on the
* front page would be a lie the same afternoon.
*
* Run: npm run db:ingest
* Dry run: npm run db:ingest -- --dry
*/
import { DatabaseSync } from 'node:sqlite';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { ADAPTERS, looksLikeUserSoftware } from './sources.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const DB_PATH = join(HERE, '..', 'bigtech.db');
const DRY = process.argv.includes('--dry');
/** What to go looking for, and what to rank it by. */
const TARGETS = [
{ category: 'messaging', source: 'flathub', query: 'messaging' },
{ category: 'messaging', source: 'github', query: 'messaging' },
{ category: 'email', source: 'flathub', query: 'email' },
{ category: 'email', source: 'github', query: 'mail-client' },
{ category: 'passwords', source: 'flathub', query: 'password' },
{ category: 'browser', source: 'flathub', query: 'web browser' },
{ category: 'notes', source: 'flathub', query: 'notes' },
{ category: 'files', source: 'github', query: 'file-sync' },
{ category: 'photos', source: 'flathub', query: 'photo' },
// Flathub is a curated store of end-user Linux apps, so relevance is free.
// GitHub topics are uncurated, so relevance has to be earned by a classifier.
// Prefer the store wherever it has the category. Verified 2026-10-29: the
// GitHub topic 'local-llm' returned Langroid and Pangu.js, neither of which
// is a product a person installs.
{ category: 'ai', source: 'flathub', query: 'llm' },
];
const slug = (s) =>
s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48);
const now = new Date().toISOString();
async function main() {
const db = new DatabaseSync(DB_PATH);
db.exec('PRAGMA foreign_keys = ON');
db.exec(readFileSync(join(HERE, '..', 'schema.sql'), 'utf8'));
const existing = new Map(
db.prepare('SELECT id, LOWER(name) AS n FROM product').all().map((r) => [r.n, r.id]),
);
const taken = new Set(db.prepare('SELECT id FROM product').all().map((r) => r.id));
const insProduct = db.prepare(`
INSERT OR IGNORE INTO product
(id, vendor_id, name, category_id, platform, source_url, license, sovereign, boss, art_slot, monthly_cost, notes)
VALUES (?, 'unknown', ?, ?, ?, ?, ?, 0, 0, NULL, NULL, ?)`);
const insDiscovery = db.prepare(`
INSERT OR IGNORE INTO discovery
(product_id, source, source_url, usage_signal, usage_metric, observed_at, raw)
VALUES (?, ?, ?, ?, ?, ?, ?)`);
const stats = { targets: 0, rowsSeen: 0, newProducts: 0, newReceipts: 0, failed: [] };
for (const target of TARGETS) {
const adapter = ADAPTERS[target.source];
if (!adapter) { console.warn(` ! unknown source ${target.source}`); continue; }
stats.targets++;
let rows;
try {
rows = await adapter(target.query);
} catch (err) {
stats.failed.push(`${target.source}/${target.query}: ${err.message}`);
console.warn(` FAIL ${target.source}/${target.query} ${err.message}`);
continue;
}
let added = 0;
let dropped = 0;
for (const row of rows) {
// Relevance gate first. A highly starred message broker is not a product
// a person chooses, and a roster full of them is a roster nobody trusts.
if (row.platform === 'source' && !looksLikeUserSoftware(row)) { dropped++; continue; }
stats.rowsSeen++;
const key = row.name.toLowerCase();
let id = existing.get(key);
if (!id) {
let candidate = slug(row.name);
if (taken.has(candidate)) candidate = `${candidate}-${slug(target.category)}`;
if (taken.has(candidate)) continue;
if (!DRY) {
insProduct.run(
candidate, row.name, target.category, row.platform,
row.sourceUrl, row.raw?.license ?? null,
`Discovered by ${target.source} ingest on ${now.slice(0, 10)}. Unassessed.`,
);
}
taken.add(candidate);
existing.set(key, candidate);
id = candidate;
stats.newProducts++;
added++;
}
if (!DRY) {
const r = insDiscovery.run(
id, target.source, row.sourceUrl, row.usage, row.usageMetric,
now, JSON.stringify(row.raw),
);
if (r.changes) stats.newReceipts++;
}
}
console.log(
` ${target.source.padEnd(8)} ${String(target.query).padEnd(16)} ${target.category.padEnd(11)} ${String(rows.length).padStart(3)} seen, ${added} new, ${dropped} not-an-app`,
);
}
const backlog = db.prepare('SELECT * FROM v_backlog LIMIT 12').all();
const cov = db.prepare('SELECT * FROM v_coverage').all();
const assessed = cov.filter((c) => c.status !== 'unassessed').length;
db.close();
console.log('');
console.log(DRY ? 'DRY RUN, nothing written' : 'INGEST COMPLETE');
console.log(` targets attempted : ${stats.targets}`);
console.log(` rows seen : ${stats.rowsSeen}`);
console.log(` new products : ${stats.newProducts}`);
console.log(` new receipts : ${stats.newReceipts}`);
console.log(` failed sources : ${stats.failed.length}`);
console.log('');
console.log(`COVERAGE ${assessed} assessed / ${cov.length} products`);
if (backlog.length) {
console.log('');
console.log('BACKLOG, ordered by usage');
for (const b of backlog) {
console.log(` ${String(b.usage).padStart(8)} ${b.name.padEnd(28)} ${String(b.category_id).padEnd(11)} via ${b.found_via}`);
}
}
}
main().catch((err) => { console.error(err); process.exit(1); });

View file

@ -0,0 +1,97 @@
/**
* Discovery adapters.
*
* Each adapter returns a normalised shape:
* { name, platform, sourceUrl, usage, usageMetric, raw }
*
* Rules for an adapter:
* - it discovers SOFTWARE, it never scores it
* - it records where it found the thing and how much it is used
* - it fails loudly, never silently returning an empty list on a blocked fetch
*
* Sources currently live:
* github open API, no key needed, 60 requests/hour unauthenticated
* flathub open API, POST /api/v2/search, no key
*
* Sources deliberately not automated:
* alternativeto Cloudflare-protected. Readable through a real browser engine
* (verified 2026-10-29) but their robots.txt asks ai-train=no.
* Treated as a demand signal for manual/periodic import, not a
* crawler target. Do not add a scraper here without a decision.
*/
const UA = 'bigtech-sucks-ingest/0.1 (+https://bigtech.sucks; assessment roster)';
async function getJson(url, init = {}) {
const res = await fetch(url, {
...init,
headers: { 'User-Agent': UA, Accept: 'application/json', ...(init.headers || {}) },
});
if (!res.ok) throw new Error(`${url} -> HTTP ${res.status}`);
return res.json();
}
// ------------------------------------------------------------------ relevance
// Usage rank alone is not a roster. `topic:messaging` sorts RabbitMQ to the top,
// because a message broker is highly starred and not a thing a human installs.
// A candidate must look like software a person uses, or it is dropped and the
// reason is recorded.
const NOT_AN_APP =
/\b(broker|queue|mqtt|amqp|kafka|nats|pulsar|rocketmq|emqx|nsq|rabbitmq|database|sdk|library|libraries|framework|protocol|server|infrastructure|plugin|extension|binding|middleware|gateway|proxy|driver|api client|spec|specification|awesome list)\b/i;
const APP_HINT =
/\b(app|application|client|desktop|chat|messenger|messaging|mobile|gui|editor|browser|vault|manager|suite)\b/i;
export function looksLikeUserSoftware(row) {
const text = `${row.name} ${row.raw?.description ?? ''}`;
if (NOT_AN_APP.test(text)) return false;
if (row.raw?.homepage) return true;
return APP_HINT.test(text);
}
// ------------------------------------------------------------------ GitHub
export async function githubByTopic(topic, { minStars = 500, limit = 10 } = {}) {
const q = encodeURIComponent(`topic:${topic} stars:>=${minStars} archived:false`);
const data = await getJson(
`https://api.github.com/search/repositories?q=${q}&sort=stars&order=desc&per_page=${limit}`,
);
return (data.items || []).map((r) => ({
name: r.name.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
platform: 'source',
sourceUrl: r.html_url,
usage: r.stargazers_count ?? 0,
usageMetric: 'github_stars',
raw: {
full_name: r.full_name,
stars: r.stargazers_count,
forks: r.forks_count,
license: r.license?.spdx_id ?? null,
description: r.description,
homepage: r.homepage || null,
language: r.language,
},
}));
}
// ------------------------------------------------------------------ Flathub
export async function flathubSearch(query, { limit = 10 } = {}) {
const data = await getJson('https://flathub.org/api/v2/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, filters: [] }),
});
return (data.hits || []).slice(0, limit).map((h) => ({
name: h.name,
platform: 'linux-flatpak',
sourceUrl: h.app_id ? `https://flathub.org/apps/${h.app_id}` : null,
// Flathub does not publish install counts in this endpoint. Presence in the
// distro store is a weak but real usage signal, scored low on purpose.
usage: 100,
usageMetric: 'flathub_presence',
raw: { app_id: h.app_id, summary: h.summary, developer: h.developer_name ?? null },
}));
}
export const ADAPTERS = {
github: (topic) => githubByTopic(topic),
flathub: (query) => flathubSearch(query),
};

View file

@ -113,6 +113,24 @@ CREATE TABLE IF NOT EXISTS offramp (
PRIMARY KEY (product_id, replacement) 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 -- ---------------------------------------------------------------- derived views
-- The single number the game and the report both use. -- The single number the game and the report both use.
CREATE VIEW IF NOT EXISTS v_product_score AS CREATE VIEW IF NOT EXISTS v_product_score AS
@ -161,3 +179,18 @@ SELECT
FROM product p FROM product p
LEFT JOIN assessment a ON a.product_id = p.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); 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;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,183 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>The Ranch Oath — bigtech.sucks</title>
<style>
:root{
--ink:#0d0f0e;
--bone:#e8e2d4;
--dim:#8a8272;
--amber:#d9a441;
--good:#7fae7a;
--bad:#c9563f;
--rule:#2a2d2a;
}
*{box-sizing:border-box}
body{
margin:0;padding:3rem 1.25rem 6rem;
background:var(--ink);color:var(--bone);
font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
font-size:15px;line-height:1.7;
-webkit-font-smoothing:antialiased;
}
.sheet{max-width:62rem;margin:0 auto}
pre{overflow-x:auto;margin:0;font-size:clamp(7px,1.35vw,13px);line-height:1.15;color:var(--dim)}
.book{text-align:center;margin:0 0 .5rem}
.book pre{display:inline-block;text-align:left;color:var(--bone);
font-size:clamp(8px,1.6vw,15px);line-height:1.22;letter-spacing:0}
h1{
text-align:center;font-size:clamp(1.1rem,3vw,1.75rem);letter-spacing:.42em;
font-weight:400;margin:1.5rem 0 .35rem;text-transform:uppercase;
}
.sub{text-align:center;color:var(--dim);letter-spacing:.18em;font-size:.72rem;text-transform:uppercase;margin-bottom:2.5rem}
.rule{border:0;border-top:1px solid var(--rule);margin:2.5rem 0}
.oath{max-width:44rem;margin:0 auto;font-size:clamp(.86rem,1.5vw,1.02rem)}
.oath p{margin:0 0 1.15rem}
.oath .open{color:var(--dim)}
.oath .vow{color:var(--bone)}
.oath .coda{
text-align:center;margin:2.25rem 0 0;letter-spacing:.06em;
}
.oath .coda strong{font-weight:400;color:var(--amber)}
.swear{
text-align:center;margin:2.5rem 0 0;font-size:clamp(1rem,2.4vw,1.35rem);
letter-spacing:.12em;color:var(--bone);
}
.subpoena{
text-align:center;color:var(--dim);font-size:.8rem;letter-spacing:.14em;
text-transform:uppercase;margin-top:.6rem;
}
.seal{
max-width:44rem;margin:3rem auto 0;padding:1.25rem 1.35rem;
border:1px solid var(--rule);background:rgba(255,255,255,.015);
}
.seal dl{display:grid;grid-template-columns:max-content 1fr;gap:.35rem 1.25rem;margin:0;font-size:.82rem}
.seal dt{color:var(--dim);white-space:nowrap}
.seal dd{margin:0;word-break:break-all}
.status{margin:1.1rem 0 0;padding-top:1rem;border-top:1px dashed var(--rule);font-size:.82rem}
.status .tag{letter-spacing:.16em;font-weight:700}
.ok{color:var(--good)}
.bad{color:var(--bad)}
.unknown{color:var(--dim)}
.note{max-width:44rem;margin:2.5rem auto 0;color:var(--dim);font-size:.8rem;line-height:1.6}
a{color:var(--amber);text-decoration:none;border-bottom:1px solid rgba(217,164,65,.35)}
a:hover{border-bottom-color:var(--amber)}
</style>
</head>
<body>
<div class="sheet">
<div class="book"><pre id="crest"></pre></div>
<h1>The Ranch Oath</h1>
<div class="sub">Taken at bigtech.sucks &middot; over a living database</div>
<hr class="rule" />
<div class="oath">
<p class="open">I, Christian Sass, of sound mind and sober judgment, do solemnly swear, with my hand on the Book:</p>
<p class="vow">That every number published on this site is a number we measured, and that every measurement names the artifact it came from.</p>
<p class="vow">That where we could not prove a thing, we have said so in the same size type as the things we could.</p>
<p class="vow">That no claim here rests on our word alone.</p>
<p class="vow">That we would rather publish a lower number than a comfortable one.</p>
<p class="vow">That when we are shown to be wrong, the correction is published with its date and the original stays visible.</p>
</div>
<div class="swear">the truth, the whole truth, and nothing but the truth</div>
<div class="subpoena">so help me God</div>
<div class="seal">
<dl>
<dt>Sworn by</dt><dd>Christian Sass, Binary Ranch</dd>
<dt>On</dt><dd id="sworn-on">&mdash;</dd>
<dt>By the method</dt><dd id="method">&mdash;</dd>
<dt>Over the digest</dt><dd id="digest">&mdash;</dd>
<dt>Covering</dt><dd id="coverage">&mdash;</dd>
</dl>
<p class="status"><span class="tag" id="verdict">CHECKING</span> <span id="verdict-detail">&nbsp;</span></p>
</div>
<p class="note">
This page recomputes the SHA-256 of the catalog the site is serving, in your browser, and compares it to
the digest printed above. Nothing is trusted in transit. If the two disagree, the oath has been broken and
you are reading a promise that has gone stale: the data changed and the words did not.
Verification is a plain <code>crypto.subtle.digest</code> over <code>catalog.json</code>, and you can repeat it
yourself with <code>sha256sum catalog.json</code>.
</p>
</div>
<script>
// The crest. A Bible, patriarchal cross on the cover.
const CREST = String.raw`
______________________
/ /|
/ _|_ / |
/ | / |
/ __|__ / |
/ | / |
/ | / |
/_____________________/ |
| | |
| | /
| | /
| | /
|_____________________| /
|/
`;
document.getElementById('crest').textContent = CREST.trim('\n');
const $ = (id) => document.getElementById(id);
async function verify() {
try {
const [oathRes, catalogRes] = await Promise.all([
fetch('oath.json', { cache: 'no-store' }),
fetch('catalog.json', { cache: 'no-store' }),
]);
if (!oathRes.ok || !catalogRes.ok) throw new Error('artifacts missing');
const oath = await oathRes.json();
const bytes = await catalogRes.arrayBuffer();
const hash = await crypto.subtle.digest('SHA-256', bytes);
const hex = Array.from(new Uint8Array(hash))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
$('sworn-on').textContent = oath.rebuilt_at.replace('T', ' ').replace(/\..*$/, ' UTC');
$('method').textContent = oath.method_version;
$('digest').textContent = hex;
$('coverage').textContent =
`${oath.assessed} of ${oath.products} products assessed, ${oath.byte_length.toLocaleString()} bytes`;
const verdict = $('verdict');
const detail = $('verdict-detail');
if (hex === oath.digest) {
verdict.textContent = 'VERIFIED';
verdict.className = 'tag ok';
detail.textContent = `The catalog you are reading hashes to the digest this oath was sworn over.`;
} else {
verdict.textContent = 'BROKEN';
verdict.className = 'tag bad';
detail.textContent =
`MISMATCH. Sworn over ${oath.digest.slice(0, 16)}… but serving ${hex.slice(0, 16)}…. This oath is stale.`;
}
} catch (err) {
$('verdict').textContent = 'UNVERIFIED';
$('verdict').className = 'tag unknown';
$('verdict-detail').textContent = String(err && err.message ? err.message : err) +
'. The artifacts this oath is bound to could not be read.';
}
}
verify();
</script>
</body>
</html>

View file

@ -0,0 +1,10 @@
{
"digest": "5aadd709b394ab19e13a65aa21fb5182cb736f53e2b60c175abebb8dd11f1d8a",
"algorithm": "sha256",
"file": "catalog.json",
"byte_length": 79846,
"rebuilt_at": "2026-10-29T18:23:54.074Z",
"method_version": "btr-0.1",
"products": 122,
"assessed": 1
}