Semantic Systems / Language / Glyphs

Automatic AI-Driven Daily Brief with JSON, PHP, and JavaScript

Report summary

International Intelligence’s current Daily Brief implementation is already unusually close to a JSON-first publishing system. The site explicitly documents a read-only JSON API, a redacted status JSON, RSS feeds, ETag revalidation, public cache headers, and an OpenAPI 3.1 description of public endpo

Status
Research archive item
Category
Semantic Systems / Language / Glyphs
Length
5,060 words
Reading time
23 minutes
Report type
evaluation

Key topics

  • Semantic Systems / Language / Glyphs
  • Semantic Systems
  • Language
  • Glyphs
  • AI
  • Runtime
  • Privacy
  • Research Archive
  • Strategy

Research provenance

Archive status
Research archive item
Content identity
sha256:275ae28d2a4682bdd176070ba3edf80df72b2016f0fc3bf79707bf29b105e7aa

For citation, use the report title and canonical URL. Archival presence does not establish authorship or promote report statements into portfolio evidence.

Source availability: 83 citation markers in the source export have no recoverable source links. Those markers are omitted from this reader; any supplied bibliography and ordinary links remain. Check the original sources before relying on the cited claims.

This page renders the archived Markdown as safe, formatted HTML. It is background research and does not become a portfolio claim without evidence review.

Full report

On this page

Executive summary

International Intelligence’s current Daily Brief implementation is already unusually close to a JSON-first publishing system. The site explicitly documents a read-only JSON API, a redacted status JSON, RSS feeds, ETag revalidation, public cache headers, and an OpenAPI 3.1 description of public endpoints. It also states that the public interfaces are backed by local publication artifacts, require no API key, account, cookie, or database session, and intentionally exclude credentials, private configuration, internal filesystem paths, and private reviewer identifiers. That is an excellent starting point for a JSON-only architecture rather than a reason to introduce a database.

A crawl of the Daily Brief archive page, edition page, article page, status JSON, feed JSON, methodology, corrections policy, privacy policy, and security.txt shows that the current site already has a fairly mature editorial contract: immutable dated editions, explicit uncertainty labels, separated fact-vs-analysis sections, visible source trails, human review markers, additive corrections, bilingual publication, no third-party analytics, same-origin asset loading, and a CSP-oriented privacy posture. Those characteristics should be preserved directly in the proposed JSON schemas, directory layout, signing model, and deployment workflow.

The most important design choice is to separate immutable editorial artifacts from mutable discovery artifacts. In practice, that means storing each edition and article as versioned JSON files that never change in place after publication except through additive correction layers, while maintaining separate manifest, archive, feed, and status JSON files that can be regenerated and atomically swapped. This matches the site’s own methodology, which says dated publication objects are immutable and repairs should be additive, with the original record preserved and corrections made visible.

For PHP, the safest pattern is: resolve a path from a strictly validated identifier, read bytes from disk, syntactically validate JSON, decode once with throwing semantics, validate against JSON Schema 2020-12 using a proper validator, verify a detached Ed25519 signature via libsodium, compute ETag and Last-Modified from the artifact, and emit bounded application/problem+json errors on failure. Use flock() for advisory coordination and same-filesystem temp-file-plus-rename swaps for atomic publication. PHP’s manual documents json_validate(), json_decode(), json_encode(), advisory flock(), and detached signing/verification in libsodium, while the rename documentation includes the standard same-filesystem atomic replacement pattern.

For JavaScript, the right client architecture is progressive rather than SPA-only: server-render the first meaningful shell, fetch edition/article JSON, cache immutable JSON and assets in the Cache API, store structured version metadata and reading state in IndexedDB, and add a service worker for offline reading and stale-while-revalidate behavior. For live updates, prefer Server-Sent Events for one-way newsroom-to-reader updates, keep WebSocket optional for bi-directional editorial tooling, and fall back to conditional polling with ETag when real-time transports are unavailable. MDN documents the Cache API, CacheStorage, IndexedDB, service workers, EventSource, and WebSocket as the relevant browser primitives.

For AI generation, the site’s own public text is clear: OpenAI is used for daily selection and matched English/Spanish summaries, but linked publishers remain the citing authority, and methodology says AI may support structure, discovery, ranking, synthesis, drafting, and translation but may not fabricate evidence or silently upgrade provisional claims into settled history. The safest implementation is server-side orchestration using OpenAI Responses plus Structured Outputs for schema-constrained JSON generation, with function calling for retrieval/enrichment, prompt caching for repeated editorial instructions, human review gates for volatile stories, and permanent provenance fields recording model, prompt template version, input source hashes, and editorial acceptance.

If traffic is low, filesystem-only publishing is entirely practical. At medium traffic, precomputed manifests, ETag-based revalidation, service-worker caching, and CDN edge caching are usually sufficient. At high traffic, the design still works if you shard directories, publish immutable release directories, precompress JSON, serve through a CDN, and keep only a few small mutable manifests and status files hot. The limit is not “JSON files” as such; it is how carefully publication, indexing, and cache invalidation are engineered. Supported by HTTP caching, managed caches, and content-encoding standards, this model can scale much farther than people often assume.

Current Daily Brief implementation and extracted content model

The current Daily Brief archive page describes the product as “ten globally significant intelligence developments,” published as a “permanent bilingual journal” with source links, truth boundaries, and a full article page for every story. It exposes server date, running release, latest edition, missing-date controls, a “today” publishing status area, a latest-edition preview, and an archive grouped by month with search/filtering. The edition pages then expand those daily entries into ten development cards, while article pages add fact/analysis separation, timelines, quotations, source trails, review metadata, and related stories.

The site also already publishes machine-readable primitives: a normalized JSON feed, a redacted status JSON, RSS, and openapi.json. The feed is intentionally generic and currently much thinner than the full article experience. It is enough for a desk-style intelligence feed, but not enough on its own to reproduce a Daily Brief edition page or a full article page. That gap is the main reason to introduce richer edition/article JSON artifacts rather than to change underlying storage technology.

A subtle but important operational clue appeared during the crawl: the archive landing page snapshot showed 31 editions and latest edition July 25, while the status JSON later showed today_published: true, latest_date: 2026-07-26, and archive_count: 32. That kind of temporary mismatch is exactly what a filesystem design must prevent through atomic generation of related manifest/state files, or at least by ensuring that status and archive manifests roll forward together.

Extracted field inventory from the current site

SurfaceField or blockExample observed on siteWhy it matters in JSONSource evidence
Archive landingJournal identity“The Daily Brief”; “permanent bilingual journal”Top-level product metadata
Archive landingOperational statusserver date, running release, latest edition, missing dates, journal entriesSeparate status.json / control.json
Archive landingPublication controls“Check and generate now”, one-time backfill controlInternal admin-only JSON, never public by default
Archive landingEdition preview carddate, summary, story count, region count, source-link countedition.json header block
Archive landingArchive listing rowdate, two lead developments, story count, region count, source count, edition URLarchive-month.json/archive-index.json
Edition pageEdition title and dekDaily Brief · Jul 26, 2026 plus long summary paragraphedition.title, edition.summary
Edition pageEdition metadatagenerated time, model, editorial status, truth classprovenance, review state, currentness boundary
Edition pageStory card corerank, event date, region, confidence label, headlineentries[] essentials
Edition pageStory card summaryone or two body paragraphssummary.short, summary.extended
Edition pageCountries/placesIsrael · Palestinian territories · Uganda...tags, geotags, filters
Edition pageFocusDiplomacy · Conflict stabilization · Humanitarian accesstaxonomies / categories
Edition pageWhy it mattersanalysis paragraph under its own headingseparate analytical summary field
Edition pageReporting sourceslinked source listprovenance and source graph
Article pageReported facts blockdedicated facts sectionkeep fact/analysis separation
Article pageAnalysis blockdedicated analysis sectionseparate from reportable facts
Article pageEditor’s notereview statement and caution languageeditable correction/review note layer
Article pageVerified timelinedated sequence items with supporting linksstructured timeline array
Article pageAttributable wordingshort quotation cards tied to named sourcesquotation records with attribution
Article pageComparative perspectivesseven analytical lensesoptional derived asset, not primary article body
Article pageReporting boundarytruth class, human review time, data record, event datecurrentness, review, provenance
Article pageSource trailsource role, title, what it establishes, datefirst-class provenance objects
Article pageAt a glancerank, region, confidence, generated timesummary metadata for cards, feeds, search
Article pageRelated developmentsrelated entries from same editiongraph edges / backlinks
Public feedNormalized feed fieldsid, source, title, summary, url, published_at, topics, evidence_tier, confidenceuseful base envelope, but insufficient for full Daily Brief
Status JSONPublishing healthtoday_published, latest_date, missing_count, today_url, retries, error codeseparate machine status artifact

Content fields that are currently missing from the public feed but needed for a full JSON-only brief

The current feed and OpenAPI contract expose only a subset of what the article pages actually contain. They do not capture the complete article body split into reported facts and analysis, timeline arrays, attributable quotes, comparative lenses, human review timestamps, correction/change history, geotags as coordinates, embargo/expiry, detached signatures, per-field provenance, AI notes, or immutable release/version identifiers. Those richer fields must therefore live in dedicated edition/article schemas rather than be bolted onto the existing generic feed.

Existing public API clues that should be preserved

The current API documentation establishes patterns worth keeping: no authentication for public read-only endpoints, public caching with ETag on content feeds, no-store on volatile status endpoints, stable contract versioning, Problem Details style error payloads, preservation of original publication dates, the possibility of later metadata updates after review/correction, and explicit minimization of sensitive operational fields. Those are all sound design choices for a JSON-only brief system and should become part of the new contract, not be replaced.

The right schema foundation here is JSON Schema Draft 2020-12 for artifact validation and OpenAPI 3.1 for public API description. JSON Schema’s current version is 2020-12, and the specification is split into Core and Validation, with a self-descriptive meta-schema. OpenAPI 3.1 provides a standard interface description for HTTP APIs so humans and software can discover and understand service capabilities without inspecting source code. That pairing fits a filesystem-backed publishing system very well: JSON Schema validates artifacts at rest, while OpenAPI documents the routes that expose them.

The site’s own editorial rules strongly support a schema that distinguishes fact, analysis, attribution, source role, uncertainty, and correction state. Its methodology says claims must remain bounded by source role, date, confidence, and correction triggers; dated objects are immutable; reviews, source upgrades, aliases, and correction notes should be added with durable provenance; and AI assistance must not fabricate evidence or silently promote provisional reporting into history. That means the schema should not be a flat “article blob.” It should be a layered record with explicit provenance, review, and change structures.

VariantDescriptionAdvantagesDrawbacksBest fit
Minimal flat documentOne JSON file per article/edition with all fields denormalizedVery simple reads and static hostingDuplication across locale/tag/author/asset relationships; harder change historyLow traffic, simple newsroom
Normalized filesystem graphSeparate edition, article, author, tag, asset, status, archive JSON files linked by IDsClean reuse, lighter indexes, easier provenance reuseMore joins in PHP/JSMedium traffic, multi-locale
Immutable release bundleEach publish creates a full release directory with manifests plus immutable contentBest rollback story, consistent cross-file snapshots, CDN-friendlyMore disk usage; requires disciplined release toolingMedium/high traffic and strong audit needs
Hybrid recommendedImmutable release bundles for public state + normalized long-lived entities within each releaseStrong consistency, explicit rollback, manageable complexitySlightly more toolingBest overall choice

The recommended model is the hybrid: immutable releases plus normalized entities. That aligns with the site’s insistence on immutable dated objects and additive repairs, while still allowing queryable indexes and compact client payloads.

Core artifact set

A practical contract is:

ArtifactPurposeMutable or immutable
edition.jsonone day’s Daily Brief with ordered entries and edition-level metadataImmutable after release; later corrected by additive change records
article.jsonfull story pageImmutable after release; later corrected by additive change records
archive-month.jsonone month of edition summaries for archive pageMutable, regenerated
feed-latest.jsonrecent article or edition summariesMutable, regenerated
status.jsoncurrent publication healthMutable, regenerated
authors/*.jsonauthor entitiesMostly mutable
tags/*.jsontaxonomy metadataMutable
assets/*.jsonimages, documents, derived media descriptorsMostly immutable
signatures/*.sigdetached Ed25519 signatures for artifactsImmutable per version
changes/*.jsoncorrection/supersession/review recordsImmutable append-only
release.jsonmanifest for one atomic publish setImmutable

The site already uses language, confidence, evidence, source role, review, and correction boundaries in public text and API examples. The schema below extends those patterns to the richer article/edition level.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://internationalintelligence.org/schemas/daily-brief-edition.schema.json",
  "title": "Daily Brief Edition",
  "type": "object",
  "required": [
    "schema_version",
    "artifact_type",
    "id",
    "release_id",
    "locale",
    "edition_date",
    "published_at",
    "updated_at",
    "canonical_url",
    "slug",
    "title",
    "summary",
    "editorial",
    "entries",
    "provenance",
    "integrity"
  ],
  "properties": {
    "schema_version": { "type": "integer", "minimum": 1 },
    "artifact_type": { "const": "daily-brief-edition" },
    "id": { "type": "string" },
    "release_id": { "type": "string" },
    "locale": { "type": "string", "enum": ["en-US", "es-US"] },
    "edition_date": { "type": "string", "format": "date" },
    "published_at": { "type": "string", "format": "date-time" },
    "updated_at": { "type": "string", "format": "date-time" },
    "canonical_url": { "type": "string" },
    "slug": { "type": "string" },
    "title": { "type": "string" },
    "summary": {
      "type": "object",
      "required": ["dek", "short", "long"],
      "properties": {
        "dek": { "type": "string" },
        "short": { "type": "string" },
        "long": { "type": "string" }
      }
    },
    "editorial": {
      "type": "object",
      "required": ["status", "truth_class", "confidence_model", "human_review"],
      "properties": {
        "status": { "type": "string" },
        "truth_class": { "type": "string" },
        "confidence_model": { "type": "string" },
        "human_review": {
          "type": "object",
          "required": ["state"],
          "properties": {
            "state": { "type": "string", "enum": ["pending", "completed", "not-required"] },
            "reviewed_at": { "type": ["string", "null"], "format": "date-time" },
            "review_note": { "type": ["string", "null"] }
          }
        }
      }
    },
    "entries": {
      "type": "array",
      "minItems": 1,
      "maxItems": 10,
      "items": {
        "$ref": "daily-brief-article.schema.json"
      }
    },
    "provenance": {
      "type": "object",
      "required": ["generated_by", "source_bundle_hash", "source_count"],
      "properties": {
        "generated_by": { "type": "string" },
        "source_bundle_hash": { "type": "string" },
        "source_count": { "type": "integer", "minimum": 0 },
        "ai_notes_ref": { "type": ["string", "null"] }
      }
    },
    "integrity": {
      "type": "object",
      "required": ["sha256", "signature_ref", "public_key_id"],
      "properties": {
        "sha256": { "type": "string" },
        "signature_ref": { "type": "string" },
        "public_key_id": { "type": "string" }
      }
    }
  }
}
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://internationalintelligence.org/schemas/daily-brief-article.schema.json",
  "title": "Daily Brief Article",
  "type": "object",
  "required": [
    "schema_version",
    "artifact_type",
    "id",
    "edition_id",
    "rank",
    "locale",
    "event_date",
    "published_at",
    "updated_at",
    "slug",
    "canonical_url",
    "headline",
    "region",
    "confidence",
    "truth_class",
    "summary",
    "reported_facts",
    "analysis",
    "places",
    "focus",
    "sources",
    "provenance",
    "change_history",
    "ai_notes"
  ],
  "properties": {
    "schema_version": { "type": "integer", "minimum": 1 },
    "artifact_type": { "const": "daily-brief-article" },
    "id": { "type": "string" },
    "edition_id": { "type": "string" },
    "rank": { "type": "integer", "minimum": 1, "maximum": 10 },
    "locale": { "type": "string", "enum": ["en-US", "es-US"] },
    "event_date": { "type": "string", "format": "date" },
    "published_at": { "type": "string", "format": "date-time" },
    "updated_at": { "type": "string", "format": "date-time" },
    "slug": { "type": "string" },
    "canonical_url": { "type": "string" },
    "headline": { "type": "string" },
    "region": { "type": "string" },
    "confidence": { "type": "string" },
    "confidence_score": { "type": "number", "minimum": 0, "maximum": 1 },
    "truth_class": { "type": "string" },
    "summary": {
      "type": "object",
      "required": ["short", "deck"],
      "properties": {
        "short": { "type": "string" },
        "deck": { "type": "string" },
        "why_it_matters": { "type": "string" }
      }
    },
    "reported_facts": { "type": "array", "items": { "type": "string" } },
    "analysis": { "type": "array", "items": { "type": "string" } },
    "full_text": {
      "type": "object",
      "properties": {
        "html": { "type": ["string", "null"] },
        "markdown": { "type": ["string", "null"] },
        "blocks": { "type": "array" }
      }
    },
    "places": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": { "type": "string" },
          "country_code": { "type": ["string", "null"] },
          "lat": { "type": ["number", "null"] },
          "lon": { "type": ["number", "null"] }
        }
      }
    },
    "focus": { "type": "array", "items": { "type": "string" } },
    "sources": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["role", "publisher", "title", "url", "retrieved_at"],
        "properties": {
          "role": { "type": "string" },
          "publisher": { "type": "string" },
          "title": { "type": "string" },
          "url": { "type": "string" },
          "published_at": { "type": ["string", "null"], "format": "date-time" },
          "retrieved_at": { "type": "string", "format": "date-time" },
          "evidence_tier": { "type": ["integer", "null"], "minimum": 1, "maximum": 8 },
          "claim_scope": { "type": ["string", "null"] }
        }
      }
    },
    "embargo": {
      "type": ["object", "null"],
      "properties": {
        "until": { "type": ["string", "null"], "format": "date-time" },
        "status": { "type": ["string", "null"] }
      }
    },
    "expiry": {
      "type": ["object", "null"],
      "properties": {
        "review_after": { "type": ["string", "null"], "format": "date-time" },
        "stale_after": { "type": ["string", "null"], "format": "date-time" }
      }
    },
    "provenance": { "type": "object" },
    "change_history": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["version", "changed_at", "change_type", "summary"],
        "properties": {
          "version": { "type": "string" },
          "changed_at": { "type": "string", "format": "date-time" },
          "change_type": { "type": "string" },
          "summary": { "type": "string" },
          "patch_ref": { "type": ["string", "null"] }
        }
      }
    },
    "ai_notes": {
      "type": "object",
      "required": ["assisted", "model_family", "template_version"],
      "properties": {
        "assisted": { "type": "boolean" },
        "model_family": { "type": "string" },
        "response_id": { "type": ["string", "null"] },
        "template_version": { "type": "string" },
        "tool_calls": { "type": "array" },
        "draft_quality_flags": { "type": "array", "items": { "type": "string" } }
      }
    }
  }
}

Example JSON artifact

This example mirrors the site’s observed structure but adds integrity, provenance, and change history fields that the current article page exposes only through HTML. The visible sections themselves—headline, region, confidence, facts, analysis, source trail, human review, and related items—are directly grounded in the crawled pages.

{
  "schema_version": 1,
  "artifact_type": "daily-brief-article",
  "id": "db-2026-07-25-01-en-US",
  "edition_id": "db-2026-07-25-en-US",
  "rank": 1,
  "locale": "en-US",
  "event_date": "2026-07-24",
  "published_at": "2026-07-25T15:30:00-05:00",
  "updated_at": "2026-07-26T09:00:00-05:00",
  "slug": "belgian-prosecutor-says-shape-intern-was-placed-under-arrest-warrant-in-espionage-case",
  "canonical_url": "/en-US/news/2026-07-25/01-belgian-prosecutor-says-shape-intern-was-placed-under-arrest-warrant-in-espionage-case/",
  "headline": "Belgian prosecutor says SHAPE intern was placed under arrest warrant in espionage case",
  "region": "Europe",
  "confidence": "Corroborated",
  "confidence_score": 0.86,
  "truth_class": "Current / ongoing",
  "summary": {
    "short": "Belgian prosecutors said a Canadian SHAPE intern was placed under arrest warrant on espionage allegations.",
    "deck": "The case raises questions about vetting and insider-threat detection while the beneficiary state remains unidentified.",
    "why_it_matters": "SHAPE is NATO’s strategic military headquarters and public records do not establish operational compromise."
  },
  "reported_facts": [
    "Belgium’s Federal Public Prosecutor’s Office said a Canadian national interning at SHAPE was placed under arrest warrant on July 24, 2026.",
    "The prosecutor said searches took place on July 23 and the suspect is alleged to have engaged in espionage on behalf of an unidentified third country."
  ],
  "analysis": [
    "Public reporting raises questions about personnel vetting and insider-threat detection at SHAPE.",
    "Public records do not identify the alleged beneficiary state or establish that operational data was compromised."
  ],
  "places": [
    { "name": "Belgium", "country_code": "BE", "lat": 50.8503, "lon": 4.3517 },
    { "name": "Canada", "country_code": "CA", "lat": null, "lon": null }
  ],
  "focus": ["Espionage & counterintelligence", "Alliance security"],
  "sources": [
    {
      "role": "Primary official record",
      "publisher": "Belgium Federal Public Prosecutor’s Office",
      "title": "Espionage on behalf of a third country: suspect placed under arrest warrant after operation at SHAPE in Mons",
      "url": "https://www.om-mp.be/...",
      "published_at": "2026-07-25T00:00:00Z",
      "retrieved_at": "2026-07-25T15:10:00-05:00",
      "evidence_tier": 2,
      "claim_scope": "arrest-warrant date, searches, allegations, ongoing-investigation boundary"
    }
  ],
  "provenance": {
    "generated_by": "editorial-source-linked-release-seed",
    "human_review": {
      "state": "completed",
      "reviewed_at": "2026-07-26T09:00:00-05:00"
    },
    "source_bundle_hash": "sha256:abc123...",
    "language_source": "en-US"
  },
  "change_history": [
    {
      "version": "1.1.0",
      "changed_at": "2026-07-26T09:00:00-05:00",
      "change_type": "analytical clarification",
      "summary": "Separated reported facts from analysis and added human review metadata.",
      "patch_ref": "/data/changes/2026/07/26/db-2026-07-25-01-en-US.patch.json"
    }
  ],
  "ai_notes": {
    "assisted": true,
    "model_family": "gpt-5.6",
    "response_id": "resp_...",
    "template_version": "daily-brief-article-v3",
    "tool_calls": ["web_search", "source_bundle_validator"],
    "draft_quality_flags": ["ongoing-investigation", "legal-status-sensitive"]
  },
  "integrity": {
    "sha256": "sha256:...",
    "signature_ref": "/data/releases/2026-07-26T170000-0500/signatures/db-2026-07-25-01-en-US.sig",
    "public_key_id": "ed25519-prod-2026-q3"
  }
}

System architecture

flowchart LR
    A[Editorial inputs and crawled sources] --> B[Normalization pipeline]
    B --> C[AI drafting with Structured Outputs]
    C --> D[Human review and correction gate]
    D --> E[Immutable JSON artifacts]
    E --> F[Release manifest]
    F --> G[PHP read-only API]
    F --> H[HTML server render]
    G --> I[JS client]
    H --> I
    E --> J[RSS and archive indexes]
    E --> K[Detached signatures]

Filesystem layout, atomic publication, provenance, and rollback

A flat-file architecture only becomes reliable when the filesystem layout itself expresses editorial truth. The site’s methodology and corrections policy already require that dated objects be preserved, that repairs be visible and traceable, that original publication dates remain unchanged, and that headline changes preserve redirects from old URLs. The filesystem should therefore be organized around immutable release bundles and append-only change records, not around files that are edited in place without history.

/data
  /schemas
    daily-brief-edition.schema.json
    daily-brief-article.schema.json
    author.schema.json
    asset.schema.json
    status.schema.json
  /releases
    /2026-07-26T170000-0500
      release.json
      /en-US
        /editions/2026/07/26/edition.json
        /articles/2026/07/26/01-belgian-...json
        /archive/2026/07.json
        /feeds/latest.json
        /status/status.json
      /es-US
        ...
      /authors/...
      /tags/...
      /assets/images/...
      /changes/2026/07/26/...
      /signatures/...
  /current
    en-US-release.json
    es-US-release.json
  /keys
    public-keys.json
  /staging
    /txn-<uuid>/
  /logs
    publication-audit.jsonl
/public
  /api
    /en-US/...
    /es-US/...
  /assets

Naming conventions

ObjectRecommended nameRule
Release directoryYYYY-MM-DDThhmmss±hhmmsortable, timezone-explicit
Edition fileedition.json inside dated directoryone canonical file per date/locale
Article fileNN-slug.jsonrank prefix preserves edition order
Signature filesame basename + .sigdetached signature per artifact
Patch/change filesame basename + .patch.json or .change.jsonappend-only change objects
Manifest pointeren-US-release.jsontiny mutable file that names active release
IDsdb-YYYY-MM-DD-NN-localestable across API, feed, and client cache
SlugsASCII lower-kebab-caseURL-safe, deterministic

Atomic update pattern using only filesystem operations

On Unix, the standard atomic replacement pattern is: write new content to a temp file on the same filesystem and then rename() over the old file; the PHP manual’s rename notes describe this explicitly and stress the same-filesystem requirement. PHP also documents flock() as portable advisory locking across major platforms. This makes a two-phase publish feasible without any database: lock publication, build a complete staged release directory, sign and validate artifacts, then atomically flip a tiny current-release manifest or symlink. Readers either see the previous release or the next one, but never a half-written mix.

A robust publish transaction looks like this:

flowchart TD
    A[Acquire publish lock] --> B[Create staging txn directory]
    B --> C[Generate artifacts]
    C --> D[Validate JSON syntax]
    D --> E[Validate JSON Schema]
    E --> F[Verify signatures or sign new artifacts]
    F --> G[Build archive/feed/status manifests]
    G --> H[Write release.json]
    H --> I[Atomic rename staging to /releases/<release_id>]
    I --> J[Atomic swap /current/<locale>-release.json]
    J --> K[Append audit log]
    K --> L[Release lock]

Rollback strategy

Rollback is trivial if current/<locale>-release.json is the only mutable pointer. To roll back, publish the previous release ID back into that small pointer file with the same temp-file-plus-rename procedure. Because the release bundles themselves are immutable, rollback is O(1) metadata change, not bulk copy. That satisfies the site’s preference for preserved original records and visible repair layers.

Provenance and audit records

The site’s methodology requires preservation of canonical URL, source identifier, publication time, retrieval time, and transformations applied; its corrections page requires visible, traceable repairs; and the API docs emphasize preserving source names, evidence tiers, confidence labels, and publication dates. Therefore every artifact should carry:

Provenance fieldWhy
source_url, source_id, publisher, retrieved_atreproduce source trail
source_hashlock source bundle to what AI/editor actually saw
prompt_template_version, model_family, response_idaudit AI output lineage
human_review.state, reviewed_at, reviewer_rolemake review visible
change_history[] with type and summarysatisfy correction policy
integrity.sha256, signature_ref, public_key_iddetect tampering
release_idtie artifact to atomic publish set

Traffic-level options

Assumed trafficFilesystem strategyWhat to optimize first
LowDirect reads from current release; simple ETag; nightly/monthly archive rebuildcorrectness, schema validation, signatures
MediumImmutable releases + prebuilt manifests + CDN cache + service workerarchive/index generation, client cache revalidation
HighSharded directories, precompressed JSON, CDN on immutable release paths, small mutable pointers onlydirectory fan-out, edge caching, publish fan-out, SSE load distribution

Managed caches such as browsers, CDNs, reverse proxies, and service workers are a normal way to offload origin work, and ETag plus Cache-Control are the standard knobs for that. Compression via Content-Encoding is also standard for JSON payloads.

PHP publication layer

The PHP side should be boring, strict, and defensive. The current site already documents a read-only public contract, ETag support, bounded errors, and public-field minimization. Your implementation should extend that approach consistently to the richer edition/article artifacts rather than introducing dynamic complexity.

PHP routines to implement

At minimum, implement these server-side routines:

RoutineResponsibility
resolveArtifactPath()strict ID/date/locale to canonical file path resolution
readArtifactBytes()read bytes, reject missing/non-regular files
decodeArtifact()decode JSON with throwing flags and UTF-8 safety
validateArtifactSchema()Draft 2020-12 validation
verifyArtifactSignature()detached Ed25519 verification
buildHttpValidators()ETag, Last-Modified, content hash
serveJsonSuccess()content headers, caching, conditional response
serveProblem()bounded application/problem+json error output
publishRelease()staging, validation, signature, manifest swap
streamUpdatesSse()optional SSE feed for live update events

Schema validation and signature verification

PHP documents json_validate() for syntax-only checks, but also warns that calling it immediately before json_decode() reparses the same string twice unnecessarily if the decoded payload will be used. In this workflow, the right sequence is usually decode once with JSON_THROW_ON_ERROR, then validate the decoded structure against JSON Schema using a real validator. For Draft 2020-12 support in PHP, a library such as Opis JSON Schema is a practical choice; its own documentation states support for Draft 2020-12. Detached signing and verification are directly available via PHP’s sodium bindings for Ed25519 signatures.

Sample PHP read/validate/verify/serve pattern

<?php
declare(strict_types=1);

use Opis\JsonSchema\Validator;
use Opis\JsonSchema\Errors\ErrorFormatter;

final class ArtifactServer
{
    public function __construct(
        private string $dataRoot,
        private string $currentRoot,
        private Validator $validator,
        private array $publicKeysById
    ) {}

    public function serveArticle(string $locale, string $date, string $slug): never
    {
        try {
            $this->assertLocale($locale);
            $this->assertDate($date);
            $this->assertSlug($slug);

            $release = $this->loadCurrentRelease($locale);
            $path = $this->buildArticlePath($release, $locale, $date, $slug);

            $real = realpath($path);
            if ($real === false || !is_file($real)) {
                $this->problem(404, 'Artifact not found', 'No article exists for that identifier.');
            }

            $bytes = file_get_contents($real);
            if ($bytes === false) {
                throw new RuntimeException('Could not read artifact bytes.');
            }

            // Decode once; this validates syntax implicitly.
            $doc = json_decode(
                $bytes,
                true,
                512,
                JSON_THROW_ON_ERROR
                | JSON_BIGINT_AS_STRING
                | JSON_INVALID_UTF8_SUBSTITUTE
            );

            $schemaPath = $this->dataRoot . '/schemas/daily-brief-article.schema.json';
            $schema = json_decode(
                (string) file_get_contents($schemaPath),
                false,
                512,
                JSON_THROW_ON_ERROR
            );

            $result = $this->validator->validate((object) $doc, $schema);
            if (!$result->isValid()) {
                $formatter = new ErrorFormatter();
                error_log('Schema validation failed: ' . json_encode($formatter->format($result->error()), JSON_UNESCAPED_SLASHES));
                $this->problem(500, 'Artifact validation failed', 'The article payload did not match the public contract.');
            }

            $this->verifySignature($real, $bytes, $doc);

            $etag = '"' . hash('sha256', $bytes) . '"';
            $lastModified = gmdate('D, d M Y H:i:s', (int) filemtime($real)) . ' GMT';

            header('Content-Type: application/json; charset=utf-8');
            header('X-Content-Type-Options: nosniff');
            header('Referrer-Policy: no-referrer');
            header("ETag: {$etag}");
            header("Last-Modified: {$lastModified}");
            header('Cache-Control: public, max-age=300, stale-while-revalidate=60');

            if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) === $etag) {
                http_response_code(304);
                exit;
            }

            echo $bytes;
            exit;
        } catch (JsonException $e) {
            error_log('JSON failure: ' . $e->getMessage());
            $this->problem(500, 'JSON decode failure', 'The server could not decode the artifact.');
        } catch (Throwable $e) {
            error_log('Unhandled artifact error: ' . $e->getMessage());
            $this->problem(500, 'Server error', 'The artifact could not be served.');
        }
    }

    private function verifySignature(string $artifactPath, string $bytes, array $doc): void
    {
        $integrity = $doc['integrity'] ?? null;
        if (!is_array($integrity)) {
            $this->problem(500, 'Missing integrity metadata', 'The artifact integrity block is absent.');
        }

        $publicKeyId = $integrity['public_key_id'] ?? '';
        $sigRef = $integrity['signature_ref'] ?? '';

        if (!is_string($publicKeyId) || !isset($this->publicKeysById[$publicKeyId])) {
            $this->problem(500, 'Unknown signing key', 'The artifact references an unknown public key.');
        }

        $sigPath = $this->safeJoin(dirname($artifactPath), basename($sigRef));
        $sigB64 = file_get_contents($sigPath);
        if ($sigB64 === false) {
            $this->problem(500, 'Missing signature', 'The artifact signature could not be loaded.');
        }

        $signature = base64_decode(trim($sigB64), true);
        $publicKey = base64_decode($this->publicKeysById[$publicKeyId], true);

        if ($signature === false || $publicKey === false) {
            $this->problem(500, 'Invalid key material', 'The signature or key could not be decoded.');
        }

        if (!sodium_crypto_sign_verify_detached($signature, $bytes, $publicKey)) {
            $this->problem(500, 'Signature verification failed', 'The artifact signature did not verify.');
        }
    }

    private function loadCurrentRelease(string $locale): string
    {
        $pointer = $this->currentRoot . '/' . $locale . '-release.json';
        $doc = json_decode((string) file_get_contents($pointer), true, 512, JSON_THROW_ON_ERROR);
        return $doc['release_id'] ?? throw new RuntimeException('No active release');
    }

    private function buildArticlePath(string $release, string $locale, string $date, string $slug): string
    {
        [$y, $m, $d] = explode('-', $date);
        return "{$this->dataRoot}/releases/{$release}/{$locale}/articles/{$y}/{$m}/{$d}/{$slug}.json";
    }

    private function safeJoin(string $base, string $name): string
    {
        return rtrim($base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($name, DIRECTORY_SEPARATOR);
    }

    private function assertLocale(string $locale): void
    {
        if (!preg_match('/^(en-US|es-US)$/', $locale)) {
            $this->problem(400, 'Invalid locale', 'Locale must be en-US or es-US.');
        }
    }

    private function assertDate(string $date): void
    {
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            $this->problem(400, 'Invalid date', 'Date must be YYYY-MM-DD.');
        }
    }

    private function assertSlug(string $slug): void
    {
        if (!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $slug)) {
            $this->problem(400, 'Invalid slug', 'Slug format is invalid.');
        }
    }

    private function problem(int $status, string $title, string $detail): never
    {
        http_response_code($status);
        header('Content-Type: application/problem+json; charset=utf-8');
        header('Cache-Control: no-store, no-cache, must-revalidate');
        header('X-Content-Type-Options: nosniff');

        echo json_encode([
            'type' => 'about:blank',
            'title' => $title,
            'status' => $status,
            'detail' => $detail,
            'instance' => $_SERVER['REQUEST_URI'] ?? ''
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        exit;
    }
}

The error format above mirrors the site’s own OpenAPI and API documentation, which describe Problem Details fields type, title, status, detail, and instance for JSON errors.

Sample atomic publish routine

<?php
declare(strict_types=1);

function publishRelease(string $stagingDir, string $releaseDir, string $currentPointerPath, string $releaseId): void
{
    $lockPath = dirname($currentPointerPath) . '/publish.lock';
    $lock = fopen($lockPath, 'c+');
    if (!$lock) {
        throw new RuntimeException('Cannot open publish lock.');
    }

    try {
        if (!flock($lock, LOCK_EX)) {
            throw new RuntimeException('Cannot acquire publish lock.');
        }

        // Validate the staged release before promotion...
        if (!is_dir($stagingDir)) {
            throw new RuntimeException('Staging directory missing.');
        }

        // Promote immutable release directory.
        if (!rename($stagingDir, $releaseDir)) {
            throw new RuntimeException('Release promotion failed.');
        }

        // Atomically flip active release pointer.
        $tmp = $currentPointerPath . '.tmp.' . bin2hex(random_bytes(6));
        $pointerJson = json_encode([
            'release_id' => $releaseId,
            'switched_at' => date(DATE_ATOM)
        ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

        if (file_put_contents($tmp, $pointerJson, LOCK_EX) === false) {
            throw new RuntimeException('Writing current pointer failed.');
        }

        if (!rename($tmp, $currentPointerPath)) {
            @unlink($tmp);
            throw new RuntimeException('Atomic pointer swap failed.');
        }
    } finally {
        flock($lock, LOCK_UN);
        fclose($lock);
    }
}

This pattern follows PHP’s documented advisory locking via flock() and same-filesystem atomic replacement via rename(). file_put_contents(..., LOCK_EX) is documented as performing an flock() between open and write.

SSE endpoint pattern for live updates

Because EventSource uses a persistent HTTP connection that carries text/event-stream data, PHP can provide a simple real-time channel without requiring a database or a WebSocket broker.

<?php
declare(strict_types=1);

header('Content-Type: text/event-stream; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('X-Accel-Buffering: no');

$locale = ($_GET['locale'] ?? 'en-US');
$lastSeen = $_SERVER['HTTP_LAST_EVENT_ID'] ?? null;

while (!connection_aborted()) {
    $statusPath = __DIR__ . "/../data/current/{$locale}-release.json";
    $status = json_decode((string) file_get_contents($statusPath), true, 512, JSON_THROW_ON_ERROR);

    $eventId = $status['release_id'];

    if ($eventId !== $lastSeen) {
        echo "id: " . $eventId . "\n";
        echo "event: release\n";
        echo "data: " . json_encode($status, JSON_UNESCAPED_SLASHES) . "\n\n";
        @ob_flush();
        @flush();
        $lastSeen = $eventId;
    }

    sleep(15);
}

Security and performance checklist for PHP

AreaRecommendationWhySupport
Path safetynever accept arbitrary file paths; derive from validated identifiers onlyprevents traversal and direct artifact tamperingOWASP recommends strict input validation and rejecting unexpected content.
JSON servinguse Content-Type: application/json and X-Content-Type-Options: nosniffavoids MIME confusion/sniffing
Cachingemit stable ETag, honor If-None-Match, set route-specific Cache-Controlreduces bandwidth and aligns with current site behavior
Error responsesbounded application/problem+json, no stack traces or pathsmatches current public contract
External linksuse Referrer-Policy, and preserve no-referrer behavior for source linksmatches privacy posture
XSSescape server-rendered text and do not trust stored HTMLOWASP recommends layered XSS defenses and context-sensitive output encoding
Privacykeep same-origin assets and do not expose internal paths, keys, provider bodiesmatches current site policy

JavaScript client architecture

The current site’s privacy model is especially friendly to a progressive web architecture: no analytics, no third-party assets or trackers, URL-based language selection instead of tracking cookies, no automatic browser contact with source publishers, same-origin public assets, and a CSP that blocks outside services by default. A Daily Brief client should preserve that model by keeping publication JSON same-origin, using a service worker and local browser storage for resilience, and avoiding client-side calls to third-party source sites.

The browser stack should use three layers of storage and rendering:

LayerResponsibilityBrowser primitive
HTTP cache / CDN cachevalidator-aware network cachingETag, Cache-Control, conditional requests
Cache APIimmutable JSON and shell/assets for offline readCache, CacheStorage
IndexedDBmanifests, reading state, diff history, saved versionsIndexedDB

The Cache API stores Request/Response pairs persistently, IndexedDB is designed for significant amounts of structured client-side data, and service workers act as a proxy between app and network to enable effective offline behavior.

Caching strategy comparison

StrategyHow it worksStrengthsWeaknessesRecommended use
Browser HTTP cache onlyrely on ETag / Cache-Controlsimplestpoor offline UX controlbaseline for all routes
Cache API onlystore responses in named cachesgood offline support for immutable artifactsno structured query / metadata layeredition JSON + shell assets
IndexedDB onlystore parsed JSON docs and metadatagreat structured accessextra serialization workmanifests, version graph, saved reading state
Service worker + Cache API + IndexedDBoffline-first shell plus structured local statestrongest UX and controlmost coderecommended default
Polling onlyfetch() on intervalworks everywhereinefficient/live lagfallback only
SSE + polling fallbackpersistent one-way updates plus fallbackcheap real-timeserver must hold connectionsrecommended for reader updates
WebSockettwo-way channelgreat for editorial collaborationmore server complexity than SSEoptional admin/editor tooling

The site already recommends respecting Cache-Control and ETag instead of polling continuously, so the client should be built around revalidation rather than blind refetches.

Client data flow

flowchart LR
    A[Initial HTML shell] --> B[Fetch current release manifest]
    B --> C[Fetch edition/article JSON]
    C --> D[Render semantic HTML]
    C --> E[Cache API store]
    C --> F[IndexedDB metadata store]
    G[Service Worker] --> E
    H[SSE or polling update event] --> B
    F --> I[Diff/merge UI]
    E --> J[Offline reading]

Sample JavaScript bootstrap with ETag-aware fetch and IndexedDB

const DB_NAME = "daily-brief";
const DB_VERSION = 1;

function openDb() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains("manifests")) {
        db.createObjectStore("manifests", { keyPath: "key" });
      }
      if (!db.objectStoreNames.contains("documents")) {
        db.createObjectStore("documents", { keyPath: "id" });
      }
      if (!db.objectStoreNames.contains("versions")) {
        db.createObjectStore("versions", { keyPath: "versionKey" });
      }
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

async function getManifest(locale = "en-US") {
  const db = await openDb();
  const cached = await readStore(db, "manifests", `current:${locale}`);

  const headers = { Accept: "application/json" };
  if (cached?.etag) headers["If-None-Match"] = cached.etag;

  const response = await fetch(`/api/${locale}/current-release.json`, {
    headers,
    cache: "no-cache"
  });

  if (response.status === 304 && cached?.value) {
    return cached.value;
  }

  if (!response.ok) throw new Error(`HTTP ${response.status}`);

  const value = await response.json();
  const etag = response.headers.get("ETag");
  await writeStore(db, "manifests", {
    key: `current:${locale}`,
    etag,
    value,
    fetchedAt: Date.now()
  });

  return value;
}

async function getJsonDocument(url, documentId) {
  const cache = await caches.open("daily-brief-v1");
  const cached = await cache.match(url);
  if (cached) {
    // Return cached immediately; caller can revalidate separately.
    const body = await cached.clone().json();
    revalidateIntoCache(url).catch(console.error);
    return body;
  }

  const response = await fetch(url, {
    headers: { Accept: "application/json" },
    cache: "no-cache"
  });

  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  await cache.put(url, response.clone());

  const body = await response.json();

  const db = await openDb();
  await writeStore(db, "documents", {
    id: documentId,
    url,
    doc: body,
    updatedAt: Date.now()
  });

  return body;
}

async function revalidateIntoCache(url) {
  const cache = await caches.open("daily-brief-v1");
  const cached = await cache.match(url);
  const headers = { Accept: "application/json" };

  if (cached) {
    const etag = cached.headers.get("ETag");
    if (etag) headers["If-None-Match"] = etag;
  }

  const response = await fetch(url, { headers, cache: "no-cache" });
  if (response.status === 304) return;
  if (response.ok) await cache.put(url, response);
}

function readStore(db, storeName, key) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(storeName, "readonly");
    const req = tx.objectStore(storeName).get(key);
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

function writeStore(db, storeName, value) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(storeName, "readwrite");
    tx.objectStore(storeName).put(value);
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

Service worker pattern for offline reading

// sw.js
const SHELL_CACHE = "db-shell-v1";
const DOC_CACHE = "db-docs-v1";

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(SHELL_CACHE).then((cache) => cache.addAll([
      "/",
      "/assets/app.css",
      "/assets/app.js",
      "/offline.html"
    ]))
  );
});

self.addEventListener("fetch", (event) => {
  const url = new URL(event.request.url);

  // JSON docs: stale-while-revalidate
  if (url.pathname.startsWith("/api/") && url.pathname.endsWith(".json")) {
    event.respondWith((async () => {
      const cache = await caches.open(DOC_CACHE);
      const cached = await cache.match(event.request);
      const networkPromise = fetch(event.request).then((res) => {
        if (res.ok) cache.put(event.request, res.clone());
        return res;
      }).catch(() => null);

      return cached || networkPromise || caches.match("/offline.html");
    })());
    return;
  }

  // Shell/assets: cache first
  event.respondWith((async () => {
    const cached = await caches.match(event.request);
    return cached || fetch(event.request);
  })());
});

Service workers are specifically intended to enable effective offline experiences and to intercept network requests so apps can use cached assets first, then refresh from the network.

Live updates with SSE, WebSocket fallback, and polling fallback

EventSource opens a persistent HTTP connection for server-sent events, making it a good fit for “new edition published,” “correction posted,” or “archive rebuilt” notifications. WebSocket is a full two-way channel and is better reserved for collaborative editorial tooling or future admin consoles.

function connectLiveUpdates(locale = "en-US") {
  if ("EventSource" in window) {
    const es = new EventSource(`/api/live/briefs?locale=${encodeURIComponent(locale)}`);
    es.addEventListener("release", async (event) => {
      const payload = JSON.parse(event.data);
      console.log("New release detected", payload);
      await refreshCurrentEdition(locale);
    });
    es.onerror = () => {
      es.close();
      startPolling(locale);
    };
    return;
  }

  if ("WebSocket" in window) {
    const ws = new WebSocket(`wss://${location.host}/ws/briefs?locale=${encodeURIComponent(locale)}`);
    ws.onmessage = async (event) => {
      const payload = JSON.parse(event.data);
      if (payload.type === "release") await refreshCurrentEdition(locale);
    };
    ws.onerror = () => startPolling(locale);
    return;
  }

  startPolling(locale);
}

function startPolling(locale) {
  setInterval(() => refreshCurrentEdition(locale).catch(console.error), 60_000);
}

Diff and merge UI for updates

For a reader-facing “what changed?” UI, store prior versions of article JSON and compute either:

  • JSON Patch when you want precise operation lists like add, remove, replace, move, copy, and test, or
  • JSON Merge Patch when you want human-readable object-oriented patch previews.

JSON Patch is defined in RFC 6902 as a sequence of operations on a JSON document; JSON Merge Patch is defined in RFC 7386 as a patch document that resembles the target document and uses null to indicate removals. In practice, JSON Patch is better for machine-accurate change tracking, while Merge Patch is better for “editor note / updated fields” views.

// Version graph idea:
// versions store: { versionKey, articleId, releaseId, savedAt, doc }

function showArticleDelta(previousDoc, currentDoc) {
  // Use your preferred diff lib or server-precomputed patch.
  // Recommended:
  // - store RFC 6902 patch for exact technical diff
  // - store a human summary in change_history[].summary
  const previous = JSON.stringify(previousDoc, null, 2);
  const current = JSON.stringify(currentDoc, null, 2);

  document.querySelector("#diff-old").textContent = previous;
  document.querySelector("#diff-new").textContent = current;
}

On the security side, render trusted text with textContent, not innerHTML, unless you have a carefully sanitized allowlist pipeline. OWASP’s XSS guidance is explicit that no single technique solves XSS, and CSP is a defense-in-depth layer rather than a substitute for correct output handling.

AI workflows, automation, migration, and trade-offs

The site already discloses the institutional rule that matters most for AI integration: OpenAI is used to generate daily selections and matched English/Spanish summaries, but it is “never the citing authority”; methodology adds that AI may assist structure, source discovery, ranking, synthesis, drafting, and paired translation, yet may not fabricate evidence, replace primary-source verification, assign guilt, or silently turn provisional reporting into history. That is the correct governance frame for the new system as well.

Use server-side AI for authoritative publication tasks. OpenAI’s Responses API is designed for stateful model interactions and tool use; Structured Outputs are specifically intended to make model responses adhere to a supplied JSON Schema; function calling is intended for the pattern where the model asks your application to perform retrieval or system actions; and prompt caching reduces latency/cost when long instruction prefixes repeat. Those capabilities map directly onto a newsroom pipeline that emits JSON artifacts.

Client-side AI can still have a role, but only for non-authoritative tasks such as reader-side clustering, optional translation preview, or offline personal notes. It should not be trusted for the public archive, because the public archive must preserve source-bounded facts, human review state, and stable provenance in a controlled server environment. That recommendation follows naturally from the site’s methodology and privacy posture.

AI pipeline stages

StageInputsModel/tool behaviorOutput
Source acquisitioncrawled URLs, editorial notes, official recordsno model or model only for triagenormalized source bundle
Source bundlingcanonical URLs, timestamps, hashes, source rolesserver function calls may enrich metadataimmutable bundle JSON
Draft generationsource bundle + schema + promptResponses API + Structured Outputscandidate edition.json / article.json
Entity extractiondraft + sourcesstructured extraction promptpeople/org/place/entity arrays
Confidence scoringevidence counts + source roles + editorial ruleshybrid system rule + model rationalelabel + numeric score + explanation
Translationaccepted source-bounded English articlepaired translation prompt preserving attribution/uncertaintylocalized artifact
Human reviewcandidate artifact + diff vs previouseditor approves, edits, or rejectssigned release-ready artifact

Prompt template pattern

Because Structured Outputs are specifically intended to enforce JSON Schema adherence, the prompt should be concise about editorial rules and explicit about what the model must not do.

SYSTEM
You are generating a Daily Brief article record for public publication.
You must follow the provided JSON Schema exactly.
Never invent a source, quote, date, location, or legal status.
Keep reported facts separate from analysis.
Retain uncertainty and attribution.
Do not promote provisional or alleged claims into settled fact.
If evidence is insufficient, leave optional fields null and add a quality flag.

DEVELOPER / POLICY
Editorial rules:
- publishers and official records remain the citing authority
- preserve source role, evidence tier, confidence label, event date, retrieval date
- current reporting is not settled history
- translation must preserve actor, time, causality, and quoted status
- output locale: en-US

USER INPUT
Bundle:
- source documents with canonical URLs, retrieval timestamps, publisher roles, hashes
- target schema: daily-brief-article.schema.json
- ranking slot: 01
- edition date: YYYY-MM-DD
- locale: en-US

RETURN
Strict JSON only.

Confidence scoring model

The site’s methodology distinguishes evidence tiers from confidence labels; confidence is about the current state of the finding, not the prestige of the source. Therefore confidence should be computed as a hybrid:

  • deterministic rules from source roles, source count, contradiction presence, and recency, plus
  • a model-generated rationale constrained into a small structured object, plus
  • human override for high-risk stories.

That is much better than asking a model for a naked probability. It also preserves the site’s public label vocabulary such as Confirmed, Corroborated, Likely, Disputed, Stale, Unknown, and Unverified.

Automation workflows

Cron remains a good scheduler for pull-based workflows; crontab defines scheduled commands for the cron daemon and cron evaluates jobs each minute. Git hooks can trigger scripts at defined points in Git execution, and GitHub Actions is explicitly a CI/CD platform for automated build, test, and deployment workflows defined in YAML. Git worktrees are useful for previewing or validating multiple release branches in parallel without cloning full repositories repeatedly.

WorkflowTriggerBest forStrengthsRisks
Cron-only on origin servertime-basedsmall, self-hosted deploymentminimal dependenciesweaker audit trail unless you log aggressively
Git commit driveneditor commit/pusheditorial-first workflownatural version history and rollbackextra discipline needed for generated artifacts
Webhook triggeredexternal editorial CMS/form submitintegrated newsroom toolingnear-real-time generationretry/idempotency complexity
CI pipelinepush, PR, schedule, manual dispatchmedium/high maturityvalidation, tests, signatures, deploy gatesmore moving parts
Hybrid recommendedcron for ingestion + Git/CI for promotionmost production casesseparates source refresh from public publishrequires clear ownership

Example update pipeline

flowchart TD
    A[cron or webhook trigger] --> B[Fetch or receive editorial inputs]
    B --> C[Normalize and hash source bundle]
    C --> D[AI draft with Structured Outputs]
    D --> E[Schema validation]
    E --> F[Editorial review]
    F --> G[Generate locale variants]
    G --> H[Sign artifacts]
    H --> I[Build release manifests and indexes]
    I --> J[Run CI tests]
    J --> K[Git commit and tag optional]
    K --> L[Deploy release directory]
    L --> M[Atomic current-release swap]
    M --> N[Emit SSE release event]

Validation, testing, and deployment steps

A production-grade filesystem pipeline should test at least these invariants before public release:

Test classExample checks
Schema testsevery artifact validates against 2020-12 schema
Integrity testsdetached signature verifies; hash matches
URL testscanonical URLs stable; changed headlines get redirects
Archive consistency testsarchive row counts equal generated edition count
Locale parity testsen-US and es-US contain equivalent IDs and source bundles
Currentness testsstatus.json, today_url, archive manifest, and current release agree
Security testsno secrets, no internal paths, no raw provider bodies in public JSON
Rendering testsPHP and JS can render article/edition without runtime schema surprises

These test concerns are directly motivated by the site’s current contract language around versioning, bounded failures, stable identifiers, public-field minimization, preserved publication dates, and correction-aware metadata.

The current site already models a conservative stance that should continue: no reader analytics, no third-party assets, minimized logs, same-origin assets, CSP restrictions, no-referrer handling for external sources, and a clear warning not to send secrets or classified material over ordinary channels. The public JSON system should retain that posture.

From a web security perspective:

TopicRecommendationSupport
CORSdefault to same-origin; if syndicating, allow only explicit origins and required methods/headersCORS is header-based and controls which origins may access responses.
CSPkeep a strict CSP and avoid inline JS where possibleCSP constrains what resources/code can load and helps mitigate XSS.
MIME safetyset correct Content-Type and X-Content-Type-Options: nosniffprevents MIME confusion/sniffing.
XSScontext-sensitive output encoding; avoid trusting stored HTMLOWASP recommends layered XSS defenses and correct output encoding.
Referrer leakageuse Referrer-Policy and element-level referrerpolicy on source linksreduces source-click metadata leakage.
Rate limitsuse bounded acquisition and avoid aggressive pollingmethodology already requires server-side fetches within reasonable limits.

On privacy and compliance, the GDPR’s principles include purpose limitation, data minimization, storage limitation, integrity/confidentiality, and accountability. The European Commission’s overview summarizes those principles, and the site’s privacy/methodology pages already echo them: minimize unnecessary identifiers, keep retention short where possible, restrict access, and publish purpose/retention/risk before adding new data-collecting features. If geotags or people metadata are stored, keep them tightly tied to editorial necessity and avoid over-collecting personal data about bystanders or victims.

On attribution and copyright, the site’s terms already say external records remain attributable to their publishers and that the site adds normalization, context, topic labels, and evidence classification rather than implying endorsement. The site’s methodology also says to preserve origin URLs, identifiers, publication times, retrieval times, and transformations. In practice, that means your JSON should store source metadata and your own summaries/analysis, not silently mirror third-party full text unless you have a clear license basis.

Migration plan from the current site to JSON-only briefs

The least risky migration is an internal one first, not a public rewrite. Because the site already has stable archive, edition, article, API, RSS, and OpenAPI routes, the new JSON layer should initially feed the existing rendering routes invisibly.

PhaseOutcomeNotes
Discoveryinventory current fields from archive, edition, article, feed, statuscompleted by the crawl summarized above
Shadow schemadefine 2020-12 schemas and generate JSON behind current pagesno public route changes yet
Dual renderingPHP pages render from JSON artifacts instead of ad hoc templatescompare output parity page-by-page
Public artifact routesadd dedicated edition.json, article.json, archive-month.json, upgraded feedpreserve current route semantics
Client enhancementadd service worker, offline reading, ETag-aware hydration, SSE updatesno need to force SPA rewrite
Editorial workflow cutoverAI + human review pipeline writes release bundlesretain additive corrections
Operational hardeningsignatures, audit logs, rollback drills, CDN config, high-traffic tuningfinal production posture

Trade-offs versus using a database

DimensionJSON-only filesystemDatabase-backed
Deployment simplicityexcellent; easy on flat hosting and same-origin static/API mixmore infrastructure
Editorial immutabilitynaturally strong with release directoriesrequires stronger application discipline
Human inspectabilityexcellent; artifacts are readable filesweaker unless export tooling is built
Rollbackvery strong with pointer swapdepends on migrations/replication/app logic
Query flexibilityweaker for ad hoc joins unless you precompute indexesmuch stronger
Concurrencyweaker for many simultaneous writersstronger
Very high cardinality metadatacan become unwieldy without sharding/index generationbetter fit
Edge cachingexcellent with immutable filenames/releasesalso good, but less inherent
Cost and vendor lock-inlowoften higher
Operational ceilinglower for write-heavy, highly relational workflowshigher

For this specific site, the current evidence points toward staying filesystem-first. The public contract already assumes local publication artifacts, no database session, immutable publication objects, visible corrections, and privacy-first delivery. Introduce a database only if future requirements become heavily relational or write-concurrent in ways that precomputed JSON indexes cannot comfortably absorb.