openvibers/openvibe.reviews Commits

Adopt openvibe-publishing 1.3.0 and add /llms-full.txt with the site…

OP OpenVibers committed 4 Oct 2026 · b21ffb8c59 · 8 files, +67 −11

Asked: T11 step 4b — OpenVibe.Reviews: adopt openvibe-publishing v1.3.0 and add /llms-full.txt. ONE pull request from current origin/main (git fetch first; the local checkout is stale). 1) Bump openvibe-publishing v1.2.0 to v1.3.0 (tagged at 2eedb98; renderDocument now forwards summary, facts, updated and…
Harness job reviews-t11-step-4b-openvibe-reviews-221217.

Changes

ddiff --git a/STATUS.json b/STATUS.json
iindex c41757d..73e08d4 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -9,7 +9,7 @@
"domain": "openvibe.reviews",
"contracts": "openvibe-contracts v0.76.0",
"packages": [
-- "openvibe-publishing v1.2.0",
++ "openvibe-publishing v1.3.0",
"openvibe-shared v2.6.0",
"openvibe-sdk v0.26.0"
],
ddiff --git a/package-lock.json b/package-lock.json
iindex b4000b2..d4acc0d 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -15,7 +15,7 @@
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
-- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.2.0",
++ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pg": "^8.23.0"
@@@ -1149,9 +1149,9 @@
}
},
"node_modules/openvibe-publishing": {
-- "version": "1.2.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.2.0",
-- "integrity": "sha512-n/20yxXo5jRs0wi7KsR+R9jDkGsoBak3gqElHV6XyU+i1t3XCzaoz+sDb10G2cbc4uEaNB/F09VtQKrQzLp6YQ==",
++ "version": "1.3.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
++ "integrity": "sha512-dIXLK5JpFqUEztywlWgSupUa9NEkovVczR2qeIvpst5evOyKZlwGzQktArvWUQn1ly6CoLL5mn/FnKiqoPvZFA==",
"license": "MIT",
"engines": {
"node": ">=20"
ddiff --git a/package.json b/package.json
iindex 53b9762..9f41a27 100644
---- a/package.json
++++ b/package.json
@@@ -25,7 +25,7 @@
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
-- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.2.0",
++ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pg": "^8.23.0"
ddiff --git a/server/http/machine.js b/server/http/machine.js
iindex e21d17f..785decc 100644
---- a/server/http/machine.js
++++ b/server/http/machine.js
@@@ -8,13 +8,16 @@
* /feed.atom published summaries (Atom), /feed.json (JSON Feed 1.1); one entry per published
* revision, so a correction is a new entry that starts with its note
* /llms.txt orientation for language models
++ * /llms-full.txt the same header plus a short summary of every indexable review page
*/
const express = require('express');
const seo = require('openvibe-publishing/seo');
const sharedSeo = require('openvibe-shared/seo');
const cache = require('openvibe-shared/cache-policy');
++const { SITE_SUMMARY } = require('../render/layout');
const PER_SITEMAP = 45000;
++const ALL_SUMMARIES = 50000;
function createMachine({ svc, config }) {
const router = express.Router();
@@@ -76,7 +79,7 @@ function createMachine({ svc, config }) {
router.get('/llms.txt', (_req, res) => {
setCache(res, 3600).type('text/plain').send(sharedSeo.llmsTxt({
name: 'OpenVibe.Reviews',
-- summary: 'Review signals from named sources (via OpenVibe.Sources) resolved to entities, with provenance for every number, aggregates that exist only when signals do, and editor-reviewed summaries.',
++ summary: SITE_SUMMARY,
details: 'Every entity page has a JSON representation at the same address plus ".json" (same content). Each signal names its source item, retrieval time and licence note; the aggregate lists its inputs, exclusions and computation (method reviews-aggregate@1). Summaries never carry a rating; AI-drafted summaries are labelled and published only after a person approves them. Review text is not republished.',
sections: [
{ title: 'Start here', links: [{ title: 'Entities', url: `${origin}/` }, { title: 'How it works', url: `${origin}/about` }] },
@@@ -89,6 +92,23 @@ function createMachine({ svc, config }) {
}));
});
++ router.get('/llms-full.txt', async (_req, res) => {
++ // The /llms.txt header plus a section of only indexable reviews — the same gate the sitemap
++ // applies — each as its short summary. Review text is never stored, so no body can appear.
++ const pages = (await svc.recentSummaries(ALL_SUMMARIES)).filter(({ decision }) => decision.indexable).map(({ entity, rev }) => ({
++ title: entity.name,
++ url: svc.entityUrl(entity),
++ text: [rev.content || '', ...(rev.fields.pros || []).map((p) => p.text), ...(rev.fields.cons || []).map((p) => p.text)].join('\n').slice(0, 1000),
++ }));
++ setCache(res, 3600).type('text/plain').send(sharedSeo.llmsFull({
++ site: 'OpenVibe.Reviews',
++ summary: SITE_SUMMARY,
++ base: origin,
++ maxBytes: 512 * 1024,
++ sections: [{ title: 'Reviews', pages }],
++ }));
++ });
++
return router;
}
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex a57496f..aac702f 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -25,7 +25,7 @@ const ovServe = require('openvibe-shared/serve');
const cache = require('openvibe-shared/cache-policy');
const frame = require('openvibe-shared/frame');
const seo = require('openvibe-publishing/seo');
--const { renderPage } = require('../render/layout');
++const { renderPage, SITE_SUMMARY } = require('../render/layout');
const views = require('../render/views');
const { ratingForStructuredData } = require('../reviews/aggregate');
const { actorMiddleware, crossSite } = require('./common');
@@@ -146,7 +146,7 @@ function createPages({ svc, platform, sync, viewers, config, log = console, limi
router.get('/', async (req, res) => {
const all = await svc.listEntities({ limit: 1 });
send(req, res, 200, views.home({ entities: await svc.entitiesWithData(60), total: all.total, editor: isEditor(req) }), {
-- robots: 'index, follow', cache: 'public', active: 'home', path: '/',
++ robots: 'index, follow', cache: 'public', active: 'home', path: '/', summary: SITE_SUMMARY,
jsonLd: seo.structuredData.webPage({ url: `${config.baseUrl}/`, name: 'OpenVibe.Reviews', description: 'Review signals from named sources, with provenance and honest aggregates.' }),
});
});
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex 5083355..273fa62 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -16,6 +16,7 @@ const { escapeHtml: esc } = require('openvibe-publishing/ssr');
const SITE_NAME = 'OpenVibe.Reviews';
const NETWORK_URL = 'https://openvibe.network';
++const SITE_SUMMARY = 'Review signals from named sources (via OpenVibe.Sources) resolved to entities, with provenance for every number, aggregates that exist only when signals do, and editor-reviewed summaries.';
const DEFAULT_DESCRIPTION = 'OpenVibe.Reviews: review signals gathered from named sources, with provenance for every number and no rating without source data. Part of the OpenVibe network.';
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
@@@ -78,6 +79,9 @@ function renderPage(o) {
lang: 'en',
title,
description: o.description || DEFAULT_DESCRIPTION,
++ // The one-line AI summary: on the home page it becomes the ai-summary meta and the WebPage
++ // JSON-LD (openvibe-publishing 1.3.0 forwards it to the shell). Other pages leave it out.
++ summary: o.summary,
canonical: o.canonical || `${config.baseUrl}${o.path || '/'}`,
decision: o.decision,
robots: o.robots,
@@@ -99,4 +103,4 @@ function renderPage(o) {
});
}
--module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, DEFAULT_DESCRIPTION, NETWORK_URL };
++module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, SITE_SUMMARY, DEFAULT_DESCRIPTION, NETWORK_URL };
ddiff --git a/test/layout.test.js b/test/layout.test.js
iindex 07d9c8b..ac5d245 100644
---- a/test/layout.test.js
++++ b/test/layout.test.js
@@@ -82,7 +82,9 @@ t('the document head and frame come from openvibe-publishing/layout, robots exac
const home = (await req(h, 'GET', '/')).text;
const homeHead = home.slice(0, home.indexOf('</head>'));
assert.ok(homeHead.includes('<meta name="robots" content="index, follow">'));
-- assert.strictEqual(count(homeHead, /<script type="application\/ld\+json">/g), 1, 'the home page JSON-LD');
++ // The home carries its own WebPage JSON-LD and, since llms-full.txt, the site summary's WebPage JSON-LD.
++ assert.strictEqual(count(homeHead, /<script type="application\/ld\+json">/g), 2, 'the home page JSON-LD');
++ assert.match(homeHead, /<meta name="ai-summary" content="[^"]+">/, 'the AI summary head');
assert.ok(home.includes('Recently shipped on OpenVibe.Reviews'), 'the shipped line stays on the home page');
} finally { await h.stop(); }
});
ddiff --git a/test/nojs.test.js b/test/nojs.test.js
iindex 9464ba8..dd1e2fa 100644
---- a/test/nojs.test.js
++++ b/test/nojs.test.js
@@@ -118,6 +118,36 @@ t('every public page reads without JavaScript; sitemaps, feeds and robots follow
} finally { await h.stop(); }
});
++t('/llms-full.txt carries only indexable reviews and the home page an AI summary', async () => {
++ const h = await boot();
++ try {
++ const e = await createEntity(h, { name: 'Portal 2', kind: 'game', aliases: [{ type: 'source', value: 'steam-reviews-portal-2' }] });
++ await importItem(h, steamItem({ votedUp: true }));
++ const thin = await createEntity(h, { name: 'Thin Widget', kind: 'product' });
++ const sig = await h.db.prepare('SELECT id FROM review_signals').all();
++ const pub = await req(h, 'POST', `/api/v1/entities/${e.slug}/summary/revisions`, { token: editorToken(), body: {
++ overview: 'One recent review recommends it, which is the whole sample Reviews has read so far and says nothing about the game beyond that.',
++ overview_signals: sig.map((s) => s.id),
++ publish: true,
++ } });
++ assert.strictEqual(pub.status, 201, pub.text.slice(0, 300));
++
++ const full = await req(h, 'GET', '/llms-full.txt');
++ assert.strictEqual(full.status, 200);
++ assert.match(full.headers.get('content-type'), /^text\/plain/);
++ assert.strictEqual(full.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');
++ assert.ok(full.text.includes('OpenVibe.Reviews'), 'the llms.txt header');
++ assert.ok(full.text.includes(`http://reviews.test/e/${e.slug}`), 'an indexable review is listed');
++ assert.match(full.text, /One recent review recommends it/, 'its short summary is the text');
++ assert.ok(!full.text.includes(`/e/${thin.slug}`), 'a thin, non-indexable review is never listed');
++ assert.ok(Buffer.byteLength(full.text) <= 512 * 1024, 'stays within maxBytes');
++
++ const home = await req(h, 'GET', '/');
++ assert.match(home.text, /<meta name="ai-summary" content="Review signals from named sources/);
++ assert.match(home.text, /"@type":"WebPage"/, 'the home WebPage JSON-LD');
++ } finally { await h.stop(); }
++});
++
t('editors work with plain forms: create, resolve an ambiguous item, summarise, merge, split, correct', async () => {
const h = await boot();
try {