From be0fb99160df7c96fb69b5ebf767cb422716c72a Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:42:55 +0200 Subject: [PATCH] a way to publish what a provider says without publishing it as a measurement Aurora asked us to cover swap insurance, confidentiality, and whether users are shielded from questionable liquidity. Those are real properties a reader would want, and this site has no instrument for any of them. It also has no way to say so: the registry carries free-text fields with no source and no date, and a page that printed a vendor's claim unqualified would make us the thing we were built against. The /about page says it plainly, that marketing pages quote "real-time" without a number and this one quotes the number. So a claim carries three things a measurement does not need, all required: where we read it, when a human last looked, and the provider's own words. The product page renders them in their own section, after the description and before the measured leaderboard, under a heading that names the speaker: "Stated by , not measured here". The note under it draws the line the section exists for, that we checked the statement appears at the source on that date and did not verify that it holds. A claim never enters a benchmark, never ranks anything, never moves a leader. Two guards. A claim goes stale at six months and says so on the page rather than ageing quietly into something that reads current, and stale ones sort last so the section opens on what we stand behind. And a superlative in a claim value fails the test: the measured columns are where a provider gets to be fastest, and laundering a boast through a "claim" would be worse than not having the mechanism. The registry test walks every entry, so a malformed claim fails the build instead of reaching a page unsourced. Nothing is populated. Each claim needs a human to read the source and date it, and inventing them to demonstrate the feature would be the exact failure this is built to prevent. Also corrected a false comment while in there: seven registry fields said they were "surfaced on the product detail page when present" and not one of them is read by any code. They are now marked as what they are. --- src/app/products/[slug]/page.tsx | 53 +++++++++++++++++++ src/data/provider-registry.ts | 40 ++++++++++++-- src/lib/provider-claims.test.ts | 90 ++++++++++++++++++++++++++++++++ src/lib/provider-claims.ts | 89 +++++++++++++++++++++++++++++++ 4 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 src/lib/provider-claims.test.ts create mode 100644 src/lib/provider-claims.ts diff --git a/src/app/products/[slug]/page.tsx b/src/app/products/[slug]/page.tsx index 39c898de8..b5953eaa4 100644 --- a/src/app/products/[slug]/page.tsx +++ b/src/app/products/[slug]/page.tsx @@ -1,3 +1,4 @@ +import { claimAgeLabel, viewClaims } from "@/lib/provider-claims"; import { distinctBenchCount } from "@/lib/providers"; import type { Metadata } from "next"; import { loadSitemapBlob } from "@/lib/sitemap-blob"; @@ -1061,6 +1062,58 @@ export default async function ProviderPage({ )} + {(() => { + // Properties this site has no instrument for: insurance, + // confidentiality, whether users are shielded from questionable + // liquidity. Real, and worth a reader knowing, but not measured. They + // get their own section with the source and the date attached, never + // a column next to the measured ones, and the heading names the + // speaker. See lib/provider-claims.ts. + const claims = viewClaims(reg?.claims); + if (claims.length === 0) return null; + return ( +
+

+ Stated by {p.name}, not measured here +

+
+

+ These are the provider’s own statements about properties no + benchmark on this site measures. We checked that each one appears + at the source on the date given; we did not verify that it holds. +

+
+ {claims.map((c) => ( +
+
+ {c.label} +
+
+ {c.value} +
+
+ + source + + {" · "} + + {c.stale && ( + + not re-checked recently + + )} +
+
+ ))} +
+
+ ); + })()} + {(() => { // Most recent lastRunAt across every appearance is the truest // "last measured" signal a Google freshness ranker, an AI citation diff --git a/src/data/provider-registry.ts b/src/data/provider-registry.ts index 29b8faeb8..13abef00d 100644 --- a/src/data/provider-registry.ts +++ b/src/data/provider-registry.ts @@ -10,14 +10,43 @@ * factual, no marketing adjectives. Twitter handle without the URL. */ +/** + * A statement the provider makes about itself that no harness can measure: + * swap insurance, confidentiality, whether users are shielded from + * questionable liquidity. Aurora asked for exactly these, and they are real + * properties, but they are not measurements. + * + * Three fields a measurement does not need, all required: where we read it, + * when we last looked, and the provider's own words for it. The product page + * renders all three under a heading that says the provider is speaking, and + * flags a claim nobody has re-checked in six months. See lib/provider-claims.ts. + * + * A claim never enters a benchmark, never ranks anything, and never moves a + * leader. If a property can be probed, probe it and publish the measurement + * instead; this is only for the ones that cannot. + */ +export type ProviderClaim = { + /** What the claim is about. "Swap insurance", "Confidentiality". */ + label: string; + /** The provider's position, in plain words, no superlatives. */ + value: string; + /** https URL where this was read. An unsourced claim is a rumour. */ + source: string; + /** YYYY-MM-DD the source was last checked by a human. */ + verifiedAt: string; +}; + export type ProviderRegistryEntry = { url: string; description: string; twitter?: string; - // Optional rich content surfaced on the product detail page when present. - // Every field is independently optional; the page renders only the - // sections that have data, so partial enrichment is safe. + // Optional enrichment. NOTE: of the seven fields below, none is currently + // read by any page. They were added for a product-page section that was + // never built, and the comment here used to claim they were "surfaced on + // the product detail page", which is not true. Either wire them or drop + // them; do not add an eighth expecting it to render. `claims` below IS + // rendered, and its test fails the build if a claim is malformed. longDescription?: string; chains?: string[]; features?: string[]; @@ -33,6 +62,11 @@ export type ProviderRegistryEntry = { * Sub-products keep their own bench rankings — this is editorial * cross-linking, not data merging. */ parent?: string; + + /** Provider statements we cannot measure, each with a source and a date. + * Rendered in their own section, never mixed with measured columns. + * See lib/provider-claims.ts for the rules and the staleness window. */ + claims?: ProviderClaim[]; }; export const PROVIDER_REGISTRY: Record = { diff --git a/src/lib/provider-claims.test.ts b/src/lib/provider-claims.test.ts new file mode 100644 index 000000000..8ea8f96c4 --- /dev/null +++ b/src/lib/provider-claims.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, test } from "bun:test"; +import { PROVIDER_REGISTRY, type ProviderClaim } from "@/data/provider-registry"; +import { claimAgeDays, claimAgeLabel, claimProblems, viewClaims } from "./provider-claims"; + +const ok: ProviderClaim = { + label: "Swap insurance", + value: "Swaps are covered by a third-party policy up to the notional amount.", + source: "https://example.com/terms", + verifiedAt: "2026-10-01", +}; + +describe("a claim must be sourced and dated to be published", () => { + test("a well-formed claim has no problems", () => { + expect(claimProblems("x", ok)).toEqual([]); + }); + + test("an unsourced claim is a rumour", () => { + expect(claimProblems("x", { ...ok, source: "" })[0]).toContain("https URL"); + expect(claimProblems("x", { ...ok, source: "http://example.com" })[0]).toContain("https URL"); + }); + + test("a claim without a readable date cannot be aged", () => { + expect(claimProblems("x", { ...ok, verifiedAt: "last tuesday" })[0]).toContain("YYYY-MM-DD"); + }); + + test("a future verification date is a typo, not a fresh check", () => { + expect(claimProblems("x", { ...ok, verifiedAt: "2099-01-01" })[0]).toContain("future"); + }); + + test("a superlative in a claim would be marketing we are laundering", () => { + // The measured columns are where a provider gets to be fastest. This site + // exists because marketing pages quote "real-time" without a number. + const p = claimProblems("x", { ...ok, value: "The fastest settlement in the market." }); + expect(p[0]).toContain("superlative"); + }); +}); + +describe("age and staleness", () => { + const now = new Date("2026-10-02T12:00:00Z"); + + test("counts whole days from the verification date", () => { + expect(claimAgeDays("2026-10-02", now)).toBe(0); + expect(claimAgeDays("2026-09-02", now)).toBe(30); + }); + + test("a future date yields no age rather than a negative one", () => { + expect(claimAgeDays("2027-01-01", now)).toBeNull(); + }); + + test("past six months a claim says so instead of ageing quietly", () => { + const fresh = viewClaims([{ ...ok, verifiedAt: "2026-09-20" }], now)[0]; + const old = viewClaims([{ ...ok, verifiedAt: "2026-01-01" }], now)[0]; + expect(fresh.stale).toBe(false); + expect(old.stale).toBe(true); + }); + + test("stale claims sort last, so the section opens on what we stand behind", () => { + const views = viewClaims( + [ + { ...ok, label: "Old", verifiedAt: "2025-01-01" }, + { ...ok, label: "New", verifiedAt: "2026-09-30" }, + ], + now, + ); + expect(views.map((v) => v.label)).toEqual(["New", "Old"]); + }); + + test("the age label reads like a person wrote it", () => { + expect(claimAgeLabel(viewClaims([{ ...ok, verifiedAt: "2026-10-02" }], now)[0])).toBe("checked today"); + expect(claimAgeLabel(viewClaims([{ ...ok, verifiedAt: "2026-10-01" }], now)[0])).toBe("checked yesterday"); + expect(claimAgeLabel(viewClaims([{ ...ok, verifiedAt: "2026-09-22" }], now)[0])).toBe("checked 10 days ago"); + expect(claimAgeLabel(viewClaims([{ ...ok, verifiedAt: "2026-04-02" }], now)[0])).toContain("months ago"); + }); + + test("no claims renders nothing rather than an empty section", () => { + expect(viewClaims(undefined)).toEqual([]); + expect(viewClaims([])).toEqual([]); + }); +}); + +describe("the registry itself", () => { + test("every claim in the registry is publishable", () => { + // This is the gate: a malformed claim fails the build rather than + // reaching a page unsourced or undated. + const problems = Object.entries(PROVIDER_REGISTRY).flatMap(([slug, entry]) => + (entry.claims ?? []).flatMap((c) => claimProblems(slug, c)), + ); + expect(problems).toEqual([]); + }); +}); diff --git a/src/lib/provider-claims.ts b/src/lib/provider-claims.ts new file mode 100644 index 000000000..e15d36c2d --- /dev/null +++ b/src/lib/provider-claims.ts @@ -0,0 +1,89 @@ +import type { ProviderClaim } from "@/data/provider-registry"; + +/** + * Statements a provider makes about itself, which no harness can measure. + * + * Aurora asked us to cover things this site has no instrument for: whether a + * swap is insured, whether it is confidential, whether users are shielded + * from questionable liquidity. Those are real properties and they matter to a + * reader, but they are not measurements and must never be printed as if they + * were. This site exists because marketing pages quote "real-time" without a + * number; publishing a vendor's claim unqualified would make us the thing we + * were built against. + * + * So a claim carries three things a measurement does not need: where we read + * it, when we last looked, and the fact that it is the provider speaking. The + * page renders all three, and a claim nobody has re-checked in a long time + * says so rather than ageing silently into something that reads current. + * + * What a claim is NOT: a benchmark value, a ranking input, or anything that + * can move a leader. Nothing here touches the ledger. + */ + +/** A claim goes stale at six months. Long enough that re-checking is not + * busywork, short enough that a pricing page or a terms change is unlikely to + * have passed unnoticed for two of them. */ +export const CLAIM_STALE_AFTER_DAYS = 183; + +export type ClaimView = ProviderClaim & { + /** Whole days since it was last verified; null when the date will not parse. */ + ageDays: number | null; + /** Past CLAIM_STALE_AFTER_DAYS, so the page says so instead of ageing quietly. */ + stale: boolean; +}; + +/** Days between an ISO date and now, or null when the date is unusable. */ +export function claimAgeDays(verifiedAt: string, now = new Date()): number | null { + const d = new Date(`${verifiedAt}T00:00:00Z`); + if (Number.isNaN(d.getTime())) return null; + const ms = now.getTime() - d.getTime(); + if (ms < 0) return null; // a future date is a typo, not a fresh check + return Math.floor(ms / 86_400_000); +} + +/** Claims in the order a reader should meet them: anything stale last, so the + * section opens on what we actually stand behind. */ +export function viewClaims(claims: ProviderClaim[] | undefined, now = new Date()): ClaimView[] { + if (!claims || claims.length === 0) return []; + return claims + .map((c) => { + const ageDays = claimAgeDays(c.verifiedAt, now); + return { ...c, ageDays, stale: ageDays == null || ageDays > CLAIM_STALE_AFTER_DAYS }; + }) + .sort((a, b) => Number(a.stale) - Number(b.stale) || a.label.localeCompare(b.label)); +} + +/** "checked 12 days ago", for the line under each claim. A claim whose date + * will not parse says that plainly rather than printing a wrong age. */ +export function claimAgeLabel(c: ClaimView): string { + if (c.ageDays == null) return "verification date unreadable"; + if (c.ageDays === 0) return "checked today"; + if (c.ageDays === 1) return "checked yesterday"; + if (c.ageDays < 60) return `checked ${c.ageDays} days ago`; + const months = Math.round(c.ageDays / 30.4); + return `checked ${months} months ago`; +} + +/** Problems that should stop a claim reaching a page. Returned rather than + * thrown so a test can assert the whole registry at once. */ +export function claimProblems(slug: string, c: ProviderClaim): string[] { + const out: string[] = []; + const where = `${slug} claim "${c.label}"`; + if (!c.label.trim()) out.push(`${where}: empty label`); + if (!c.value.trim()) out.push(`${where}: empty value`); + if (!/^https:\/\//.test(c.source)) { + // An unsourced claim is a rumour, and http is not a source we can stand on. + out.push(`${where}: source must be an https URL, got ${c.source || "(none)"}`); + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(c.verifiedAt)) { + out.push(`${where}: verifiedAt must be YYYY-MM-DD, got ${c.verifiedAt || "(none)"}`); + } else if (claimAgeDays(c.verifiedAt) == null) { + out.push(`${where}: verifiedAt is in the future or unparseable (${c.verifiedAt})`); + } + if (/\b(fastest|best|cheapest|leading|#1|number one)\b/i.test(c.value)) { + // A superlative in a claim is marketing we would be laundering. The + // measured columns are where a provider gets to be fastest. + out.push(`${where}: value carries a superlative; quote the substance, not the boast`); + } + return out; +}