openvibers/openvibe.design Commits
Patterns: Honest numbers, five rules with live demos (#226)
Changes
@@@ -127,3 +127,24 @@.six-demo span{padding:4px 8px;border-radius:999px;color:var(--text-secondary)}.six-more{border:1px solid var(--border)}@media (prefers-reduced-motion:reduce){.sheet-demo,.tray-demo li i{transition:none}.menu-demo.open .menu-panel{animation:none}}.status-word{font-weight:700;font-size:15px}++++/* Honest numbers (2026-11-09) */++.hn-quiet{color:var(--text-muted);font-weight:400}++.hn-table td,.hn-table th{white-space:nowrap}++.hn-rank{list-style:decimal;margin:0;padding-left:1.4em;display:grid;gap:6px}++.hn-rank li{padding:8px 10px;border:1px solid var(--border);border-radius:10px;background:var(--bg-card)}++.hn-rank li b{margin-right:10px}++.hn-rank li span{font-variant-numeric:tabular-nums}++.hn-pick{margin:0 0 10px;font-weight:700}++.hn-pick.hn-wrong{color:var(--danger,#ef4444)}++.hn-card{display:grid;gap:4px;max-width:280px;padding:14px;border:1px solid var(--border);border-radius:14px;background:var(--bg-card)}++.hn-score{font-size:1.6rem;font-weight:800;font-variant-numeric:tabular-nums}++.hn-score.hn-quiet{font-size:1.05rem;font-weight:600}++.hn-card small{color:var(--text-muted)}++.hn-price b{font-size:1.6rem;font-variant-numeric:tabular-nums}++.hn-price.stale{border-style:dashed}++.hn-price.stale small{color:var(--warning,#f59e0b)}++.hn-fig{display:grid;gap:2px;text-align:left;max-width:320px;padding:14px;border:1px solid var(--border);border-radius:14px;background:var(--bg-card);color:inherit;font:inherit;cursor:pointer}++.hn-fig b{font-size:1.6rem;font-variant-numeric:tabular-nums}++.hn-fig .hn-src{color:var(--text-muted);max-height:0;overflow:hidden;transition:max-height var(--ov-dur-2,260ms) ease}++.hn-fig.open .hn-src{max-height:4em}@@@ -231,6 +231,44 @@draw(); widget.append(bar, work);control('Next state', () => { k = (k + 1) % states.length; draw(); }, true);control('The bug', () => { work.classList.add('covered'); say('A blurred cover over the work: nobody can watch it being made.'); });++ } else if (id === 'column-earns-place') {++ const rows = [['atlas-1', '$0.40', '1.4 s'], ['nimbus-2', '$2.00', '2.6 s'], ['orbit', '$1.20', null]];++ const table = node('table', 'num-demo hn-table'); widget.append(table);++ const draw = (bug) => { const cols = bug ? ['Model', 'Price', 'Reply time', 'Tokens/s', 'First token', 'Licence'] : ['Model', 'Price', 'Reply time'];++ const head = node('tr'); cols.forEach((h) => head.append(node('th', '', h))); const thead = node('thead'); thead.append(head); const body = node('tbody');++ rows.forEach((r) => { const tr = node('tr'); cols.forEach((_, i) => { const v = r[i]; const td = node('td', v == null ? 'hn-quiet' : '', v == null ? (i < 3 ? 'No traffic yet' : 'Not measured yet') : v); tr.append(td); }); body.append(tr); });++ table.replaceChildren(thead, body); };++ draw(false); say('Three columns, each with values; one missing value says why.');++ control('Columns with values', () => { draw(false); say('Three columns, each with values; one missing value says why.'); }, true);++ control('The bug', () => { draw(true); say('Three more columns, nine cells of “Not measured yet”: the real numbers are harder to find.'); });++ } else if (id === 'missing-not-zero') {++ const data = [['atlas-1', 1400], ['nimbus-2', 2600], ['orbit', null]];++ const list = node('ol', 'hn-rank'); const pick = node('p', 'hn-pick'); widget.append(pick, list);++ const draw = (bug) => { const val = (v) => (v == null ? (bug ? 0 : Infinity) : v); const sorted = [...data].sort((a, b) => val(a[1]) - val(b[1]));++ list.replaceChildren(...sorted.map(([m, v]) => { const li = node('li'); li.append(node('b', '', m), node('span', v == null ? 'hn-quiet' : '', v == null ? (bug ? '0 ms' : 'No traffic yet') : `${(v / 1000).toFixed(1)} s`)); return li; }));++ pick.textContent = `Fastest: ${sorted[0][0]}`; pick.classList.toggle('hn-wrong', bug); };++ draw(false); say('The unmeasured model sorts last and wins nothing.');++ control('Missing sorts last', () => { draw(false); say('The unmeasured model sorts last and wins nothing.'); }, true);++ control('The bug', () => { draw(true); say('null became 0: a model nobody measured is “Fastest”.'); });++ } else if (id === 'not-rated-yet') {++ const card = node('div', 'hn-card'); const name = node('b', '', 'orbit'); const score = node('span', 'hn-score'); const sub = node('small', ''); card.append(name, score, sub); widget.append(card);++ const draw = (games, bug) => { if (games || bug) { score.textContent = String(1000 + games * 6); score.classList.remove('hn-quiet'); sub.textContent = games ? `± ${Math.max(12, 80 - games * 4)} · ${games} games` : '0 games'; } else { score.textContent = 'Not rated yet'; score.classList.add('hn-quiet'); sub.textContent = 'No votes so far'; } };++ let games = 0; draw(0, false); say('No games: the words say so.');++ control('Play a game', () => { games += 1; draw(games, false); say(`${games} ${games === 1 ? 'game' : 'games'}: now there is a rating, with its spread.`); }, true);++ control('The bug', () => { games = 0; draw(0, true); say('1000 with no games reads as a result. It is only where everyone starts.'); });++ } else if (id === 'number-names-source') {++ const fig = node('button', 'hn-fig'); fig.type = 'button'; const v = node('b', '', '1.4 s'); const what = node('span', '', 'typical reply'); const src = node('small', 'hn-src', 'Real traffic through the Exchange · 13,208 requests · last 7 days'); fig.append(v, what, src); widget.append(fig);++ fig.addEventListener('click', () => { fig.classList.toggle('open'); say(fig.classList.contains('open') ? 'Where it came from, how much it rests on, and when.' : 'Tap the number for its source.'); });++ fig.classList.add('open'); say('The value, then what it rests on.');++ control('With its source', () => { fig.classList.add('open'); fig.classList.remove('bare'); say('The value, then what it rests on.'); }, true);++ control('The bug', () => { fig.classList.remove('open'); fig.classList.add('bare'); say('A bare “1.4 s”: measured how, on what, when?'); });++ } else if (id === 'price-freshness') {++ const card = node('div', 'hn-card hn-price'); const p = node('b', '', '$0.90'); const unit = node('span', '', 'per 1M output tokens'); const when = node('small', ''); card.append(p, unit, when); widget.append(card);++ const draw = (hours, bug) => { card.classList.toggle('stale', hours > 72 && !bug); when.textContent = bug ? '' : hours < 24 ? `Checked ${hours} hours ago on the provider’s page` : `Checked ${Math.round(hours / 24)} days ago · may have changed`; };++ draw(3, false); say('Checked 3 hours ago.');++ control('Checked today', () => { draw(3, false); say('Checked 3 hours ago.'); }, true);++ control('Nine days later', () => { draw(216, false); say('Old: the price says it may have changed.'); });++ control('The bug', () => { draw(216, true); say('No date: a nine-day-old price looks as live as one we set.'); });} else if (id === 'mark-system') {const row=node('div','mark-demo'),mark=node('span','ov-mark','ov');row.append(mark,node('span','','OpenVibe'));widget.append(row);control('Loading state',()=>{mark.classList.toggle('loading');say(mark.classList.contains('loading')?'Loading state active.':'Loading state stopped.');});control('Change site',()=>{mark.textContent=mark.textContent==='ov'?'lv':'ov';mark.style.borderRadius=mark.textContent==='lv'?'50%':'16px';say('The mark morphs for the active site.');});control('Open five studies',()=>{window.location.href='/patterns/mark/studies';});}@@@ -60,6 +60,11 @@ const patterns = [['ask-your-agent','Agents','Ask your agent, with the question waiting','Where a page shows one thing (a stream, a video, a file, a thread), one quiet chip beside Share opens the person\u2019s agent with a sentence about that thing already in its box. It is never sent for them: they read it, change it, and press Send. Signed out, the chip signs in first.', '2026-11-07 · Agents could do things on every site, but every ask started from a blank box on another page.', 'One chip, the context in the box, never sent.', t('radiusPill', 'tap')],['levels-with-a-floor','Agents','Levels with a floor that never moves','An agent\u2019s freedom is four levels in words a person can act on (asks first, edits on its own, a second model checks, full trust), as four cards with one line each. Under them, always in view, the four things that ask at every level: spending money, a password or key, deleting something, and messaging someone new. The floor is enforced on the server, not drawn as an option.', '2026-11-07 · Approval levels existed in the API, and nothing on the page said what full trust still could not do.', 'Four cards, and the floor in view.', t('tap', 'spaceMd')],['agent-line','Agents','The agent\u2019s line leaves the work in view','While an agent works on something the person is looking at (a space, a document, a page), its state is one line above the work, not a cover over it: a live dot while it builds, amber while it asks, green when done, red when stuck, with the Dock one tap away. The person watches the work change as the agent makes it.', '2026-11-07 · Space\u2019s builder covered the preview with a blurred \u201cbuilding\u201d screen, so nobody could watch their agent build.', 'One line above the work, four states.', t('durationBase', 'tap')],++ ['column-earns-place','Honest numbers','A column earns its place','A table shows a column only when some row has a value for it, and a section only when something fills it. When everything is missing, one line says why and what would fill it.', '2026-11-09 · Report’s AI leaderboard had twelve columns of “Not measured yet” around the three numbers that were real.', 'Empty columns stay out; one plain line says what is missing.', t('text','surface')],++ ['missing-not-zero','Honest numbers','A missing number is not zero','A value nobody reported is left out or said in words. It never sorts, ranks or wins as zero: in code, null must not become 0.', '2026-11-09 · A model with no measured speed was named “Fastest” because null had become 0 ms.', 'Missing sorts last and wins nothing.', t('text','accent')],++ ['not-rated-yet','Honest numbers','A starting value is not a result','A score with nothing behind it reads “Not rated yet”. The 1000 every entrant starts at appears only once a game has been played.', '2026-11-09 · The leaderboard rated models at 1000 on boards that said “No votes yet”.', 'No games, no rating: the words say so.', t('text')],++ ['number-names-source','Honest numbers','A number names its source','Beside or one tap from every number: where it came from, the day, and how much it rests on. A rating says its games, a price whose page, a speed that it is real traffic.', '2026-11-09 · Comparison pages showed prices and speeds nobody could check.', 'The value, then what it rests on.', t('text','surface')],++ ['price-freshness','Honest numbers','A price says when it was checked','A price read from someone else’s page shows the day it was checked, and says it may have changed once that is old. It never looks as live as a price we set.', '2026-11-09 · Second-hand prices drifted from what providers charged, with nothing on the page to say so.', 'Checked today, or “may have changed”.', t('text','accent')],].map(([slug,group,title,rule,bug,summary,tokens]) => ({ slug, group, title, rule, bug, summary, tokens: [...new Set([...tokens, 'tap', 'spaceMd', 'durationBase'])] }));function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }@@@ -38,16 +38,19 @@ const RULES = Object.freeze([R('missing-is-not-zero', 'honest', 'A missing number is left out or said in words, never shown or sorted as zero.','When a source did not report a value, the page says "No traffic yet" or "Not measured", and sorting and picks skip it. In code, null must not become 0.','2026-11-09', 'A model with no measured speed was named "Fastest" because null had become 0 ms.',-- [{ label: 'Open stats', href: 'https://openvibe.network/stats' }], ['Report test: picks use measured values']),++ [{ label: 'A missing number is not zero', href: '/patterns/missing-not-zero' }, { label: 'Open stats', href: 'https://openvibe.network/stats' }], ['Report test: picks use measured values']),R('number-has-source', 'honest', 'Every number names its source, its date and how much it rests on.','A rating says how many games; a price says whose page and when it was checked; a speed says it is from real traffic over seven days. One tap shows it.',-- '2026-11-09', 'Comparison pages showed numbers nobody could check.'),++ '2026-11-09', 'Comparison pages showed numbers nobody could check.',++ [{ label: 'A number names its source', href: '/patterns/number-names-source' }]),R('not-a-rating-yet', 'honest', 'A starting value is not a result.','A model with no votes shows "Not rated yet", not the 1000 every entrant starts at. The same goes for any score with no samples behind it.',-- '2026-11-09', 'The leaderboard rated models at 1000 on boards that said "No votes yet".'),++ '2026-11-09', 'The leaderboard rated models at 1000 on boards that said "No votes yet".',++ [{ label: 'A starting value is not a result', href: '/patterns/not-rated-yet' }]),R('columns-earn-their-place', 'honest', 'A column or section appears when something fills it.','A table shows a column only when some row has a value for it; a section with nothing in it is not drawn. When everything is missing, one plain line says why and what would fill it.',-- '2026-11-09', 'Twelve columns of "Not measured yet" buried the three numbers that were real.'),++ '2026-11-09', 'Twelve columns of "Not measured yet" buried the three numbers that were real.',++ [{ label: 'A column earns its place', href: '/patterns/column-earns-place' }]),R('state-in-words', 'honest', 'Status is a word, never colour alone.','A dot or colour helps the eye; the word carries the meaning for everyone.','2026-10-30', 'Colour alone made live, done and failed look alike.',@@@ -179,7 +182,8 @@ const RULES = Object.freeze([// ── Data, AI and consent ──R('own-page-prices', 'data', 'A price comes from the provider’s own page, with the day it was checked.','Never an aggregator’s price; a price older than its check window says it may have changed.',-- '2026-11-08', 'Second-hand prices drifted from what providers charged.'),++ '2026-11-08', 'Second-hand prices drifted from what providers charged.',++ [{ label: 'A price says when it was checked', href: '/patterns/price-freshness' }]),R('measured-on-real-traffic', 'data', 'Speed, errors and uptime are measured on real requests.','Synthetic probes fill gaps only where traffic is thin, and say so.','2026-11-09', 'Real traffic is the number a person will actually get.'),
@@ -127,3 +127,24 @@
@@ -127,3 +127,24 @@
.six-demo span{padding:4px 8px;border-radius:999px;color:var(--text-secondary)}.six-more{border:1px solid var(--border)}
.six-demo span{padding:4px 8px;border-radius:999px;color:var(--text-secondary)}.six-more{border:1px solid var(--border)}
@media (prefers-reduced-motion:reduce){.sheet-demo,.tray-demo li i{transition:none}.menu-demo.open .menu-panel{animation:none}}
@media (prefers-reduced-motion:reduce){.sheet-demo,.tray-demo li i{transition:none}.menu-demo.open .menu-panel{animation:none}}
.status-word{font-weight:700;font-size:15px}
.status-word{font-weight:700;font-size:15px}
+
+/* Honest numbers (2026-11-09) */
+.hn-quiet{color:var(--text-muted);font-weight:400}
+.hn-table td,.hn-table th{white-space:nowrap}
+.hn-rank{list-style:decimal;margin:0;padding-left:1.4em;display:grid;gap:6px}
+.hn-rank li{padding:8px 10px;border:1px solid var(--border);border-radius:10px;background:var(--bg-card)}
+.hn-rank li b{margin-right:10px}
+.hn-rank li span{font-variant-numeric:tabular-nums}
+.hn-pick{margin:0 0 10px;font-weight:700}
+.hn-pick.hn-wrong{color:var(--danger,#ef4444)}
+.hn-card{display:grid;gap:4px;max-width:280px;padding:14px;border:1px solid var(--border);border-radius:14px;background:var(--bg-card)}
+.hn-score{font-size:1.6rem;font-weight:800;font-variant-numeric:tabular-nums}
+.hn-score.hn-quiet{font-size:1.05rem;font-weight:600}
+.hn-card small{color:var(--text-muted)}
+.hn-price b{font-size:1.6rem;font-variant-numeric:tabular-nums}
+.hn-price.stale{border-style:dashed}
+.hn-price.stale small{color:var(--warning,#f59e0b)}
+.hn-fig{display:grid;gap:2px;text-align:left;max-width:320px;padding:14px;border:1px solid var(--border);border-radius:14px;background:var(--bg-card);color:inherit;font:inherit;cursor:pointer}
+.hn-fig b{font-size:1.6rem;font-variant-numeric:tabular-nums}
+.hn-fig .hn-src{color:var(--text-muted);max-height:0;overflow:hidden;transition:max-height var(--ov-dur-2,260ms) ease}
+.hn-fig.open .hn-src{max-height:4em}
@@ -231,6 +231,44 @@
@@ -231,6 +231,44 @@
draw(); widget.append(bar, work);
draw(); widget.append(bar, work);
control('Next state', () => { k = (k + 1) % states.length; draw(); }, true);
control('Next state', () => { k = (k + 1) % states.length; draw(); }, true);
control('The bug', () => { work.classList.add('covered'); say('A blurred cover over the work: nobody can watch it being made.'); });
control('The bug', () => { work.classList.add('covered'); say('A blurred cover over the work: nobody can watch it being made.'); });
+ } else if (id === 'column-earns-place') {
+ const rows = [['atlas-1', '$0.40', '1.4 s'], ['nimbus-2', '$2.00', '2.6 s'], ['orbit', '$1.20', null]];
+ const table = node('table', 'num-demo hn-table'); widget.append(table);
+ const draw = (bug) => { const cols = bug ? ['Model', 'Price', 'Reply time', 'Tokens/s', 'First token', 'Licence'] : ['Model', 'Price', 'Reply time'];
+ const head = node('tr'); cols.forEach((h) => head.append(node('th', '', h))); const thead = node('thead'); thead.append(head); const body = node('tbody');
+ rows.forEach((r) => { const tr = node('tr'); cols.forEach((_, i) => { const v = r[i]; const td = node('td', v == null ? 'hn-quiet' : '', v == null ? (i < 3 ? 'No traffic yet' : 'Not measured yet') : v); tr.append(td); }); body.append(tr); });
+ table.replaceChildren(thead, body); };
+ draw(false); say('Three columns, each with values; one missing value says why.');
+ control('Columns with values', () => { draw(false); say('Three columns, each with values; one missing value says why.'); }, true);
+ control('The bug', () => { draw(true); say('Three more columns, nine cells of “Not measured yet”: the real numbers are harder to find.'); });
+ } else if (id === 'missing-not-zero') {
+ const data = [['atlas-1', 1400], ['nimbus-2', 2600], ['orbit', null]];
+ const list = node('ol', 'hn-rank'); const pick = node('p', 'hn-pick'); widget.append(pick, list);
+ const draw = (bug) => { const val = (v) => (v == null ? (bug ? 0 : Infinity) : v); const sorted = [...data].sort((a, b) => val(a[1]) - val(b[1]));
+ list.replaceChildren(...sorted.map(([m, v]) => { const li = node('li'); li.append(node('b', '', m), node('span', v == null ? 'hn-quiet' : '', v == null ? (bug ? '0 ms' : 'No traffic yet') : `${(v / 1000).toFixed(1)} s`)); return li; }));
+ pick.textContent = `Fastest: ${sorted[0][0]}`; pick.classList.toggle('hn-wrong', bug); };
+ draw(false); say('The unmeasured model sorts last and wins nothing.');
+ control('Missing sorts last', () => { draw(false); say('The unmeasured model sorts last and wins nothing.'); }, true);
+ control('The bug', () => { draw(true); say('null became 0: a model nobody measured is “Fastest”.'); });
+ } else if (id === 'not-rated-yet') {
+ const card = node('div', 'hn-card'); const name = node('b', '', 'orbit'); const score = node('span', 'hn-score'); const sub = node('small', ''); card.append(name, score, sub); widget.append(card);
+ const draw = (games, bug) => { if (games || bug) { score.textContent = String(1000 + games * 6); score.classList.remove('hn-quiet'); sub.textContent = games ? `± ${Math.max(12, 80 - games * 4)} · ${games} games` : '0 games'; } else { score.textContent = 'Not rated yet'; score.classList.add('hn-quiet'); sub.textContent = 'No votes so far'; } };
+ let games = 0; draw(0, false); say('No games: the words say so.');
+ control('Play a game', () => { games += 1; draw(games, false); say(`${games} ${games === 1 ? 'game' : 'games'}: now there is a rating, with its spread.`); }, true);
+ control('The bug', () => { games = 0; draw(0, true); say('1000 with no games reads as a result. It is only where everyone starts.'); });
+ } else if (id === 'number-names-source') {
+ const fig = node('button', 'hn-fig'); fig.type = 'button'; const v = node('b', '', '1.4 s'); const what = node('span', '', 'typical reply'); const src = node('small', 'hn-src', 'Real traffic through the Exchange · 13,208 requests · last 7 days'); fig.append(v, what, src); widget.append(fig);
+ fig.addEventListener('click', () => { fig.classList.toggle('open'); say(fig.classList.contains('open') ? 'Where it came from, how much it rests on, and when.' : 'Tap the number for its source.'); });
+ fig.classList.add('open'); say('The value, then what it rests on.');
+ control('With its source', () => { fig.classList.add('open'); fig.classList.remove('bare'); say('The value, then what it rests on.'); }, true);
+ control('The bug', () => { fig.classList.remove('open'); fig.classList.add('bare'); say('A bare “1.4 s”: measured how, on what, when?'); });
+ } else if (id === 'price-freshness') {
+ const card = node('div', 'hn-card hn-price'); const p = node('b', '', '$0.90'); const unit = node('span', '', 'per 1M output tokens'); const when = node('small', ''); card.append(p, unit, when); widget.append(card);
+ const draw = (hours, bug) => { card.classList.toggle('stale', hours > 72 && !bug); when.textContent = bug ? '' : hours < 24 ? `Checked ${hours} hours ago on the provider’s page` : `Checked ${Math.round(hours / 24)} days ago · may have changed`; };
+ draw(3, false); say('Checked 3 hours ago.');
+ control('Checked today', () => { draw(3, false); say('Checked 3 hours ago.'); }, true);
+ control('Nine days later', () => { draw(216, false); say('Old: the price says it may have changed.'); });
+ control('The bug', () => { draw(216, true); say('No date: a nine-day-old price looks as live as one we set.'); });
} else if (id === 'mark-system') {
} else if (id === 'mark-system') {
const row=node('div','mark-demo'),mark=node('span','ov-mark','ov');row.append(mark,node('span','','OpenVibe'));widget.append(row);control('Loading state',()=>{mark.classList.toggle('loading');say(mark.classList.contains('loading')?'Loading state active.':'Loading state stopped.');});control('Change site',()=>{mark.textContent=mark.textContent==='ov'?'lv':'ov';mark.style.borderRadius=mark.textContent==='lv'?'50%':'16px';say('The mark morphs for the active site.');});control('Open five studies',()=>{window.location.href='/patterns/mark/studies';});
const row=node('div','mark-demo'),mark=node('span','ov-mark','ov');row.append(mark,node('span','','OpenVibe'));widget.append(row);control('Loading state',()=>{mark.classList.toggle('loading');say(mark.classList.contains('loading')?'Loading state active.':'Loading state stopped.');});control('Change site',()=>{mark.textContent=mark.textContent==='ov'?'lv':'ov';mark.style.borderRadius=mark.textContent==='lv'?'50%':'16px';say('The mark morphs for the active site.');});control('Open five studies',()=>{window.location.href='/patterns/mark/studies';});
}
}
@@ -60,6 +60,11 @@ const patterns = [
@@ -60,6 +60,11 @@ const patterns = [
['ask-your-agent','Agents','Ask your agent, with the question waiting','Where a page shows one thing (a stream, a video, a file, a thread), one quiet chip beside Share opens the person\u2019s agent with a sentence about that thing already in its box. It is never sent for them: they read it, change it, and press Send. Signed out, the chip signs in first.', '2026-11-07 · Agents could do things on every site, but every ask started from a blank box on another page.', 'One chip, the context in the box, never sent.', t('radiusPill', 'tap')],
['ask-your-agent','Agents','Ask your agent, with the question waiting','Where a page shows one thing (a stream, a video, a file, a thread), one quiet chip beside Share opens the person\u2019s agent with a sentence about that thing already in its box. It is never sent for them: they read it, change it, and press Send. Signed out, the chip signs in first.', '2026-11-07 · Agents could do things on every site, but every ask started from a blank box on another page.', 'One chip, the context in the box, never sent.', t('radiusPill', 'tap')],
['levels-with-a-floor','Agents','Levels with a floor that never moves','An agent\u2019s freedom is four levels in words a person can act on (asks first, edits on its own, a second model checks, full trust), as four cards with one line each. Under them, always in view, the four things that ask at every level: spending money, a password or key, deleting something, and messaging someone new. The floor is enforced on the server, not drawn as an option.', '2026-11-07 · Approval levels existed in the API, and nothing on the page said what full trust still could not do.', 'Four cards, and the floor in view.', t('tap', 'spaceMd')],
['levels-with-a-floor','Agents','Levels with a floor that never moves','An agent\u2019s freedom is four levels in words a person can act on (asks first, edits on its own, a second model checks, full trust), as four cards with one line each. Under them, always in view, the four things that ask at every level: spending money, a password or key, deleting something, and messaging someone new. The floor is enforced on the server, not drawn as an option.', '2026-11-07 · Approval levels existed in the API, and nothing on the page said what full trust still could not do.', 'Four cards, and the floor in view.', t('tap', 'spaceMd')],
['agent-line','Agents','The agent\u2019s line leaves the work in view','While an agent works on something the person is looking at (a space, a document, a page), its state is one line above the work, not a cover over it: a live dot while it builds, amber while it asks, green when done, red when stuck, with the Dock one tap away. The person watches the work change as the agent makes it.', '2026-11-07 · Space\u2019s builder covered the preview with a blurred \u201cbuilding\u201d screen, so nobody could watch their agent build.', 'One line above the work, four states.', t('durationBase', 'tap')],
['agent-line','Agents','The agent\u2019s line leaves the work in view','While an agent works on something the person is looking at (a space, a document, a page), its state is one line above the work, not a cover over it: a live dot while it builds, amber while it asks, green when done, red when stuck, with the Dock one tap away. The person watches the work change as the agent makes it.', '2026-11-07 · Space\u2019s builder covered the preview with a blurred \u201cbuilding\u201d screen, so nobody could watch their agent build.', 'One line above the work, four states.', t('durationBase', 'tap')],
+ ['column-earns-place','Honest numbers','A column earns its place','A table shows a column only when some row has a value for it, and a section only when something fills it. When everything is missing, one line says why and what would fill it.', '2026-11-09 · Report’s AI leaderboard had twelve columns of “Not measured yet” around the three numbers that were real.', 'Empty columns stay out; one plain line says what is missing.', t('text','surface')],
+ ['missing-not-zero','Honest numbers','A missing number is not zero','A value nobody reported is left out or said in words. It never sorts, ranks or wins as zero: in code, null must not become 0.', '2026-11-09 · A model with no measured speed was named “Fastest” because null had become 0 ms.', 'Missing sorts last and wins nothing.', t('text','accent')],
+ ['not-rated-yet','Honest numbers','A starting value is not a result','A score with nothing behind it reads “Not rated yet”. The 1000 every entrant starts at appears only once a game has been played.', '2026-11-09 · The leaderboard rated models at 1000 on boards that said “No votes yet”.', 'No games, no rating: the words say so.', t('text')],
+ ['number-names-source','Honest numbers','A number names its source','Beside or one tap from every number: where it came from, the day, and how much it rests on. A rating says its games, a price whose page, a speed that it is real traffic.', '2026-11-09 · Comparison pages showed prices and speeds nobody could check.', 'The value, then what it rests on.', t('text','surface')],
+ ['price-freshness','Honest numbers','A price says when it was checked','A price read from someone else’s page shows the day it was checked, and says it may have changed once that is old. It never looks as live as a price we set.', '2026-11-09 · Second-hand prices drifted from what providers charged, with nothing on the page to say so.', 'Checked today, or “may have changed”.', t('text','accent')],
].map(([slug,group,title,rule,bug,summary,tokens]) => ({ slug, group, title, rule, bug, summary, tokens: [...new Set([...tokens, 'tap', 'spaceMd', 'durationBase'])] }));
].map(([slug,group,title,rule,bug,summary,tokens]) => ({ slug, group, title, rule, bug, summary, tokens: [...new Set([...tokens, 'tap', 'spaceMd', 'durationBase'])] }));
function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
@@ -38,16 +38,19 @@ const RULES = Object.freeze([
@@ -38,16 +38,19 @@ const RULES = Object.freeze([
R('missing-is-not-zero', 'honest', 'A missing number is left out or said in words, never shown or sorted as zero.',
R('missing-is-not-zero', 'honest', 'A missing number is left out or said in words, never shown or sorted as zero.',
'When a source did not report a value, the page says "No traffic yet" or "Not measured", and sorting and picks skip it. In code, null must not become 0.',
'When a source did not report a value, the page says "No traffic yet" or "Not measured", and sorting and picks skip it. In code, null must not become 0.',
'2026-11-09', 'A model with no measured speed was named "Fastest" because null had become 0 ms.',
'2026-11-09', 'A model with no measured speed was named "Fastest" because null had become 0 ms.',
- [{ label: 'Open stats', href: 'https://openvibe.network/stats' }], ['Report test: picks use measured values']),
+ [{ label: 'A missing number is not zero', href: '/patterns/missing-not-zero' }, { label: 'Open stats', href: 'https://openvibe.network/stats' }], ['Report test: picks use measured values']),
R('number-has-source', 'honest', 'Every number names its source, its date and how much it rests on.',
R('number-has-source', 'honest', 'Every number names its source, its date and how much it rests on.',
'A rating says how many games; a price says whose page and when it was checked; a speed says it is from real traffic over seven days. One tap shows it.',
'A rating says how many games; a price says whose page and when it was checked; a speed says it is from real traffic over seven days. One tap shows it.',
- '2026-11-09', 'Comparison pages showed numbers nobody could check.'),
+ '2026-11-09', 'Comparison pages showed numbers nobody could check.',
+ [{ label: 'A number names its source', href: '/patterns/number-names-source' }]),
R('not-a-rating-yet', 'honest', 'A starting value is not a result.',
R('not-a-rating-yet', 'honest', 'A starting value is not a result.',
'A model with no votes shows "Not rated yet", not the 1000 every entrant starts at. The same goes for any score with no samples behind it.',
'A model with no votes shows "Not rated yet", not the 1000 every entrant starts at. The same goes for any score with no samples behind it.',
- '2026-11-09', 'The leaderboard rated models at 1000 on boards that said "No votes yet".'),
+ '2026-11-09', 'The leaderboard rated models at 1000 on boards that said "No votes yet".',
+ [{ label: 'A starting value is not a result', href: '/patterns/not-rated-yet' }]),
R('columns-earn-their-place', 'honest', 'A column or section appears when something fills it.',
R('columns-earn-their-place', 'honest', 'A column or section appears when something fills it.',
'A table shows a column only when some row has a value for it; a section with nothing in it is not drawn. When everything is missing, one plain line says why and what would fill it.',
'A table shows a column only when some row has a value for it; a section with nothing in it is not drawn. When everything is missing, one plain line says why and what would fill it.',
- '2026-11-09', 'Twelve columns of "Not measured yet" buried the three numbers that were real.'),
+ '2026-11-09', 'Twelve columns of "Not measured yet" buried the three numbers that were real.',
+ [{ label: 'A column earns its place', href: '/patterns/column-earns-place' }]),
R('state-in-words', 'honest', 'Status is a word, never colour alone.',
R('state-in-words', 'honest', 'Status is a word, never colour alone.',
'A dot or colour helps the eye; the word carries the meaning for everyone.',
'A dot or colour helps the eye; the word carries the meaning for everyone.',
'2026-10-30', 'Colour alone made live, done and failed look alike.',
'2026-10-30', 'Colour alone made live, done and failed look alike.',
@@ -179,7 +182,8 @@ const RULES = Object.freeze([
@@ -179,7 +182,8 @@ const RULES = Object.freeze([
// ── Data, AI and consent ──
// ── Data, AI and consent ──
R('own-page-prices', 'data', 'A price comes from the provider’s own page, with the day it was checked.',
R('own-page-prices', 'data', 'A price comes from the provider’s own page, with the day it was checked.',
'Never an aggregator’s price; a price older than its check window says it may have changed.',
'Never an aggregator’s price; a price older than its check window says it may have changed.',
- '2026-11-08', 'Second-hand prices drifted from what providers charged.'),
+ '2026-11-08', 'Second-hand prices drifted from what providers charged.',
+ [{ label: 'A price says when it was checked', href: '/patterns/price-freshness' }]),
R('measured-on-real-traffic', 'data', 'Speed, errors and uptime are measured on real requests.',
R('measured-on-real-traffic', 'data', 'Speed, errors and uptime are measured on real requests.',
'Synthetic probes fill gaps only where traffic is thin, and say so.',
'Synthetic probes fill gaps only where traffic is thin, and say so.',
'2026-11-09', 'Real traffic is the number a person will actually get.'),
'2026-11-09', 'Real traffic is the number a person will actually get.'),