openvibers/openvibe.news Commits

News: the story API returns the real indexability decision; docs match production ingest (#40)

OP OpenVibers committed 11 Oct 2026 · 9738ea7462 · 4 files, +18 −8

- `storyDto()` returned `indexability` as an un-awaited promise, which JSON serializes as `{}`, so `GET /api/v1/stories/:id` and the publish and revise responses never carried the gate's robots decision. It is now awaited and returned as `{ indexable, robots, reasons }`. `test/stories.test.js` asserts it on the publish response and on the detail read.
- README and STATUS now match production: 70 Sources items in 18 clusters since 2026-10-11, no stories until an editor opens one from a cluster and publishes it.

Investigated with a DeepSeek agent. The pipeline is intact up to the editor's step. News drafts nothing on its own, by design (README: "News publishes nothing until an editor does").

Changes

ddiff --git a/README.md b/README.md
iindex 8195d5a..5697010 100644
---- a/README.md
++++ b/README.md
@@@ -7,8 +7,9 @@
**public at `openvibe.news` since 2026-10-09**, after an independent pre-launch security review whose
fixes shipped first; the domain no longer shows the OpenVibe.Sites placeholder. Its capabilities and
service manifest are registered in openvibe-contracts v0.97.0.
--The production database holds 12 topics and no stories: it has not ingested anything from the
--running OpenVibe.Sources service, because every Sources seed is disabled.
++Since 2026-10-11 production ingests from the running OpenVibe.Sources service (NASA news releases, GOV.UK
++news, the EU press corner): 70 source items in 18 clusters, 12 topics, and no stories yet, because a story
++exists only once an editor opens one from a cluster and publishes it (below).
**Domain:** `openvibe.news` · **Port:** 4820 · **Service id:** `news`
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §12.7; roadmap §15.13, §29, §32.
**License:** AGPL-3.0 (same as every OpenVibe service).
@@@ -285,8 +286,8 @@ Each grant is `[client, capability, audience]`:
| The contract proposals are valid and match the code; every emitted event type is declared. | `test/contracts.test.js` |
| Health, readiness, release, robots, llms.txt, llms-full.txt, sitemaps, legal, problems, CORS, topics-only seed. | `test/ops.test.js` |
--Not yet demonstrated: ingestion from the deployed OpenVibe.Sources (its seeds are disabled until a
--person verifies their terms; production has 181 ingest runs and 0 items), delivery of a real item
++Demonstrated in production on 2026-10-11: ingestion from the deployed OpenVibe.Sources (70 items, 18
++clusters). Not yet demonstrated: a published story (it waits on an editor), delivery of a real item
through the running OpenVibe.Events (the `sources.item.*` and `sources.fetch.failed` subscriptions
exist), and an OpenVibe.AI run (OpenVibe.AI runs on the host, but News in production has no
`OV_AI_INTERNAL_URL`; the seam is tested against a mock of its runs API).
ddiff --git a/STATUS.json b/STATUS.json
iindex 6536071..f815098 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -31,11 +31,11 @@
],
"capabilitiesRegistered": true,
"serviceManifestRegistered": true,
-- "inputs": "OpenVibe.Sources items (category news) via the Events subscriptions sources.item.* and sources.fetch.failed (created in production 2026-09-23) and a cursor pull; every Sources seed is disabled, so no item has arrived yet (181 empty ingest runs, 0 source items)",
++ "inputs": "OpenVibe.Sources items (category news) via the Events subscriptions sources.item.* and sources.fetch.failed (created in production 2026-09-23) and a cursor pull; in production since 2026-10-11: 70 source items from NASA news releases, GOV.UK news and the EU press corner, grouped into 18 clusters",
"ai": "optional seam (news.summarize_story, news.compare_perspectives) tested against a mock; not configured in production (OV_AI_INTERNAL_URL unset); OpenVibe.AI output only ever lands as a draft that needs a person's review",
-- "seed": "topics only (12 in production); no stories, source items or clusters",
++ "seed": "topics only (12 in production); stories exist only once an editor opens and publishes one",
"deployed": true,
-- "deployment": "Deployed with `sudo ovhost deploy news`: unit openvibe-news on 127.0.0.1:4820 behind nginx; on 2026-09-23 /api/ready answered 200 and the production database held 12 topics and 0 stories. Public at openvibe.news since 2026-10-09, after an independent pre-launch security review whose fixes shipped first; the OpenVibe.Sites placeholder is gone. The database still holds no stories: no source item has arrived while every Sources seed is disabled",
++ "deployment": "Deployed with `sudo ovhost deploy news`: unit openvibe-news on 127.0.0.1:4820 behind nginx; on 2026-09-23 /api/ready answered 200 and the production database held 12 topics and 0 stories. Public at openvibe.news since 2026-10-09, after an independent pre-launch security review whose fixes shipped first; the OpenVibe.Sites placeholder is gone. The database holds 70 source items in 18 clusters and no stories: the first waits on an editor (NEWS_EDITORS or a Network admin) opening one from a cluster at /edit",
"placeholderInSites": false,
"features": [
"Sources ingestion by Events subscription and cursor pull, with dedupe",
ddiff --git a/server/http/api.js b/server/http/api.js
iindex 6fa136f..db93173 100644
---- a/server/http/api.js
++++ b/server/http/api.js
@@@ -102,13 +102,17 @@ function createApi(ctx) {
async function storyDto(story) {
const head = await stories.head(story);
const topic = await publication.topicOf(story);
++ // The gate's decision for the published revision. Awaited here: a returned promise would
++ // serialize as {} in the JSON, leaving the editor/API without the real robots decision.
++ const publishedRev = story.published_revision ? await stories.revision(story, story.published_revision) : null;
++ const indexability = publishedRev ? await publication.decide(story, publishedRev) : null;
return {
id: story.id, slug: story.slug, url: publication.storyUrl(story), state: story.state, cluster_id: story.cluster_id,
topic: topic ? topic.slug : null, working_headline: story.working_headline, noindex: Boolean(story.noindex),
published_revision: story.published_revision, first_published_at: iso(story.first_published_at), published_at: iso(story.published_at), retracted_at: iso(story.retracted_at),
head: head ? { revision: head.number, headline: head.fields.headline, paragraphs: head.fields.paragraphs, authorship: head.meta.authorship || null, system: head.meta.system || null, gaps: head.meta.gaps || null, author: head.author, created_at: head.createdAt } : null,
publishable: head ? await stories.problems(story, head) : [{ code: 'story.no_text', detail: 'Nothing has been written yet' }],
-- indexability: story.published_revision ? (async () => { const d = await publication.decide(story, await stories.revision(story, story.published_revision)); return { indexable: d.indexable, robots: d.robots, reasons: d.reasons }; })() : null,
++ indexability: indexability ? { indexable: indexability.indexable, robots: indexability.robots, reasons: indexability.reasons } : null,
sources: (await stories.sources(story, { includeDetached: true })).map((s) => ({ n: s.n, perspective_id: s.perspective_id, detached_at: iso(s.detached_at), item: itemDto(s.item) })),
timeline: (await stories.timeline(story)).map((t) => ({ id: t.id, occurred_on: t.occurred_on, text: t.text, source_item_id: t.source_item_id })),
perspectives: (await stories.perspectives(story)).map((p) => ({ id: p.id, label: p.label, description: p.description })),
ddiff --git a/test/stories.test.js b/test/stories.test.js
iindex 453f48c..a113226 100644
---- a/test/stories.test.js
++++ b/test/stories.test.js
@@@ -92,6 +92,11 @@ const BODY = [
const r = await t.api(`/stories/${story.id}/publish`, { json: {} });
assert.strictEqual(r.status, 200, r.text);
assert.strictEqual(r.json().story.state, 'published');
++ // The editor/API DTO carries the gate's real decision, not a promise that serializes as {}.
++ assert.deepStrictEqual(r.json().story.indexability, { indexable: true, robots: 'index, follow', reasons: [] });
++ const detail = (await t.api(`/stories/${story.id}`)).json();
++ assert.deepStrictEqual(detail.story.indexability, { indexable: true, robots: 'index, follow', reasons: [] });
++ assert.deepStrictEqual(detail.story.publishable, []);
const page = await t.get('/stories/europa-clipper-launches-for-jupiter');
assert.strictEqual(page.status, 200);
assert.match(page.text, /<h1>Europa Clipper launches for Jupiter<\/h1>/);