openvibers/openvibe.wiki Commits
A wiki page's head says what a reader may check before trusting it (market outcomes) (#176)
Changes
@@@ -231,3 +231,13 @@ main .wk-h-group h3 { display: flex; flex-wrap: wrap; align-items: baseline; gap.wk-fold-block .sc-sec { padding: 0; }}@media (max-width: 640px) { .wk-fold-block { margin-top: 24px; } .wk-fold-block > summary span { display: none; } }++/* What a reader may check before trusting a page, in its head (market outcomes 2, 3 and 5) */++.wk-facts { flex-basis: 100%; margin: 6px 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 0; font-size: .86rem; color: var(--text-secondary, #96a7c2); }++.wk-facts a { color: var(--text-secondary, #96a7c2); text-decoration-color: color-mix(in srgb, currentColor 40%, transparent); }++.wk-facts a:hover { color: var(--accent, #3b82f6); }++.wk-facts > span[aria-hidden] { padding: 0 6px; color: var(--text-muted, #7386a3); }++.wk-fact-wait { color: var(--warning-text, var(--warning, #f59e0b)); }++.wk-review { margin-left: 10px; padding: 2px 10px; border-radius: 999px; font-size: .76rem; font-weight: 700; white-space: nowrap; }++.wk-review--ok { color: var(--success-text, var(--success, #22c55e)); background: color-mix(in srgb, var(--success, #22c55e) 12%, transparent); }++.wk-review--no { color: var(--warning-text, var(--warning, #f59e0b)); background: color-mix(in srgb, var(--warning, #f59e0b) 12%, transparent); }++@media (max-width: 600px) { .wk-review { margin-left: 0; margin-top: 4px; } }@@@ -387,6 +387,29 @@ ${comments.length ? html`<ul class="wk-comments">${comments.map((c) => html`<li>${form}</section>`;}++/**++ * What a reader may want to check before trusting a page, in its head (market outcomes 2, 3 and 5): how many sources it++ * cites and how many revisions it has had, when the text they read was accepted, whether a newer edit is waiting, and++ * whether a person reviewed it. No other wiki shows these together.++ */++function pageFacts(v, base) {++ const { revision: rev, page } = v;++ const n = (x, one, many = `${one}s`) => `${x} ${x === 1 ? one : many}`;++ const facts = [++ html`<a href="${base}/sources${v.isPublishedRevision ? '' : `?rev=${rev.number}`}">${n(v.citations.length, 'source')} cited</a>`,++ html`<a href="${base}/history">${n(Number(v.headNumber || rev.number), 'revision')}</a>`,++ v.isPublishedRevision ? html`this text accepted ${t(rev.createdAt)}` : html`revision ${String(rev.number)}, saved ${t(rev.createdAt)}`,++ ];++ if (v.offeredAt && v.isPublishedRevision) facts.push(html`<span class="wk-fact-wait">a newer edit, offered ${t(v.offeredAt)}, is waiting</span>`);++ const r = v.review;++ const reviewed = !!(r && /approv|accept/i.test(String(r.decision || '')));++ const ai = !!(v.authorship && v.authorship.mode && v.authorship.mode !== 'human');++ const badge = reviewed++ ? html`<span class="wk-review wk-review--ok">Reviewed by a person${r.reviewed_at || r.reviewedAt ? html` ${t(r.reviewed_at || r.reviewedAt)}` : ''}</span>`++ : ai ? html`<span class="wk-review wk-review--no">Not yet reviewed by a person</span>` : '';++ return html`<p class="wk-facts">${facts.map((f, i) => html`${i ? html`<span aria-hidden="true"> · </span>` : ''}<span>${f}</span>`)}${badge ? html` ${badge}` : ''}</p>`;++}++function articlePage(v, { html: contentHtml, discussion, actor, mediaUrl, resolve, flash }) {const { space, page, revision: rev } = v;const base = wpath(space, page);@@@ -405,7 +428,7 @@ function articlePage(v, { html: contentHtml, discussion, actor, mediaUrl, resolvif (v.canEdit) actions.unshift(html`<a href="${base}/edit">Edit</a>`), actions.push(html`<a href="${base}/settings">Page settings</a>`);return html`${crumbs(trail)}<article class="wk-article" data-page-id="${page.id}" data-revision="${String(rev.number)}">--<header><h1>${rev.fields.title || page.title}</h1><nav class="wk-actions" aria-label="Page actions">${actions.map((a, i) => html`${i ? ' · ' : ''}${a}`)}</nav></header>++<header><h1>${rev.fields.title || page.title}</h1><nav class="wk-actions" aria-label="Page actions">${actions.map((a, i) => html`${i ? ' · ' : ''}${a}`)}</nav>${pageFacts(v, base)}</header>${banners}${raw(infoboxHtml(v.infobox, resolve, outRel(space), { media: (id) => { const a = v.attachments.find((x) => x.mediaId === id); return a && mediaUrl ? figureHtml(a, { urlFor: mediaUrl, unavailableText: UNAVAILABLE[a.brokenReason] || UNAVAILABLE.not_found }) : null; } }))}<div class="wk-content">${raw(contentHtml)}</div>@@@ -1048,8 +1048,10 @@ function createWikiService({ db, stores, outbox, config, community = null, vip =actor && actor.subject ? await q.isWatching.get(page.id, actor.subject) : null,canEdit ? await q.proposalsOfPage.all(page.id) : [],canEdit ? scheduler.jobs(page.id) : [],-- canEdit ? revisions.headNumber(page.id) : null,++ // Everyone sees how many revisions a page has had and whether a newer edit waits (market outcomes 2-3).++ revisions.headNumber(page.id),]);++ const offered = headNumber && headNumber > n ? await revisions.get(page.id, headNumber) : null;const rec = rev.meta.authorship || null;const facts = gateFactsWith(space, page, rev, cites.length, review);const decision = page.state === 'published' && n === page.published_revision@@@ -1073,6 +1075,7 @@ function createWikiService({ db, stores, outbox, config, community = null, vip =pendingProposals: proposals.filter((p) => p.status === 'pending'),jobs: jobs.filter((j) => j.status === 'pending' || j.status === 'running'),headNumber,++ offeredAt: offered ? offered.createdAt : null,};},@@@ -87,6 +87,15 @@ const data = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'seeds', 'opeassert.ok(r.text.includes('<meta name="robots" content="index, follow">'));assert.ok(r.text.includes('Reviewed by a person.'));assert.ok(!r.text.includes('Not yet reviewed by a person.'));++ // The head says what a reader may check (market outcomes 2, 3 and 5): sources, revisions, when this text was++ // accepted, and the review, with its date.++ const facts = (/<p class="wk-facts">[\s\S]*?<\/p>/.exec(r.text) || [''])[0];++ assert.match(facts, /<a href="\/w\/openvibe\/openvibe-search\/sources">\d+ sources? cited<\/a>/);++ assert.match(facts, /<a href="\/w\/openvibe\/openvibe-search\/history">\d+ revisions?<\/a>/);++ assert.match(facts, /this text accepted <time/);++ assert.match(facts, /<span class="wk-review wk-review--ok">Reviewed by a person <time/);++ const waiting = await H.req(h, 'GET', `/w/openvibe/${legacy.page.slug}`);++ assert.match(waiting.text, /<span class="wk-review wk-review--no">Not yet reviewed by a person<\/span>/, 'an AI-assisted page nobody reviewed says so in its head');assert.ok((await H.req(h, 'GET', '/sitemaps/pages-1.xml')).text.includes('http://wiki.test/w/openvibe/openvibe-search'));assert.ok((await H.req(h, 'GET', '/feed.atom')).text.includes('http://wiki.test/w/openvibe/openvibe-search'));assert.ok((await H.req(h, 'GET', '/feed.json')).json.items.some((i) => i.url === 'http://wiki.test/w/openvibe/openvibe-search'));
@@ -231,3 +231,13 @@ main .wk-h-group h3 { display: flex; flex-wrap: wrap; align-items: baseline; gap
@@ -231,3 +231,13 @@ main .wk-h-group h3 { display: flex; flex-wrap: wrap; align-items: baseline; gap
.wk-fold-block .sc-sec { padding: 0; }
.wk-fold-block .sc-sec { padding: 0; }
}
}
@media (max-width: 640px) { .wk-fold-block { margin-top: 24px; } .wk-fold-block > summary span { display: none; } }
@media (max-width: 640px) { .wk-fold-block { margin-top: 24px; } .wk-fold-block > summary span { display: none; } }
+/* What a reader may check before trusting a page, in its head (market outcomes 2, 3 and 5) */
+.wk-facts { flex-basis: 100%; margin: 6px 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 0; font-size: .86rem; color: var(--text-secondary, #96a7c2); }
+.wk-facts a { color: var(--text-secondary, #96a7c2); text-decoration-color: color-mix(in srgb, currentColor 40%, transparent); }
+.wk-facts a:hover { color: var(--accent, #3b82f6); }
+.wk-facts > span[aria-hidden] { padding: 0 6px; color: var(--text-muted, #7386a3); }
+.wk-fact-wait { color: var(--warning-text, var(--warning, #f59e0b)); }
+.wk-review { margin-left: 10px; padding: 2px 10px; border-radius: 999px; font-size: .76rem; font-weight: 700; white-space: nowrap; }
+.wk-review--ok { color: var(--success-text, var(--success, #22c55e)); background: color-mix(in srgb, var(--success, #22c55e) 12%, transparent); }
+.wk-review--no { color: var(--warning-text, var(--warning, #f59e0b)); background: color-mix(in srgb, var(--warning, #f59e0b) 12%, transparent); }
+@media (max-width: 600px) { .wk-review { margin-left: 0; margin-top: 4px; } }
@@ -387,6 +387,29 @@ ${comments.length ? html`<ul class="wk-comments">${comments.map((c) => html`<li>
@@ -387,6 +387,29 @@ ${comments.length ? html`<ul class="wk-comments">${comments.map((c) => html`<li>
${form}</section>`;
${form}</section>`;
}
}
+/**
+ * What a reader may want to check before trusting a page, in its head (market outcomes 2, 3 and 5): how many sources it
+ * cites and how many revisions it has had, when the text they read was accepted, whether a newer edit is waiting, and
+ * whether a person reviewed it. No other wiki shows these together.
+ */
+function pageFacts(v, base) {
+ const { revision: rev, page } = v;
+ const n = (x, one, many = `${one}s`) => `${x} ${x === 1 ? one : many}`;
+ const facts = [
+ html`<a href="${base}/sources${v.isPublishedRevision ? '' : `?rev=${rev.number}`}">${n(v.citations.length, 'source')} cited</a>`,
+ html`<a href="${base}/history">${n(Number(v.headNumber || rev.number), 'revision')}</a>`,
+ v.isPublishedRevision ? html`this text accepted ${t(rev.createdAt)}` : html`revision ${String(rev.number)}, saved ${t(rev.createdAt)}`,
+ ];
+ if (v.offeredAt && v.isPublishedRevision) facts.push(html`<span class="wk-fact-wait">a newer edit, offered ${t(v.offeredAt)}, is waiting</span>`);
+ const r = v.review;
+ const reviewed = !!(r && /approv|accept/i.test(String(r.decision || '')));
+ const ai = !!(v.authorship && v.authorship.mode && v.authorship.mode !== 'human');
+ const badge = reviewed
+ ? html`<span class="wk-review wk-review--ok">Reviewed by a person${r.reviewed_at || r.reviewedAt ? html` ${t(r.reviewed_at || r.reviewedAt)}` : ''}</span>`
+ : ai ? html`<span class="wk-review wk-review--no">Not yet reviewed by a person</span>` : '';
+ return html`<p class="wk-facts">${facts.map((f, i) => html`${i ? html`<span aria-hidden="true"> · </span>` : ''}<span>${f}</span>`)}${badge ? html` ${badge}` : ''}</p>`;
+}
+
function articlePage(v, { html: contentHtml, discussion, actor, mediaUrl, resolve, flash }) {
function articlePage(v, { html: contentHtml, discussion, actor, mediaUrl, resolve, flash }) {
const { space, page, revision: rev } = v;
const { space, page, revision: rev } = v;
const base = wpath(space, page);
const base = wpath(space, page);
@@ -405,7 +428,7 @@ function articlePage(v, { html: contentHtml, discussion, actor, mediaUrl, resolv
@@ -405,7 +428,7 @@ function articlePage(v, { html: contentHtml, discussion, actor, mediaUrl, resolv
if (v.canEdit) actions.unshift(html`<a href="${base}/edit">Edit</a>`), actions.push(html`<a href="${base}/settings">Page settings</a>`);
if (v.canEdit) actions.unshift(html`<a href="${base}/edit">Edit</a>`), actions.push(html`<a href="${base}/settings">Page settings</a>`);
return html`${crumbs(trail)}
return html`${crumbs(trail)}
<article class="wk-article" data-page-id="${page.id}" data-revision="${String(rev.number)}">
<article class="wk-article" data-page-id="${page.id}" data-revision="${String(rev.number)}">
-<header><h1>${rev.fields.title || page.title}</h1><nav class="wk-actions" aria-label="Page actions">${actions.map((a, i) => html`${i ? ' · ' : ''}${a}`)}</nav></header>
+<header><h1>${rev.fields.title || page.title}</h1><nav class="wk-actions" aria-label="Page actions">${actions.map((a, i) => html`${i ? ' · ' : ''}${a}`)}</nav>${pageFacts(v, base)}</header>
${banners}
${banners}
${raw(infoboxHtml(v.infobox, resolve, outRel(space), { media: (id) => { const a = v.attachments.find((x) => x.mediaId === id); return a && mediaUrl ? figureHtml(a, { urlFor: mediaUrl, unavailableText: UNAVAILABLE[a.brokenReason] || UNAVAILABLE.not_found }) : null; } }))}
${raw(infoboxHtml(v.infobox, resolve, outRel(space), { media: (id) => { const a = v.attachments.find((x) => x.mediaId === id); return a && mediaUrl ? figureHtml(a, { urlFor: mediaUrl, unavailableText: UNAVAILABLE[a.brokenReason] || UNAVAILABLE.not_found }) : null; } }))}
<div class="wk-content">${raw(contentHtml)}</div>
<div class="wk-content">${raw(contentHtml)}</div>
@@ -1048,8 +1048,10 @@ function createWikiService({ db, stores, outbox, config, community = null, vip =
@@ -1048,8 +1048,10 @@ function createWikiService({ db, stores, outbox, config, community = null, vip =
actor && actor.subject ? await q.isWatching.get(page.id, actor.subject) : null,
actor && actor.subject ? await q.isWatching.get(page.id, actor.subject) : null,
canEdit ? await q.proposalsOfPage.all(page.id) : [],
canEdit ? await q.proposalsOfPage.all(page.id) : [],
canEdit ? scheduler.jobs(page.id) : [],
canEdit ? scheduler.jobs(page.id) : [],
- canEdit ? revisions.headNumber(page.id) : null,
+ // Everyone sees how many revisions a page has had and whether a newer edit waits (market outcomes 2-3).
+ revisions.headNumber(page.id),
]);
]);
+ const offered = headNumber && headNumber > n ? await revisions.get(page.id, headNumber) : null;
const rec = rev.meta.authorship || null;
const rec = rev.meta.authorship || null;
const facts = gateFactsWith(space, page, rev, cites.length, review);
const facts = gateFactsWith(space, page, rev, cites.length, review);
const decision = page.state === 'published' && n === page.published_revision
const decision = page.state === 'published' && n === page.published_revision
@@ -1073,6 +1075,7 @@ function createWikiService({ db, stores, outbox, config, community = null, vip =
@@ -1073,6 +1075,7 @@ function createWikiService({ db, stores, outbox, config, community = null, vip =
pendingProposals: proposals.filter((p) => p.status === 'pending'),
pendingProposals: proposals.filter((p) => p.status === 'pending'),
jobs: jobs.filter((j) => j.status === 'pending' || j.status === 'running'),
jobs: jobs.filter((j) => j.status === 'pending' || j.status === 'running'),
headNumber,
headNumber,
+ offeredAt: offered ? offered.createdAt : null,
};
};
},
},
@@ -87,6 +87,15 @@ const data = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'seeds', 'ope
@@ -87,6 +87,15 @@ const data = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'seeds', 'ope
assert.ok(r.text.includes('<meta name="robots" content="index, follow">'));
assert.ok(r.text.includes('<meta name="robots" content="index, follow">'));
assert.ok(r.text.includes('Reviewed by a person.'));
assert.ok(r.text.includes('Reviewed by a person.'));
assert.ok(!r.text.includes('Not yet reviewed by a person.'));
assert.ok(!r.text.includes('Not yet reviewed by a person.'));
+ // The head says what a reader may check (market outcomes 2, 3 and 5): sources, revisions, when this text was
+ // accepted, and the review, with its date.
+ const facts = (/<p class="wk-facts">[\s\S]*?<\/p>/.exec(r.text) || [''])[0];
+ assert.match(facts, /<a href="\/w\/openvibe\/openvibe-search\/sources">\d+ sources? cited<\/a>/);
+ assert.match(facts, /<a href="\/w\/openvibe\/openvibe-search\/history">\d+ revisions?<\/a>/);
+ assert.match(facts, /this text accepted <time/);
+ assert.match(facts, /<span class="wk-review wk-review--ok">Reviewed by a person <time/);
+ const waiting = await H.req(h, 'GET', `/w/openvibe/${legacy.page.slug}`);
+ assert.match(waiting.text, /<span class="wk-review wk-review--no">Not yet reviewed by a person<\/span>/, 'an AI-assisted page nobody reviewed says so in its head');
assert.ok((await H.req(h, 'GET', '/sitemaps/pages-1.xml')).text.includes('http://wiki.test/w/openvibe/openvibe-search'));
assert.ok((await H.req(h, 'GET', '/sitemaps/pages-1.xml')).text.includes('http://wiki.test/w/openvibe/openvibe-search'));
assert.ok((await H.req(h, 'GET', '/feed.atom')).text.includes('http://wiki.test/w/openvibe/openvibe-search'));
assert.ok((await H.req(h, 'GET', '/feed.atom')).text.includes('http://wiki.test/w/openvibe/openvibe-search'));
assert.ok((await H.req(h, 'GET', '/feed.json')).json.items.some((i) => i.url === 'http://wiki.test/w/openvibe/openvibe-search'));
assert.ok((await H.req(h, 'GET', '/feed.json')).json.items.some((i) => i.url === 'http://wiki.test/w/openvibe/openvibe-search'));