openvibers/openvibe.codes Commits

Add Codes' /llms-full.txt (#14)

OP OpenVibers committed 5 Oct 2026 · 49c7fc71d1 · 9 files, +133 −504

* Add Codes' /llms-full.txt

**Files changed**
- `server/http/discovery.js`: added the `GET /llms-full.txt` route in the same router as `/llms.txt`/`sitemap.xml`, using `seo.llmsFull` over the fixed public pages `publicPages()` feeds the sitemap (Home · Reference `/docs` · Playgrounds/tool pages · Site), with `maxBytes: 512 * 1024` and `Cache-Control` `maxAge: 3600`; added a `PAGE_TEXT` map plus `describePage()` so each page gets a real title and one-line description (generated docs pages derive theirs from the same contracts/SDK data the pages render — nothing fetched or invented); cross-linked `/llms-full.txt` from llms.txt's "Machine-readable" section; updated the module doc comment.
- `test/discovery.test.js`: new check — `/llms-full.txt` is 200 `text/plain`, starts with `# OpenVibe.Codes`, lists every `<loc>` the sitemap does, includes the `/docs` and `/tools/webhooks` entries, and carries no private/API paths; asserted the new llms.txt cross-link; updated the file header.
- `server/http/pages.js`: comment updates naming `llms-full.txt` among the crawl artifacts.
- `README.md`: added a "Crawl artifacts" surface row documenting all four files, and added crawl-artifact coverage to the Tests paragraph.
- `STATUS.json`: recorded the crawl artifacts in `works` and `features`, and removed the now-false `notYet` line "No JSON-LD or llms.txt on the public pages".

**Tests run**
- `node test/discovery.test.js` → all 5 checks passed (incl. the new `/llms-full.txt` check). Output is 252 KB / 949 pages, under the 512 KB cap, no truncation.
- `ov test` → 14 reachable tests passed (157 s); an earlier run on the pre-`pages.js` tree passed 13. (The tool prints a trailing "no test reaches server/http/discovery.js" hint, but `test/discovery.test.js` was selected as changed and passed.)

**Not done / caveat**
- Didn't commit, push, deploy, or install packages, per the brief.
- `/llms-full.txt` enumerates the fixed public page set (the same set `publicPages()` gives the sitemap). It intentionally does not enumerate the dynamic `/apps/:id` and `/releases/:id` pages the sitemap adds from published releases — the brief scoped it to home, `/docs` reference and the public tool/playground pages, and they'd be unbounded and mostly URL-only. The test's sitemap comparison therefore holds in the test database (no releases seeded); the check would need widening if release pages were to be included later.

* Stop tracking the node_modules link and ignore it as a link too

* Pin openvibe-contracts v0.97.0 (the pin-drift check allows at most 10 minors behind)

Changes

ddiff --git a/.gitignore b/.gitignore
iindex fafde14..c7d9202 100644
---- a/.gitignore
++++ b/.gitignore
@@@ -8,3 +8,4 @@ dist/
*.pem
*.key
!server/data/
++node_modules
ddiff --git a/README.md b/README.md
iindex 335a8a6..8851987 100644
---- a/README.md
++++ b/README.md
@@@ -40,6 +40,7 @@ A place where a developer outside the network goes from an OpenVibe account to a
| Policy | `/policy/*` | Proposal process and the decision record; compatibility and deprecation policy rendered from ADR-002 and ADR-016 as published; licensing read from package metadata; transparency (what Codes stores and does not); governance — code of conduct ([CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)), contribution guide ([CONTRIBUTING.md](CONTRIBUTING.md)), [contributor ladder](docs/governance/contributor-ladder.md) and [moderation policy](docs/governance/moderation.md) — rendered from those files. All four are **drafts pending owner review**: marked at the top, shown with a notice, not indexed; deleting the draft line publishes one. |
| API | `/api/v1/*` | Public release reads (`codes.release.read`: no token needed; a presented token must hold it), manifest validation, docs versions; release management by an app with its own token (`codes.release.manage`). Guarded with openvibe-contracts `requireCapability`. RFC 9457 problems. |
| Machine | `/api/health`, `/api/ready`, `/release.json`, `/metrics` | Readiness is truthful (db and docs required; Network, JWKS, OAuth client and events relay reported as optional checks). `/metrics` answers loopback callers only. |
++| Crawl artifacts | `/robots.txt`, `/sitemap.xml`, `/llms.txt`, `/llms-full.txt` | Built with openvibe-shared/seo for search and AI crawlers. robots.txt keeps every disallow (the portal, staff, sign-in and the API are not crawlable) and names the sitemap; the sitemap lists the fixed public pages with STATUS.json's content date and each public app and published release with its real publish time (never "today"); llms.txt maps the site; llms-full.txt lists every fixed public page the sitemap does, one title and one line of text each. |
Everything is server-rendered and usable without JavaScript. The only scripts of Codes' own are optional: in-browser webhook verification and a copy button.
@@@ -149,7 +150,7 @@ fnm exec --using=22.22.1 npm run dev # http://localhost:4900
npx openvibe-contracts-check --service codes --src server
```
--Tests: ADR-013 trust-tier migration; secrets never persisted or re-displayed; Network errors surfaced honestly (404/403/409/422/503, unreachable, expired session refresh); scope editor offers only grantable capabilities and refuses forged requests before Network sees them; generated docs match the pinned versions (every contract, capability, event type, SDK module); webhook verification, v2 only (tampering, lengths, malformed headers, the ±300 s window both ways, timestamp header mismatch, a v1-only delivery refused, samples without v1, constant-time, the in-browser verifier agreeing); playgrounds refuse without the grant and call nothing; the usage page (owner, admins and staff only, Network not asked for anyone else, filters forwarded as offered, a Network failure shown as it answered); project export (complete, no secrets, audit for admin+ only, never partial) and delete (owner only, Network first, drafts and runs removed, public releases revoked with an event); manifest validation; release lifecycle and events; no internal key anywhere; PKCE sign-in; readiness, release.json, loopback metrics; the released codes manifest matching the code; IndexNow (off without a key, the key file served with a key, a publish pinging the page and the sitemap, a draft never pinging); and the home page's size budgets (`test/perf-budget.test.js`, openvibe-shared/perf-budget).
++Tests: the crawl artifacts (robots.txt, sitemap.xml, llms.txt and llms-full.txt — the public pages only, llms-full.txt listing every page the sitemap does, and lastmod from STATUS.json, never the clock); ADR-013 trust-tier migration; secrets never persisted or re-displayed; Network errors surfaced honestly (404/403/409/422/503, unreachable, expired session refresh); scope editor offers only grantable capabilities and refuses forged requests before Network sees them; generated docs match the pinned versions (every contract, capability, event type, SDK module); webhook verification, v2 only (tampering, lengths, malformed headers, the ±300 s window both ways, timestamp header mismatch, a v1-only delivery refused, samples without v1, constant-time, the in-browser verifier agreeing); playgrounds refuse without the grant and call nothing; the usage page (owner, admins and staff only, Network not asked for anyone else, filters forwarded as offered, a Network failure shown as it answered); project export (complete, no secrets, audit for admin+ only, never partial) and delete (owner only, Network first, drafts and runs removed, public releases revoked with an event); manifest validation; release lifecycle and events; no internal key anywhere; PKCE sign-in; readiness, release.json, loopback metrics; the released codes manifest matching the code; IndexNow (off without a key, the key file served with a key, a publish pinging the page and the sitemap, a draft never pinging); and the home page's size budgets (`test/perf-budget.test.js`, openvibe-shared/perf-budget).
## Acceptance (must be true before "done")
ddiff --git a/STATUS.json b/STATUS.json
iindex 449bc19..788822b 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,9 +7,9 @@
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4900,
"domain": "openvibe.codes",
-- "contracts": "openvibe-contracts v0.86.0",
++ "contracts": "openvibe-contracts v0.97.0",
"packages": [
-- "openvibe-contracts v0.86.0",
++ "openvibe-contracts v0.97.0",
"openvibe-sdk v0.26.0",
"openvibe-shared v2.6.0"
],
@@@ -30,14 +30,14 @@
"Policy pages rendered from the published ADRs; licensing read from package metadata; governance pages (code of conduct, contributing, contributor ladder, moderation policy) rendered from the repository's Markdown",
"Release API guarded by openvibe-contracts requireCapability (codes.release.manage, codes.release.read)",
"Readiness (/api/ready), /release.json and loopback-only /metrics",
-- "Coding-harness registry: a validated catalog of harnesses and their agents (capabilities, address, price and limits), a task placement adapter over openvibe-sdk/placement, and a public read API and read-only docs page (GET /api/v1/harnesses, POST /api/v1/harnesses/route, /docs/harnesses); each harness × agent pair is routed as a platform.resource-offer@1 of kind harness with task:<kind> and harness:<flag> capabilities and a platform.harness-offer@1 detail"
++ "Coding-harness registry: a validated catalog of harnesses and their agents (capabilities, address, price and limits), a task placement adapter over openvibe-sdk/placement, and a public read API and read-only docs page (GET /api/v1/harnesses, POST /api/v1/harnesses/route, /docs/harnesses); each harness × agent pair is routed as a platform.resource-offer@1 of kind harness with task:<kind> and harness:<flag> capabilities and a platform.harness-offer@1 detail",
++ "Crawl and machine-readable artifacts built with openvibe-shared/seo: robots.txt (every previous Disallow kept, search and AI crawlers named), sitemap.xml (the fixed public pages with STATUS.json's content date, each public app and published release with its real publish time — never \"today\"), llms.txt and llms-full.txt (every fixed public page with a one-line description), and the home page's WebSite/WebApplication/WebPage JSON-LD; the portal, staff console, sign-in and API are absent from all of them and Disallowed in robots.txt"
],
"notYet": [
"The playgrounds have never been used on production (0 playground runs); the one production end-to-end run used curl, and it is not a committed, repeatable check",
"Usage counts come from the services' hourly *.usage.recorded rollups (Network adds them up per day; the current hour is not counted) and cost lives in Billing, not on the usage page; no trace or error dashboards beyond the sampled failures",
"Billing sandbox, Chat bot and Realtime playgrounds; CLI and scaffolding; templates",
"Consent screen for third-party production apps (a Network gap)",
-- "No JSON-LD or llms.txt on the public pages",
"Governance material (code of conduct, contribution guide, contributor ladder, moderation policy) is written as drafts pending owner review: not in effect, not indexed"
],
"capabilitiesRegistered": true,
@@@ -58,6 +58,7 @@
"ADR-013 trust tiers",
"policy and governance pages (governance drafts pending owner review)",
"per-actor limits (openvibe-sdk/limits)",
-- "coding harness catalog, task routing and a read-only docs page"
++ "coding harness catalog, task routing and a read-only docs page",
++ "crawl artifacts (robots.txt, sitemap.xml, llms.txt, llms-full.txt) and the home page's JSON-LD"
]
}
ddiff --git a/package-lock.json b/package-lock.json
iindex fa3ad5a..860d9e9 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -16,7 +16,7 @@
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.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"
@@@ -93,65 +93,6 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
-- "node_modules/base64-js": {
-- "version": "1.5.1",
-- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
-- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
-- "funding": [
-- {
-- "type": "github",
-- "url": "https://github.com/sponsors/feross"
-- },
-- {
-- "type": "patreon",
-- "url": "https://www.patreon.com/feross"
-- },
-- {
-- "type": "consulting",
-- "url": "https://feross.org/support"
-- }
-- ],
-- "license": "MIT",
-- "optional": true,
-- "peer": true
-- },
-- "node_modules/better-sqlite3": {
-- "version": "11.10.0",
-- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
-- "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
-- "hasInstallScript": true,
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "bindings": "^1.5.0",
-- "prebuild-install": "^7.1.1"
-- }
-- },
-- "node_modules/bindings": {
-- "version": "1.5.0",
-- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
-- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "file-uri-to-path": "1.0.0"
-- }
-- },
-- "node_modules/bl": {
-- "version": "4.1.0",
-- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
-- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "buffer": "^5.5.0",
-- "inherits": "^2.0.4",
-- "readable-stream": "^3.4.0"
-- }
-- },
"node_modules/body-parser": {
"version": "1.20.8",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
@@@ -176,32 +117,6 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
-- "node_modules/buffer": {
-- "version": "5.7.1",
-- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
-- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
-- "funding": [
-- {
-- "type": "github",
-- "url": "https://github.com/sponsors/feross"
-- },
-- {
-- "type": "patreon",
-- "url": "https://www.patreon.com/feross"
-- },
-- {
-- "type": "consulting",
-- "url": "https://feross.org/support"
-- }
-- ],
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "base64-js": "^1.3.1",
-- "ieee754": "^1.1.13"
-- }
-- },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@@ -257,14 +172,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
-- "node_modules/chownr": {
-- "version": "1.1.4",
-- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
-- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
-- "license": "ISC",
-- "optional": true,
-- "peer": true
-- },
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@@@ -332,34 +239,6 @@
"ms": "2.0.0"
}
},
-- "node_modules/decompress-response": {
-- "version": "6.0.0",
-- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
-- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "mimic-response": "^3.1.0"
-- },
-- "engines": {
-- "node": ">=10"
-- },
-- "funding": {
-- "url": "https://github.com/sponsors/sindresorhus"
-- }
-- },
-- "node_modules/deep-extend": {
-- "version": "0.6.0",
-- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
-- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "engines": {
-- "node": ">=4.0.0"
-- }
-- },
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@@ -388,17 +267,6 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
-- "node_modules/detect-libc": {
-- "version": "2.1.2",
-- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
-- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
-- "license": "Apache-2.0",
-- "optional": true,
-- "peer": true,
-- "engines": {
-- "node": ">=8"
-- }
-- },
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@@ -449,17 +317,6 @@
"node": ">= 0.8"
}
},
-- "node_modules/end-of-stream": {
-- "version": "1.4.5",
-- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
-- "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "once": "^1.4.0"
-- }
-- },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@@ -505,17 +362,6 @@
"node": ">= 0.6"
}
},
-- "node_modules/expand-template": {
-- "version": "2.0.3",
-- "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
-- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
-- "license": "(MIT OR WTFPL)",
-- "optional": true,
-- "peer": true,
-- "engines": {
-- "node": ">=6"
-- }
-- },
"node_modules/express": {
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",
@@@ -599,14 +445,6 @@
],
"license": "BSD-3-Clause"
},
-- "node_modules/file-uri-to-path": {
-- "version": "1.0.0",
-- "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
-- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true
-- },
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@@@ -643,14 +481,6 @@
"node": ">= 0.6"
}
},
-- "node_modules/fs-constants": {
-- "version": "1.0.0",
-- "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
-- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true
-- },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@@ -697,14 +527,6 @@
"node": ">= 0.4"
}
},
-- "node_modules/github-from-package": {
-- "version": "0.0.0",
-- "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
-- "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true
-- },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@@ -785,42 +607,12 @@
"node": ">=0.10.0"
}
},
-- "node_modules/ieee754": {
-- "version": "1.2.1",
-- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
-- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
-- "funding": [
-- {
-- "type": "github",
-- "url": "https://github.com/sponsors/feross"
-- },
-- {
-- "type": "patreon",
-- "url": "https://www.patreon.com/feross"
-- },
-- {
-- "type": "consulting",
-- "url": "https://feross.org/support"
-- }
-- ],
-- "license": "BSD-3-Clause",
-- "optional": true,
-- "peer": true
-- },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
-- "node_modules/ini": {
-- "version": "1.3.8",
-- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
-- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
-- "license": "ISC",
-- "optional": true,
-- "peer": true
-- },
"node_modules/iovalkey": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.4.0.tgz",
@@@ -1051,53 +843,12 @@
"node": ">= 0.6"
}
},
-- "node_modules/mimic-response": {
-- "version": "3.1.0",
-- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
-- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "engines": {
-- "node": ">=10"
-- },
-- "funding": {
-- "url": "https://github.com/sponsors/sindresorhus"
-- }
-- },
-- "node_modules/minimist": {
-- "version": "1.2.8",
-- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
-- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "funding": {
-- "url": "https://github.com/sponsors/ljharb"
-- }
-- },
-- "node_modules/mkdirp-classic": {
-- "version": "0.5.3",
-- "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
-- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true
-- },
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
-- "node_modules/napi-build-utils": {
-- "version": "2.0.0",
-- "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
-- "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true
-- },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@@ -1107,20 +858,6 @@
"node": ">= 0.6"
}
},
-- "node_modules/node-abi": {
-- "version": "3.96.0",
-- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",
-- "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "semver": "^7.3.5"
-- },
-- "engines": {
-- "node": ">=10"
-- }
-- },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@@ -1145,20 +882,10 @@
"node": ">= 0.8"
}
},
-- "node_modules/once": {
-- "version": "1.4.0",
-- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
-- "license": "ISC",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "wrappy": "1"
-- }
-- },
"node_modules/openvibe-contracts": {
-- "version": "0.86.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
++ "version": "0.97.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0",
++ "integrity": "sha512-HPGBJYx6Dh2kaTg5B41WGhpw38zUdoU1VnWtkcsFkaU4E521uQOJxf6sIaLdHMsahqov7b9k2HKEFCnD9065MQ==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
@@@ -1355,35 +1082,6 @@
"node": ">=0.10.0"
}
},
-- "node_modules/prebuild-install": {
-- "version": "7.1.3",
-- "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
-- "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
-- "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "detect-libc": "^2.0.0",
-- "expand-template": "^2.0.3",
-- "github-from-package": "0.0.0",
-- "minimist": "^1.2.3",
-- "mkdirp-classic": "^0.5.3",
-- "napi-build-utils": "^2.0.0",
-- "node-abi": "^3.3.0",
-- "pump": "^3.0.0",
-- "rc": "^1.2.7",
-- "simple-get": "^4.0.0",
-- "tar-fs": "^2.0.0",
-- "tunnel-agent": "^0.6.0"
-- },
-- "bin": {
-- "prebuild-install": "bin.js"
-- },
-- "engines": {
-- "node": ">=10"
-- }
-- },
"node_modules/proxy-addr": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
@@@ -1401,18 +1099,6 @@
"url": "https://opencollective.com/express"
}
},
-- "node_modules/pump": {
-- "version": "3.0.4",
-- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
-- "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "end-of-stream": "^1.1.0",
-- "once": "^1.3.1"
-- }
-- },
"node_modules/qs": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
@@@ -1453,39 +1139,6 @@
"node": ">= 0.8"
}
},
-- "node_modules/rc": {
-- "version": "1.2.8",
-- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
-- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
-- "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "deep-extend": "^0.6.0",
-- "ini": "~1.3.0",
-- "minimist": "^1.2.0",
-- "strip-json-comments": "~2.0.1"
-- },
-- "bin": {
-- "rc": "cli.js"
-- }
-- },
-- "node_modules/readable-stream": {
-- "version": "3.6.2",
-- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
-- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "inherits": "^2.0.3",
-- "string_decoder": "^1.1.1",
-- "util-deprecate": "^1.0.1"
-- },
-- "engines": {
-- "node": ">= 6"
-- }
-- },
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
@@@ -1677,55 +1330,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
-- "node_modules/simple-concat": {
-- "version": "1.0.1",
-- "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
-- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
-- "funding": [
-- {
-- "type": "github",
-- "url": "https://github.com/sponsors/feross"
-- },
-- {
-- "type": "patreon",
-- "url": "https://www.patreon.com/feross"
-- },
-- {
-- "type": "consulting",
-- "url": "https://feross.org/support"
-- }
-- ],
-- "license": "MIT",
-- "optional": true,
-- "peer": true
-- },
-- "node_modules/simple-get": {
-- "version": "4.0.1",
-- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
-- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
-- "funding": [
-- {
-- "type": "github",
-- "url": "https://github.com/sponsors/feross"
-- },
-- {
-- "type": "patreon",
-- "url": "https://www.patreon.com/feross"
-- },
-- {
-- "type": "consulting",
-- "url": "https://feross.org/support"
-- }
-- ],
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "decompress-response": "^6.0.0",
-- "once": "^1.3.1",
-- "simple-concat": "^1.0.0"
-- }
-- },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@@ -1758,60 +1362,6 @@
"node": ">=10.0.0"
}
},
-- "node_modules/string_decoder": {
-- "version": "1.3.0",
-- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
-- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "safe-buffer": "~5.2.0"
-- }
-- },
-- "node_modules/strip-json-comments": {
-- "version": "2.0.1",
-- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
-- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "engines": {
-- "node": ">=0.10.0"
-- }
-- },
-- "node_modules/tar-fs": {
-- "version": "2.1.5",
-- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
-- "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "chownr": "^1.1.1",
-- "mkdirp-classic": "^0.5.2",
-- "pump": "^3.0.0",
-- "tar-stream": "^2.1.4"
-- }
-- },
-- "node_modules/tar-stream": {
-- "version": "2.2.0",
-- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
-- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "bl": "^4.0.3",
-- "end-of-stream": "^1.4.1",
-- "fs-constants": "^1.0.0",
-- "inherits": "^2.0.3",
-- "readable-stream": "^3.1.1"
-- },
-- "engines": {
-- "node": ">=6"
-- }
-- },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@@ -1821,20 +1371,6 @@
"node": ">=0.6"
}
},
-- "node_modules/tunnel-agent": {
-- "version": "0.6.0",
-- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
-- "license": "Apache-2.0",
-- "optional": true,
-- "peer": true,
-- "dependencies": {
-- "safe-buffer": "^5.0.1"
-- },
-- "engines": {
-- "node": "*"
-- }
-- },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@@ -1857,14 +1393,6 @@
"node": ">= 0.8"
}
},
-- "node_modules/util-deprecate": {
-- "version": "1.0.2",
-- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
-- "license": "MIT",
-- "optional": true,
-- "peer": true
-- },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@@ -1883,14 +1411,6 @@
"node": ">= 0.8"
}
},
-- "node_modules/wrappy": {
-- "version": "1.0.2",
-- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
-- "license": "ISC",
-- "optional": true,
-- "peer": true
-- },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
ddiff --git a/package.json b/package.json
iindex 9d4b74a..cd4cb17 100644
---- a/package.json
++++ b/package.json
@@@ -28,7 +28,7 @@
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.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 b27aa2e..1ca4f2f 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -1,9 +1,10 @@
'use strict';
/**
-- * Crawl and machine-readability artifacts (plan T11): GET /robots.txt, GET /sitemap.xml and
-- * GET /llms.txt, plus the home page's JSON-LD. Every one is built with openvibe-shared/seo — the
-- * same toolkit the other OpenVibe sites use — so they say the same things here as everywhere.
++ * Crawl and machine-readability artifacts (plan T11): GET /robots.txt, GET /sitemap.xml,
++ * GET /llms.txt and GET /llms-full.txt, plus the home page's JSON-LD. Every one is built with
++ * openvibe-shared/seo — the same toolkit the other OpenVibe sites use — so they say the same things
++ * here as everywhere.
*
* Public pages only, and never the viewer: the portal, staff console, sign-in and the API are absent
* from all of them and Disallowed in robots.txt. lastmod is a real timestamp from the site's own
@@@ -24,6 +25,34 @@ const DESCRIPTION = 'The OpenVibe developer portal: create projects and apps in
const DISALLOW = ['/projects', '/auth/', '/oauth/test-callback', '/staff', '/api/'];
const RELEASE_LIMIT = 5000;
++// The one line /llms-full.txt says about each fixed public page: a title and what the page serves.
++// These are the pages publicPages() lists (and so the sitemap), so the two cannot name different sets.
++const PAGE_TEXT = {
++ '/': ['OpenVibe.Codes home', 'The developer portal: create a project, get scoped credentials and request capability grants over OpenVibe.Network.'],
++ '/updates': ['What shipped on OpenVibe.Codes', 'This site\'s update log, from the network changelog feed.'],
++ '/docs': ['Platform reference', 'Reference generated at boot from the pinned openvibe-contracts and openvibe-sdk.'],
++ '/docs/api': ['API explorer', 'Every service\'s routes from the contracts\' OpenAPI 3.1 documents.'],
++ '/docs/updates': ['The update system', 'The feed, the data-ov-shipped markup and the shared helpers every OpenVibe site shows.'],
++ '/docs/contracts': ['Contracts', 'The JSON Schemas with their fields, examples and versions.'],
++ '/docs/capabilities': ['Capabilities', 'Every capability, which of them can be granted to apps, and its owner, visibility and status.'],
++ '/docs/events': ['Event types', 'Every event type a service manifest declares it produces or consumes.'],
++ '/docs/services': ['Service registry', 'The Network\'s registry as it answers, with health (never invented).'],
++ '/docs/billing': ['Billing policy', 'The billing policy as OpenVibe.Billing\'s /policy.json states it.'],
++ '/docs/limits': ['Limits and tiers', 'What a project may do in sandbox and production, from the services that enforce it.'],
++ '/docs/tools': ['Tools API', 'The OpenVibe.Tools registry, callable from code.'],
++ '/docs/harnesses': ['Coding harnesses', 'The harnesses and agents a coding task can be routed to, with their prices and limits.'],
++ '/docs/export': ['Project export', 'What a project export holds: the metadata JSON and the full archive.'],
++ '/docs/sdk': ['SDK reference', 'openvibe-sdk modules, from their type definitions.'],
++ '/oauth': ['OAuth callback helper', 'Authorization code + PKCE, with a callback that never exchanges the code.'],
++ '/tools/webhooks': ['Webhook signature tester', 'Decides as a receiver requiring signature v2 does.'],
++ '/manifests/validate': ['Manifest validation', 'App and mod manifests, validated against the pinned contracts.'],
++ '/policy': ['Policy', 'The portal\'s policy pages: decisions, compatibility, licensing, transparency and community.'],
++ '/policy/rfc': ['Proposals and decisions', 'How a contract, capability or event changes, and the ADR record.'],
++ '/policy/compatibility': ['Compatibility and deprecation', 'ADR-002 and ADR-016 as published, with the current deprecations.'],
++ '/policy/licensing': ['Licensing', 'What each package Codes runs is licensed under.'],
++ '/policy/transparency': ['Transparency', 'What Codes stores, what it does not, and what works today.'],
++};
++
/** "2026-09-28" or "2026-09-28T12:00:00Z" as YYYY-MM-DD; null when the value is unusable. */
function dayOf(ts) {
const m = String(ts == null ? '' : ts).match(/^(\d{4}-\d{2}-\d{2})/);
@@@ -89,6 +118,42 @@ function publicPages({ docs, governance = [] }) {
return pages;
}
++/**
++ * The title and one-line text /llms-full.txt gives a public page: the fixed pages from PAGE_TEXT,
++ * governance pages from their own blurb, and the generated docs pages from the same data the page
++ * renders. Nothing here is fetched or invented; an unknown path (there is none today) falls back to
++ * the path as its title.
++ */
++function describePage(pathname, { docs, governance = [], apiIndex = [] }) {
++ if (PAGE_TEXT[pathname]) return { title: PAGE_TEXT[pathname][0], text: PAGE_TEXT[pathname][1] };
++ let m;
++ if ((m = /^\/policy\/([^/]+)$/.exec(pathname))) {
++ const g = governance.find((x) => x.slug === m[1]);
++ if (g) return { title: g.title, text: g.blurb || '' };
++ }
++ if ((m = /^\/docs\/contracts\/([^/]+)$/.exec(pathname))) {
++ const c = docs.contracts.find((x) => x.id === m[1]);
++ if (c) return { title: c.title, text: seo.clip(c.description, 200) };
++ }
++ if ((m = /^\/docs\/capabilities\/([^/]+)$/.exec(pathname))) {
++ const c = docs.capabilities.find((x) => x.id === m[1]);
++ if (c) return { title: c.id, text: seo.clip(c.description, 200) };
++ }
++ if ((m = /^\/docs\/api\/([^/]+)$/.exec(pathname))) {
++ const s = apiIndex.find((x) => x.service === m[1]);
++ if (s) return { title: `${s.name} API`, text: `${s.operations} routes performing ${s.capabilities} capabilities.` };
++ }
++ if ((m = /^\/docs\/sdk\/([^/]+)$/.exec(pathname))) {
++ const mod = docs.sdk.find((x) => x.slug === m[1]);
++ if (mod) return { title: mod.name, text: `From ${mod.typesFile} (${mod.browser}).` };
++ }
++ if ((m = /^\/docs\/adr\/([^/]+)$/.exec(pathname))) {
++ const a = docs.adrs.find((x) => x.id === m[1]);
++ if (a) return { title: a.title, text: `Status: ${a.status}. Published in openvibe-contracts.` };
++ }
++ return { title: pathname, text: '' };
++}
++
function createDiscoveryRoutes(ctx) {
const { config, docs, releases, governance = [] } = ctx;
const r = asyncRouter();
@@@ -133,6 +198,7 @@ function createDiscoveryRoutes(ctx) {
const machine = [
{ title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages below, each with a real lastmod' },
{ title: 'robots.txt', url: abs('/robots.txt'), note: 'search and AI crawlers are welcome on the public pages' },
++ { title: 'Full text for language models', url: abs('/llms-full.txt'), note: 'every fixed public page, one line each' },
{ title: 'Release metadata (JSON)', url: abs('/release.json'), note: 'this service\'s current release, per ADR-016' },
];
if (firstContract) machine.push({ title: 'Contract JSON Schema', url: abs(`/docs/contracts/${firstContract}.json`), note: 'append .json to any contract page for its schema' });
@@@ -175,6 +241,29 @@ function createDiscoveryRoutes(ctx) {
}));
});
++ // /llms-full.txt: the same public pages the sitemap lists, one title and one line of text each,
++ // so a language model sees what this origin serves without fetching every page. Never a private
++ // page (the portal, sign-in, staff and the API are absent) and maxBytes caps the file.
++ r.get('/llms-full.txt', (_req, res) => {
++ const pages = publicPages({ docs, governance });
++ const apiIndex = contracts.openapi.index();
++ const isDocs = (p) => p === '/docs' || p.startsWith('/docs/');
++ const tools = ['/oauth', '/tools/webhooks', '/manifests/validate'];
++ const group = (pick) => pages.filter((p) => pick(p.path)).map((p) => ({ url: p.path, ...describePage(p.path, { docs, governance, apiIndex }) }));
++ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsFull({
++ site: SITE_NAME,
++ summary: 'Every fixed public page of OpenVibe.Codes, one line each: the home and update log, the reference generated from the pinned contracts and SDK, the developer tools, and the policy pages.',
++ base: site,
++ maxBytes: 512 * 1024,
++ sections: [
++ { title: 'Home', pages: group((p) => p === '/') },
++ { title: 'Reference', pages: group(isDocs) },
++ { title: 'Playgrounds', pages: group((p) => tools.includes(p)) },
++ { title: 'Site', pages: group((p) => p !== '/' && !isDocs(p) && !tools.includes(p)) },
++ ],
++ }));
++ });
++
r.get('/sitemap.xml', async (_req, res) => {
const entries = await sitemapEntries();
const urls = entries.map((e) => ({ loc: abs(e.path), ...(e.lastmod ? { lastmod: e.lastmod } : {}), changefreq: e.changefreq, priority: e.priority }));
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex d1f9439..f74416a 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -2,7 +2,7 @@
/**
* Public pages: the landing page, policy pages, public release and trust pages, the staff trust
-- * console. Crawl artifacts (robots.txt, sitemap.xml, llms.txt, the home page's JSON-LD) live in
++ * console. Crawl artifacts (robots.txt, sitemap.xml, llms.txt, llms-full.txt, the home page's JSON-LD) live in
* http/discovery.js and are mounted here.
*
* Policy pages are derived, not paraphrased: the compatibility and deprecation policy renders
@@@ -315,7 +315,7 @@ ${signedIn ? releaseActions(req, rel) : ''}
}
});
-- // ── Discovery: robots.txt, sitemap.xml, llms.txt and the home page's JSON-LD (http/discovery.js,
++ // ── Discovery: robots.txt, sitemap.xml, llms.txt, llms-full.txt and the home page's JSON-LD (http/discovery.js,
// built with openvibe-shared/seo) ─────────────────────
r.use(createDiscoveryRoutes({ ...ctx, governance }));
ddiff --git a/test/discovery.test.js b/test/discovery.test.js
iindex 11e9855..df21b90 100644
---- a/test/discovery.test.js
++++ b/test/discovery.test.js
@@@ -1,9 +1,9 @@
'use strict';
/**
* The public crawl artifacts (plan T11), fetched from the booted app: /robots.txt, /sitemap.xml,
-- * /llms.txt and the home page's JSON-LD. Each is served with the right status and content type,
-- * carries at least one real entry, lists public pages only, and takes lastmod from the site's own
-- * data (STATUS.json) — never from the clock.
++ * /llms.txt, /llms-full.txt and the home page's JSON-LD. Each is served with the right status and
++ * content type, carries at least one real entry, lists public pages only, and takes lastmod from the
++ * site's own data (STATUS.json) — never from the clock.
*/
const assert = require('assert');
const fs = require('fs');
@@@ -50,10 +50,27 @@ const PRIVATE = ['/projects', '/staff', '/auth/', '/oauth/test-callback'];
assert.ok(r.text.includes('(https://openvibe.codes/docs)'), 'no reference link');
assert.ok(r.text.includes('(https://openvibe.codes/docs/harnesses)'), 'no harnesses reference link');
assert.ok(r.text.includes('https://openvibe.codes/sitemap.xml'), 'sitemap not listed');
++ assert.ok(r.text.includes('(https://openvibe.codes/llms-full.txt)'), 'llms-full.txt not listed');
for (const p of PRIVATE) assert.ok(!r.text.includes(`https://openvibe.codes${p}`), `private path in llms.txt: ${p}`);
assert.ok(!r.text.includes('https://openvibe.codes/api/'), 'api path in llms.txt');
});
++ await check('llms-full.txt: 200 text/plain, the site title, and every page the sitemap lists', async () => {
++ const r = await t.get('/llms-full.txt');
++ assert.strictEqual(r.status, 200);
++ assert.match(r.headers.get('content-type'), /^text\/plain/);
++ assert.match(r.headers.get('cache-control') || '', /public, max-age=\d+/);
++ assert.ok(r.text.startsWith('# OpenVibe.Codes'), 'no title');
++ const sitemap = await t.get('/sitemap.xml');
++ const locs = [...sitemap.text.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]);
++ assert.ok(locs.length >= 20, `sitemap too small: ${locs.length}`);
++ for (const loc of locs) assert.ok(r.text.includes(loc), `llms-full.txt is missing ${loc}`);
++ assert.ok(r.text.includes('URL: https://openvibe.codes/docs\n'), 'no /docs entry');
++ assert.ok(r.text.includes('URL: https://openvibe.codes/tools/webhooks\n'), 'no playground entry');
++ for (const p of PRIVATE) assert.ok(!r.text.includes(`https://openvibe.codes${p}`), `private path in llms-full.txt: ${p}`);
++ assert.ok(!r.text.includes('https://openvibe.codes/api/'), 'api path in llms-full.txt');
++ });
++
await check('home page: WebSite + the site\'s primary type, the same for every crawler', async () => {
const r = await t.get('/');
assert.strictEqual(r.status, 200);
ddiff --git a/test/docs.test.js b/test/docs.test.js
iindex 75238cb..5fc76c6 100644
---- a/test/docs.test.js
++++ b/test/docs.test.js
@@@ -19,11 +19,11 @@ const sdkPkg = require('openvibe-sdk/package.json');
await check('package.json pins the contracts, SDK and shared tags that are installed', async () => {
const tag = (dep) => (pkg.dependencies[dep].match(/refs\/tags\/(v[0-9.]+)$/) || [])[1];
-- assert.strictEqual(tag('openvibe-contracts'), 'v0.86.0');
++ assert.strictEqual(tag('openvibe-contracts'), 'v0.97.0');
assert.strictEqual(tag('openvibe-sdk'), `v${sdkPkg.version}`);
assert.strictEqual(tag('openvibe-shared'), `v${require('openvibe-shared/package.json').version}`);
assert.strictEqual(sdkPkg.version, '0.26.0');
-- // The contracts tag v0.86.0 carries the released codes manifest and codes.app-manifest@1.
++ // The contracts tag v0.97.0 carries the released codes manifest and codes.app-manifest@1.
assert.ok(contracts.services.get('codes') && contracts.services.get('codes').status === 'alpha');
assert.ok(contracts.resolve('codes.app-manifest@1'));
});
@@@ -33,13 +33,13 @@ const sdkPkg = require('openvibe-sdk/package.json');
const r = await t.get(p);
assert.strictEqual(r.status, 200, p);
assert.ok(r.text.includes(`openvibe-contracts v${contractsVersion}`), `${p} names the installed contracts version`);
-- assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.86.0'), `${p} links the pinned tag`);
-- if (contractsVersion !== '0.86.0') assert.ok(r.text.includes('(tag v0.86.0)'), `${p} says the tag differs from the package version`);
++ assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.97.0'), `${p} links the pinned tag`);
++ if (contractsVersion !== '0.97.0') assert.ok(r.text.includes('(tag v0.97.0)'), `${p} says the tag differs from the package version`);
assert.ok(r.text.includes(`openvibe-sdk v${sdkPkg.version}`), `${p} names the SDK version`);
}
const v = (await t.get('/api/v1/docs/versions')).json();
assert.strictEqual(v.contracts, contractsVersion);
-- assert.strictEqual(v.contracts_tag, 'v0.86.0');
++ assert.strictEqual(v.contracts_tag, 'v0.97.0');
assert.strictEqual(v.sdk, sdkPkg.version);
});