openvibers/openvibe.trade Commits
Adopt openvibe-publishing 1.3.0 and add llms-full.txt
Changes
@@@ -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) |@@@ -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"],@@@ -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"@@@ -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"@@@ -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)));@@@ -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 }),@@@ -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 };@@@ -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();})();@@@ -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);
@@ -124,7 +124,7 @@ deployment has no instruments (editors add them), no documents until the source
@@ -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`) |
| `/watchlists` | your watchlists, alert rules and deliveries (private, noindex, `private, no-store`) |
| `/editor`, `/editor/i/:symbol` | the editor (plain forms, editors only) |
| `/editor`, `/editor/i/:symbol` | the editor (plain forms, editors only) |
| `/sitemap.xml` → `/sitemaps/instruments.xml` | indexable instrument pages 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 |
| `/terms`, `/privacy`, `/dmca` | openvibe-shared legal pages |
| `/auth/*` | sign-in; the same session layer as Blog and Community |
| `/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) |
| `/api/health`, `/api/ready`, `/release.json`, `POST /release-metrics`, `/metrics` | health, readiness, release, open tabs' update reports (counted in `/metrics`), metrics (loopback only) |
@@ -11,7 +11,7 @@
@@ -11,7 +11,7 @@
"contracts": "openvibe-contracts v0.79.0",
"contracts": "openvibe-contracts v0.79.0",
"packages": [
"packages": [
"openvibe-contracts v0.79.0",
"openvibe-contracts v0.79.0",
- "openvibe-publishing v1.2.0",
+ "openvibe-publishing v1.3.0",
"openvibe-shared v2.6.0",
"openvibe-shared v2.6.0",
"openvibe-sdk v0.25.0"
"openvibe-sdk v0.25.0"
],
],
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -1163,8 +1163,9 @@
@@ -1163,8 +1163,9 @@
}
}
},
},
"node_modules/openvibe-publishing": {
"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",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
@@ -28,7 +28,7 @@
@@ -28,7 +28,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -5,6 +5,8 @@
@@ -5,6 +5,8 @@
*
*
* GET /robots.txt sitemap + explicit automated-consumer policy
* GET /robots.txt sitemap + explicit automated-consumer policy
* GET /llms.txt orientation for language models
* 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 /sitemap.xml sitemap index
* GET /sitemaps/instruments.xml INDEXABLE instrument pages only (the gate decides:
* GET /sitemaps/instruments.xml INDEXABLE instrument pages only (the gate decides:
* person-reviewed context, no stale price); lastmod = the
* person-reviewed context, no stale price); lastmod = the
@@ -20,10 +22,10 @@ const seo = require('openvibe-publishing/seo');
@@ -20,10 +22,10 @@ const seo = require('openvibe-publishing/seo');
const sharedSeo = require('openvibe-shared/seo');
const sharedSeo = require('openvibe-shared/seo');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
const { define } = require('./routes');
const { define } = require('./routes');
-const { DISCLAIMER } = require('../render/layout');
+const { DISCLAIMER, SITE_NAME, SITE_SUMMARY } = require('../render/layout');
function createDiscovery(ctx) {
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 router = express.Router();
const abs = urls.abs;
const abs = urls.abs;
const xml = (res, body, type = 'application/xml') => res.type(type).set('Cache-Control', cache.htmlHeaders({ maxAge: 300 })).send(body);
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) {
@@ -67,8 +69,8 @@ function createDiscovery(ctx) {
define(router, 'get', '/llms.txt', 'llmsTxt', (_req, res) => {
define(router, 'get', '/llms.txt', 'llmsTxt', (_req, res) => {
res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(sharedSeo.llmsTxt({
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.',
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: [
sections: [
{ title: 'Start here', links: [
{ title: 'Start here', links: [
@@ -85,6 +87,22 @@ function createDiscovery(ctx) {
@@ -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) => {
define(router, 'get', '/sitemap.xml', 'sitemapIndex', async (_req, res) => {
const active = await instruments.active();
const active = await instruments.active();
const decisions = await Promise.all(active.map(async (i) => await indexing.decide(i)));
const decisions = await Promise.all(active.map(async (i) => await indexing.decide(i)));
@@ -19,6 +19,7 @@ const seo = require('openvibe-publishing/seo');
@@ -19,6 +19,7 @@ const seo = require('openvibe-publishing/seo');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
const { paginate } = require('openvibe-publishing/ssr');
const { paginate } = require('openvibe-publishing/ssr');
const views = require('../render/views');
const views = require('../render/views');
+const { SITE_SUMMARY } = require('../render/layout');
const { define } = require('./routes');
const { define } = require('./routes');
function createPages(ctx) {
function createPages(ctx) {
@@ -42,6 +43,8 @@ function createPages(ctx) {
@@ -42,6 +43,8 @@ function createPages(ctx) {
const canonical = urls.abs(pageNo === 1 ? '/' : `/?page=${pageNo}`);
const canonical = urls.abs(pageNo === 1 ? '/' : `/?page=${pageNo}`);
common.page(req, res, {
common.page(req, res, {
title: null, canonical, decision: listing(pageNo === 1 ? '/' : `/?page=${pageNo}`),
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)' }],
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' })],
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 }),
body: views.home({ page, recent, urls, sync: await sync.state(), pager }),
@@ -21,6 +21,8 @@ const frame = require('openvibe-shared/frame');
@@ -21,6 +21,8 @@ const frame = require('openvibe-shared/frame');
const NETWORK_URL = 'https://openvibe.network';
const NETWORK_URL = 'https://openvibe.network';
const SITE_NAME = 'OpenVibe.Trade';
const SITE_NAME = 'OpenVibe.Trade';
const DISCLAIMER = 'Information only — not investment advice; no trading here.';
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 PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
const hashes = new Map();
const hashes = new Map();
@@ -46,7 +48,8 @@ const NAV_LINKS = [
@@ -46,7 +48,8 @@ const NAV_LINKS = [
/**
/**
* o: title, description, decision (required), canonical, type, jsonLd [], feeds [{ type, href, title }],
* 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) {
function renderPage(o) {
if (!o.decision) throw new TypeError('renderPage needs the gate decision');
if (!o.decision) throw new TypeError('renderPage needs the gate decision');
@@ -83,6 +86,12 @@ function renderPage(o) {
@@ -83,6 +86,12 @@ function renderPage(o) {
image: o.image,
image: o.image,
jsonLd: o.jsonLd,
jsonLd: o.jsonLd,
feeds: o.feeds,
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,
navbar: nav,
footer,
footer,
navLinks: NAV_LINKS,
navLinks: NAV_LINKS,
@@ -100,4 +109,4 @@ function renderPage(o) {
@@ -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 };
@@ -10,11 +10,12 @@ const path = require('path');
@@ -10,11 +10,12 @@ const path = require('path');
const { boot, check, done } = require('./helpers/boot');
const { boot, check, done } = require('./helpers/boot');
const DISCLAIMER = 'Information only — not investment advice; no trading here.';
const DISCLAIMER = 'Information only — not investment advice; no trading here.';
+const WORDS = (n) => Array.from({ length: n }, (_, i) => `word${i}`).join(' ');
(async () => {
(async () => {
const t = await boot();
const t = await boot();
const acme = await t.instrument({ symbol: 'ACME', name: 'Acme Corp', cik: '1234567', exchange: 'NYSE' });
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.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/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) });
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.
@@ -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(','));
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();
await t.close();
done();
done();
})();
})();
@@ -91,7 +91,7 @@ const { boot, check, done } = require('./helpers/boot');
@@ -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 () => {
await check('watchlists never appear in sitemaps, feeds, robots-allowed paths, Search documents or events', async () => {
const blobs = [];
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);
const r = await t.get(p);
assert.ok(r.status < 400, `${p} ${r.status}`);
assert.ok(r.status < 400, `${p} ${r.status}`);
blobs.push(r.text);
blobs.push(r.text);