The existing 32 checks prove the deployment answers -- TLS, auth, quorum commit,
convergence, isolation, dashboards, backups. Not one wrote a signal and observed
an order change, so VISION.md:17 "Ranking is not a feature. It is a primitive."
was unverified. This adds a 60-item content-feed app and five assertions that
verify the product's semantics, on a hermetic standalone node.
Added
- tests/e2e/app/: fixture contract (60 items, 4 categories, each owning one
unoccupied 100-id embedding cluster), a deep-module harness owning the whole
lifecycle behind startApp(), the product page, and an app:dev entry point.
- tidal-stress/src/bin/feed-fixture.rs: seeds the catalog and emits brute-force
ground truth, reusing recall::embedding_for rather than adding a third copy of
the corpus generator (tidal/src/db/items.rs already holds a second).
- GroundTruth::from_ids: the oracle now serves sparse id sets. build() delegates,
so there is no transient copy even at 1M, and top_k indexes positionally.
- 10-ranking-semantics.spec.ts (5 hermetic checks) and
11-ranking-integrity.spec.ts (2 cluster tripwires).
- playwright.semantics.config.ts + CAP-016 demo beat (walkthrough 82s -> 90s).
Measured, not merely green
- like: index 59 -> 0, like_boost 2.0, with no sleep between write and read.
- decay: implied half-lives 7.0007 d and 14.0014 d against a schema declaring
7 d and 14 d, recovered from a 4-second window via H = t*ln2 / -ln(v2/v1) and
compared against the schema the node actually loaded, not a hardcoded copy.
- ANN: top-10 identical to brute-force cosine on all four probes; self-distance
0.0148-0.0197 against a 0.05 tolerance.
- rank: dense 1..60 on standalone vs [1,1,1,2,2,3,4,3,4,5,6,5] on the cluster.
Three product findings, pinned and routed to @tidal-engineer
- BUG-018 (High) skip is durably accepted and query-time inert. Penalty is fully
implemented (ranking/profile.rs:227 -> executor/signal_values.rs:183, labelled
{signal}_penalty at executor/mod.rs:65) but skeleton() sets penalties: vec![]
(ranking/builtins.rs:62) and none of the 27 built-ins overrides it. So
VISION.md:187 "negative signals are equal citizens" holds for no shipped
profile. Same anti-pattern as the reseed defects and scatter_merge: a guard
present on one path, absent on its sibling.
- BUG-019 (Medium) three built-ins read signals this schema does not declare --
trending/share_velocity, hidden_gems/completion, controversial/dislike -- so
those terms are permanently 0 and trending ranks on view_velocity alone.
- BUG-020 (Low) for_you declares Scan{sort_field:"created_at"} but ignores a
created_at metadata value; an order matching neither id-asc nor
created_at-desc came back strictly id-ascending.
Two assertions therefore report a gap rather than a success, written as tripwires
whose failure message says what to do when the gap closes. The rank defect is
localised, not fixed: scatter_merge (cluster/node.rs:7542) returns a merged slice
without re-stamping rank while scores stay correctly ordered, so the fault is the
missing stamp and not the merge's sort.
Notes
- Hermetic by construction: its own config, because FullConfig.projects is not
filtered by --project and globalSetup publishes credentials into the main
process that forked workers inherit -- so a setup project cannot replace it,
and weakening globalSetup would destroy the fail-loud behaviour that is its
purpose. Verified with KUBECONFIG=/nonexistent and all E2E_* unset.
- Never touches the deployed corpus: skip is permanent: true, so seeding it into
production would be irreversible.
- The page contains no sort, no hostname and no credential; the harness proxy
injects auth server-side so no bearer reaches a browser or a capture.
- Schema comes from k8s/cluster/schema-configmap.yaml, asserted at 1536 dims;
tidal-server/config/default-schema.yaml declares 128 and would 422 every write.
Verification: 5 semantics + 34 regression + 10 demo captures green; tsc clean;
tidal-stress clippy clean under clippy::all=deny with unwrap_used=deny; 2101
tidaldb lib tests; preflight 10/10 perfect; render 90.05s/2700 frames with zero
empty boundary frames; zero orphan processes or temp dirs after teardown.
205 lines
8.2 KiB
HTML
205 lines
8.2 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Content feed — ranked by tidalDB</title>
|
|
<!--
|
|
The product surface: a ranked list of items with the signal explainability
|
|
tidalDB returned alongside them.
|
|
|
|
Three rules govern this file, all of them load-bearing:
|
|
|
|
1. Every request is RELATIVE (`/api/*`). `.sdlc/guidance.md:165` forbids a
|
|
hardcoded host in frontend code, and the bearer is injected by the harness
|
|
proxy so it never reaches this page. There is no hostname, port or
|
|
credential anywhere below.
|
|
2. Rows render in ARRAY ORDER. There is no `.sort()` in this file.
|
|
`CODING_GUIDELINES.md:88` puts ranking in the database — "the application
|
|
never computes trending_score" — and a page that re-sorted would make a
|
|
broken server rank look correct on screen, which is the one thing this app
|
|
must never do.
|
|
3. No timers. The feed is re-read on explicit user action only
|
|
(`.sdlc/guidance.md:183`).
|
|
|
|
It shows the server's own `rank` next to the display position, so the two
|
|
disagreeing is visible rather than hidden. On the deployed cluster they DO
|
|
disagree (`scatter_merge` does not re-stamp rank); on this standalone node they
|
|
agree.
|
|
-->
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117; --panel: #131922; --line: #222c3a;
|
|
--text: #d6deeb; --dim: #7c8ba1;
|
|
--cmd: #79c0ff; --good: #56d364; --expected: #d29922;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; }
|
|
/* Sized so ten rows PLUS the environment footer fit inside 1600x900 — the
|
|
capture viewport. Overflowing clipped the footer, and the footer is what
|
|
stops a fixture screenshot reading as production. */
|
|
body {
|
|
background: var(--bg); color: var(--text);
|
|
font: 15px/1.55 ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
padding: 28px 44px;
|
|
}
|
|
/* Capped and centred: at 1600px the row grid stretched the title column to
|
|
~780px of empty space before the signal values, which read as a broken
|
|
layout rather than a list. */
|
|
.wrap { max-width: 1240px; margin: 0 auto; }
|
|
header { border-bottom: 1px solid var(--line); padding-bottom: 14px; margin-bottom: 16px; }
|
|
h1 {
|
|
margin: 0 0 5px; font-size: 25px; letter-spacing: -0.2px; font-weight: 650;
|
|
font-family: ui-sans-serif, -apple-system, "Segoe UI", system-ui, sans-serif;
|
|
}
|
|
.sub {
|
|
margin: 0; color: var(--dim); font-size: 14.5px;
|
|
font-family: ui-sans-serif, -apple-system, "Segoe UI", system-ui, sans-serif;
|
|
}
|
|
ol { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
|
|
li {
|
|
display: grid; grid-template-columns: 58px 1fr 236px 146px;
|
|
align-items: center; gap: 16px;
|
|
background: var(--panel); border: 1px solid var(--line);
|
|
border-radius: 8px; padding: 9px 16px;
|
|
}
|
|
/* Only the item that MOVED UP is highlighted. Nine rows shifting down by one
|
|
because a tenth was promoted is arithmetic, not nine warnings — amber on all
|
|
of them buried the single real event. Amber stays reserved for genuine
|
|
negatives. */
|
|
li.moved-up { border-color: var(--good); }
|
|
.pos { font-size: 22px; line-height: 1.2; font-weight: 700; text-align: right; color: var(--cmd); }
|
|
.pos .rank { display: block; font-size: 11px; font-weight: 400; color: var(--dim); }
|
|
.pos .rank.disagree { color: var(--expected); }
|
|
.title {
|
|
font-size: 16px; line-height: 1.35; font-weight: 600;
|
|
font-family: ui-sans-serif, -apple-system, "Segoe UI", system-ui, sans-serif;
|
|
}
|
|
.meta { color: var(--dim); font-size: 12.5px; line-height: 1.35; margin-top: 2px; }
|
|
.delta { color: var(--good); font-weight: 700; }
|
|
.delta.down { color: var(--dim); font-weight: 400; }
|
|
.signals { font-size: 12.5px; line-height: 1.35; color: var(--dim); }
|
|
.signals span { display: block; }
|
|
.signals b { color: var(--text); font-weight: 600; }
|
|
.actions { display: flex; gap: 8px; justify-content: flex-end; }
|
|
button {
|
|
font: inherit; font-size: 13px; cursor: pointer; padding: 7px 14px;
|
|
border-radius: 6px; border: 1px solid var(--line);
|
|
background: #1b2430; color: var(--text);
|
|
}
|
|
button:hover { border-color: var(--cmd); }
|
|
button.skip:hover { border-color: var(--expected); }
|
|
button[disabled] { opacity: 0.45; cursor: default; }
|
|
footer {
|
|
margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line);
|
|
color: var(--dim); font-size: 13px;
|
|
font-family: ui-sans-serif, -apple-system, "Segoe UI", system-ui, sans-serif;
|
|
}
|
|
#status { color: var(--expected); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<header>
|
|
<h1>Content feed</h1>
|
|
<p class="sub">
|
|
Ranked by tidalDB. This page holds the catalog; the database holds the
|
|
ranking. Position is whatever order the query returned — nothing here
|
|
sorts.
|
|
</p>
|
|
</header>
|
|
|
|
<ol id="feed"></ol>
|
|
|
|
<footer>
|
|
<span id="env">local standalone node · 60-item fixture catalog · profile <code>for_you</code></span><span id="status"></span>
|
|
</footer>
|
|
</div>
|
|
|
|
<script type="module">
|
|
const LIMIT = Number(new URLSearchParams(location.search).get('limit') ?? 10);
|
|
|
|
const feedEl = document.getElementById('feed');
|
|
const statusEl = document.getElementById('status');
|
|
|
|
/** The catalog, served from the same contract the seeder used. */
|
|
const catalog = await (await fetch('/catalog.json')).json();
|
|
const byId = new Map(catalog.map((item) => [item.entityId, item]));
|
|
|
|
/** Display position of each entity on the previous render, for the delta arrow. */
|
|
let previousPositions = new Map();
|
|
|
|
function signalCell(signals) {
|
|
if (signals.length === 0) return '<span>no signals</span>';
|
|
return signals
|
|
.map((s) => `<span><b>${s.name}</b> ${Number(s.value).toFixed(3)}</span>`)
|
|
.join('');
|
|
}
|
|
|
|
/**
|
|
* Render in ARRAY ORDER. `moved` is the change in display position since the
|
|
* last render — presentation of a delta, not computation of a rank.
|
|
*/
|
|
function render(items) {
|
|
feedEl.replaceChildren(...items.map((row, index) => {
|
|
const item = byId.get(row.entity_id);
|
|
const was = previousPositions.get(row.entity_id);
|
|
const moved = was === undefined ? 0 : was - index;
|
|
const signals = row.signals ?? []; // absent, not [], when there are none
|
|
|
|
const li = document.createElement('li');
|
|
if (moved > 0) li.className = 'moved-up';
|
|
li.innerHTML = `
|
|
<div class="pos">${index + 1}
|
|
<span class="rank${row.rank === index + 1 ? '' : ' disagree'}">rank ${row.rank}</span>
|
|
</div>
|
|
<div>
|
|
<div class="title">${item ? item.title : `unknown item ${row.entity_id}`}
|
|
${moved !== 0 ? `<span class="delta${moved < 0 ? ' down' : ''}">${moved > 0 ? '\u25b2' : '\u25bc'}${Math.abs(moved)}</span>` : ''}
|
|
</div>
|
|
<div class="meta">${item ? item.category : ''} · id ${row.entity_id} · score ${Number(row.score).toFixed(3)}</div>
|
|
</div>
|
|
<div class="signals">${signalCell(signals)}</div>
|
|
<div class="actions">
|
|
<button data-id="${row.entity_id}" data-signal="like">Like</button>
|
|
<button class="skip" data-id="${row.entity_id}" data-signal="skip">Skip</button>
|
|
</div>`;
|
|
return li;
|
|
}));
|
|
|
|
previousPositions = new Map(items.map((row, index) => [row.entity_id, index]));
|
|
}
|
|
|
|
async function refresh() {
|
|
const response = await fetch(`/api/feed?profile=for_you&limit=${LIMIT}`);
|
|
if (!response.ok) {
|
|
statusEl.textContent = ` \u2014 feed failed: HTTP ${response.status}`;
|
|
return;
|
|
}
|
|
render((await response.json()).items ?? []);
|
|
}
|
|
|
|
/** One signal write, then exactly one re-read. No polling, no timer. */
|
|
async function sendSignal(entityId, signal) {
|
|
for (const button of feedEl.querySelectorAll('button')) button.disabled = true;
|
|
const response = await fetch('/api/signals', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ entity_id: Number(entityId), signal, weight: 1.0 }),
|
|
});
|
|
statusEl.textContent = response.ok
|
|
? ` \u2014 wrote ${signal} on ${entityId} (HTTP ${response.status})`
|
|
: ` \u2014 ${signal} on ${entityId} failed: HTTP ${response.status}`;
|
|
await refresh();
|
|
}
|
|
|
|
feedEl.addEventListener('click', (event) => {
|
|
const button = event.target.closest('button[data-signal]');
|
|
if (button) void sendSignal(button.dataset.id, button.dataset.signal);
|
|
});
|
|
|
|
await refresh();
|
|
</script>
|
|
</body>
|
|
</html>
|