openvibers/openvibe.trade Commits

Adopt openvibe-publishing 1.3.0 and add llms-full.txt

OP OpenVibers committed 4 Oct 2026 · 8801cd2ef0 · 9 files, +71 −14

Asked: T11 step 4b — OpenVibe.Trade: 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 u…
Harness job trade-t11-step-4b-openvibe-trade-221217.

Changes

ddiff --git a/README.md b/README.md
iindex c671131..963010d 100644
---- a/README.md
++++ b/README.md
@@@ -124,7 +124,7 @@ deployment has no instruments (editors add them), no documents until the source
| `/watchlists` | your watchlists, alert rules and deliveries (private, noindex, `private, no-store`) |
| `/editor`, `/editor/i/:symbol` | the editor (plain forms, editors only) |
| `/sitemap.xml` → `/sitemaps/instruments.xml` | indexable instrument pages only |
--| `/robots.txt`, `/llms.txt` | crawler policy (private paths disallowed) and orientation |
++| `/robots.txt`, `/llms.txt`, `/llms-full.txt` | crawler policy (private paths disallowed), orientation, and a short summary of every indexable instrument |
| `/terms`, `/privacy`, `/dmca` | openvibe-shared legal pages |
| `/auth/*` | sign-in; the same session layer as Blog and Community |
| `/api/health`, `/api/ready`, `/release.json`, `POST /release-metrics`, `/metrics` | health, readiness, release, open tabs' update reports (counted in `/metrics`), metrics (loopback only) |
ddiff --git a/STATUS.json b/STATUS.json
iindex 8b70b21..086bd05 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -11,7 +11,7 @@
"contracts": "openvibe-contracts v0.79.0",
"packages": [
"openvibe-contracts v0.79.0",
-- "openvibe-publishing v1.2.0",
++ "openvibe-publishing v1.3.0",
"openvibe-shared v2.6.0",
"openvibe-sdk v0.25.0"
],
ddiff --git a/package-lock.json b/package-lock.json
iindex 00e8b13..c055e35 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -16,7 +16,7 @@
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.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"
@@@ -1163,8 +1163,9 @@
}
},
"node_modules/openvibe-publishing": {
-- "version": "1.2.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.2.0",
++ "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 fffe90f..aeb0bf6 100644
---- a/package.json
++++ b/package.json
@@@ -28,7 +28,7 @@
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.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/discovery.js b/server/http/discovery.js
iindex b64921a..5e6e61a 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -5,6 +5,8 @@
*
* GET /robots.txt sitemap + explicit automated-consumer policy
* GET /llms.txt orientation for language models
++ * GET /llms-full.txt the same map, with a short summary of every indexable
++ * instrument (never the source's full text)
* GET /sitemap.xml sitemap index
* GET /sitemaps/instruments.xml INDEXABLE instrument pages only (the gate decides:
* person-reviewed context, no stale price); lastmod = the
@@@ -20,10 +22,10 @@ const seo = require('openvibe-publishing/seo');
const sharedSeo = require('openvibe-shared/seo');
const cache = require('openvibe-shared/cache-policy');
const { define } = require('./routes');
--const { DISCLAIMER } = require('../render/layout');
++const { DISCLAIMER, SITE_NAME, SITE_SUMMARY } = require('../render/layout');
function createDiscovery(ctx) {
-- const { store, instruments, documents, indexing, urls, common } = ctx;
++ const { config, store, instruments, documents, indexing, context, urls, common } = ctx;
const router = express.Router();
const abs = urls.abs;
const xml = (res, body, type = 'application/xml') => res.type(type).set('Cache-Control', cache.htmlHeaders({ maxAge: 300 })).send(body);
@@@ -67,8 +69,8 @@ function createDiscovery(ctx) {
define(router, 'get', '/llms.txt', 'llmsTxt', (_req, res) => {
res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(sharedSeo.llmsTxt({
-- name: 'OpenVibe.Trade',
-- summary: 'Informational market context: instruments, timestamped observations with their sources, filings from OpenVibe.Sources, and reviewed context. Information only — not investment advice; no trading here.',
++ name: SITE_NAME,
++ summary: SITE_SUMMARY,
details: 'Every number on an instrument page is an observation with observed_at, retrieved_at and its source; an instrument without observations shows no number. Stale sources are labelled "stale since <time>" and never replaced. Each instrument page has a JSON twin at /i/<SYMBOL>.json with the same data. AI-written context is labelled and is not published before a person reviews it. There is no custody, order execution, escrow, marketplace or personal advice.',
sections: [
{ title: 'Start here', links: [
@@@ -85,6 +87,22 @@ function createDiscovery(ctx) {
}));
});
++ // The full-text AI map: the llms.txt header, then a short summary of every indexable instrument
++ // page — the same gate the sitemap uses. The summary is the reviewed context, clipped; the
++ // source's own full text is never reproduced (llms.txt states the rule).
++ define(router, 'get', '/llms-full.txt', 'llmsFullTxt', async (_req, res) => {
++ const active = await instruments.active();
++ const decisions = await Promise.all(active.map(async (i) => await indexing.decide(i)));
++ const pages = await Promise.all(active.filter((_i, n) => decisions[n].indexable).map(async (i) => {
++ const rev = await context.published(i);
++ return { title: `${i.symbol} — ${i.name}`, url: urls.instrument(i), text: rev ? String(rev.text || '').replace(/\s+/g, ' ').trim().slice(0, 300) : '' };
++ }));
++ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(sharedSeo.llmsFull({
++ site: SITE_NAME, summary: SITE_SUMMARY, base: config.baseUrl, maxBytes: 512 * 1024,
++ sections: [{ title: 'Instruments', pages }],
++ }));
++ });
++
define(router, 'get', '/sitemap.xml', 'sitemapIndex', async (_req, res) => {
const active = await instruments.active();
const decisions = await Promise.all(active.map(async (i) => await indexing.decide(i)));
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 74f65db..8fb6762 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -19,6 +19,7 @@ const seo = require('openvibe-publishing/seo');
const cache = require('openvibe-shared/cache-policy');
const { paginate } = require('openvibe-publishing/ssr');
const views = require('../render/views');
++const { SITE_SUMMARY } = require('../render/layout');
const { define } = require('./routes');
function createPages(ctx) {
@@@ -42,6 +43,8 @@ function createPages(ctx) {
const canonical = urls.abs(pageNo === 1 ? '/' : `/?page=${pageNo}`);
common.page(req, res, {
title: null, canonical, decision: listing(pageNo === 1 ? '/' : `/?page=${pageNo}`),
++ // The home page carries the site's ai-summary meta and WebPage JSON-LD (v1.3.0 forwards it).
++ summary: pageNo === 1 ? SITE_SUMMARY : null,
feeds: [{ type: 'rss', href: '/feed.xml', title: 'Recent documents (RSS)' }, { type: 'atom', href: '/atom.xml', title: 'Recent documents (Atom)' }, { type: 'json', href: '/feed.json', title: 'Recent documents (JSON Feed)' }],
jsonLd: [seo.compact({ '@context': 'https://schema.org', '@type': 'WebSite', url: urls.abs('/'), name: 'OpenVibe.Trade', inLanguage: 'en' })],
body: views.home({ page, recent, urls, sync: await sync.state(), pager }),
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex 70f367d..0d8c79c 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -21,6 +21,8 @@ const frame = require('openvibe-shared/frame');
const NETWORK_URL = 'https://openvibe.network';
const SITE_NAME = 'OpenVibe.Trade';
const DISCLAIMER = 'Information only — not investment advice; no trading here.';
++// The site's one-line AI summary: /llms.txt, /llms-full.txt and the home page's ai-summary all use it.
++const SITE_SUMMARY = 'Informational market context: instruments, timestamped observations with their sources, filings from OpenVibe.Sources, and reviewed context. Information only — not investment advice; no trading here.';
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
const hashes = new Map();
@@@ -46,7 +48,8 @@ const NAV_LINKS = [
/**
* o: title, description, decision (required), canonical, type, jsonLd [], feeds [{ type, href, title }],
-- * body (HTML), viewer, config, path, bodyClass
++ * body (HTML), viewer, config, path, bodyClass, summary (one-line AI summary; with facts, updated
++ * and url it becomes the ai-summary meta and WebPage JSON-LD via openvibe-publishing/layout)
*/
function renderPage(o) {
if (!o.decision) throw new TypeError('renderPage needs the gate decision');
@@@ -83,6 +86,12 @@ function renderPage(o) {
image: o.image,
jsonLd: o.jsonLd,
feeds: o.feeds,
++ // openvibe-publishing v1.3.0: the AI summary, its facts and its date reach shell.page's
++ // seo.pageSummary (ai-summary meta + WebPage JSON-LD); without one, nothing is emitted.
++ summary: o.summary,
++ facts: o.facts,
++ updated: o.updated,
++ url: o.url,
navbar: nav,
footer,
navLinks: NAV_LINKS,
@@@ -100,4 +109,4 @@ function renderPage(o) {
});
}
--module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, NETWORK_URL, DISCLAIMER };
++module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, NETWORK_URL, DISCLAIMER, SITE_SUMMARY };
ddiff --git a/test/pages.test.js b/test/pages.test.js
iindex 1b8a45b..59528fd 100644
---- a/test/pages.test.js
++++ b/test/pages.test.js
@@@ -10,11 +10,12 @@ const path = require('path');
const { boot, check, done } = require('./helpers/boot');
const DISCLAIMER = 'Information only — not investment advice; no trading here.';
++const WORDS = (n) => Array.from({ length: n }, (_, i) => `word${i}`).join(' ');
(async () => {
const t = await boot();
const acme = await t.instrument({ symbol: 'ACME', name: 'Acme Corp', cik: '1234567', exchange: 'NYSE' });
-- await t.observe(acme, { metric: 'price.close', value: '10.50', source_ref: 'a', observed_at: t.iso(t.T0 - 60e3), retrieved_at: t.iso(t.T0) });
++ const { observation } = await t.observe(acme, { metric: 'price.close', value: '10.50', source_ref: 'a', observed_at: t.iso(t.T0 - 60e3), retrieved_at: t.iso(t.T0) });
t.sources.setSource('sec-xbrl-filings', { lastSuccessAt: t.iso(t.T0), staleAfterSec: 2700 });
t.sources.putItem({ canonical_url: 'https://www.sec.gov/Archives/edgar/data/1234567/000123456726000001/0001234567-26-000001-index.htm', title: 'ACME CORP', summary: '10-K', published_at: '2026-09-22T10:00:00Z', retrieved_at: t.iso(t.T0) });
t.sources.putItem({ canonical_url: 'https://www.sec.gov/Archives/edgar/data/1234567/000123456726000002/0001234567-26-000002-index.htm', title: 'ACME CORP', summary: '8-K', retrieved_at: t.iso(t.T0) });
@@@ -108,6 +109,31 @@ const DISCLAIMER = 'Information only — not investment advice; no trading here.
assert.ok(!tables.some((n) => /order|listing|escrow|wallet|custody|payment|cart/.test(n)), tables.join(','));
});
++ await check('llms-full.txt lists only indexable instruments with a short summary; the home carries the AI summary', async () => {
++ const before = await t.get('/llms-full.txt');
++ assert.strictEqual(before.status, 200);
++ assert.ok(/text\/plain/.test(before.headers.get('content-type')));
++ assert.ok(before.text.startsWith('# OpenVibe.Trade'));
++ assert.ok(before.text.includes('not investment advice'));
++ assert.ok(!before.text.includes('https://openvibe.trade/i/ACME'), 'nothing indexable is listed yet');
++ assert.ok(Buffer.byteLength(before.text) <= 512 * 1024, 'llms-full.txt stays within maxBytes');
++
++ // A person-reviewed context makes the page indexable; then it — and only it — is listed.
++ await t.instrument({ symbol: 'OTHR', name: 'Other Co' });
++ const pub = await t.get('/editor/i/ACME/context', { as: t.editor, form: { _csrf: t.csrf(t.editor), body: `Acme's price is stated by the source. ${WORDS(70)}`, cite: `observation:${observation.id}`, publish: '1' } });
++ assert.ok(/n=context_published/.test(pub.headers.get('location')), 'the context published');
++ const after = await t.get('/llms-full.txt');
++ assert.ok(after.text.includes('https://openvibe.trade/i/ACME'));
++ assert.ok(after.text.includes('### ACME — Acme Corp'));
++ assert.ok(after.text.includes('word0'), 'the entry carries the context summary as text');
++ assert.ok(!after.text.includes('https://openvibe.trade/i/OTHR'), 'an instrument without reviewed context is not listed');
++ assert.ok(Buffer.byteLength(after.text) <= 512 * 1024);
++
++ const home = (await t.get('/')).text;
++ assert.ok(home.includes('<meta name="ai-summary"'), 'the home carries the ai-summary meta');
++ assert.ok(home.includes('"@type":"WebPage"'), 'the home carries WebPage JSON-LD');
++ });
++
await t.close();
done();
})();
ddiff --git a/test/privacy.test.js b/test/privacy.test.js
iindex 086277d..bbceb22 100644
---- a/test/privacy.test.js
++++ b/test/privacy.test.js
@@@ -91,7 +91,7 @@ const { boot, check, done } = require('./helpers/boot');
await check('watchlists never appear in sitemaps, feeds, robots-allowed paths, Search documents or events', async () => {
const blobs = [];
-- for (const p of ['/sitemap.xml', '/sitemaps/instruments.xml', '/feed.xml', '/atom.xml', '/feed.json', '/i/ACME/documents.xml', '/llms.txt', '/i/ACME.json', '/', '/sources']) {
++ for (const p of ['/sitemap.xml', '/sitemaps/instruments.xml', '/feed.xml', '/atom.xml', '/feed.json', '/i/ACME/documents.xml', '/llms.txt', '/llms-full.txt', '/i/ACME.json', '/', '/sources']) {
const r = await t.get(p);
assert.ok(r.status < 400, `${p} ${r.status}`);
blobs.push(r.text);