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
Key topics
- Semantic Systems / Language / Glyphs
- Semantic Systems
- Language
- Glyphs
- AI
- Runtime
- Privacy
- Research Archive
- Strategy
Research provenance
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
| Surface | Field or block | Example observed on site | Why it matters in JSON | Source evidence |
|---|---|---|---|---|
| Archive landing | Journal identity | “The Daily Brief”; “permanent bilingual journal” | Top-level product metadata | |
| Archive landing | Operational status | server date, running release, latest edition, missing dates, journal entries | Separate status.json / control.json | |
| Archive landing | Publication controls | “Check and generate now”, one-time backfill control | Internal admin-only JSON, never public by default | |
| Archive landing | Edition preview card | date, summary, story count, region count, source-link count | edition.json header block | |
| Archive landing | Archive listing row | date, two lead developments, story count, region count, source count, edition URL | archive-month.json/archive-index.json | |
| Edition page | Edition title and dek | Daily Brief · Jul 26, 2026 plus long summary paragraph | edition.title, edition.summary | |
| Edition page | Edition metadata | generated time, model, editorial status, truth class | provenance, review state, currentness boundary | |
| Edition page | Story card core | rank, event date, region, confidence label, headline | entries[] essentials | |
| Edition page | Story card summary | one or two body paragraphs | summary.short, summary.extended | |
| Edition page | Countries/places | Israel · Palestinian territories · Uganda... | tags, geotags, filters | |
| Edition page | Focus | Diplomacy · Conflict stabilization · Humanitarian access | taxonomies / categories | |
| Edition page | Why it matters | analysis paragraph under its own heading | separate analytical summary field | |
| Edition page | Reporting sources | linked source list | provenance and source graph | |
| Article page | Reported facts block | dedicated facts section | keep fact/analysis separation | |
| Article page | Analysis block | dedicated analysis section | separate from reportable facts | |
| Article page | Editor’s note | review statement and caution language | editable correction/review note layer | |
| Article page | Verified timeline | dated sequence items with supporting links | structured timeline array | |
| Article page | Attributable wording | short quotation cards tied to named sources | quotation records with attribution | |
| Article page | Comparative perspectives | seven analytical lenses | optional derived asset, not primary article body | |
| Article page | Reporting boundary | truth class, human review time, data record, event date | currentness, review, provenance | |
| Article page | Source trail | source role, title, what it establishes, date | first-class provenance objects | |
| Article page | At a glance | rank, region, confidence, generated time | summary metadata for cards, feeds, search | |
| Article page | Related developments | related entries from same edition | graph edges / backlinks | |
| Public feed | Normalized feed fields | id, source, title, summary, url, published_at, topics, evidence_tier, confidence | useful base envelope, but insufficient for full Daily Brief | |
| Status JSON | Publishing health | today_published, latest_date, missing_count, today_url, retries, error code | separate 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.
Recommended JSON data model and contracts
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.
Schema variants and recommended choice
| Variant | Description | Advantages | Drawbacks | Best fit |
|---|---|---|---|---|
| Minimal flat document | One JSON file per article/edition with all fields denormalized | Very simple reads and static hosting | Duplication across locale/tag/author/asset relationships; harder change history | Low traffic, simple newsroom |
| Normalized filesystem graph | Separate edition, article, author, tag, asset, status, archive JSON files linked by IDs | Clean reuse, lighter indexes, easier provenance reuse | More joins in PHP/JS | Medium traffic, multi-locale |
| Immutable release bundle | Each publish creates a full release directory with manifests plus immutable content | Best rollback story, consistent cross-file snapshots, CDN-friendly | More disk usage; requires disciplined release tooling | Medium/high traffic and strong audit needs |
| Hybrid recommended | Immutable release bundles for public state + normalized long-lived entities within each release | Strong consistency, explicit rollback, manageable complexity | Slightly more tooling | Best 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:
| Artifact | Purpose | Mutable or immutable |
|---|---|---|
edition.json | one day’s Daily Brief with ordered entries and edition-level metadata | Immutable after release; later corrected by additive change records |
article.json | full story page | Immutable after release; later corrected by additive change records |
archive-month.json | one month of edition summaries for archive page | Mutable, regenerated |
feed-latest.json | recent article or edition summaries | Mutable, regenerated |
status.json | current publication health | Mutable, regenerated |
authors/*.json | author entities | Mostly mutable |
tags/*.json | taxonomy metadata | Mutable |
assets/*.json | images, documents, derived media descriptors | Mostly immutable |
signatures/*.sig | detached Ed25519 signatures for artifacts | Immutable per version |
changes/*.json | correction/supersession/review records | Immutable append-only |
release.json | manifest for one atomic publish set | Immutable |
Recommended schema shape
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.
Recommended directory layout
/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
| Object | Recommended name | Rule |
|---|---|---|
| Release directory | YYYY-MM-DDThhmmss±hhmm | sortable, timezone-explicit |
| Edition file | edition.json inside dated directory | one canonical file per date/locale |
| Article file | NN-slug.json | rank prefix preserves edition order |
| Signature file | same basename + .sig | detached signature per artifact |
| Patch/change file | same basename + .patch.json or .change.json | append-only change objects |
| Manifest pointer | en-US-release.json | tiny mutable file that names active release |
| IDs | db-YYYY-MM-DD-NN-locale | stable across API, feed, and client cache |
| Slugs | ASCII lower-kebab-case | URL-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 field | Why |
|---|---|
source_url, source_id, publisher, retrieved_at | reproduce source trail |
source_hash | lock source bundle to what AI/editor actually saw |
prompt_template_version, model_family, response_id | audit AI output lineage |
human_review.state, reviewed_at, reviewer_role | make review visible |
change_history[] with type and summary | satisfy correction policy |
integrity.sha256, signature_ref, public_key_id | detect tampering |
release_id | tie artifact to atomic publish set |
Traffic-level options
| Assumed traffic | Filesystem strategy | What to optimize first |
|---|---|---|
| Low | Direct reads from current release; simple ETag; nightly/monthly archive rebuild | correctness, schema validation, signatures |
| Medium | Immutable releases + prebuilt manifests + CDN cache + service worker | archive/index generation, client cache revalidation |
| High | Sharded directories, precompressed JSON, CDN on immutable release paths, small mutable pointers only | directory 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:
| Routine | Responsibility |
|---|---|
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
| Area | Recommendation | Why | Support |
|---|---|---|---|
| Path safety | never accept arbitrary file paths; derive from validated identifiers only | prevents traversal and direct artifact tampering | OWASP recommends strict input validation and rejecting unexpected content. |
| JSON serving | use Content-Type: application/json and X-Content-Type-Options: nosniff | avoids MIME confusion/sniffing | |
| Caching | emit stable ETag, honor If-None-Match, set route-specific Cache-Control | reduces bandwidth and aligns with current site behavior | |
| Error responses | bounded application/problem+json, no stack traces or paths | matches current public contract | |
| External links | use Referrer-Policy, and preserve no-referrer behavior for source links | matches privacy posture | |
| XSS | escape server-rendered text and do not trust stored HTML | OWASP recommends layered XSS defenses and context-sensitive output encoding | |
| Privacy | keep same-origin assets and do not expose internal paths, keys, provider bodies | matches 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.
Recommended client architecture
The browser stack should use three layers of storage and rendering:
| Layer | Responsibility | Browser primitive |
|---|---|---|
| HTTP cache / CDN cache | validator-aware network caching | ETag, Cache-Control, conditional requests |
| Cache API | immutable JSON and shell/assets for offline read | Cache, CacheStorage |
| IndexedDB | manifests, reading state, diff history, saved versions | IndexedDB |
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
| Strategy | How it works | Strengths | Weaknesses | Recommended use |
|---|---|---|---|---|
| Browser HTTP cache only | rely on ETag / Cache-Control | simplest | poor offline UX control | baseline for all routes |
| Cache API only | store responses in named caches | good offline support for immutable artifacts | no structured query / metadata layer | edition JSON + shell assets |
| IndexedDB only | store parsed JSON docs and metadata | great structured access | extra serialization work | manifests, version graph, saved reading state |
| Service worker + Cache API + IndexedDB | offline-first shell plus structured local state | strongest UX and control | most code | recommended default |
| Polling only | fetch() on interval | works everywhere | inefficient/live lag | fallback only |
| SSE + polling fallback | persistent one-way updates plus fallback | cheap real-time | server must hold connections | recommended for reader updates |
| WebSocket | two-way channel | great for editorial collaboration | more server complexity than SSE | optional 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, andtest, 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.
Recommended AI architecture
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
| Stage | Inputs | Model/tool behavior | Output |
|---|---|---|---|
| Source acquisition | crawled URLs, editorial notes, official records | no model or model only for triage | normalized source bundle |
| Source bundling | canonical URLs, timestamps, hashes, source roles | server function calls may enrich metadata | immutable bundle JSON |
| Draft generation | source bundle + schema + prompt | Responses API + Structured Outputs | candidate edition.json / article.json |
| Entity extraction | draft + sources | structured extraction prompt | people/org/place/entity arrays |
| Confidence scoring | evidence counts + source roles + editorial rules | hybrid system rule + model rationale | label + numeric score + explanation |
| Translation | accepted source-bounded English article | paired translation prompt preserving attribution/uncertainty | localized artifact |
| Human review | candidate artifact + diff vs previous | editor approves, edits, or rejects | signed 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.
| Workflow | Trigger | Best for | Strengths | Risks |
|---|---|---|---|---|
| Cron-only on origin server | time-based | small, self-hosted deployment | minimal dependencies | weaker audit trail unless you log aggressively |
| Git commit driven | editor commit/push | editorial-first workflow | natural version history and rollback | extra discipline needed for generated artifacts |
| Webhook triggered | external editorial CMS/form submit | integrated newsroom tooling | near-real-time generation | retry/idempotency complexity |
| CI pipeline | push, PR, schedule, manual dispatch | medium/high maturity | validation, tests, signatures, deploy gates | more moving parts |
| Hybrid recommended | cron for ingestion + Git/CI for promotion | most production cases | separates source refresh from public publish | requires 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 class | Example checks |
|---|---|
| Schema tests | every artifact validates against 2020-12 schema |
| Integrity tests | detached signature verifies; hash matches |
| URL tests | canonical URLs stable; changed headlines get redirects |
| Archive consistency tests | archive row counts equal generated edition count |
| Locale parity tests | en-US and es-US contain equivalent IDs and source bundles |
| Currentness tests | status.json, today_url, archive manifest, and current release agree |
| Security tests | no secrets, no internal paths, no raw provider bodies in public JSON |
| Rendering tests | PHP 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.
Security, privacy, and legal considerations
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:
| Topic | Recommendation | Support |
|---|---|---|
| CORS | default to same-origin; if syndicating, allow only explicit origins and required methods/headers | CORS is header-based and controls which origins may access responses. |
| CSP | keep a strict CSP and avoid inline JS where possible | CSP constrains what resources/code can load and helps mitigate XSS. |
| MIME safety | set correct Content-Type and X-Content-Type-Options: nosniff | prevents MIME confusion/sniffing. |
| XSS | context-sensitive output encoding; avoid trusting stored HTML | OWASP recommends layered XSS defenses and correct output encoding. |
| Referrer leakage | use Referrer-Policy and element-level referrerpolicy on source links | reduces source-click metadata leakage. |
| Rate limits | use bounded acquisition and avoid aggressive polling | methodology 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.
| Phase | Outcome | Notes |
|---|---|---|
| Discovery | inventory current fields from archive, edition, article, feed, status | completed by the crawl summarized above |
| Shadow schema | define 2020-12 schemas and generate JSON behind current pages | no public route changes yet |
| Dual rendering | PHP pages render from JSON artifacts instead of ad hoc templates | compare output parity page-by-page |
| Public artifact routes | add dedicated edition.json, article.json, archive-month.json, upgraded feed | preserve current route semantics |
| Client enhancement | add service worker, offline reading, ETag-aware hydration, SSE updates | no need to force SPA rewrite |
| Editorial workflow cutover | AI + human review pipeline writes release bundles | retain additive corrections |
| Operational hardening | signatures, audit logs, rollback drills, CDN config, high-traffic tuning | final production posture |
Trade-offs versus using a database
| Dimension | JSON-only filesystem | Database-backed |
|---|---|---|
| Deployment simplicity | excellent; easy on flat hosting and same-origin static/API mix | more infrastructure |
| Editorial immutability | naturally strong with release directories | requires stronger application discipline |
| Human inspectability | excellent; artifacts are readable files | weaker unless export tooling is built |
| Rollback | very strong with pointer swap | depends on migrations/replication/app logic |
| Query flexibility | weaker for ad hoc joins unless you precompute indexes | much stronger |
| Concurrency | weaker for many simultaneous writers | stronger |
| Very high cardinality metadata | can become unwieldy without sharding/index generation | better fit |
| Edge caching | excellent with immutable filenames/releases | also good, but less inherent |
| Cost and vendor lock-in | low | often higher |
| Operational ceiling | lower for write-heavy, highly relational workflows | higher |
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.