Skip to content

Repository files navigation

Equinox Haber

Quality Checks Deploy License: AGPL-3.0

Equinox Haber is an editorial-first, AI-assisted Turkish news publishing system for haber.sametbasbug.dev.

The project explores a practical question: can a small public news surface stay fast, readable, transparent, and auditable when software handles ingestion, deduplication, queueing, audits, image generation, static builds, and deployment discipline — while an editorial agent/human-in-the-loop keeps the final news judgment?

The reusable part is the workflow. The live output is a global-focused Turkish news stream.

Quick evaluation

If you want to inspect the reusable workflow layer without touching providers or production data:

python3 -m venv newsroom/.venv
newsroom/.venv/bin/pip install -e "newsroom[test]"
npm install
npm run quality

npm run quality runs the newsroom test suite and an Astro build. The tests never reach the network and never call a provider.

What is open source here?

This repository is intentionally split into two layers:

  • Source code and workflow tooling are licensed under AGPL-3.0. This includes the Astro site, the newsroom package, its CLI, gates, tests, and the build/deploy workflow.
  • Editorial content, generated/published images, brand identity, and protected media are not part of that grant. See CONTENT_LICENSE.md.

In short: the system can be studied, reused, forked, and adapted — provided changes stay open, including when run as a network service; the published news archive and brand layer are not open content.

Why this repository exists

Most small publishing projects eventually hit the same maintenance wall:

  • collecting sources is easy, but selecting responsibly is hard;
  • automation is fast, but blind autopublish is risky;
  • duplicate stories and repeated angles quietly lower quality;
  • AI-generated drafts can help, but they must not become the editor;
  • image generation needs strict size/licensing/visual guardrails;
  • publishing needs boring safeguards: audits, build checks, narrow commits, CI, and rollback-friendly history.

Equinox Haber is a working experiment around those constraints.

Why this matters for OSS maintainers

Many maintainers now have to review AI-assisted changes, generated summaries, bot-authored PRs, or automated release notes. The hard part is not calling a model; it is keeping the workflow auditable when automation touches public output.

This repo is useful as a small, concrete reference for:

  • separating ingestion/automation from final editorial authority;
  • keeping provider-dependent steps out of CI;
  • writing tests for stale-source, duplicate, manual-review, image, and leak-prevention gates;
  • documenting where automation must stop and a human/maintainer must decide;
  • preserving a clear license boundary between reusable tooling and protected published content.

Public site surface

The current live site is a standalone GitHub Pages news surface at haber.sametbasbug.dev.

Key routes:

  • / — live news homepage with category tabs and automatic featured signals
  • /?kategori=siyaset, /?kategori=ekonomi, /?kategori=teknoloji, /?kategori=bilim — category-filtered homepage views
  • /<slug>/ — article pages with canonical news metadata
  • /sayfa/<n>/ — paginated public archive
  • /icerik-paneli/ — expanded content-panel stream
  • /rss.xml — RSS feed for the latest public items
  • /news-sitemap.xml — Google News sitemap for the recent publication window
  • /sitemap-index.xml — sitemap index generated by Astro

Transparency and policy pages:

  • /hakkimizda/
  • /yazarlar/
  • /iletisim/
  • /editorial-ilkeler/
  • /duzeltme-politikasi/
  • /yapay-zeka-ve-yayin-sureci/
  • /gizlilik-politikasi/

The header is intentionally category-focused. Trust, policy, RSS, and ecosystem links live in the footer so the news reading surface stays clean.

Homepage model

The homepage has two editorial layers:

  • Canlı akış — the main chronological stream.
  • Öne çıkanlar / Akıştan seçilenler — an automatic signal block, not a fake manual editor pick.

The automatic featured model scores recent items using recency, source count, breaking/global signal terms, and optional editorPick as a boost. Old manual flags are penalized so stale stories do not stay pinned forever.

When a category tab is active, the side block becomes category-specific, for example Teknoloji akışından, and keeps its items inside that category. In the default Tümü view, it prefers category diversity.

Architecture

The repository has two main parts:

  • Astro publishing surface — static news pages, category UI, article templates, RSS, Google News sitemap, sitemap index, transparency pages, author/site shell, and GitHub Pages deployment.
  • newsroom, the Python rail — RSS collection, mechanical screening, candidate board, response contract, Turkish language measurement, markdown generation, hero normalization with a stock fallback, audits, and a publish step that rolls back rather than leave a half-finished story.
haber-project/
  src/
    components/news/          # News UI components
    content/equinoxHaber/       # Published markdown news items
    data/                     # Site/category helpers
    layouts/                  # News/article/info page shells
    pages/                    # Astro routes, RSS, news sitemap
  public/images/generated/    # Generated hero images
  newsroom/
    POLICY.md                 # The single editorial source of truth
    newsroom/
      ingest.py               # RSS collection
      screen.py               # Mechanical eligibility gates
      brief.py                # Candidate board for the editorial agent
      accept.py               # Response contract validation
      lang.py                 # Turkish/untranslated measurement
      publish.py              # Markdown rendering and slugs
      hero.py                 # Hero normalization and stock fallback
      cycle.py                # prepare / publish orchestration
    docs/                     # Runbook and decision log
    tests/                    # Suite plus the corpus it measures against
    data/                     # Runtime state, git-ignored

Agent access

A human's agent can publish on that human's behalf, and no key ever reaches the agent. Orbit holds the permission, signs a 60-second document per call, and delivers it to Haber itself. Revocation is one switch in the Orbit dashboard: turn agent access off and the next call is refused.

  • Haber declares what an agent can do in public/orbit-actions.json. Orbit reads that file and caches it for ten minutes; adding an operation needs no change on the Orbit side.
  • Orbit posts to /api/orbit-eylem with an ES256 document that carries the human's pairwise subject, the acting agent (act, RFC 8693) and the operation. Haber verifies it against Orbit's JWKS.
  • Permission to publish is still Haber's decision, not Orbit's. The publishers row must name the agent and the human it acts for; the byline comes from that row, never from the request body.

Six operations exist today, in two permission tiers.

Writing and internal state need a publishers row: haber.panoYaz pins the candidate board, haber.panoOku reads it back (an agent that loses the briefId between turns would otherwise strand a board for six hours), haber.yayinla publishes one story from it, and haber.yayindanKaldir / haber.yayinaAlGeri withdraw and restore one.

Reading the archive does not: haber.yayinlariOku is open to the agent of anyone who has connected Haber in Orbit and opened agent access. It is not the open web — the request still carries an Orbit-signed document — but it needs no publisher row.

That operation is deliberately not an RSS clone. The feed stays exactly as it is, public and unchanged; the Orbit path simply reaches further. RSS gives the latest stories. haber.yayinlariOku gives the whole archive with search, tag, category and date filters, pagination, and the article body on request. The advantage of holding an Orbit account is built by adding, not by withholding.

Withdrawal is deliberately narrower than publishing. It hides rather than deletes, requires a written reason, and only applies to stories published in the last 24 hours: the case it exists for is "we just published something wrong". Anything older is a human decision. A withdrawn address returns 410 Gone with a short page rather than a silent 404 — it was live, it may have been shared, and pretending it never existed would be the wrong answer to both readers and crawlers.

Publishers can see the trail at /denetim: recent actions, refusals, withdrawals. Refused attempts reach the calling agent as an opaque error — Orbit does not carry a site's error body — so this page is the only place the refusal is legible.

The full contract lives in orbit-project/docs/baglisite-ajan-eylemleri.md.

Granting an agent is one row. subject is the Orbit agent id prefixed with agent:, acts_for is the human's pairwise subject as Haber knows it (the readers.orbit_subject written at sign-in), and author must be one of the supported bylines:

INSERT INTO publishers (subject, acts_for, author, may_write_brief, may_publish, created_at)
VALUES ('agent:<orbit agent id>', '<human pairwise subject>', 'Selene AI', 1, 1, datetime('now'));

The older publisher-key path (hbr_pub_v1_…) still works and is intentionally left open during the transition. Closing it means clearing publishers.key_digest — one visible, reversible step.

Editorial model

The project is editorial-first.

Python is not the editor. The split is deliberate and it is the one rule the whole system is built from:

Python does only what can be verified. Judgement lives in POLICY.md.

If a rule is decidable — is this text Turkish, is the source older than 24 hours, does this slug already exist, does the hero file actually exist — it lives in code and is not negotiable. If a rule needs judgement — is this story worth publishing, which source is more trustworthy, is this paragraph carrying its weight — it lives in newsroom/POLICY.md and the editorial agent applies it. No rule is written in both places.

Asteria AI is the narrow editorial agent. Each cycle it receives one brief — candidates with their extracted source text, plus the distribution of recent publications — reads the policy from disk, and returns a JSON response. The rail validates that response against a contract and carries it into the Astro site, or rejects it and publishes nothing.

Current category set:

  • Siyaset
  • Ekonomi
  • Teknoloji
  • Bilim

Turkey-related stories are included only when the global context is strong enough for one of those categories.

Core workflow

One cycle is three steps.

# 1. Collect, screen, and build the candidate board
newsroom/.venv/bin/newsroom prepare

# 2. The editorial agent reads POLICY.md and the brief, then writes a response
#    (no command here — this step is the editorial work)

# 3. Validate the response and carry it into the site
newsroom/.venv/bin/newsroom publish --response response.json

prepare writes the full brief to newsroom/data/current-brief.json. Extracted source text travels inside the brief, so the editorial step does not need to fetch article pages itself.

publish validates against the acceptance contract, renders the markdown, resolves the hero, runs the audits and the Astro build, and commits a narrow path set. It never pushes. If any gate fails, the written file and any generated image are removed and nothing is committed — a cycle either lands completely or leaves no trace.

Selecting nothing is a valid outcome, not a failure (POLICY.md §7).

Quality and safety gates

Every gate here is mechanical. None of them encodes an editorial preference — that separation is the point.

Before a candidate reaches the board:

  • source age, with future-dating and undated feeds both rejected;
  • liveblog and sponsored-path formats;
  • duplicate URL or near-duplicate headline against everything published;
  • paywalled pages, detected by how little text they yield rather than by a host list.

Before a response is published:

  • schema and contract completeness;
  • Turkish body measurement by density, not word count, so length does not bias the result; proper-noun spans are excluded from the English signal;
  • untranslated-headline detection against the source title;
  • body-depth floor as a truncation guard, and a paragraph-count range;
  • leaked internal notes and audit markers;
  • frontmatter validity, sources section, category;
  • hero file existence and 1200×675 WebP under 400 KB;
  • working-tree scope: only that story's files may have changed;
  • Astro build.

Thresholds are not guesses. They were chosen against a corpus of 585 published articles and 976 source texts kept in newsroom/tests/corpus, and the suite replays them on every run.

CI is provider-free: newsroom tests and an Astro build. It performs no collection, calls no AI provider, and runs no publish command.

Feeds and Google News readiness

The public release includes the surfaces expected by news/feed consumers:

  • rss.xml is limited to the latest 50 items and uses absolute image enclosure URLs.
  • news-sitemap.xml lists recently published items with Google News metadata.
  • sitemap-index.xml is generated for the static site.
  • Article pages include canonical URLs and structured news metadata.
  • Public transparency pages explain ownership, authorship, corrections, privacy, editorial principles, and AI use.

AI hero image policy

Generated hero images are intentionally local and normalized:

  • final path: public/images/generated/equinox-haber/<slug>.webp
  • dimensions: 1200×675
  • format: WebP
  • default quality target: 82
  • audit maximum: 400 KB

The generator writes a raw file wherever it likes; the rail then resizes, crops to centre, strips metadata, and writes the final WebP. Provider output dimensions never reach the site directly.

Resolution order, in full:

  1. an image already exists for this slug — never regenerated;
  2. a generated file was supplied — normalized;
  3. neither — a stock photo is fetched as a fallback;
  4. none of the above — the story publishes without a hero, on purpose.

Step four is deliberate. A missing image does not stop a story.

Two constraints worth stating plainly:

Publisher images are not reused. Source, RSS, and OG images are blocked outright. A photograph's licence cannot be determined by looking at it, and a gate that treats an unverifiable thing as verified is not a gate.

Stock searches look for the subject, not the event. A stock archive has a photograph of a fire in it, but it is not the fire in the story. A frame that reads as a news scene shows the reader something that did not happen — the same objection that rules out reusing the publisher's image. The search term asks for the fact, the setting, the object, the geography: the field, not the moment. When a story runs on stock, its alt text is omitted rather than reused, because the text was written for an image that was never made.

Installation

Requirements:

  • Python 3.12+
  • Node.js 24+
  • npm
python3 -m venv newsroom/.venv
newsroom/.venv/bin/pip install -e "newsroom[test]"
npm install

ImageMagick (magick) is needed for hero normalization. Tests that require it skip themselves when it is absent.

Development commands

# Run the Astro dev server (static preview, no Worker bindings)
npm run dev

# Run the deployed Worker locally, with D1/R2 bindings and server routes.
# This also applies pending migrations to the LOCAL database — skipping that
# step leaves the local D1 empty and every page fails with
# "no such table: site_state".
npm run dev:ssr

# Build the static site
npm run build

# Run provider-free local quality checks
npm run quality

# Run the newsroom test suite
newsroom/.venv/bin/python -m pytest newsroom/tests

# Show cycle state between runs
newsroom/.venv/bin/newsroom status

Useful npm wrappers:

npm run news:prepare
npm run news:status
npm run test:newsroom

Documentation

Current status

This is a working public project, not a polished framework package. It powers the live Equinox Haber site and contains real operational history.

The current maintenance priority is to keep the production surface boringly reliable:

  • keep the category tabs, feeds, sitemap, and transparency pages current;
  • keep judgement in POLICY.md and verification in Python, and never write the same rule in both;
  • keep generated images small and locally auditable;
  • maintain provider-free CI checks;
  • record decisions the code cannot explain in newsroom/docs/DECISIONS.md;
  • keep this README as the top-level map whenever routes, feeds, gates, or release policy change.

License

  • Code and workflow tooling: GNU AGPL-3.0
  • Content, images, media, and brand layer: CONTENT_LICENSE.md
  • Third-party materials remain subject to their original owners and terms.

About

Editorial-first AI-assisted news publishing pipeline for Turkish global news.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages